@c7-digital/scribe 0.0.1
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/README.md +336 -0
- package/lib/src/analyze.d.ts +22 -0
- package/lib/src/analyze.js +175 -0
- package/lib/src/cli.d.ts +9 -0
- package/lib/src/cli.js +35 -0
- package/lib/src/config.d.ts +224 -0
- package/lib/src/config.js +59 -0
- package/lib/src/discover.d.ts +13 -0
- package/lib/src/discover.js +70 -0
- package/lib/src/generate/exports.d.ts +13 -0
- package/lib/src/generate/exports.js +65 -0
- package/lib/src/generate/index.d.ts +13 -0
- package/lib/src/generate/index.js +91 -0
- package/lib/src/generate/registry.d.ts +9 -0
- package/lib/src/generate/registry.js +123 -0
- package/lib/src/generate/types.d.ts +9 -0
- package/lib/src/generate/types.js +40 -0
- package/lib/src/index.d.ts +5 -0
- package/lib/src/index.js +5 -0
- package/lib/src/transform.d.ts +11 -0
- package/lib/src/transform.js +174 -0
- package/lib/src/vite-plugin.d.ts +29 -0
- package/lib/src/vite-plugin.js +39 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# @c7-digital/scribe
|
|
2
|
+
|
|
3
|
+
Transform raw DAML codegen output into unified, modern ESM TypeScript packages for use with `@c7-digital/ledger`.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`dpm codegen-js` produces raw TypeScript/CommonJS packages -- one per DAR -- with global template registration, mixed import conventions, and self-referencing type imports. Scribe absorbs all of that complexity:
|
|
8
|
+
|
|
9
|
+
- **CJS to ESM**: bundles CommonJS `require()`/`module.exports` into clean ESM `import`/`export` via Rollup
|
|
10
|
+
- **Global registry bypass**: strips `damlTypes.registerTemplate()` side-effects, replaces them with a version-aware registry
|
|
11
|
+
- **Import fixups**: rewrites `@mojotech/json-type-validation` (default -> namespace), `@daml/types` (default -> namespace), strips `@daml/ledger` side-effect imports
|
|
12
|
+
- **Package unification**: merges multiple DAR packages into a single entry point with named exports
|
|
13
|
+
- **Self-reference resolution**: resolves `.d.ts` self-referencing imports (e.g. `@mypackage/codegen/daml-prim-DA-Types-1.0.0`) to local paths
|
|
14
|
+
- **Version-aware registry**: generates a `versionedRegistry` function that conforms to `VersionedRegistry` from `@c7-digital/ledger`
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D @c7-digital/scribe
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
### Zero-config
|
|
25
|
+
|
|
26
|
+
Point scribe at the raw `dpm codegen-js` output:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm exec scribe -i ./codegen/js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Scribe auto-detects:
|
|
33
|
+
|
|
34
|
+
| What | How |
|
|
35
|
+
|------|-----|
|
|
36
|
+
| Main package | The non-`daml-prim`/`daml-stdlib`/`splice-*` package |
|
|
37
|
+
| Modules | Every directory under `lib/` |
|
|
38
|
+
| Templates & interfaces | Exports with `templateId` in `.d.ts` files |
|
|
39
|
+
| Package ID | From `lib/index.d.ts` |
|
|
40
|
+
| Version | From directory name (e.g. `my-project-0.1.0` -> `0.1.0`) |
|
|
41
|
+
| Vendor packages | `splice-*` packages (third-party DARs your model depends on; exported separately from your main modules) |
|
|
42
|
+
| Stdlib | `daml-prim-*`, `daml-stdlib-*`, `ghc-stdlib-*` |
|
|
43
|
+
|
|
44
|
+
Output:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
dist/
|
|
48
|
+
├── codegen.js # Bundled ESM (all modules + registry)
|
|
49
|
+
├── index.js # Re-exports from codegen.js
|
|
50
|
+
├── index.d.ts # TypeScript declarations
|
|
51
|
+
├── version.js # PACKAGE_VERSION export
|
|
52
|
+
└── version.d.ts # Version type declaration
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### With config
|
|
56
|
+
|
|
57
|
+
For projects that need aliases, selective exports, or compat versions, create a `scribe.yaml`:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
schema: "1.0"
|
|
61
|
+
input: "../../codegen/js"
|
|
62
|
+
|
|
63
|
+
main:
|
|
64
|
+
pattern: "domain-verification-model-*"
|
|
65
|
+
|
|
66
|
+
vendor:
|
|
67
|
+
"splice-amulet-*":
|
|
68
|
+
modules:
|
|
69
|
+
"Splice.Amulet":
|
|
70
|
+
alias: Splice_Amulet
|
|
71
|
+
templates:
|
|
72
|
+
- FeaturedAppActivityMarker
|
|
73
|
+
|
|
74
|
+
compat:
|
|
75
|
+
"domain-verification-model-*":
|
|
76
|
+
versions:
|
|
77
|
+
- hash: "8afd289e3ba826fcb23d955cfc108470b53527b0615745554185e5c9625b5832"
|
|
78
|
+
version: "0.0.5"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm exec scribe --config scribe.yaml
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## CLI
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
scribe [options]
|
|
89
|
+
|
|
90
|
+
Options:
|
|
91
|
+
-i, --input <path> Path to codegen/js directory (default: ./codegen/js)
|
|
92
|
+
-c, --config <path> Path to scribe.yaml config file
|
|
93
|
+
-v, --version <ver> Override package version
|
|
94
|
+
-o, --output <path> Output directory (default: dist)
|
|
95
|
+
--dry-run Preview without writing files
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Version resolution priority**: `--version` flag > `$DAR_VERSION` env var > directory name.
|
|
99
|
+
|
|
100
|
+
## Config reference
|
|
101
|
+
|
|
102
|
+
All fields are optional except `schema`.
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
schema: "1.0" # Required. Config schema version.
|
|
106
|
+
|
|
107
|
+
input: "./codegen/js" # Path to raw codegen output.
|
|
108
|
+
# Default: ./codegen/js
|
|
109
|
+
|
|
110
|
+
output:
|
|
111
|
+
dir: "dist" # Output directory. Default: dist
|
|
112
|
+
bundle: true # Run CJS->ESM bundling. Default: true
|
|
113
|
+
|
|
114
|
+
main:
|
|
115
|
+
pattern: "my-project-*" # Glob to identify the main package.
|
|
116
|
+
# Only needed when auto-detection is ambiguous.
|
|
117
|
+
|
|
118
|
+
modules: # Restrict which modules are exported.
|
|
119
|
+
# Omit to export all discovered modules.
|
|
120
|
+
MyModule:
|
|
121
|
+
templates: # Restrict which templates are registered.
|
|
122
|
+
- MyTemplate # Omit to register all.
|
|
123
|
+
interfaces:
|
|
124
|
+
- MyInterface
|
|
125
|
+
alias: CustomName # Override the export name.
|
|
126
|
+
OtherModule: {} # All templates/interfaces in this module.
|
|
127
|
+
|
|
128
|
+
vendor:
|
|
129
|
+
"splice-amulet-*": # Glob pattern matching vendor package names.
|
|
130
|
+
modules:
|
|
131
|
+
"Splice.Amulet":
|
|
132
|
+
alias: Splice_Amulet # Export alias.
|
|
133
|
+
templates:
|
|
134
|
+
- FeaturedAppActivityMarker
|
|
135
|
+
|
|
136
|
+
compat:
|
|
137
|
+
"my-project-*": # Glob matching package names (main or vendor).
|
|
138
|
+
versions: # Backward-compat hashes for contract upgrades.
|
|
139
|
+
- hash: "8afd289e..." # Package ID hash from a previous version.
|
|
140
|
+
version: "0.0.5" # Version label for that hash.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**The config exists to constrain, not to describe.** If you omit `modules`, all are exported. If you omit `templates`, all are registered. Use config when you want *less* than the default, or need aliases and compat hashes.
|
|
144
|
+
|
|
145
|
+
### Multi-version main packages
|
|
146
|
+
|
|
147
|
+
When `codegen/js` contains multiple versions of the main package (e.g. `model-0.0.7`, `model-0.0.8`, `model-0.0.9`), scribe selects one:
|
|
148
|
+
|
|
149
|
+
1. If `--version` or `$DAR_VERSION` is set, use that version
|
|
150
|
+
2. Otherwise, pick the latest by semver
|
|
151
|
+
|
|
152
|
+
Non-selected versions are demoted to stdlib (bundled as dependencies but not exported).
|
|
153
|
+
|
|
154
|
+
### Vendor filtering
|
|
155
|
+
|
|
156
|
+
When a `vendor` section is present, only `splice-*` packages matching a vendor pattern are included. Unmatched `splice-*` packages are excluded (demoted to stdlib). Without a `vendor` section, all `splice-*` packages are included as vendors.
|
|
157
|
+
|
|
158
|
+
## Vite plugin
|
|
159
|
+
|
|
160
|
+
Scribe ships a companion Vite plugin for consumer apps at `@c7-digital/scribe/vite`. This handles the runtime concerns that can't be solved at build time -- `@daml/types` and `@mojotech/json-type-validation` need special resolution in the consumer's dev server and build.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { defineConfig } from 'vite';
|
|
164
|
+
import react from '@vitejs/plugin-react';
|
|
165
|
+
import { damlCodegenPlugin } from '@c7-digital/scribe/vite';
|
|
166
|
+
|
|
167
|
+
export default defineConfig({
|
|
168
|
+
plugins: [
|
|
169
|
+
damlCodegenPlugin(),
|
|
170
|
+
react(),
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### What `damlCodegenPlugin()` configures
|
|
176
|
+
|
|
177
|
+
- `@mojotech/json-type-validation` -> UMD build (required for ESM compat)
|
|
178
|
+
- `@daml/types` -> local node_modules copy
|
|
179
|
+
- `optimizeDeps` for `@daml/types`, `@c7-digital/ledger`, `@c7-digital/react`, and transitive deps
|
|
180
|
+
|
|
181
|
+
### Options
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
damlCodegenPlugin({
|
|
185
|
+
// Override node_modules lookup path.
|
|
186
|
+
// Useful in monorepos where node_modules isn't at cwd().
|
|
187
|
+
nodeModulesPath: __dirname,
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The plugin is independent of scribe's CLI -- it works for any app that imports `@daml/types`, even without using scribe for codegen.
|
|
192
|
+
|
|
193
|
+
## Programmatic API
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { run, loadConfig, discoverPackages, analyzePackage, generate } from '@c7-digital/scribe';
|
|
197
|
+
|
|
198
|
+
// Run the full pipeline
|
|
199
|
+
await run({ input: './codegen/js' });
|
|
200
|
+
|
|
201
|
+
// Or use individual steps
|
|
202
|
+
const config = await loadConfig({ config: './scribe.yaml' });
|
|
203
|
+
const packages = await discoverPackages(config);
|
|
204
|
+
const analyzed = await Promise.all(
|
|
205
|
+
packages.map(pkg => analyzePackage(pkg, config))
|
|
206
|
+
);
|
|
207
|
+
await generate(analyzed, config);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Generated output
|
|
211
|
+
|
|
212
|
+
### Exports
|
|
213
|
+
|
|
214
|
+
Scribe generates named exports for each module and a flat `versionedRegistry`:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Main package modules
|
|
218
|
+
import { InternetDomainName, AddressBook, C7Credentials } from '@mypackage/codegen';
|
|
219
|
+
|
|
220
|
+
// Vendor modules (with aliases from config)
|
|
221
|
+
import { Splice_Amulet } from '@mypackage/codegen';
|
|
222
|
+
|
|
223
|
+
// Registry (conforms to VersionedRegistry from @c7-digital/ledger)
|
|
224
|
+
import { versionedRegistry, PACKAGE_VERSION } from '@mypackage/codegen';
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Using with `@c7-digital/ledger`
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { Ledger } from '@c7-digital/ledger';
|
|
231
|
+
import { versionedRegistry, InternetDomainName } from '@mypackage/codegen';
|
|
232
|
+
|
|
233
|
+
const ledger = new Ledger({
|
|
234
|
+
url: 'http://localhost:7575',
|
|
235
|
+
token: authToken,
|
|
236
|
+
versionedRegistry, // plugs directly into ledger
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const domains = await ledger.query(InternetDomainName.DomainOwnershipToken, {
|
|
240
|
+
owner: myParty,
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The `versionedRegistry` function is structurally compatible with `VersionedRegistry` from `@c7-digital/ledger` without a hard import dependency. TypeScript accepts it wherever `VersionedRegistry` is expected.
|
|
245
|
+
|
|
246
|
+
## How it works
|
|
247
|
+
|
|
248
|
+
### Pipeline
|
|
249
|
+
|
|
250
|
+
1. **Load config** -- read `scribe.yaml` (if provided), merge with CLI flags and defaults
|
|
251
|
+
2. **Discover** -- scan input directory, classify packages as main/vendor/stdlib
|
|
252
|
+
3. **Analyze** -- extract `packageId`, modules, templates, and interfaces from each package's `.d.ts` files
|
|
253
|
+
4. **Generate** -- produce `index.js` (imports, exports, registry), `index.d.ts`, `version.js`, `version.d.ts`
|
|
254
|
+
5. **Bundle** -- Vite/Rollup build: CJS->ESM, import fixups, `registerTemplate` stripping, self-reference resolution
|
|
255
|
+
|
|
256
|
+
### Architecture
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
dpm codegen-js scribe consumer app
|
|
260
|
+
┌─────────────┐ ┌──────────────────┐ ┌──────────────────────┐
|
|
261
|
+
│ model-0.0.8/│ │ discover │ │ vite.config.ts │
|
|
262
|
+
│ splice-*/ │--->│ analyze │ │ damlCodegenPlugin()│
|
|
263
|
+
│ daml-prim-*/│ │ generate + bundle│--->│ │
|
|
264
|
+
│ daml-stdlib/│ │ │ │ import { ... } │
|
|
265
|
+
└─────────────┘ └──────────────────┘ │ from '@pkg/codegen'│
|
|
266
|
+
└──────────────────────┘
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Scribe owns the **build** side (raw codegen -> clean ESM bundle). The `damlCodegenPlugin()` Vite plugin owns the **consumer** side (`@daml/types` resolution, optimizeDeps). Neither depends on the other.
|
|
270
|
+
|
|
271
|
+
## Consumer `package.json` setup
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"name": "@mypackage/codegen",
|
|
276
|
+
"version": "0.0.1",
|
|
277
|
+
"type": "module",
|
|
278
|
+
"main": "dist/index.js",
|
|
279
|
+
"types": ".scribe/index.d.ts",
|
|
280
|
+
"exports": {
|
|
281
|
+
".": {
|
|
282
|
+
"import": "./dist/index.js",
|
|
283
|
+
"types": "./.scribe/index.d.ts"
|
|
284
|
+
},
|
|
285
|
+
"./version": {
|
|
286
|
+
"import": "./dist/version.js",
|
|
287
|
+
"types": "./.scribe/version.d.ts"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"scripts": {
|
|
291
|
+
"build": "scribe --config scribe.yaml",
|
|
292
|
+
"prepare": "scribe --config scribe.yaml"
|
|
293
|
+
},
|
|
294
|
+
"dependencies": {
|
|
295
|
+
"@daml/types": "3.4.9",
|
|
296
|
+
"@mojotech/json-type-validation": "^3.1.0"
|
|
297
|
+
},
|
|
298
|
+
"devDependencies": {
|
|
299
|
+
"@c7-digital/scribe": "^0.0.1"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Scribe creates a `.scribe/` staging directory (symlinked raw codegen packages + generated `.d.ts` files) and a `dist/` directory (bundled ESM). The `.scribe/` directory is tool-managed and should not be edited manually. The `types` field points at `.scribe/index.d.ts` so TypeScript resolves types through the original `.d.ts` chain, while `main`/`exports` point at the bundled `dist/` output for runtime.
|
|
305
|
+
|
|
306
|
+
## Development
|
|
307
|
+
|
|
308
|
+
### Testing
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Unit tests (fixture-based, no external dependencies)
|
|
312
|
+
pnpm test
|
|
313
|
+
|
|
314
|
+
# E2E tests (requires dpm or daml CLI)
|
|
315
|
+
pnpm test:e2e
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Unit tests validate scribe's transformation logic against hand-crafted fixtures in `src/__fixtures__/`. They're fast and don't require the Daml SDK.
|
|
319
|
+
|
|
320
|
+
E2E tests validate the full pipeline against real codegen output: compile a Daml model, run `dpm codegen-js`, run scribe, then verify that a downstream consumer can type-check (`tsc --noEmit`) and bundle (`vite.build()`) the output. They require `dpm` (preferred) or the legacy `daml` assistant and will skip gracefully if neither is installed.
|
|
321
|
+
|
|
322
|
+
### SDK version pin
|
|
323
|
+
|
|
324
|
+
The Daml SDK version is pinned in two places:
|
|
325
|
+
|
|
326
|
+
- `e2e/daml/daml.yaml` — `sdk-version` used to compile the test model
|
|
327
|
+
- `.github/workflows/ci.yml` — version installed on CI runners
|
|
328
|
+
|
|
329
|
+
Both must match `ledger`'s `@daml/types` dependency (currently **3.4.9**).
|
|
330
|
+
|
|
331
|
+
## Requirements
|
|
332
|
+
|
|
333
|
+
- Node.js >= 18.0.0
|
|
334
|
+
- pnpm >= 8.0.0
|
|
335
|
+
- Vite 5.x or 6.x (optional, for bundling and the companion plugin)
|
|
336
|
+
- `dpm` or `daml` CLI (optional, for e2e tests only)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.js";
|
|
2
|
+
import type { DiscoveredPackage } from "./discover.js";
|
|
3
|
+
export interface AnalyzedTemplate {
|
|
4
|
+
name: string;
|
|
5
|
+
kind: "template" | "interface";
|
|
6
|
+
}
|
|
7
|
+
export interface AnalyzedModule {
|
|
8
|
+
/** Module name, e.g. "InternetDomainName" */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Relative path from package root to module directory */
|
|
11
|
+
modulePath: string;
|
|
12
|
+
/** Export alias (from config or auto-generated) */
|
|
13
|
+
alias: string;
|
|
14
|
+
/** Templates and interfaces found in this module */
|
|
15
|
+
members: AnalyzedTemplate[];
|
|
16
|
+
}
|
|
17
|
+
export interface AnalyzedPackage extends DiscoveredPackage {
|
|
18
|
+
packageId: string;
|
|
19
|
+
version: string;
|
|
20
|
+
modules: AnalyzedModule[];
|
|
21
|
+
}
|
|
22
|
+
export declare function analyzePackage(pkg: DiscoveredPackage, config: ResolvedConfig): Promise<AnalyzedPackage>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Extract packageId from lib/index.d.ts.
|
|
5
|
+
* Looks for: export declare const packageId = "...";
|
|
6
|
+
*/
|
|
7
|
+
async function extractPackageId(pkgPath) {
|
|
8
|
+
const indexDts = join(pkgPath, "lib", "index.d.ts");
|
|
9
|
+
const content = await readFile(indexDts, "utf-8");
|
|
10
|
+
const match = content.match(/export\s+declare\s+const\s+packageId\s*=\s*['"]([0-9a-f]+)['"]/);
|
|
11
|
+
if (!match?.[1]) {
|
|
12
|
+
throw new Error(`Could not extract packageId from ${indexDts}`);
|
|
13
|
+
}
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Scan a module.d.ts file and classify exports as templates or interfaces.
|
|
18
|
+
*
|
|
19
|
+
* Templates have a `templateId` field and a `Template` type in their declaration.
|
|
20
|
+
* Interfaces have an `InterfaceCompanion` type.
|
|
21
|
+
*/
|
|
22
|
+
async function discoverMembers(moduleDtsPath) {
|
|
23
|
+
let content;
|
|
24
|
+
try {
|
|
25
|
+
content = await readFile(moduleDtsPath, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const code = err.code;
|
|
29
|
+
if (code !== "ENOENT") {
|
|
30
|
+
console.warn(`Warning: could not read ${moduleDtsPath}: ${code ?? err}`);
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const members = [];
|
|
35
|
+
// Match exported consts that look like template or interface companions.
|
|
36
|
+
// Pattern: export declare const Foo : ...Template<...>... or ...InterfaceCompanion<...>...
|
|
37
|
+
const regex = /export\s+declare\s+const\s+(\w+)\s*:\s*.*?(Template|InterfaceCompanion)\s*</g;
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = regex.exec(content)) !== null) {
|
|
40
|
+
const name = m[1];
|
|
41
|
+
const kind = m[2] === "InterfaceCompanion" ? "interface" : "template";
|
|
42
|
+
members.push({ name, kind });
|
|
43
|
+
}
|
|
44
|
+
return members;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Discover modules inside a package's lib/ directory.
|
|
48
|
+
* Each subdirectory under lib/ that contains module.d.ts (or an index.d.ts
|
|
49
|
+
* for nested modules like Splice/Amulet/) is a module.
|
|
50
|
+
*/
|
|
51
|
+
async function discoverModules(pkgPath, role) {
|
|
52
|
+
const libPath = join(pkgPath, "lib");
|
|
53
|
+
const modules = [];
|
|
54
|
+
async function walk(dir, prefix) {
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const code = err.code;
|
|
61
|
+
if (code !== "ENOENT") {
|
|
62
|
+
console.warn(`Warning: could not read directory ${dir}: ${code ?? err}`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!entry.isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
const subdir = join(dir, entry.name);
|
|
70
|
+
const moduleDts = join(subdir, "module.d.ts");
|
|
71
|
+
const indexDts = join(subdir, "index.d.ts");
|
|
72
|
+
// Check if this directory has module.d.ts (leaf module)
|
|
73
|
+
let dtsPath;
|
|
74
|
+
try {
|
|
75
|
+
await stat(moduleDts);
|
|
76
|
+
dtsPath = moduleDts;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
try {
|
|
80
|
+
await stat(indexDts);
|
|
81
|
+
dtsPath = indexDts;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Not a leaf module; recurse
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const moduleName = prefix ? `${prefix}.${entry.name}` : entry.name;
|
|
88
|
+
if (dtsPath) {
|
|
89
|
+
const members = await discoverMembers(dtsPath);
|
|
90
|
+
const alias = moduleName.replace(/\./g, "_");
|
|
91
|
+
modules.push({
|
|
92
|
+
name: moduleName,
|
|
93
|
+
modulePath: dtsPath.replace(libPath + "/", "").replace(/\.d\.ts$/, ""),
|
|
94
|
+
alias,
|
|
95
|
+
members,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Always recurse for nested modules (e.g., Splice/Amulet/)
|
|
99
|
+
await walk(subdir, moduleName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await walk(libPath, "");
|
|
103
|
+
return modules;
|
|
104
|
+
}
|
|
105
|
+
export async function analyzePackage(pkg, config) {
|
|
106
|
+
const packageId = await extractPackageId(pkg.path);
|
|
107
|
+
// Config version override only applies to the main package
|
|
108
|
+
const version = pkg.role === "main"
|
|
109
|
+
? (config.version ?? pkg.detectedVersion ?? "0.0.0")
|
|
110
|
+
: (pkg.detectedVersion ?? "0.0.0");
|
|
111
|
+
let modules = await discoverModules(pkg.path, pkg.role);
|
|
112
|
+
// Apply config overrides for main package
|
|
113
|
+
if (pkg.role === "main" && config.main.modules) {
|
|
114
|
+
modules = applyModuleOverrides(modules, config.main.modules);
|
|
115
|
+
}
|
|
116
|
+
// Apply config overrides for vendor packages
|
|
117
|
+
if (pkg.role === "vendor" && config.vendor) {
|
|
118
|
+
const vendorOverride = findVendorOverride(pkg.name, config.vendor);
|
|
119
|
+
if (vendorOverride?.modules) {
|
|
120
|
+
modules = applyModuleOverrides(modules, vendorOverride.modules);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...pkg,
|
|
125
|
+
packageId,
|
|
126
|
+
version,
|
|
127
|
+
modules,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Apply config-driven overrides to discovered modules:
|
|
132
|
+
* - If overrides specify modules, only include those (filter)
|
|
133
|
+
* - Apply alias overrides
|
|
134
|
+
* - If overrides specify templates/interfaces, only include those (filter members)
|
|
135
|
+
*/
|
|
136
|
+
function applyModuleOverrides(discovered, overrides) {
|
|
137
|
+
const overrideKeys = Object.keys(overrides);
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const key of overrideKeys) {
|
|
140
|
+
const override = overrides[key];
|
|
141
|
+
// Find discovered module matching this key (by name)
|
|
142
|
+
const mod = discovered.find((m) => m.name === key);
|
|
143
|
+
if (!mod) {
|
|
144
|
+
console.warn(`Config references module "${key}" but it was not found.`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let members = mod.members;
|
|
148
|
+
// Filter templates if specified
|
|
149
|
+
if (override.templates) {
|
|
150
|
+
const allowedTemplates = new Set(override.templates);
|
|
151
|
+
members = members.filter((m) => m.kind !== "template" || allowedTemplates.has(m.name));
|
|
152
|
+
}
|
|
153
|
+
// Filter interfaces if specified
|
|
154
|
+
if (override.interfaces) {
|
|
155
|
+
const allowedInterfaces = new Set(override.interfaces);
|
|
156
|
+
members = members.filter((m) => m.kind !== "interface" || allowedInterfaces.has(m.name));
|
|
157
|
+
}
|
|
158
|
+
result.push({
|
|
159
|
+
...mod,
|
|
160
|
+
alias: override.alias ?? mod.alias,
|
|
161
|
+
members,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/** Find the vendor override config entry matching a package name. */
|
|
167
|
+
function findVendorOverride(pkgName, vendorConfig) {
|
|
168
|
+
for (const pattern of Object.keys(vendorConfig)) {
|
|
169
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
170
|
+
if (regex.test(pkgName)) {
|
|
171
|
+
return vendorConfig[pattern];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
package/lib/src/cli.d.ts
ADDED
package/lib/src/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { discoverPackages } from "./discover.js";
|
|
5
|
+
import { analyzePackage } from "./analyze.js";
|
|
6
|
+
import { generate } from "./generate/index.js";
|
|
7
|
+
export async function run(opts = {}) {
|
|
8
|
+
const config = await loadConfig(opts);
|
|
9
|
+
const packages = await discoverPackages(config);
|
|
10
|
+
const analyzed = await Promise.all(packages.map((pkg) => analyzePackage(pkg, config)));
|
|
11
|
+
await generate(analyzed, config);
|
|
12
|
+
}
|
|
13
|
+
async function main() {
|
|
14
|
+
const { values } = parseArgs({
|
|
15
|
+
options: {
|
|
16
|
+
input: { type: "string", short: "i" },
|
|
17
|
+
config: { type: "string", short: "c" },
|
|
18
|
+
version: { type: "string", short: "v" },
|
|
19
|
+
output: { type: "string", short: "o" },
|
|
20
|
+
"dry-run": { type: "boolean", default: false },
|
|
21
|
+
},
|
|
22
|
+
strict: true,
|
|
23
|
+
});
|
|
24
|
+
await run({
|
|
25
|
+
input: values.input,
|
|
26
|
+
config: values.config,
|
|
27
|
+
version: values.version,
|
|
28
|
+
output: values.output,
|
|
29
|
+
dryRun: values["dry-run"],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
main().catch((err) => {
|
|
33
|
+
console.error(err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|