@codama/fragments 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +542 -0
- package/dist/index.browser.cjs +208 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.mjs +176 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.node.cjs +195 -0
- package/dist/index.node.cjs.map +1 -0
- package/dist/index.node.mjs +163 -0
- package/dist/index.node.mjs.map +1 -0
- package/dist/index.react-native.mjs +176 -0
- package/dist/index.react-native.mjs.map +1 -0
- package/dist/javascript.browser.cjs +403 -0
- package/dist/javascript.browser.cjs.map +1 -0
- package/dist/javascript.browser.mjs +353 -0
- package/dist/javascript.browser.mjs.map +1 -0
- package/dist/javascript.node.cjs +390 -0
- package/dist/javascript.node.cjs.map +1 -0
- package/dist/javascript.node.mjs +340 -0
- package/dist/javascript.node.mjs.map +1 -0
- package/dist/javascript.react-native.mjs +353 -0
- package/dist/javascript.react-native.mjs.map +1 -0
- package/dist/rust.browser.cjs +371 -0
- package/dist/rust.browser.cjs.map +1 -0
- package/dist/rust.browser.mjs +322 -0
- package/dist/rust.browser.mjs.map +1 -0
- package/dist/rust.node.cjs +358 -0
- package/dist/rust.node.cjs.map +1 -0
- package/dist/rust.node.mjs +309 -0
- package/dist/rust.node.mjs.map +1 -0
- package/dist/rust.react-native.mjs +322 -0
- package/dist/rust.react-native.mjs.map +1 -0
- package/dist/types/core/BaseFragment.d.ts +21 -0
- package/dist/types/core/BaseFragment.d.ts.map +1 -0
- package/dist/types/core/casing.d.ts +52 -0
- package/dist/types/core/casing.d.ts.map +1 -0
- package/dist/types/core/createFragmentTemplate.d.ts +38 -0
- package/dist/types/core/createFragmentTemplate.d.ts.map +1 -0
- package/dist/types/core/fs.d.ts +28 -0
- package/dist/types/core/fs.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +9 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/mapFragmentContent.d.ts +43 -0
- package/dist/types/core/mapFragmentContent.d.ts.map +1 -0
- package/dist/types/core/path.d.ts +43 -0
- package/dist/types/core/path.d.ts.map +1 -0
- package/dist/types/core/renderMap.d.ts +61 -0
- package/dist/types/core/renderMap.d.ts.map +1 -0
- package/dist/types/core/setFragmentContent.d.ts +23 -0
- package/dist/types/core/setFragmentContent.d.ts.map +1 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/javascript/ImportMap.d.ts +61 -0
- package/dist/types/javascript/ImportMap.d.ts.map +1 -0
- package/dist/types/javascript/addToImportMap.d.ts +25 -0
- package/dist/types/javascript/addToImportMap.d.ts.map +1 -0
- package/dist/types/javascript/fragment.d.ts +135 -0
- package/dist/types/javascript/fragment.d.ts.map +1 -0
- package/dist/types/javascript/getDocblockFragment.d.ts +53 -0
- package/dist/types/javascript/getDocblockFragment.d.ts.map +1 -0
- package/dist/types/javascript/getExportAllFragment.d.ts +21 -0
- package/dist/types/javascript/getExportAllFragment.d.ts.map +1 -0
- package/dist/types/javascript/getExternalDependencies.d.ts +29 -0
- package/dist/types/javascript/getExternalDependencies.d.ts.map +1 -0
- package/dist/types/javascript/importMapToString.d.ts +40 -0
- package/dist/types/javascript/importMapToString.d.ts.map +1 -0
- package/dist/types/javascript/index.d.ts +23 -0
- package/dist/types/javascript/index.d.ts.map +1 -0
- package/dist/types/javascript/mergeImportMaps.d.ts +34 -0
- package/dist/types/javascript/mergeImportMaps.d.ts.map +1 -0
- package/dist/types/javascript/removeFromImportMap.d.ts +21 -0
- package/dist/types/javascript/removeFromImportMap.d.ts.map +1 -0
- package/dist/types/javascript/resolveImportMap.d.ts +33 -0
- package/dist/types/javascript/resolveImportMap.d.ts.map +1 -0
- package/dist/types/rust/ImportMap.d.ts +52 -0
- package/dist/types/rust/ImportMap.d.ts.map +1 -0
- package/dist/types/rust/addAliasToImportMap.d.ts +24 -0
- package/dist/types/rust/addAliasToImportMap.d.ts.map +1 -0
- package/dist/types/rust/addToImportMap.d.ts +27 -0
- package/dist/types/rust/addToImportMap.d.ts.map +1 -0
- package/dist/types/rust/fragment.d.ts +118 -0
- package/dist/types/rust/fragment.d.ts.map +1 -0
- package/dist/types/rust/getDocblockFragment.d.ts +53 -0
- package/dist/types/rust/getDocblockFragment.d.ts.map +1 -0
- package/dist/types/rust/getExternalDependencies.d.ts +30 -0
- package/dist/types/rust/getExternalDependencies.d.ts.map +1 -0
- package/dist/types/rust/importMapToString.d.ts +30 -0
- package/dist/types/rust/importMapToString.d.ts.map +1 -0
- package/dist/types/rust/index.d.ts +23 -0
- package/dist/types/rust/index.d.ts.map +1 -0
- package/dist/types/rust/mergeImportMaps.d.ts +23 -0
- package/dist/types/rust/mergeImportMaps.d.ts.map +1 -0
- package/dist/types/rust/removeFromImportMap.d.ts +20 -0
- package/dist/types/rust/removeFromImportMap.d.ts.map +1 -0
- package/dist/types/rust/resolveImportMap.d.ts +32 -0
- package/dist/types/rust/resolveImportMap.d.ts.map +1 -0
- package/package.json +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Codama
|
|
4
|
+
Copyright (c) 2024 Metaplex Foundation
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
7
|
+
a copy of this software and associated documentation files (the
|
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
12
|
+
the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be
|
|
15
|
+
included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Codama ➤ Fragments
|
|
2
|
+
|
|
3
|
+
[![npm][npm-image]][npm-url]
|
|
4
|
+
[![npm-downloads][npm-downloads-image]][npm-url]
|
|
5
|
+
|
|
6
|
+
[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/fragments.svg?style=flat
|
|
7
|
+
[npm-image]: https://img.shields.io/npm/v/@codama/fragments.svg?style=flat&label=%40codama%2Ffragments
|
|
8
|
+
[npm-url]: https://www.npmjs.com/package/@codama/fragments
|
|
9
|
+
|
|
10
|
+
This package provides the building blocks Codama renderers and code generators use to compose generated source. The core idea is the `Fragment` — a snippet of code paired with the imports it depends on — and the `fragment` tagged template that lets you stitch fragments together while propagating imports automatically.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm install @codama/fragments
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> [!NOTE]
|
|
19
|
+
> This package is **not** included in the main [`codama`](../library) package. The language-agnostic core primitives are also re-exported from [`@codama/renderers-core`](../renderers-core) for backward compatibility with existing renderers; new code should import from `@codama/fragments` directly.
|
|
20
|
+
|
|
21
|
+
## What's a fragment?
|
|
22
|
+
|
|
23
|
+
When you generate code, you usually need to track two things in parallel: the source string itself, and the imports that source depends on. Threading imports through every helper that builds a piece of code becomes painful very quickly. Fragments solve this by carrying both pieces together.
|
|
24
|
+
|
|
25
|
+
A fragment, at minimum, is just an object with a `content` string. Each language flavor — JavaScript or Rust — extends this with an `ImportMap`, a symbolic record of what the content imports.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { fragment, use } from '@codama/fragments/javascript';
|
|
29
|
+
|
|
30
|
+
const pubkey = use('type Address', '@solana/kit');
|
|
31
|
+
const interfaceFragment = fragment`
|
|
32
|
+
export interface Account {
|
|
33
|
+
readonly owner: ${pubkey};
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
console.log(interfaceFragment.content);
|
|
38
|
+
// export interface Account {
|
|
39
|
+
// readonly owner: Address;
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
console.log(interfaceFragment.imports);
|
|
43
|
+
// Map { '@solana/kit' => Map { 'Address' => { ..., isType: true } } }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Notice that `pubkey` was a small fragment carrying both the identifier `Address` and an import for it. When `pubkey` was interpolated into `interfaceFragment`, both pieces propagated upwards — the outer fragment ended up with the right content **and** with the import already attached.
|
|
47
|
+
|
|
48
|
+
That propagation is the whole point. You can build a fragment for a single field, compose it into a fragment for a struct, compose that into a fragment for a file, and only at the very end stringify the imports into actual `import { … } from '…';` (or `use foo::Bar;`) lines.
|
|
49
|
+
|
|
50
|
+
## Subpaths
|
|
51
|
+
|
|
52
|
+
The package exposes three subpaths so the JavaScript and Rust flavors don't clash on names like `Fragment`, `ImportMap`, or `fragment`.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// Root: language-agnostic core primitives only.
|
|
56
|
+
import type { BaseFragment } from '@codama/fragments';
|
|
57
|
+
import { createFragmentTemplate } from '@codama/fragments';
|
|
58
|
+
|
|
59
|
+
// JavaScript / TypeScript flavor.
|
|
60
|
+
import { fragment, use } from '@codama/fragments/javascript';
|
|
61
|
+
|
|
62
|
+
// Rust flavor.
|
|
63
|
+
import { addFragmentImports, fragment } from '@codama/fragments/rust';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you're writing a renderer that emits JavaScript or TypeScript, you'll spend most of your time in `@codama/fragments/javascript`. If you're emitting Rust, in `@codama/fragments/rust`. Each subpath re-exports the core too, so a single import per file is enough.
|
|
67
|
+
|
|
68
|
+
## Core primitives
|
|
69
|
+
|
|
70
|
+
The root entrypoint contains a handful of language-agnostic primitives. Most renderers won't import from here directly — the language subpaths re-export everything — but if you're building your own fragment flavor (for a new target language, for example), this is where you start.
|
|
71
|
+
|
|
72
|
+
### `BaseFragment`
|
|
73
|
+
|
|
74
|
+
The minimal shape every flavored fragment extends. It only requires a `content` string; flavored fragments layer on additional fields like an import map.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import type { BaseFragment } from '@codama/fragments';
|
|
78
|
+
|
|
79
|
+
type MyFragment = BaseFragment & Readonly<{ tags: ReadonlySet<string> }>;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `createFragmentTemplate`
|
|
83
|
+
|
|
84
|
+
The generic engine behind every flavor's `fragment` tagged template. You only need this when defining a new flavor: pass it the template parts, the items, an `isFragment` predicate, and a `mergeFragments` function. Everything else is handled for you.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createFragmentTemplate } from '@codama/fragments';
|
|
88
|
+
|
|
89
|
+
function fragment(template: TemplateStringsArray, ...items: unknown[]): MyFragment {
|
|
90
|
+
return createFragmentTemplate(template, items, isMyFragment, mergeMyFragments);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `mapFragmentContent` and `mapFragmentContentAsync`
|
|
95
|
+
|
|
96
|
+
Apply a content transformation while preserving every other field on the fragment.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { mapFragmentContent } from '@codama/fragments';
|
|
100
|
+
|
|
101
|
+
const upper = mapFragmentContent(myFragment, content => content.toUpperCase());
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
An async variant is available for transformations that return promises (e.g. running Prettier on the content).
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { mapFragmentContentAsync } from '@codama/fragments';
|
|
108
|
+
|
|
109
|
+
const formatted = await mapFragmentContentAsync(myFragment, formatWithPrettier);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `setFragmentContent`
|
|
113
|
+
|
|
114
|
+
Replace a fragment's content outright.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { setFragmentContent } from '@codama/fragments';
|
|
118
|
+
|
|
119
|
+
const replaced = setFragmentContent(myFragment, '/* removed */');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## JavaScript flavor
|
|
123
|
+
|
|
124
|
+
`@codama/fragments/javascript` provides the concrete machinery for emitting TypeScript and JavaScript source.
|
|
125
|
+
|
|
126
|
+
### Composing JavaScript fragments
|
|
127
|
+
|
|
128
|
+
The `fragment` tagged template builds a fragment from a string template. Interpolated values that are themselves fragments get inlined and contribute their imports; primitives are coerced to strings.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { fragment } from '@codama/fragments/javascript';
|
|
132
|
+
|
|
133
|
+
const greeting = fragment`hello ${'world'}`;
|
|
134
|
+
// greeting.content: 'hello world'
|
|
135
|
+
// greeting.imports.size: 0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When you need a fragment that references an imported identifier, the `use` helper is the shortcut. It accepts the same shorthand TypeScript accepts inside an `import { … }` block — bare names, `type` prefixes, and `as` aliases all work.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { fragment, use } from '@codama/fragments/javascript';
|
|
142
|
+
|
|
143
|
+
const address = use('type Address', '@solana/kit');
|
|
144
|
+
const owner = use('PublicKey as PK', '@solana/kit');
|
|
145
|
+
|
|
146
|
+
const struct = fragment`
|
|
147
|
+
export interface Owner {
|
|
148
|
+
readonly address: ${address};
|
|
149
|
+
readonly pubkey: ${owner};
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Both interpolated fragments propagated their imports into `struct`. The outer fragment's import map will now have one entry for `@solana/kit` containing both `Address` (as a type-only import) and `PublicKey as PK`.
|
|
155
|
+
|
|
156
|
+
### Common helpers
|
|
157
|
+
|
|
158
|
+
Two small helpers cover the most repetitive bits of code generators.
|
|
159
|
+
|
|
160
|
+
`getDocblockFragment(lines)` builds a JSDoc block from an array of lines. Empty input returns `undefined`, which composes naturally with the `fragment` tag (interpolated `undefined` renders as the empty string), so optional documentation can be threaded through without explicit checks. Any `*/` sequences inside the lines are silently defanged so user-supplied content cannot accidentally close the comment early.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { fragment, getDocblockFragment } from '@codama/fragments/javascript';
|
|
164
|
+
|
|
165
|
+
const docs = getDocblockFragment(['Greets the user.', '', 'Returns nothing.']);
|
|
166
|
+
const fn = fragment`${docs}\nexport function greet(name: string): void {}`;
|
|
167
|
+
// /**
|
|
168
|
+
// * Greets the user.
|
|
169
|
+
// *
|
|
170
|
+
// * Returns nothing.
|
|
171
|
+
// */
|
|
172
|
+
// export function greet(name: string): void {}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`getExportAllFragment(module)` builds a re-export line. The fragment carries no imports — `export * from` only forwards bindings out, it does not bring `module` into local scope.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { getExportAllFragment } from '@codama/fragments/javascript';
|
|
179
|
+
|
|
180
|
+
getExportAllFragment('./accounts').content;
|
|
181
|
+
// export * from './accounts';
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### The JavaScript `ImportMap`
|
|
185
|
+
|
|
186
|
+
The JavaScript `ImportMap` is a frozen, immutable `ReadonlyMap<Module, ReadonlyMap<UsedIdentifier, ImportInfo>>`. The outer key is the source module (e.g. `'@solana/kit'`), and the inner key is the identifier as it appears in the consuming code (after aliasing). Every operation returns a new map — there are no methods or mutation.
|
|
187
|
+
|
|
188
|
+
The shorthand strings the API accepts follow TypeScript's own:
|
|
189
|
+
|
|
190
|
+
| Input | Imported identifier | Used identifier | Type-only? |
|
|
191
|
+
| ------------------- | ------------------- | --------------- | ---------- |
|
|
192
|
+
| `'Foo'` | `Foo` | `Foo` | no |
|
|
193
|
+
| `'type Foo'` | `Foo` | `Foo` | yes |
|
|
194
|
+
| `'Foo as Bar'` | `Foo` | `Bar` | no |
|
|
195
|
+
| `'type Foo as Bar'` | `Foo` | `Bar` | yes |
|
|
196
|
+
|
|
197
|
+
### Building import maps
|
|
198
|
+
|
|
199
|
+
#### `createImportMap`
|
|
200
|
+
|
|
201
|
+
Returns an empty frozen map.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { createImportMap } from '@codama/fragments/javascript';
|
|
205
|
+
|
|
206
|
+
const map = createImportMap();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `addToImportMap`
|
|
210
|
+
|
|
211
|
+
Returns a new map with extra identifiers attached to a module. When the same identifier appears as both a value and a type-only import — anywhere in a single batch or across merged maps — the value form wins.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { addToImportMap, createImportMap } from '@codama/fragments/javascript';
|
|
215
|
+
|
|
216
|
+
let map = createImportMap();
|
|
217
|
+
map = addToImportMap(map, '@solana/kit', ['Address', 'type SignerKey']);
|
|
218
|
+
map = addToImportMap(map, '../shared', ['CamelCaseString']);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `removeFromImportMap`
|
|
222
|
+
|
|
223
|
+
Drops identifiers from a module entry. If no identifiers remain, the module entry itself is removed.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { removeFromImportMap } from '@codama/fragments/javascript';
|
|
227
|
+
|
|
228
|
+
const trimmed = removeFromImportMap(map, '@solana/kit', ['SignerKey']);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `mergeImportMaps`
|
|
232
|
+
|
|
233
|
+
Combines multiple maps into one, applying the same value-wins rule on identifier conflicts.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { mergeImportMaps } from '@codama/fragments/javascript';
|
|
237
|
+
|
|
238
|
+
const merged = mergeImportMaps([mapA, mapB, mapC]);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `parseImportInput`
|
|
242
|
+
|
|
243
|
+
If you ever need to inspect the parsed shape of a shorthand string without putting it in a map, this is the underlying parser.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { parseImportInput } from '@codama/fragments/javascript';
|
|
247
|
+
|
|
248
|
+
parseImportInput('type Foo as Bar');
|
|
249
|
+
// { importedIdentifier: 'Foo', usedIdentifier: 'Bar', isType: true }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Attaching imports to a fragment
|
|
253
|
+
|
|
254
|
+
The fragment helpers mirror the import-map helpers, except they take a fragment and update its `imports` field in place (returning a new frozen fragment).
|
|
255
|
+
|
|
256
|
+
#### `addFragmentImports`
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { addFragmentImports, fragment } from '@codama/fragments/javascript';
|
|
260
|
+
|
|
261
|
+
let f = fragment`hello`;
|
|
262
|
+
f = addFragmentImports(f, '@solana/kit', ['type Address']);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### `mergeFragmentImports`
|
|
266
|
+
|
|
267
|
+
Useful when you have a standalone import map (e.g. from a `getExternalDependencies` result) you want to fold into a fragment.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { mergeFragmentImports } from '@codama/fragments/javascript';
|
|
271
|
+
|
|
272
|
+
const updated = mergeFragmentImports(fragment, [extraMapA, extraMapB]);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### `removeFragmentImports`
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { removeFragmentImports } from '@codama/fragments/javascript';
|
|
279
|
+
|
|
280
|
+
const trimmed = removeFragmentImports(fragment, '@solana/kit', ['SignerKey']);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Resolving symbolic modules
|
|
284
|
+
|
|
285
|
+
Renderers usually don't write the final module path everywhere. Instead, they accumulate imports under symbolic names like `'solanaAddresses'` or `'generatedAccounts'` and resolve those names to real specifiers at the very end. This keeps the rendering code decoupled from the consumer's package layout.
|
|
286
|
+
|
|
287
|
+
#### `resolveImportMap`
|
|
288
|
+
|
|
289
|
+
Replaces the keys of a map according to the entries of a dependency record. Keys not present in the record are kept as-is. When two source modules resolve to the same target, their inner identifier maps are merged automatically.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { resolveImportMap } from '@codama/fragments/javascript';
|
|
293
|
+
|
|
294
|
+
const resolved = resolveImportMap(map, {
|
|
295
|
+
solanaAddresses: '@solana/kit',
|
|
296
|
+
generatedAccounts: '../accounts',
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### `getExternalDependencies`
|
|
301
|
+
|
|
302
|
+
Returns the set of root package names referenced by an import map (after resolution). For scoped packages, the scope is included; for relative imports, nothing is reported. Useful when a renderer needs to sync a generated `package.json` with the imports it actually emitted.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { getExternalDependencies } from '@codama/fragments/javascript';
|
|
306
|
+
|
|
307
|
+
const externals = getExternalDependencies(map, dependencyMap);
|
|
308
|
+
// Set { '@solana/kit', '@codama/spec' }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### `importMapToString`
|
|
312
|
+
|
|
313
|
+
Renders the map as a block of `import { … } from '…';` lines. The dependency record is applied first, then non-relative paths are listed before relative ones, identifiers are alphabetized within each module, and a block-level `import type { … }` form is used when every identifier on the line is type-only.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { importMapToString } from '@codama/fragments/javascript';
|
|
317
|
+
|
|
318
|
+
console.log(importMapToString(map, { solanaAddresses: '@solana/kit' }));
|
|
319
|
+
// import type { Address } from '@solana/kit';
|
|
320
|
+
// import { CamelCaseString } from '../shared';
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Rust flavor
|
|
324
|
+
|
|
325
|
+
`@codama/fragments/rust` mirrors the JavaScript flavor for emitting Rust source. The shape of the import map is different to match Rust's `use` syntax, but the operations follow the same naming and immutability discipline.
|
|
326
|
+
|
|
327
|
+
### Composing Rust fragments
|
|
328
|
+
|
|
329
|
+
The `fragment` tagged template behaves the same way as on the JavaScript side: interpolated fragments propagate their imports, primitives are coerced to strings.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { fragment } from '@codama/fragments/rust';
|
|
333
|
+
|
|
334
|
+
const struct = fragment`
|
|
335
|
+
pub struct AccountNode {
|
|
336
|
+
pub pubkey: Pubkey,
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Unlike the JavaScript flavor, there is no `use(input, module)` helper. Rust references identifiers inline by their full `::`-qualified path, so building content and attaching imports happen in two steps: write the content with `fragment`, then attach imports with `addFragmentImports`.
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { addFragmentImports, fragment } from '@codama/fragments/rust';
|
|
345
|
+
|
|
346
|
+
let body = fragment`
|
|
347
|
+
pub struct AccountNode {
|
|
348
|
+
pub pubkey: Pubkey,
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
body = addFragmentImports(body, ['solana_program::pubkey::Pubkey']);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
When you need an alias, use `addFragmentImportAlias`.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { addFragmentImportAlias, addFragmentImports, fragment } from '@codama/fragments/rust';
|
|
358
|
+
|
|
359
|
+
let body = fragment`pub fn fail() -> ProgError { ProgError::InvalidArgument }`;
|
|
360
|
+
body = addFragmentImports(body, ['solana_program::program_error::ProgramError']);
|
|
361
|
+
body = addFragmentImportAlias(body, 'solana_program::program_error::ProgramError', 'ProgError');
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Common helpers
|
|
365
|
+
|
|
366
|
+
`getDocblockFragment(lines)` builds a Rust doc-comment block. Empty input returns `undefined`, which composes naturally with the `fragment` tag (interpolated `undefined` renders as the empty string), so optional documentation can be threaded through without explicit checks. Pass `{ internal: true }` to emit inner doc comments (`//!`) instead of outer ones (`///`).
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { fragment, getDocblockFragment } from '@codama/fragments/rust';
|
|
370
|
+
|
|
371
|
+
const docs = getDocblockFragment(['Greets the user.', '', 'Returns nothing.']);
|
|
372
|
+
const fn = fragment`${docs}\npub fn greet(name: &str) {}`;
|
|
373
|
+
// /// Greets the user.
|
|
374
|
+
// ///
|
|
375
|
+
// /// Returns nothing.
|
|
376
|
+
// pub fn greet(name: &str) {}
|
|
377
|
+
|
|
378
|
+
const moduleDocs = getDocblockFragment(['Crate-level docs.'], { internal: true });
|
|
379
|
+
// //! Crate-level docs.
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### The Rust `ImportMap`
|
|
383
|
+
|
|
384
|
+
The Rust `ImportMap` is a flat, frozen `ReadonlyMap<ImportPath, ImportInfo>`. The key is the fully-qualified `::`-separated path; the value is `{ importedPath, alias? }`. There is no per-module grouping the way JavaScript imports have, because each Rust `use` statement names a single path.
|
|
385
|
+
|
|
386
|
+
Like the JavaScript flavor, every operation returns a new map. There are no methods, no classes, no mutation.
|
|
387
|
+
|
|
388
|
+
### Building import maps
|
|
389
|
+
|
|
390
|
+
#### `createImportMap`
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { createImportMap } from '@codama/fragments/rust';
|
|
394
|
+
|
|
395
|
+
const map = createImportMap();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### `addToImportMap`
|
|
399
|
+
|
|
400
|
+
Appends one or more paths. Accepts a single string, an array, or a `Set`. Paths already present in the map are skipped, so any existing aliases are preserved.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { addToImportMap, createImportMap } from '@codama/fragments/rust';
|
|
404
|
+
|
|
405
|
+
let map = createImportMap();
|
|
406
|
+
map = addToImportMap(map, ['borsh::BorshDeserialize', 'borsh::BorshSerialize']);
|
|
407
|
+
map = addToImportMap(map, 'solana_program::pubkey::Pubkey');
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### `addAliasToImportMap`
|
|
411
|
+
|
|
412
|
+
Records an alias for a path. If the path isn't yet imported, it is added; if it's already present, the alias replaces any existing one.
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { addAliasToImportMap } from '@codama/fragments/rust';
|
|
416
|
+
|
|
417
|
+
const aliased = addAliasToImportMap(map, 'solana_program::program_error::ProgramError', 'ProgError');
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### `removeFromImportMap`
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { removeFromImportMap } from '@codama/fragments/rust';
|
|
424
|
+
|
|
425
|
+
const trimmed = removeFromImportMap(map, 'borsh::BorshDeserialize');
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `mergeImportMaps`
|
|
429
|
+
|
|
430
|
+
Combines multiple maps. When the same path appears in more than one map, the latest occurrence wins on alias conflicts.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import { mergeImportMaps } from '@codama/fragments/rust';
|
|
434
|
+
|
|
435
|
+
const merged = mergeImportMaps([mapA, mapB]);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Attaching imports to a fragment
|
|
439
|
+
|
|
440
|
+
#### `addFragmentImports`
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { addFragmentImports, fragment } from '@codama/fragments/rust';
|
|
444
|
+
|
|
445
|
+
let f = fragment`Pubkey`;
|
|
446
|
+
f = addFragmentImports(f, 'solana_program::pubkey::Pubkey');
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### `addFragmentImportAlias`
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { addFragmentImportAlias } from '@codama/fragments/rust';
|
|
453
|
+
|
|
454
|
+
const aliased = addFragmentImportAlias(f, 'solana_program::pubkey::Pubkey', 'Pk');
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### `mergeFragmentImports`
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { mergeFragmentImports } from '@codama/fragments/rust';
|
|
461
|
+
|
|
462
|
+
const updated = mergeFragmentImports(f, [extraMap]);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### `removeFragmentImports`
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
import { removeFragmentImports } from '@codama/fragments/rust';
|
|
469
|
+
|
|
470
|
+
const trimmed = removeFragmentImports(f, 'borsh::BorshDeserialize');
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Resolving symbolic prefixes
|
|
474
|
+
|
|
475
|
+
Where the JavaScript flavor resolves entire module names, the Rust flavor resolves `::`-prefixes. This matches how Rust crate paths compose: a renderer might accumulate imports under `'generated::accounts::Foo'` and resolve `generated` to `crate::generated` at the end.
|
|
476
|
+
|
|
477
|
+
#### `resolveImportMap`
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { resolveImportMap } from '@codama/fragments/rust';
|
|
481
|
+
|
|
482
|
+
const resolved = resolveImportMap(map, { generated: 'crate::generated' });
|
|
483
|
+
// 'generated::accounts::Foo' -> 'crate::generated::accounts::Foo'
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### `getExternalDependencies`
|
|
487
|
+
|
|
488
|
+
Returns the set of top-level crate names referenced by the map after resolution. Rust's standard-library crates and crate-local prefixes are filtered out via `RUST_CORE_IMPORTS` (`std`, `crate`, `self`, `super`, `core`, `alloc`, `clippy`).
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import { getExternalDependencies } from '@codama/fragments/rust';
|
|
492
|
+
|
|
493
|
+
const externals = getExternalDependencies(map, { generated: 'crate::generated' });
|
|
494
|
+
// Set { 'borsh', 'solana_program' }
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### `importMapToString`
|
|
498
|
+
|
|
499
|
+
Renders the map as a block of `use foo::Bar;` (and `use foo::Bar as Baz;`) lines, alphabetized for stable diffs and resolved against the dependency record.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { importMapToString } from '@codama/fragments/rust';
|
|
503
|
+
|
|
504
|
+
console.log(importMapToString(map, { generated: 'crate::generated' }));
|
|
505
|
+
// use borsh::BorshSerialize;
|
|
506
|
+
// use crate::generated::accounts::Foo;
|
|
507
|
+
// use solana_program::pubkey::Pubkey;
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Putting it all together
|
|
511
|
+
|
|
512
|
+
Here's a tiny end-to-end example: a generator that emits a TypeScript interface for an account, given the account's name and the type of its `owner` field.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
import { fragment, importMapToString, use } from '@codama/fragments/javascript';
|
|
516
|
+
|
|
517
|
+
function emitAccountInterface(name: string, ownerModule: string): string {
|
|
518
|
+
const owner = use(`type ${name}OwnerKey`, ownerModule);
|
|
519
|
+
const body = fragment`
|
|
520
|
+
export interface ${name}Account {
|
|
521
|
+
readonly owner: ${owner};
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
const importBlock = importMapToString(body.imports);
|
|
525
|
+
return `${importBlock}\n\n${body.content.trim()}\n`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
console.log(emitAccountInterface('Mint', '@solana/kit'));
|
|
529
|
+
// import type { MintOwnerKey } from '@solana/kit';
|
|
530
|
+
//
|
|
531
|
+
// export interface MintAccount {
|
|
532
|
+
// readonly owner: MintOwnerKey;
|
|
533
|
+
// }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The interesting bit is what we _didn't_ have to do: at no point did `emitAccountInterface` keep a list of imports next to the content. The `use` helper attached the import to a small fragment; interpolation propagated it; `importMapToString` rendered it at the end.
|
|
537
|
+
|
|
538
|
+
Real generators take this further. They build dozens of small fragments per file, compose them into pages, and use `resolveImportMap` to translate symbolic module names into the consumer's actual package layout. The same patterns apply on the Rust side — replace `use` with explicit `addFragmentImports` calls and `importMapToString` will emit `use foo::Bar;` lines instead.
|
|
539
|
+
|
|
540
|
+
## Related packages
|
|
541
|
+
|
|
542
|
+
- [`@codama/renderers-core`](../renderers-core) — re-exports the language-agnostic core for backward compatibility with existing renderers.
|