@ontrails/library 0.2.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/CHANGELOG.md +195 -0
- package/README.md +89 -0
- package/package.json +38 -0
- package/src/compile.ts +508 -0
- package/src/derive.ts +207 -0
- package/src/errors.ts +191 -0
- package/src/index.ts +56 -0
- package/src/kernel.ts +63 -0
- package/src/layer-input.ts +188 -0
- package/src/surface.ts +173 -0
package/src/compile.ts
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compile` — the package emitter. Consumes the `LibraryRenderingPlan` (never the
|
|
3
|
+
* topo directly) and produces the source files of a generated TypeScript
|
|
4
|
+
* package: consumer-fluent root, `/result`, `/schemas`, `/trails` subpaths.
|
|
5
|
+
*
|
|
6
|
+
* v0 is runtime-backed: the generated package imports the source topo and the
|
|
7
|
+
* `@ontrails/library` runtime (the kernel seam), so execution delegates to the
|
|
8
|
+
* shared pipeline. The standalone trajectory (vendoring the kernel) does not
|
|
9
|
+
* change consumer code — see the runtime-kernel section of the ADR.
|
|
10
|
+
*
|
|
11
|
+
* This slice returns the file plan (path + content); writing it to disk is a
|
|
12
|
+
* thin apply step. Pure: derives the rendering and builds strings, no I/O.
|
|
13
|
+
*/
|
|
14
|
+
import packageJson from '@ontrails/library/package.json' with { type: 'json' };
|
|
15
|
+
|
|
16
|
+
import { deriveLibraryApi } from './derive.js';
|
|
17
|
+
import type {
|
|
18
|
+
DeriveLibraryApiOptions,
|
|
19
|
+
LibraryExport,
|
|
20
|
+
LibraryRenderingPlan,
|
|
21
|
+
} from './derive.js';
|
|
22
|
+
import type { Topo } from './kernel.js';
|
|
23
|
+
|
|
24
|
+
/** Options for emitting a generated library package. */
|
|
25
|
+
export interface CompileOptions extends DeriveLibraryApiOptions {
|
|
26
|
+
/** The generated package name (e.g. `@acme/core`). */
|
|
27
|
+
readonly packageName: string;
|
|
28
|
+
/** Import specifier the generated code uses to reach the source topo. */
|
|
29
|
+
readonly appImportPath: string;
|
|
30
|
+
/** The exported binding name of the topo at `appImportPath` (default `app`). */
|
|
31
|
+
readonly appExportName?: string;
|
|
32
|
+
/** Runtime dependency range; defaults to a caret range of this compiler's package version. */
|
|
33
|
+
readonly libraryDependency?: string;
|
|
34
|
+
/** Generated package version. Defaults to `0.0.0`. */
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
/** Peer dependency range for Zod in emitted package.json. */
|
|
37
|
+
readonly zodDependency?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Import specifier for source trail type bindings. Defaults to
|
|
40
|
+
* `appImportPath` when `trailTypeExports` is provided.
|
|
41
|
+
*/
|
|
42
|
+
readonly typeImportPath?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Optional mapping from rendered trail id to the source module export that
|
|
45
|
+
* owns that trail's TypeScript type. Unmapped trails intentionally keep
|
|
46
|
+
* `unknown` public signatures instead of pretending topo artifacts preserve
|
|
47
|
+
* erased source types.
|
|
48
|
+
*/
|
|
49
|
+
readonly trailTypeExports?: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A single emitted file: project-relative path and full contents. */
|
|
53
|
+
export interface CompiledFile {
|
|
54
|
+
readonly path: string;
|
|
55
|
+
readonly content: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The result of compiling a topo into a generated library package. */
|
|
59
|
+
export interface CompileResult {
|
|
60
|
+
readonly packageName: string;
|
|
61
|
+
/** The resolved rendering the files were emitted from. */
|
|
62
|
+
readonly rendering: LibraryRenderingPlan;
|
|
63
|
+
/** The emitted files, in stable path order. */
|
|
64
|
+
readonly files: readonly CompiledFile[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pascalCase = (value: string): string =>
|
|
68
|
+
value
|
|
69
|
+
.split(/[.\-_]/u)
|
|
70
|
+
.filter((word) => word.length > 0)
|
|
71
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
72
|
+
.join('');
|
|
73
|
+
|
|
74
|
+
const isStateless = (entry: LibraryExport): boolean =>
|
|
75
|
+
entry.resources.length === 0;
|
|
76
|
+
|
|
77
|
+
const statelessExports = (
|
|
78
|
+
rendering: LibraryRenderingPlan
|
|
79
|
+
): readonly LibraryExport[] => rendering.exports.filter(isStateless);
|
|
80
|
+
|
|
81
|
+
const resourceExports = (
|
|
82
|
+
rendering: LibraryRenderingPlan
|
|
83
|
+
): readonly LibraryExport[] =>
|
|
84
|
+
rendering.exports.filter((entry) => !isStateless(entry));
|
|
85
|
+
|
|
86
|
+
const factoryName = (rendering: LibraryRenderingPlan): string =>
|
|
87
|
+
`create${pascalCase(rendering.app)}`;
|
|
88
|
+
|
|
89
|
+
const DEFAULT_LIBRARY_DEPENDENCY = `^${packageJson.version}`;
|
|
90
|
+
const DEFAULT_ZOD_DEPENDENCY = '^4.3.5';
|
|
91
|
+
|
|
92
|
+
const sanitizeJsDocLine = (value: string): string =>
|
|
93
|
+
value.replaceAll('*/', '* /').trim();
|
|
94
|
+
|
|
95
|
+
const jsDoc = (lines: readonly string[], indent = ''): string =>
|
|
96
|
+
[
|
|
97
|
+
`${indent}/**`,
|
|
98
|
+
...lines
|
|
99
|
+
.map(sanitizeJsDocLine)
|
|
100
|
+
.filter((line) => line.length > 0)
|
|
101
|
+
.map((line) => `${indent} * ${line}`),
|
|
102
|
+
`${indent} */`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
|
|
105
|
+
const exportDescription = (entry: LibraryExport): string =>
|
|
106
|
+
entry.description ?? `Call the \`${entry.trailId}\` trail.`;
|
|
107
|
+
|
|
108
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
|
|
109
|
+
|
|
110
|
+
interface ExportTypeBinding {
|
|
111
|
+
readonly input: string;
|
|
112
|
+
readonly output: string;
|
|
113
|
+
readonly sourceExport: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type TypeBindings = ReadonlyMap<string, ExportTypeBinding>;
|
|
117
|
+
|
|
118
|
+
const typeNamesFor = (
|
|
119
|
+
entry: LibraryExport
|
|
120
|
+
): Pick<ExportTypeBinding, 'input' | 'output'> => {
|
|
121
|
+
const base = pascalCase(entry.exportName);
|
|
122
|
+
return { input: `${base}Input`, output: `${base}Output` };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resolveTypeBindings = (
|
|
126
|
+
rendering: LibraryRenderingPlan,
|
|
127
|
+
options: CompileOptions
|
|
128
|
+
): TypeBindings => {
|
|
129
|
+
const configured = options.trailTypeExports ?? {};
|
|
130
|
+
const bindings = new Map<string, ExportTypeBinding>();
|
|
131
|
+
for (const entry of rendering.exports) {
|
|
132
|
+
const sourceExport = configured[entry.trailId];
|
|
133
|
+
if (!sourceExport) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!IDENTIFIER_PATTERN.test(sourceExport)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`trailTypeExports["${entry.trailId}"] must be an exported identifier`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
bindings.set(entry.trailId, { ...typeNamesFor(entry), sourceExport });
|
|
142
|
+
}
|
|
143
|
+
return bindings;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const inputTypeFor = (entry: LibraryExport, bindings: TypeBindings): string => {
|
|
147
|
+
const input = bindings.get(entry.trailId)?.input;
|
|
148
|
+
if (entry.layerInputs.length === 0) {
|
|
149
|
+
return input ?? 'unknown';
|
|
150
|
+
}
|
|
151
|
+
return input === undefined
|
|
152
|
+
? 'Record<string, unknown>'
|
|
153
|
+
: `${input} & Record<string, unknown>`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const outputTypeFor = (entry: LibraryExport, bindings: TypeBindings): string =>
|
|
157
|
+
bindings.get(entry.trailId)?.output ?? 'unknown';
|
|
158
|
+
|
|
159
|
+
const schemaTypeImport = (
|
|
160
|
+
rendering: LibraryRenderingPlan,
|
|
161
|
+
bindings: TypeBindings
|
|
162
|
+
): string | undefined => {
|
|
163
|
+
const names = rendering.exports.flatMap((entry) => {
|
|
164
|
+
const binding = bindings.get(entry.trailId);
|
|
165
|
+
return binding ? [binding.input, binding.output] : [];
|
|
166
|
+
});
|
|
167
|
+
if (names.length === 0) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return `import type { ${names.join(', ')} } from './schemas.js';`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const generatePackageJson = (options: CompileOptions): string => {
|
|
174
|
+
const manifest = {
|
|
175
|
+
dependencies: {
|
|
176
|
+
'@ontrails/library':
|
|
177
|
+
options.libraryDependency ?? DEFAULT_LIBRARY_DEPENDENCY,
|
|
178
|
+
zod: options.zodDependency ?? DEFAULT_ZOD_DEPENDENCY,
|
|
179
|
+
},
|
|
180
|
+
exports: {
|
|
181
|
+
'.': './src/index.ts',
|
|
182
|
+
'./package.json': './package.json',
|
|
183
|
+
'./result': './src/result.ts',
|
|
184
|
+
'./schemas': './src/schemas.ts',
|
|
185
|
+
'./trails': './src/trails.ts',
|
|
186
|
+
},
|
|
187
|
+
name: options.packageName,
|
|
188
|
+
type: 'module',
|
|
189
|
+
version: options.version ?? '0.0.0',
|
|
190
|
+
};
|
|
191
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const statelessFunction = (
|
|
195
|
+
entry: LibraryExport,
|
|
196
|
+
bindings: TypeBindings
|
|
197
|
+
): string =>
|
|
198
|
+
[
|
|
199
|
+
jsDoc([
|
|
200
|
+
exportDescription(entry),
|
|
201
|
+
`Renders trail \`${entry.trailId}\` as a stateless library function.`,
|
|
202
|
+
]),
|
|
203
|
+
`export const ${entry.exportName} = (`,
|
|
204
|
+
` input: ${inputTypeFor(entry, bindings)}`,
|
|
205
|
+
`): Promise<${outputTypeFor(entry, bindings)}> =>`,
|
|
206
|
+
' // The runtime validates declared output schemas before this unwrap returns.',
|
|
207
|
+
` rootClient.call.${entry.exportName}(input) as Promise<${outputTypeFor(entry, bindings)}>;`,
|
|
208
|
+
].join('\n');
|
|
209
|
+
|
|
210
|
+
const factoryMethod = (entry: LibraryExport, bindings: TypeBindings): string =>
|
|
211
|
+
[
|
|
212
|
+
jsDoc(
|
|
213
|
+
[
|
|
214
|
+
exportDescription(entry),
|
|
215
|
+
`Renders trail \`${entry.trailId}\` behind the resource client.`,
|
|
216
|
+
],
|
|
217
|
+
' '
|
|
218
|
+
),
|
|
219
|
+
` ${entry.exportName}: (`,
|
|
220
|
+
` input: ${inputTypeFor(entry, bindings)}`,
|
|
221
|
+
` ): Promise<${outputTypeFor(entry, bindings)}> =>`,
|
|
222
|
+
' // The runtime validates declared output schemas before this unwrap returns.',
|
|
223
|
+
` client.call.${entry.exportName}(input) as Promise<${outputTypeFor(entry, bindings)}>,`,
|
|
224
|
+
].join('\n');
|
|
225
|
+
|
|
226
|
+
const generateIndex = (
|
|
227
|
+
rendering: LibraryRenderingPlan,
|
|
228
|
+
bindings: TypeBindings
|
|
229
|
+
): string => {
|
|
230
|
+
const stateless = statelessExports(rendering);
|
|
231
|
+
const resourceful = resourceExports(rendering);
|
|
232
|
+
const typedSchemas = schemaTypeImport(rendering, bindings);
|
|
233
|
+
|
|
234
|
+
const parts: string[] = [
|
|
235
|
+
"import type { SurfaceLibraryOptions } from '@ontrails/library';",
|
|
236
|
+
...(typedSchemas ? [typedSchemas] : []),
|
|
237
|
+
'',
|
|
238
|
+
"import { createClient, rootClient } from './client.js';",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const entry of stateless) {
|
|
242
|
+
parts.push('', statelessFunction(entry, bindings));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (resourceful.length > 0) {
|
|
246
|
+
parts.push(
|
|
247
|
+
'',
|
|
248
|
+
`export const ${factoryName(rendering)} = async (`,
|
|
249
|
+
' options: SurfaceLibraryOptions = {}',
|
|
250
|
+
') => {',
|
|
251
|
+
' const client = await createClient(options);',
|
|
252
|
+
' return {',
|
|
253
|
+
resourceful.map((entry) => factoryMethod(entry, bindings)).join('\n'),
|
|
254
|
+
' };',
|
|
255
|
+
'};'
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return `${parts.join('\n')}\n`;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const generateClient = (): string =>
|
|
263
|
+
[
|
|
264
|
+
"import { surface } from '@ontrails/library';",
|
|
265
|
+
"import type { SurfaceLibraryOptions } from '@ontrails/library';",
|
|
266
|
+
'',
|
|
267
|
+
`import { app } from './trails.js';`,
|
|
268
|
+
'',
|
|
269
|
+
'export const rootClient = await surface(app);',
|
|
270
|
+
'export const createClient = (options: SurfaceLibraryOptions = {}) =>',
|
|
271
|
+
' surface(app, options);',
|
|
272
|
+
'',
|
|
273
|
+
].join('\n');
|
|
274
|
+
|
|
275
|
+
const generateSchemas = (
|
|
276
|
+
rendering: LibraryRenderingPlan,
|
|
277
|
+
options: CompileOptions,
|
|
278
|
+
bindings: TypeBindings
|
|
279
|
+
): string => {
|
|
280
|
+
const appExport = 'app';
|
|
281
|
+
const typedEntries = rendering.exports.filter((entry) =>
|
|
282
|
+
bindings.has(entry.trailId)
|
|
283
|
+
);
|
|
284
|
+
const sourceTypeExports = [
|
|
285
|
+
...new Set(
|
|
286
|
+
typedEntries.map((entry) => {
|
|
287
|
+
const binding = bindings.get(entry.trailId);
|
|
288
|
+
if (binding === undefined) {
|
|
289
|
+
throw new Error(`missing type binding for trail "${entry.trailId}"`);
|
|
290
|
+
}
|
|
291
|
+
return binding.sourceExport;
|
|
292
|
+
})
|
|
293
|
+
),
|
|
294
|
+
].toSorted((left, right) => left.localeCompare(right));
|
|
295
|
+
const lines = [
|
|
296
|
+
`import { ${appExport} } from './trails.js';`,
|
|
297
|
+
"import { deriveLibraryApi } from '@ontrails/library';",
|
|
298
|
+
...(typedEntries.length > 0
|
|
299
|
+
? [
|
|
300
|
+
"import type { TrailInput, TrailOutput } from '@ontrails/library';",
|
|
301
|
+
`import type { ${sourceTypeExports.join(', ')} } from '${options.typeImportPath ?? options.appImportPath}';`,
|
|
302
|
+
]
|
|
303
|
+
: []),
|
|
304
|
+
'',
|
|
305
|
+
'// Authored Zod schemas, keyed by export name, rendered from the topo.',
|
|
306
|
+
'const rendering = deriveLibraryApi(app);',
|
|
307
|
+
'const byName = new Map(',
|
|
308
|
+
' rendering.exports.map((entry) => [entry.exportName, entry])',
|
|
309
|
+
');',
|
|
310
|
+
'',
|
|
311
|
+
'const requireExport = (name: string) => {',
|
|
312
|
+
' const entry = byName.get(name);',
|
|
313
|
+
' if (!entry) {',
|
|
314
|
+
" throw new Error('missing rendered library export: ' + name);",
|
|
315
|
+
' }',
|
|
316
|
+
' return entry;',
|
|
317
|
+
'};',
|
|
318
|
+
'',
|
|
319
|
+
];
|
|
320
|
+
for (const entry of rendering.exports) {
|
|
321
|
+
const binding = bindings.get(entry.trailId);
|
|
322
|
+
if (binding) {
|
|
323
|
+
lines.push(
|
|
324
|
+
'',
|
|
325
|
+
`export type ${binding.input} = TrailInput<typeof ${binding.sourceExport}>;`,
|
|
326
|
+
`export type ${binding.output} = TrailOutput<typeof ${binding.sourceExport}>;`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
lines.push(
|
|
330
|
+
'',
|
|
331
|
+
jsDoc([
|
|
332
|
+
`Authored input schema for \`${entry.trailId}\`, exported as \`${entry.exportName}\`.`,
|
|
333
|
+
]),
|
|
334
|
+
`export const ${entry.exportName}InputSchema = requireExport('${entry.exportName}').input;`,
|
|
335
|
+
'',
|
|
336
|
+
jsDoc([
|
|
337
|
+
`Authored output schema for \`${entry.trailId}\`, if the trail declares one.`,
|
|
338
|
+
]),
|
|
339
|
+
`export const ${entry.exportName}OutputSchema = requireExport('${entry.exportName}').output;`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
lines.push('', 'export const schemas = {');
|
|
343
|
+
for (const entry of rendering.exports) {
|
|
344
|
+
lines.push(
|
|
345
|
+
` ${entry.exportName}: {`,
|
|
346
|
+
` input: ${entry.exportName}InputSchema,`,
|
|
347
|
+
` output: ${entry.exportName}OutputSchema,`,
|
|
348
|
+
' },'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
lines.push('} as const;');
|
|
352
|
+
return `${lines.join('\n')}\n`;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const generateTrails = (options: CompileOptions): string => {
|
|
356
|
+
const appExport = options.appExportName ?? 'app';
|
|
357
|
+
return [
|
|
358
|
+
'// Full Trails-native entrypoint: the resolved topo for composition,',
|
|
359
|
+
'// contract tests, and graph inspection.',
|
|
360
|
+
`export { ${appExport} as app } from '${options.appImportPath}';`,
|
|
361
|
+
'',
|
|
362
|
+
].join('\n');
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const resultStatelessFunction = (
|
|
366
|
+
entry: LibraryExport,
|
|
367
|
+
bindings: TypeBindings
|
|
368
|
+
): string =>
|
|
369
|
+
[
|
|
370
|
+
jsDoc([
|
|
371
|
+
exportDescription(entry),
|
|
372
|
+
`Returns the raw Result boundary for trail \`${entry.trailId}\`.`,
|
|
373
|
+
]),
|
|
374
|
+
`export const ${entry.exportName} = (`,
|
|
375
|
+
` input: ${inputTypeFor(entry, bindings)}`,
|
|
376
|
+
`): Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>> =>`,
|
|
377
|
+
' // The runtime validates declared output schemas before this Result resolves.',
|
|
378
|
+
` resultClient.result.${entry.exportName}(input) as Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>>;`,
|
|
379
|
+
].join('\n');
|
|
380
|
+
|
|
381
|
+
const resultFactoryMethod = (
|
|
382
|
+
entry: LibraryExport,
|
|
383
|
+
bindings: TypeBindings
|
|
384
|
+
): string =>
|
|
385
|
+
[
|
|
386
|
+
jsDoc(
|
|
387
|
+
[
|
|
388
|
+
exportDescription(entry),
|
|
389
|
+
`Returns the raw Result boundary for trail \`${entry.trailId}\`.`,
|
|
390
|
+
],
|
|
391
|
+
' '
|
|
392
|
+
),
|
|
393
|
+
` ${entry.exportName}: (`,
|
|
394
|
+
` input: ${inputTypeFor(entry, bindings)}`,
|
|
395
|
+
` ): Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>> =>`,
|
|
396
|
+
' // The runtime validates declared output schemas before this Result resolves.',
|
|
397
|
+
` client.result.${entry.exportName}(input) as Promise<Result<${outputTypeFor(entry, bindings)}, LibraryError>>,`,
|
|
398
|
+
].join('\n');
|
|
399
|
+
|
|
400
|
+
const generateResult = (
|
|
401
|
+
rendering: LibraryRenderingPlan,
|
|
402
|
+
bindings: TypeBindings
|
|
403
|
+
): string => {
|
|
404
|
+
const stateless = statelessExports(rendering);
|
|
405
|
+
const resourceful = resourceExports(rendering);
|
|
406
|
+
const typedSchemas = schemaTypeImport(rendering, bindings);
|
|
407
|
+
const parts = [
|
|
408
|
+
'// No-throw API: returns the Result envelope instead of unwrapping.',
|
|
409
|
+
"import { runLibraryResult } from '@ontrails/library';",
|
|
410
|
+
"import type { LibraryError, Result, SurfaceLibraryOptions } from '@ontrails/library';",
|
|
411
|
+
...(typedSchemas ? [typedSchemas] : []),
|
|
412
|
+
'',
|
|
413
|
+
"import { createClient, rootClient } from './client.js';",
|
|
414
|
+
"import { app } from './trails.js';",
|
|
415
|
+
'',
|
|
416
|
+
'const resultClient = rootClient;',
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
for (const entry of stateless) {
|
|
420
|
+
parts.push('', resultStatelessFunction(entry, bindings));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (resourceful.length > 0) {
|
|
424
|
+
parts.push(
|
|
425
|
+
'',
|
|
426
|
+
`export const ${factoryName(rendering)} = async (`,
|
|
427
|
+
' options: SurfaceLibraryOptions = {}',
|
|
428
|
+
') => {',
|
|
429
|
+
' const client = await createClient(options);',
|
|
430
|
+
' return {',
|
|
431
|
+
resourceful
|
|
432
|
+
.map((entry) => resultFactoryMethod(entry, bindings))
|
|
433
|
+
.join('\n'),
|
|
434
|
+
' };',
|
|
435
|
+
'};'
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
parts.push(
|
|
440
|
+
'',
|
|
441
|
+
'export const call = (',
|
|
442
|
+
' id: string,',
|
|
443
|
+
' input: unknown,',
|
|
444
|
+
' options: SurfaceLibraryOptions = {}',
|
|
445
|
+
') => runLibraryResult(app, id, input, options);',
|
|
446
|
+
''
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
return `${parts.join('\n')}\n`;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const generateTsconfig = (): string =>
|
|
453
|
+
`${JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
compilerOptions: {
|
|
456
|
+
module: 'preserve',
|
|
457
|
+
moduleResolution: 'bundler',
|
|
458
|
+
strict: true,
|
|
459
|
+
target: 'esnext',
|
|
460
|
+
},
|
|
461
|
+
include: ['src'],
|
|
462
|
+
},
|
|
463
|
+
null,
|
|
464
|
+
2
|
|
465
|
+
)}\n`;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Compile a topo into a generated library package. Returns the emitted file
|
|
469
|
+
* plan; the resolved rendering is included for inspection and governance.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* const result = compile(app, {
|
|
473
|
+
* packageName: '@acme/core',
|
|
474
|
+
* appImportPath: '@acme/app',
|
|
475
|
+
* });
|
|
476
|
+
* for (const file of result.files) {
|
|
477
|
+
* console.log(file.path);
|
|
478
|
+
* }
|
|
479
|
+
*/
|
|
480
|
+
export const compile = (
|
|
481
|
+
graph: Topo,
|
|
482
|
+
options: CompileOptions
|
|
483
|
+
): CompileResult => {
|
|
484
|
+
const rendering = deriveLibraryApi(graph, options);
|
|
485
|
+
const typeBindings = resolveTypeBindings(rendering, options);
|
|
486
|
+
const files: CompiledFile[] = [
|
|
487
|
+
{ content: generatePackageJson(options), path: 'package.json' },
|
|
488
|
+
{ content: generateTsconfig(), path: 'tsconfig.json' },
|
|
489
|
+
{
|
|
490
|
+
content: generateClient(),
|
|
491
|
+
path: 'src/client.ts',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
content: generateIndex(rendering, typeBindings),
|
|
495
|
+
path: 'src/index.ts',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
content: generateResult(rendering, typeBindings),
|
|
499
|
+
path: 'src/result.ts',
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
content: generateSchemas(rendering, options, typeBindings),
|
|
503
|
+
path: 'src/schemas.ts',
|
|
504
|
+
},
|
|
505
|
+
{ content: generateTrails(options), path: 'src/trails.ts' },
|
|
506
|
+
];
|
|
507
|
+
return { files, packageName: options.packageName, rendering };
|
|
508
|
+
};
|