Browse Source

Add package documentation — Part 2: renderers and validators (#105)

Loris Leiva 1 year ago
parent
commit
f883c88094

+ 162 - 2
packages/renderers-core/README.md

@@ -18,6 +18,166 @@ pnpm install @kinobi-so/renderers-core
 > [!NOTE]
 > This package is **not** included in the main [`kinobi`](../library) package.
 
-## Documentation
+## Filesystem wrappers
 
-_Coming soon..._
+This package offers several helper functions that delegate to the native Filesystem API — i.e. `node:fs` — when using the Node.js runtime. However, in any other environment — such as the browser — these functions will throw a `KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE` error as a Filesystem API is not available. This enables us to write renderers regardless of the runtime environment.
+
+```ts
+// Reads the UTF-8 content of a file as a JSON object.
+const json = readJson<MyJsonDefinition>(filePath);
+
+// Creates a directory at the given path, recursively.
+createDirectory(newDirectoryPath);
+
+// Deletes a directory, recursively, if it exists.
+deleteDirectory(directoryPath);
+
+// Creates a new file at the given path with the given content.
+// Creates its parent directory, recursively, if it does not exist.
+createFile(filePath, content);
+```
+
+## Render maps
+
+The `RenderMap` class is a utility class that helps manage a collection of files to be rendered. It acts as a middleman between the logic that generates the content and the logic that writes the content to the filesystem. As such, it provides a way to access the generated content outside an environment that supports the Filesystem API — such as the browser. It also helps us write tests about the generated code without having to write it to the filesystem.
+
+### Adding content to a `RenderMap`
+
+The add content to a `RenderMap`, you can use the `add` method by providing a path and the content to be written to that path.
+
+Note that the path should be **relative to the base directory** that will be provided when writing the `RenderMap` to the filesystem.
+
+```ts
+const renderMap = new RenderMap()
+    .add('programs/token.ts', 'export type TokenProgram = { /* ... */ }')
+    .add('accounts/mint.ts', 'export type MintAccount = { /* ... */ }')
+    .add('instructions/transfer.ts', 'export function getTransferInstruction = { /* ... */ }');
+```
+
+Additionally, you can use the `mergeWith` method to merge multiple `RenderMap` instances together.
+
+```ts
+const renderMapA = new RenderMap().add('programs/programA.ts', 'export type ProgramA = { /* ... */ }');
+const renderMapB = new RenderMap().add('programs/programB.ts', 'export type ProgramB = { /* ... */ }');
+const renderMapC = new RenderMap().mergeWith(renderMapA, renderMapB);
+```
+
+### Removing content from a `RenderMap`
+
+To remove files from a `RenderMap`, simply use the `remove` method by providing the relative path of the file to be removed.
+
+```ts
+renderMap.remove('programs/token.ts');
+```
+
+### Accessing content from a `RenderMap`
+
+The `RenderMap` class provides several methods to access the content of the files it manages. The `get` method returns the content of a file from its relative path. If the file does not exist on the `RenderMap`, a `KINOBI_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND` error will be thrown.
+
+```ts
+const content: string = renderMap.get('programs/token.ts');
+```
+
+To safely access the content of a file without throwing an error, you can use the `safeGet` method. This method returns the content of a file from its relative path, or `undefined` if the file does not exist.
+
+```ts
+const content: string | undefined = renderMap.safeGet('programs/token.ts');
+```
+
+The `has` and `isEmpty` methods can also be used to verify the existence of files in the `RenderMap`.
+
+```ts
+const hasTokenProgram = renderMap.has('programs/token.ts');
+const hasNoFiles = renderMap.isEmpty();
+```
+
+Finally, the `contains` method can be used to check if a file contains a specific string or matches a regular expression.
+
+```ts
+const hasTokenProgram = renderMap.contains('programs/token.ts', 'export type TokenProgram = { /* ... */ }');
+const hasMintAccount = renderMap.contains('programs/token.ts', /MintAccount/);
+```
+
+### Tranforming content from a `RenderMap`
+
+To map the content of files inside a `RenderMap`, you can use the `mapContent` method. This method accepts a function that takes the content of a file and returns a new content.
+
+```ts
+renderMap.mapContent(content => `/** Prefix for all files */\n\n${content}`);
+```
+
+An asynchronous version of this method called `mapContentAsync` is also available in case the transformation function needs to be asynchronous.
+
+```ts
+await renderMap.mapContentAsync(async content => {
+    const transformedContent = await someAsyncFunction(content);
+    return `/** Prefix for all files */\n\n${transformedContent}`;
+});
+```
+
+### Writing a `RenderMap` to the filesystem
+
+When the `RenderMap` is ready to be written to the filesystem, you can use the `write` method by providing the base directory where all files should be written. Any relative path provided by the `add` method will be appended to this base directory.
+
+```ts
+const renderMap = new RenderMap()
+    .add('programs/token.ts', 'export type TokenProgram = { /* ... */ }')
+    .add('accounts/mint.ts', 'export type MintAccount = { /* ... */ }');
+
+renderMap.write('src/generated');
+// In this example, files will be written to:
+// - src/generated/programs/token.ts
+// - src/generated/accounts/mint.ts.
+```
+
+### Using visitors
+
+When building renderers, you will most likely create a visitor that traverses the Kinobi IDL and returns a `RenderMap`. That way, you can test the generated content without having to write it to the filesystem. For instance, the [`@kinobi-so/renderers-js`](../renderers-js) package exports a `getRenderMapVisitor` function that does just that.
+
+```ts
+import { getRenderMapVisitor } from '@kinobi-so/renderers-js';
+
+const renderMap = kinobi.accept(getRenderMapVisitor());
+```
+
+If you have access to a visitor that returns a `RenderMap` — also described as `Visitor<RenderMap>` — then, you can wrap it inside the `writeRenderMapVisitor` to directly write the content to the filesystem at the given base directory.
+
+```ts
+import { getRenderMapVisitor } from '@kinobi-so/renderers-js';
+
+kinobi.accept(writeRenderMapVisitor(getRenderMapVisitor(), 'src/generated'));
+```
+
+Note however that, if you are writing your own renderer, you should probably offer a higher-level visitor that includes this logic and also does some additional work such as deleting the base directory before writing the new content if it already exists.
+
+For instance, the recommended way of using the `@kinobi-so/renderers-js` package is to use the following `renderVisitor` function.
+
+```ts
+import { renderVisitor } from '@kinobi-so/renderers-js';
+
+kinobi.accept(renderVisitor('src/generated'));
+```
+
+Here's a simple example of how to set up the basis of a renderer from an existing `getRenderMapVisitor`.
+
+```ts
+import { deleteDirectory } from '@kinobi-so/renderers-core';
+import { rootNodeVisitor, visit } from '@kinobi-so/visitors-core';
+
+type RenderOptions = {
+    deleteFolderBeforeRendering?: boolean;
+    // Any other options...
+};
+
+export function renderVisitor(path: string, options: RenderOptions = {}) {
+    return rootNodeVisitor(async root => {
+        // Delete existing generated folder.
+        if (options.deleteFolderBeforeRendering ?? true) {
+            deleteDirectory(path);
+        }
+
+        // Render the new files.
+        visit(root, writeRenderMapVisitor(getRenderMapVisitor(options), path));
+    });
+}
+```

+ 21 - 3
packages/renderers-js-umi/README.md

@@ -20,14 +20,32 @@ pnpm install @kinobi-so/renderers-js-umi
 >
 > However, note that the [`renderers`](../renderers) package re-exports the `renderVisitor` function of this package as `renderJavaScriptUmiVisitor`.
 
-## Documentation
+## Usage
 
-_Coming soon..._
+Once you have a Kinobi IDL, you can use the `renderVisitor` of this package to generate JavaScript clients compatible with Umi. You will need to provide the base directory where the generated files will be saved and an optional set of options to customize the output.
 
 ```ts
 // node ./kinobi.mjs
 import { renderVisitor } from '@kinobi-so/renderers-js-umi';
 
 const pathToGeneratedFolder = path.join(__dirname, 'clients', 'js', 'src', 'generated');
-kinobi.accept(renderVisitor(pathToGeneratedFolder));
+const options = {}; // See below.
+kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 ```
+
+## Options
+
+The `renderVisitor` accepts the following options.
+
+| Name                          | Type                                                | Default   | Description                                                                                                                                                                                  |
+| ----------------------------- | --------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `deleteFolderBeforeRendering` | `boolean`                                           | `true`    | Whether the base directory should be cleaned before generating new files.                                                                                                                    |
+| `formatCode`                  | `boolean`                                           | `true`    | Whether we should use Prettier to format the generated code.                                                                                                                                 |
+| `prettierOptions`             | `PrettierOptions`                                   | `{}`      | The options to use when formatting the code using Prettier.                                                                                                                                  |
+| `throwLevel`                  | `'debug' \| 'trace' \| 'info' \| 'warn' \| 'error'` | `'error'` | When validating the Kinobi IDL, the level at which the validation should throw an error.                                                                                                     |
+| `customAccountData`           | `string[]`                                          | `[]`      | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                         |
+| `customInstructionData`       | `string[]`                                          | `[]`      | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                     |
+| `dependencyMap`               | `Record<ImportFrom, string>`                        | `{}`      | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                        |
+| `internalNodes`               | `string[]`                                          | `[]`      | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                    |
+| `nonScalarEnums`              | `string[]`                                          | `[]`      | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL. |
+| `renderParentInstructions`    | `boolean`                                           | `false`   | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                      |

+ 23 - 3
packages/renderers-js/README.md

@@ -20,14 +20,34 @@ pnpm install @kinobi-so/renderers-js
 >
 > However, note that the [`renderers`](../renderers) package re-exports the `renderVisitor` function of this package as `renderJavaScriptVisitor`.
 
-## Documentation
+## Usage
 
-_Coming soon..._
+Once you have a Kinobi IDL, you can use the `renderVisitor` of this package to generate JavaScript clients. You will need to provide the base directory where the generated files will be saved and an optional set of options to customize the output.
 
 ```ts
 // node ./kinobi.mjs
 import { renderVisitor } from '@kinobi-so/renderers-js';
 
 const pathToGeneratedFolder = path.join(__dirname, 'clients', 'js', 'src', 'generated');
-kinobi.accept(renderVisitor(pathToGeneratedFolder));
+const options = {}; // See below.
+kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 ```
+
+## Options
+
+The `renderVisitor` accepts the following options.
+
+| Name                          | Type                         | Default | Description                                                                                                                                                                                                                                                     |
+| ----------------------------- | ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `deleteFolderBeforeRendering` | `boolean`                    | `true`  | Whether the base directory should be cleaned before generating new files.                                                                                                                                                                                       |
+| `formatCode`                  | `boolean`                    | `true`  | Whether we should use Prettier to format the generated code.                                                                                                                                                                                                    |
+| `prettierOptions`             | `PrettierOptions`            | `{}`    | The options to use when formatting the code using Prettier.                                                                                                                                                                                                     |
+| `asyncResolvers`              | `string[]`                   | `[]`    | The exhaustive list of `ResolverValueNode`'s names whose implementation is asynchronous in JavaScript.                                                                                                                                                          |
+| `customAccountData`           | `string[]`                   | `[]`    | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                                                                                            |
+| `customInstructionData`       | `string[]`                   | `[]`    | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                                                                                        |
+| `dependencyMap`               | `Record<ImportFrom, string>` | `{}`    | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                                                                                           |
+| `internalNodes`               | `string[]`                   | `[]`    | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                                                                                       |
+| `nameTransformers`            | `Partial<NameTransformers>`  | `{}`    | An object that enables us to override the names of any generated type, constant or function.                                                                                                                                                                    |
+| `nonScalarEnums`              | `string[]`                   | `[]`    | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.                                                                    |
+| `renderParentInstructions`    | `boolean`                    | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                                                                                         |
+| `useGranularImports`          | `boolean`                    | `false` | Whether to import the `@solana/web3.js` library using sub-packages such as `@solana/addresses` or `@solana/codecs-strings`. When set to `true`, the main `@solana/web3.js` library is used which enables generated clients to install it as a `peerDependency`. |

+ 17 - 3
packages/renderers-rust/README.md

@@ -20,14 +20,28 @@ pnpm install @kinobi-so/renderers-rust
 >
 > However, note that the [`renderers`](../renderers) package re-exports the `renderVisitor` function of this package as `renderRustVisitor`.
 
-## Documentation
+## Usage
 
-_Coming soon..._
+Once you have a Kinobi IDL, you can use the `renderVisitor` of this package to generate Rust clients. You will need to provide the base directory where the generated files will be saved and an optional set of options to customize the output.
 
 ```ts
 // node ./kinobi.mjs
 import { renderVisitor } from '@kinobi-so/renderers-rust';
 
 const pathToGeneratedFolder = path.join(__dirname, 'clients', 'rust', 'src', 'generated');
-kinobi.accept(renderVisitor(pathToGeneratedFolder));
+const options = {}; // See below.
+kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 ```
+
+## Options
+
+The `renderVisitor` accepts the following options.
+
+| Name                          | Type                         | Default     | Description                                                                                                                                                             |
+| ----------------------------- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `deleteFolderBeforeRendering` | `boolean`                    | `true`      | Whether the base directory should be cleaned before generating new files.                                                                                               |
+| `formatCode`                  | `boolean`                    | `false`     | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided.                                          |
+| `toolchain`                   | `string`                     | `"+stable"` | The toolchain to use when formatting the generated code.                                                                                                                |
+| `crateFolder`                 | `string`                     | none        | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`.                                                              |
+| `dependencyMap`               | `Record<ImportFrom, string>` | `{}`        | A mapping between import aliases and their actual crate name or path in Rust.                                                                                           |
+| `renderParentInstructions`    | `boolean`                    | `false`     | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered. |

+ 6 - 2
packages/renderers/README.md

@@ -18,9 +18,13 @@ pnpm install @kinobi-so/renderers
 > [!NOTE]
 > This package is **not** included in the main [`kinobi`](../library) package.
 
-## Documentation
+## Available renderers
 
-_Coming soon..._
+The following renderer packages are included in this package:
+
+-   [`@kinobi-so/renderers-js`](../renderers-js) as `renderJavaScriptVisitor`
+-   [`@kinobi-so/renderers-js-umi`](../renderers-js-umi) as `renderJavaScriptUmiVisitor`
+-   [`@kinobi-so/renderers-rust`](../renderers-rust) as `renderRustVisitor`
 
 ```ts
 // node ./kinobi.mjs

+ 37 - 2
packages/validators/README.md

@@ -22,12 +22,47 @@ pnpm install @kinobi-so/validators
 > pnpm install kinobi
 > ```
 
-## Documentation
+## Types
 
-_Coming soon..._
+### `ValidationItem`
+
+A validation item describes a single piece of information — typically a warning or an error — about a node in the Kinobi IDL.
+
+```ts
+type ValidationItem = {
+    // The level of importance of a validation item.
+    level: 'debug' | 'trace' | 'info' | 'warn' | 'error';
+    // A human-readable message describing the issue or information.
+    message: string;
+    // The node that the validation item is related to.
+    node: Node;
+    // The stack of nodes that led to the node above.
+    stack: readonly Node[];
+};
+```
+
+## Functions
+
+### `getValidationItemsVisitor(visitor)`
+
+The `getValidationItemsVisitor` function returns a visitor that collects all validation items from a Kinobi IDL. Note that this visitor is still a work in progress and does not cover all validation rules.
+
+```ts
+import { getValidationItemsVisitor } from '@kinobi-so/validators';
+
+const validationItems = kinobi.accept(getValidationItemsVisitor());
+```
+
+### `throwValidatorItemsVisitor(visitor)`
+
+The `throwValidatorItemsVisitor` function accepts a `Visitor<ValidationItemp[]>` and throws an error if any validation items above a certain level are found. By default, the level is set to `'error'` but a second argument can be passed to change it.
 
 ```ts
 import { throwValidatorItemsVisitor, getValidationItemsVisitor } from '@kinobi-so/validators';
 
+// Throw if any "error" items are found.
 kinobi.accept(throwValidatorItemsVisitor(getValidationItemsVisitor()));
+
+// Throw if any "warn" or "error" items are found.
+kinobi.accept(throwValidatorItemsVisitor(getValidationItemsVisitor(), 'warn'));
 ```