-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtypescript-sdk.dang
More file actions
661 lines (602 loc) · 26.3 KB
/
Copy pathtypescript-sdk.dang
File metadata and controls
661 lines (602 loc) · 26.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
enum Runtime {
NODE
BUN
DENO
}
type TypescriptSdk {
# Matches `"sdk": { "source": "typescript" }` in a legacy dagger.json.
let tsPattern = "\"sdk\"\\s*:\\s*\\{[^}]*\"source\"\\s*:\\s*\"typescript\""
# Matches a `[runtime]` table with `source = "typescript"` in a CLI 1.0
# dagger-module.toml. [^\[]* keeps the match inside the runtime table (it stops
# at the next [section]).
let tsTomlPattern = "\\[runtime\\][^\\[]*source\\s*=\\s*\"typescript\""
"""
Marker filename that skips generate when found at or above a TypeScript SDK module root.
"""
skipGenerateFilename: String! = ".dagger-typescript-sdk-skip-generate"
"""
Runtime source to write into modules created by this SDK.
"""
targetRuntime: String! {
"typescript"
}
"""
Config filenames that mark a Dagger module root: the CLI 1.0
`dagger-module.toml` (workspace-managed modules) and the legacy `dagger.json`.
A managed module is discovered by whichever it uses.
"""
let moduleConfigFilenames: [String!]! = ["dagger-module.toml", "dagger.json"]
"""
Starter used by init when none is named.
"""
let defaultTemplate: String! = "default"
"""
Return every TypeScript SDK module this workspace manages that is visible from
the client's current location.
Discovery is anchored at the client's cwd (never the workspace root): the
nearest enclosing module plus every module at or below the cwd, intersected
with the SDK's engine-owned list of managed modules
(currentModule.asSDK.modules). So running from a subdirectory acts on the
project you're in — and the projects beneath it — not the whole workspace.
Discovery is the polyfill's cwd-aware findConfigDirs (dagger/dagger#13688);
this maps its cwd-relative results to workspace-root-relative paths and keeps
the ones this SDK manages.
A module is found through the directory holding its config, so one whose
`source` field points elsewhere is discovered at its config path and not from
inside its own source tree. Address those by path (see `mod`).
"""
modules(ws: Workspace!): [Mod!]! {
let managed = currentModule.asSDK(workspace: ws).modules.{{path}}
let cwd = clientCwd(ws)
polyfill.workspace(ws)
.findConfigDirs(moduleConfigFilenames, exclude: ["**/node_modules/**"])
.map { dir => moduleRelPath(cwd, dir) }
.uniq
.filter { path => managed.filter { m => m.path == path }.length > 0 }
.map { path => Mod(rootPath: path, ws: ws, skipGenerateFilename: skipGenerateFilename) }
}
"""
The client's current location as a workspace-root-relative path, "." at the
root. The anchor every cwd-scoped operation measures against.
"""
let clientCwd(ws: Workspace!): String! {
let cwd = ws.cwd.trimPrefix("/").trimSuffix("/")
if (cwd == "") { "." } else { cwd }
}
"""
Whether a workspace-root-relative path is in scope from `cwd`: at or below it,
or an ancestor of it — the same cone findConfigDirs walks for modules.
"""
let inCwdScope(cwd: String!, path: String!): Boolean! {
cwd == "." or path == cwd or path.hasPrefix(cwd + "/") or cwd.hasPrefix(path + "/")
}
"""
Resolve a findConfigDirs result — a cwd-relative path, at or below the cwd
("." , "sub/dir") or a strict ancestor (".." , "../..") — against the cwd into a
workspace-root-relative path, the format both asSDK module paths and
Mod.rootPath use.
"""
let moduleRelPath(cwd: String!, dir: String!): String! {
let base = if (cwd == "" or cwd == ".") { [] } else { cwd.split("/") }
let segs = dir.split("/").reduce(base) { acc, seg =>
if (seg == "..") {
acc.dropLast(1)
} else if (seg == "." or seg == "") {
acc
} else {
acc + [seg]
}
}
if (segs.length == 0) { "." } else { segs.join("/") }
}
"""
Return the TypeScript SDK module at or above a workspace path.
Resolution is rejected when the module it lands on is owned by another SDK, so
a wrong `path` fails loudly instead of running TypeScript codegen over someone
else's module.
"""
mod(
ws: Workspace!,
"""
Workspace-relative path to resolve: any path inside the module with findUp, the module root itself without it.
"""
path: String! = ".",
"""
Walk up from `path` to the nearest enclosing module config. Turn off to address a module root directly.
"""
findUp: Boolean! = true,
): Mod! {
let modPath = if (findUp) {
# Nearest enclosing module config, regardless of filename order: the deepest
# hit wins, so a closer dagger.json is not shadowed by an ancestor
# dagger-module.toml (and vice versa) — matching polyfill findConfigDirs.
let foundConfigPath = moduleConfigFilenames.reduce(null) { acc, name =>
let found = ws.findUp(name, path)
if (configHitDepth(found) > configHitDepth(acc)) { found } else { acc }
}
if (foundConfigPath == null) {
raise "no Dagger module found containing path: " + path
} else {
let configPath = foundConfigPath.trimPrefix("/")
if (isTypescriptConfig(ws, configPath) == false) {
raise "Dagger module does not use the TypeScript SDK: " + path
} else {
let dir = configPath.split("/").dropLast(1).join("/")
if (dir == "") { "." } else { dir }
}
}
} else {
path.trimPrefix("/")
}
Mod(
rootPath: modPath,
ws: ws,
skipGenerateFilename: skipGenerateFilename,
)
}
"""
Depth of the directory holding a find-up config hit (a workspace-absolute path).
A deeper hit is nearer the search origin; a null (no hit) ranks below any hit,
so ranking by this picks the nearest enclosing config regardless of which
filename found it.
"""
let configHitDepth(hit: String): Int! {
if (hit == null) {
-1
} else {
let dir = hit.split("/").dropLast(1).join("/").trimPrefix("/")
if (dir == "") { 0 } else { dir.split("/").length }
}
}
"""
Whether the module config at `configPath` (workspace-root-relative) declares the
TypeScript runtime — `"sdk": { "source": "typescript" }` in a dagger.json, or a
`[runtime]` table with `source = "typescript"` in a dagger-module.toml. The
filename picks which pattern to match.
"""
let isTypescriptConfig(ws: Workspace!, configPath: String!): Boolean! {
let pattern = if (configPath.trimSuffix("dagger-module.toml") != configPath) { tsTomlPattern } else { tsPattern }
ws
.directory("/", include: [configPath])
.file(configPath)
.search(
pattern: pattern,
multiline: true,
dotall: true,
limit: 1,
)
.{{id}}
.length > 0
}
"""
Initialize TypeScript-owned files for a new Dagger module.
The engine resolves the destination `path` and owns the module's config; this
function only returns the SDK-owned files to layer onto `path` — the rendered
template plus runtime-specific config. Files already at `path` are merged
into, not overwritten, so an existing package.json / tsconfig.json / deno.json
keeps its scripts, path aliases, and unstable flags.
"""
initModule(
ws: Workspace!,
"""
Module name. Rendered into the template as the module's class name.
"""
name: String!,
"""
Workspace-relative directory to create the module in.
"""
path: String!,
"""
Starter to materialize from templates/<template>: `default` for a small working module, `empty` for a bare @object class.
"""
template: String! = defaultTemplate,
"""
TypeScript runtime the module runs on. The engine picks it back up from the config files this writes.
"""
runtime: Runtime! = Runtime.NODE,
"""
packageManager pin for package.json, as `name@version` or just `name`. Empty writes no field. NODE only; Bun and Deno bundle their own.
"""
packageManager: String! = "",
"""
Base container image the module builds on. Empty keeps the SDK default. Written to deno.json for DENO, package.json otherwise.
"""
baseImage: String! = "",
): Changeset! {
let rawPath = path.trimPrefix("./").trimPrefix("/")
let modPath = if (rawPath == "" or rawPath == ".") {
"."
} else if (rawPath == ".." or rawPath.trimPrefix("../") != rawPath) {
raise "path escapes workspace: " + rawPath
} else {
rawPath.trimSuffix("/")
}
let fork = polyfill.workspace(ws).fork
# An empty name means "the default", not the templates/ directory itself —
# which exists, so it would pass the check below and render every starter as
# a subdirectory of the new module.
let starter = if (template == "") { defaultTemplate } else { template }
if (currentModule.source.exists("templates/" + starter) == false) {
raise "unknown init template: " + starter
} else if (packageManager != "" and runtime != Runtime.NODE) {
raise "packageManager is only supported with --runtime NODE; Bun and Deno bundle their own"
} else {
let includePrefix = if (modPath == ".") { "" } else { modPath + "/" }
let existing = ws.directory("/", include: [
includePrefix + "package.json",
includePrefix + "tsconfig.json",
includePrefix + "deno.json",
includePrefix + "bun.lock",
])
let renderedSource = renderedTemplate(name, starter, runtime, existing, modPath)
let templateSource = configuredTemplate(renderedSource, runtime, packageManager, baseImage)
fork.withDirectory(modPath, templateSource).changes
}
}
"""
Register a typed TypeScript client for `module` at `path`.
The engine resolves `module`, records the client (generator + directory) in
workspace config, and then calls generateClient to materialize the files. The
SDK contributes nothing to the registration itself, so this returns an empty
Changeset.
"""
initClient(
ws: Workspace!,
"""
Workspace-relative directory to generate the client package into.
"""
path: String!,
"""
Module the client binds to: a workspace-relative path or a canonical module ref. A client serves exactly one module.
"""
module: String!,
"""
Bind the local development client instead of a pinned release.
"""
dev: Boolean! = false,
): Changeset! {
polyfill.workspace(ws).fork.changes
}
"""
Return init templates tracked by this module.
Templates live under templates/<name> and are materialized into the new
module. Init picks one by name, or the module default when none is named.
"""
templates: [Template!]! {
if (currentModule.source.exists("templates")) {
let root = currentModule.source.directory("templates")
root.entries.map { name =>
Template(
name: name.trimSuffix("/"),
source: root.directory(name),
)
}
} else {
directory.entries.map { name =>
Template(
name: name,
source: directory,
)
}
}
}
"""
Render the templates/<template> starter with the requested module name and runtime.
Seeds index.ts from the template, then layers in runtime-specific config
files via the config-updator helper. The config-updator reads existing user
config files (rooted at modPath in `existing`) so any user customizations are
preserved; only Dagger-required keys are added or refreshed.
"""
let renderedTemplate(name: String!, template: String!, runtime: Runtime!, existing: Directory!, modPath: String!): Directory! {
let existingPrefix = if (modPath == ".") { "/existing/" } else { "/existing/" + modPath + "/" }
let wsPrefix = if (modPath == ".") { "" } else { modPath + "/" }
let builder = container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helpers/render-template", currentModule.source.directory("helpers/render-template"))
.withDirectory("/helpers/config-updator", currentModule.source.directory("helpers/config-updator"))
.withDirectory("/template", currentModule.source.directory("templates/" + template))
.withDirectory("/existing", existing)
.withWorkdir("/helpers/render-template")
.withExec(["go", "build", "-o", "/usr/local/bin/render-template", "."])
.withWorkdir("/helpers/config-updator")
.withExec(["go", "build", "-o", "/usr/local/bin/config-updator", "."])
.withExec(["render-template", name, "/template", "/rendered"])
let withConfig = if (runtime == Runtime.NODE) {
builder
.withExec(["config-updator", "package-json", existingPrefix + "package.json", "/rendered/package.json"])
.withExec(["config-updator", "tsconfig", existingPrefix + "tsconfig.json", "/rendered/tsconfig.json"])
} else if (runtime == Runtime.BUN) {
# The Dagger TypeScript runtime picks bun over node by spotting bun.lock,
# so we emit an empty one for a fresh init. If the workspace already has
# one we leave it alone (overlay semantics via init's withDirectory).
let nodeConfig = builder
.withExec(["config-updator", "package-json", existingPrefix + "package.json", "/rendered/package.json"])
.withExec(["config-updator", "tsconfig", existingPrefix + "tsconfig.json", "/rendered/tsconfig.json"])
if (existing.exists(wsPrefix + "bun.lock")) {
nodeConfig
} else {
nodeConfig.withExec(["touch", "/rendered/bun.lock"])
}
} else {
builder
.withExec(["config-updator", "deno-config", existingPrefix + "deno.json", "/rendered/deno.json"])
}
withConfig.directory("/rendered")
}
"""
Apply non-default `packageManager` / `baseImage` flags to a rendered template.
When both flags are empty the source is returned unchanged (no helper run,
no reformatting). Otherwise the module-config helper edits the rendered
config files in place: `packageManager` always writes to package.json,
`baseImage` writes to deno.json for the DENO runtime and to package.json
otherwise — matching where ModConfig later reads from.
"""
let configuredTemplate(source: Directory!, runtime: Runtime!, packageManager: String!, baseImage: String!): Directory! {
if (packageManager == "" and baseImage == "") {
source
} else {
let baseImageFileName = if (runtime == Runtime.DENO) { "deno.json" } else { "package.json" }
# Only edit config files the template actually ships. The module-config
# helper treats a missing file as "{}" and would otherwise materialize a
# brand-new package.json/deno.json that the template intentionally omitted.
if (packageManager != "" and source.exists("package.json") == false) {
raise "cannot configure --package-manager: template has no package.json"
} else if (baseImage != "" and source.exists(baseImageFileName) == false) {
raise "cannot configure --base-image: template has no " + baseImageFileName
} else {
let built = moduleConfigBuilder.withDirectory("/rendered", source)
let withPm = if (packageManager == "") {
built
} else {
built.withExec(["module-config", "set-package-manager", "/rendered/package.json", packageManager])
}
let withImg = if (baseImage == "") {
withPm
} else {
withPm.withExec(["module-config", "set-base-image", "/rendered/" + baseImageFileName, baseImage])
}
withImg.directory("/rendered")
}
}
}
"""
Container with the module-config helper compiled and on PATH at
/usr/local/bin/module-config.
Shared by init's template configuration (configuredTemplate) and ModConfig's
per-file edits (ModConfig.tool) so the Go build recipe lives in one place
rather than being duplicated across both call sites.
"""
let moduleConfigBuilder: Container! {
container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/module-config"))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/module-config", "."])
}
"""
Container with the standalone TypeScript client generator compiled and on
PATH at /usr/local/bin/codegen. Engine-free: it turns an introspection schema
+ client meta JSON into dagger.gen.ts and one <module>.gen.ts per module.
"""
let codegenBuilder: Container! {
container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/codegen"))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/codegen", "."])
}
"""
Container with the config-updator helper compiled and on PATH at
/usr/local/bin/config-updator.
"""
let configUpdatorBuilder: Container! {
container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/config-updator"))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/config-updator", "."])
}
"""
Generate the raw client bindings (dagger.gen.ts plus one <module>.gen.ts per
module) from an introspection schema.
This is the engine-free core of client generation, run in the codegen
container. `schemaJSON` is the client-facing introspection schema contents.
`module` is a JSON object describing the single module the client is bound to
({kind, path, ref, pin}). It drives the generated serveBoundModule bootstrap:
a GIT_SOURCE module serves from its canonical ref+pin (resolves anywhere); a
LOCAL_SOURCE/DIR_SOURCE module resolves its workspace-root-relative path
against the workspace (dag.currentWorkspace().moduleSource(path)). An unknown
kind fails codegen closed.
"""
let generateClientBindings(schemaJSON: String!, moduleName: String!, engineVersion: String!, module: String! = "{}"): Directory! {
let metaJson = "{\"moduleName\":" + JSON.encode(moduleName) + ",\"engineVersion\":" + JSON.encode(engineVersion) + ",\"module\":" + module + "}"
codegenBuilder
.withNewFile("/schema.json", schemaJSON)
.withNewFile("/meta.json", metaJson)
.withExec([
"codegen",
"--introspection-json-path", "/schema.json",
"--client-meta-path", "/meta.json",
"--output", "/out",
])
.directory("/out")
}
"""
Layer the scoped-package config files (package.json, tsconfig.json) onto a set
of generated Node/Bun client bindings.
`existing` supplies the client dir's current config so user customizations are
preserved; pass an empty directory for a brand-new client. `package.json` gets
type=module, an SDK-owned @dagger.io/dagger pinned to `engineVersion`, a
typescript pin, and a scoped name derived from `moduleName` when unnamed.
"""
let configureClientNode(bindings: Directory!, existing: Directory!, engineVersion: String!, moduleName: String!): Directory! {
configUpdatorBuilder
.withDirectory("/out", bindings)
.withDirectory("/existing", existing)
.withExec(["config-updator", "client-package-json", "/existing/package.json", "/out/package.json", engineVersion, moduleName])
.withExec(["config-updator", "client-tsconfig", "/existing/tsconfig.json", "/out/tsconfig.json"])
.directory("/out")
}
"""
Serialize the bound module the generated client serves. A client binds exactly
one module, so serveBoundModule serves that one module: a local module by its
workspace-root-relative `path`, a git module by its canonical `ref` + `pin`.
`path` is used for LOCAL_SOURCE/DIR_SOURCE; `ref`/`pin` for GIT_SOURCE; codegen
picks by `kind`. Shape: {kind, path, ref, pin}.
"""
let boundModuleJSON(kindJSON: String!, path: String!, ref: String!, pin: String!): String! {
"{\"kind\":" + kindJSON +
",\"path\":" + JSON.encode(path) +
",\"ref\":" + JSON.encode(ref) +
",\"pin\":" + JSON.encode(pin) + "}"
}
"""
Build the complete Node/Bun client directory (bindings + scoped package.json +
tsconfig) from the bound module's client-facing schema and provenance.
Takes the pieces (not a live ModuleSource) so callers can supply them either
from a resolved handle or from a projected list item. `existing` supplies the
client dir's current config so user edits are preserved; pass an empty
directory for a fresh client. `kindJSON` is the already-JSON-encoded source
kind (e.g. "\"GIT_SOURCE\""). `path` is the module's workspace-root-relative
path (local kinds); `ref`/`pin` the canonical git ref + pin (GIT_SOURCE).
"""
let clientDirectory(schemaJSON: String!, moduleName: String!, engineVersion: String!, kindJSON: String!, path: String!, ref: String!, pin: String!, existing: Directory!): Directory! {
let bindings = generateClientBindings(
schemaJSON,
moduleName,
engineVersion,
boundModuleJSON(kindJSON, path, ref, pin)
)
configureClientNode(bindings, existing, engineVersion, moduleName)
}
"""
Read the client dir's current config files (package.json, tsconfig.json,
deno.json) so config-updator preserves user customizations on regeneration —
in particular a @dagger.io/dagger dependency the user has pointed at a local
bundle. Filters from the workspace root so a not-yet-created client dir simply
yields an empty directory.
"""
let existingClientConfig(ws: Workspace!, path: String!): Directory! {
let filtered = ws.directory("/", include: [
path + "/package.json",
path + "/tsconfig.json",
path + "/deno.json",
])
if (filtered.exists(path)) {
filtered.directory(path)
} else {
directory
}
}
"""
Generate a typed client for `module` and stage it at workspace-relative `path`.
The client analogue of `mod(ws, path).generate(ws)`: it resolves the bound
module in the workspace, generates a self-contained scoped npm package from
its client-facing schema — dagger.gen.ts for the core types, one
<module>.gen.ts per module in the closure, package.json and tsconfig.json —
and returns the staged changes. Config files already at `path` are merged
into, so a @dagger.io/dagger the user pointed at a local bundle survives.
"""
generateClient(
ws: Workspace!,
"""
Module the client binds to: a workspace-relative path or a canonical module ref.
"""
module: String!,
"""
Workspace-relative directory to generate the client package into.
"""
path: String!,
): Changeset! {
let pws = polyfill.workspace(ws)
let modSrc = pws.moduleSource(module).core
pws.fork.withDirectory(path, clientDirectory(
modSrc.clientSchemaIntrospectionJSON.contents,
modSrc.moduleOriginalName,
modSrc.engineVersion,
JSON.encode(modSrc.kind),
# Local: the workspace-relative module path the caller resolved against.
# Git: unused (ref/pin drive the serve).
module,
modSrc.asString,
modSrc.pin,
existingClientConfig(ws, path)
)).changes
}
"""
Generate every managed TypeScript SDK module visible from the client's current
location. Discovery goes through modules(ws), so running from a subdirectory
generates only the project you're in and the projects beneath it.
Modules with the generate skip marker are skipped.
"""
generateAllModule(ws: Workspace!): Changeset! @generate {
let pws = polyfill.workspace(ws)
let changes = modules(ws)
.filter { mod => mod.skipGenerate(ws) == false }
.map { mod =>
# Stage this module's local dependency closure first (leaf-first, possibly
# across SDKs) so its codegen sees up-to-date dependency bindings. The dep
# codegen is ephemeral: taking the changeset against the staged workspace
# cancels it out, leaving only each module's own changes.
let stagedWs = ws.withChanges(pws.moduleSource("/" + mod.rootPath).core.generateLocalDependencies(ws))
polyfill.workspace(stagedWs).moduleSource("/" + mod.rootPath).generate.changes
}
# Force the per-module codegen to evaluate concurrently: selecting a field on
# the whole list resolves every element in one pass, where folding them into
# one changeset would walk them one at a time.
changes.{{isEmpty}}
# Fold onto pws.fork.changes (empty), never ws.changes: under the engine's
# nested ModuleSource.generateLocalDependencies the incoming ws already has a
# dependency closure staged, and re-including it would be re-rooted under the
# dependent and octopus-merged against the same files it just generated.
pws.fork.changes.withChangesets(changes)
}
"""
Regenerate every client this SDK manages that is visible from the client's
current location.
Iterates the workspace-registered clients (currentModule.asSDK.clients),
resolves each bound module through the engine field
CurrentModuleAsSDKClient.moduleSource (honoring pin/remote refs), generates
the client from that module's ModuleSource.clientSchemaIntrospectionJSON, and
stages the files at the client path.
Scoped by cwd like generateAllModule: a returned changeset may only carry
paths under the caller's location, so regenerating from a subdirectory — or
nested under the engine's per-dependency generation, where the cwd is the
dependency — skips clients that live elsewhere in the workspace.
"""
generateAllClient(ws: Workspace!): Changeset! @generate {
let pws = polyfill.workspace(ws)
let cwd = clientCwd(ws)
currentModule.asSDK(ws).clients
.{{path, module, moduleSource.{{ clientSchemaIntrospectionJSON.{{ contents }}, moduleOriginalName, engineVersion, kind, pin, asString }} }}
.filter { client => inCwdScope(cwd, client.path) }
.reduce(pws.fork) { fork, client =>
let m = client.moduleSource
fork.withDirectory(client.path, clientDirectory(
m.clientSchemaIntrospectionJSON.contents,
m.moduleOriginalName,
m.engineVersion,
JSON.encode(m.kind),
# Local: the config-recorded workspace-relative module path.
# Git: unused (ref/pin drive the serve).
client.module,
m.asString,
m.pin,
existingClientConfig(ws, client.path)
))
}
.changes
}
}