@supatype/cli 0.3.2 → 0.3.3
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +163 -161
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +77 -0
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +70 -15
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +3 -3
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/seed.d.ts +27 -1
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +206 -33
- package/dist/commands/seed.js.map +1 -1
- package/dist/engine-client.d.ts +1 -0
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +47 -2
- package/dist/engine-client.js.map +1 -1
- package/dist/engine-floor.d.ts +27 -4
- package/dist/engine-floor.d.ts.map +1 -1
- package/dist/engine-floor.js +90 -9
- package/dist/engine-floor.js.map +1 -1
- package/dist/gitignore.d.ts.map +1 -1
- package/dist/gitignore.js +7 -0
- package/dist/gitignore.js.map +1 -1
- package/dist/hooks-generator.d.ts.map +1 -1
- package/dist/hooks-generator.js +18 -3
- package/dist/hooks-generator.js.map +1 -1
- package/dist/seed-connection.d.ts +60 -0
- package/dist/seed-connection.d.ts.map +1 -0
- package/dist/seed-connection.js +90 -0
- package/dist/seed-connection.js.map +1 -0
- package/dist/seed-ir.d.ts +66 -0
- package/dist/seed-ir.d.ts.map +1 -0
- package/dist/seed-ir.js +265 -0
- package/dist/seed-ir.js.map +1 -0
- package/dist/seed-runner.d.ts +44 -0
- package/dist/seed-runner.d.ts.map +1 -0
- package/dist/seed-runner.js +83 -0
- package/dist/seed-runner.js.map +1 -0
- package/dist/seed.d.ts +345 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +138 -0
- package/dist/seed.js.map +1 -1
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +14 -8
- package/dist/self-host-compose.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +73 -0
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-generation.d.ts +29 -0
- package/dist/type-generation.d.ts.map +1 -1
- package/dist/type-generation.js +52 -0
- package/dist/type-generation.js.map +1 -1
- package/package.json +2 -2
- package/src/augmentation-generator.ts +73 -14
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/generate.ts +7 -3
- package/src/commands/init.ts +31 -21
- package/src/commands/seed.ts +294 -44
- package/src/engine-client.ts +52 -3
- package/src/engine-floor.ts +100 -9
- package/src/gitignore.ts +7 -0
- package/src/hooks-generator.ts +21 -6
- package/src/seed-connection.ts +146 -0
- package/src/seed-ir.ts +311 -0
- package/src/seed-runner.ts +109 -0
- package/src/seed.ts +408 -0
- package/src/self-host-compose.ts +14 -8
- package/src/type-extractor.ts +101 -0
- package/src/type-generation.ts +64 -0
- package/tests/access-composition.test.ts +123 -0
- package/tests/augmentation-generator.test.ts +149 -9
- package/tests/engine-floor.test.ts +93 -0
- package/tests/fixtures/seed_golden_ir.json +186 -0
- package/tests/runtime-contract.test.ts +31 -6
- package/tests/seed-connection.test.ts +158 -0
- package/tests/seed-ir.test.ts +436 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/seed-ir.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect a seed's builder calls into an IR document.
|
|
3
|
+
*
|
|
4
|
+
* The only genuinely new file in the CLI half of this: everything else is rewiring. What it
|
|
5
|
+
* does is narrow. It records calls in the order they were made, encodes each value into the
|
|
6
|
+
* shape the engine reads, and hands back a document. It resolves nothing, orders nothing,
|
|
7
|
+
* validates nothing and connects to nothing, because the engine does all four and doing any
|
|
8
|
+
* of them twice is how the two ends come to disagree.
|
|
9
|
+
*
|
|
10
|
+
* **Labels are assigned lazily.** A `create` becomes a referenceable operation only when
|
|
11
|
+
* something actually reads a property off what it returned. Assigning one to every operation
|
|
12
|
+
* would put an `id` in the IR for rows nobody points at, which is noise in a document people
|
|
13
|
+
* read and diff.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
DefaultExpr,
|
|
18
|
+
ModelApi,
|
|
19
|
+
ModelManifest,
|
|
20
|
+
NumericValue,
|
|
21
|
+
Operand,
|
|
22
|
+
Predicate,
|
|
23
|
+
RefValue,
|
|
24
|
+
SeedCondition,
|
|
25
|
+
SeedConditionInput,
|
|
26
|
+
SeedData,
|
|
27
|
+
SeedDb,
|
|
28
|
+
SeedIr,
|
|
29
|
+
SeedOperation,
|
|
30
|
+
SeedValue,
|
|
31
|
+
} from "./seed.js"
|
|
32
|
+
|
|
33
|
+
/** A model's own name and the facts the encoder needs about its columns. */
|
|
34
|
+
export type Manifest = Readonly<Record<string, ModelManifest>>
|
|
35
|
+
|
|
36
|
+
/** What a collector hands back once the seed has run. */
|
|
37
|
+
export interface CollectedIr {
|
|
38
|
+
ir: SeedIr
|
|
39
|
+
/** The document as the engine will read it, which is what the ledger hashes. */
|
|
40
|
+
json: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One operation, still mutable while the seed is running. */
|
|
44
|
+
interface Pending {
|
|
45
|
+
operation: SeedOperation
|
|
46
|
+
/** Set the first time something reads a property off this operation's result. */
|
|
47
|
+
label?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SeedCollector {
|
|
51
|
+
private readonly manifest: Manifest
|
|
52
|
+
private readonly byModel: Map<string, ModelManifest>
|
|
53
|
+
/** The block currently being collected into: the document, or a `$if` body. */
|
|
54
|
+
private readonly stack: SeedOperation[][] = []
|
|
55
|
+
private readonly root: SeedOperation[] = []
|
|
56
|
+
private labels = 0
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly fingerprint: string,
|
|
60
|
+
manifest: Manifest,
|
|
61
|
+
) {
|
|
62
|
+
this.manifest = manifest
|
|
63
|
+
this.byModel = new Map(Object.values(manifest).map((entry) => [entry.model, entry]))
|
|
64
|
+
this.stack.push(this.root)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The `db` a seed's default export is handed. */
|
|
68
|
+
db(): SeedDb {
|
|
69
|
+
const api: Record<string, unknown> = {}
|
|
70
|
+
for (const [accessor, entry] of Object.entries(this.manifest)) {
|
|
71
|
+
api[accessor] = this.modelApi(entry)
|
|
72
|
+
}
|
|
73
|
+
api["$if"] = (condition: SeedConditionInput, body: () => void): void => {
|
|
74
|
+
this.group(condition, body)
|
|
75
|
+
}
|
|
76
|
+
api["$sql"] = (text: string, params?: readonly SeedValue[]): void => {
|
|
77
|
+
// Always present, even when empty: the engine defaults it either way, and a document
|
|
78
|
+
// whose shape changes with its contents is harder to read and to diff.
|
|
79
|
+
this.push({ op: "sql", text, params: params === undefined ? [] : [...params] })
|
|
80
|
+
}
|
|
81
|
+
return api as SeedDb
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The document, with every label that was actually used written in. */
|
|
85
|
+
finish(): CollectedIr {
|
|
86
|
+
const ir: SeedIr = {
|
|
87
|
+
irVersion: 1,
|
|
88
|
+
schemaFingerprint: this.fingerprint,
|
|
89
|
+
operations: this.root,
|
|
90
|
+
}
|
|
91
|
+
return { ir, json: JSON.stringify(ir, null, 2) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Operations ---
|
|
95
|
+
|
|
96
|
+
private modelApi(entry: ModelManifest): ModelApi<SeedData, SeedData, Record<string, RefValue>> {
|
|
97
|
+
return {
|
|
98
|
+
create: (data, options) => {
|
|
99
|
+
const pending: Pending = {
|
|
100
|
+
operation: { op: "create", model: entry.model, data: this.encodeRow(entry, data) },
|
|
101
|
+
}
|
|
102
|
+
if (options?.id !== undefined) this.name(pending, options.id)
|
|
103
|
+
this.push(pending.operation)
|
|
104
|
+
return this.reference(pending)
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
createMany: (rows) => {
|
|
108
|
+
this.push({
|
|
109
|
+
op: "createMany",
|
|
110
|
+
model: entry.model,
|
|
111
|
+
rows: rows.map((row) => this.encodeRow(entry, row)),
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
upsert: ({ where, data, id }) => {
|
|
116
|
+
const pending: Pending = {
|
|
117
|
+
operation: {
|
|
118
|
+
op: "upsert",
|
|
119
|
+
model: entry.model,
|
|
120
|
+
where: this.encodeRow(entry, where),
|
|
121
|
+
data: this.encodeRow(entry, data),
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
if (id !== undefined) this.name(pending, id)
|
|
125
|
+
this.push(pending.operation)
|
|
126
|
+
return this.reference(pending)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private group(condition: SeedConditionInput, body: () => void): void {
|
|
132
|
+
const operations: SeedOperation[] = []
|
|
133
|
+
this.stack.push(operations)
|
|
134
|
+
try {
|
|
135
|
+
body()
|
|
136
|
+
} finally {
|
|
137
|
+
// Popped in a `finally` so a seed that throws inside a `$if` does not leave every
|
|
138
|
+
// later operation collecting into a block that is never emitted.
|
|
139
|
+
this.stack.pop()
|
|
140
|
+
}
|
|
141
|
+
this.push({ op: "group", if: encodeCondition(condition), operations })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private push(operation: SeedOperation): void {
|
|
145
|
+
const block = this.stack[this.stack.length - 1]
|
|
146
|
+
if (block === undefined) throw new Error("the seed collector lost its block")
|
|
147
|
+
block.push(operation)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- References ---
|
|
151
|
+
|
|
152
|
+
private name(pending: Pending, label?: string): string {
|
|
153
|
+
if (pending.label !== undefined) return pending.label
|
|
154
|
+
this.labels += 1
|
|
155
|
+
pending.label = label ?? `op${this.labels}`
|
|
156
|
+
const operation = pending.operation
|
|
157
|
+
if (operation.op === "create" || operation.op === "upsert") {
|
|
158
|
+
operation.id = pending.label
|
|
159
|
+
}
|
|
160
|
+
return pending.label
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* What an operation hands back.
|
|
165
|
+
*
|
|
166
|
+
* A proxy rather than a plain object, because the columns a seed may reference are the
|
|
167
|
+
* model's columns and building one property per column per row would cost more than the
|
|
168
|
+
* seed itself. Reading a property is also what tells the collector this operation needs a
|
|
169
|
+
* label at all.
|
|
170
|
+
*/
|
|
171
|
+
private reference(pending: Pending): Record<string, RefValue> {
|
|
172
|
+
return new Proxy({} as Record<string, RefValue>, {
|
|
173
|
+
get: (_target, property): RefValue | undefined => {
|
|
174
|
+
// A proxy gets asked about `then` when it is awaited or resolved, and about
|
|
175
|
+
// symbols by anything that inspects it. Answering with a reference would make a
|
|
176
|
+
// seed that awaits a builder call hang on a thenable that never settles.
|
|
177
|
+
if (typeof property !== "string" || property === "then") return undefined
|
|
178
|
+
return { $ref: `${this.name(pending)}.${property}` }
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Values ---
|
|
184
|
+
|
|
185
|
+
private encodeRow(entry: ModelManifest, row: SeedData): SeedData {
|
|
186
|
+
const out: SeedData = {}
|
|
187
|
+
for (const [key, value] of Object.entries(row)) {
|
|
188
|
+
if (value === undefined) continue
|
|
189
|
+
out[key] = this.encode(entry, key, value)
|
|
190
|
+
}
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private encode(entry: ModelManifest, key: string, value: SeedValue): SeedValue {
|
|
195
|
+
if (isPassThrough(value)) return value
|
|
196
|
+
|
|
197
|
+
const relation = entry.relations[key]
|
|
198
|
+
if (relation !== undefined) {
|
|
199
|
+
const nested = this.nested(relation.target, value)
|
|
200
|
+
if (nested !== undefined) return nested
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (value instanceof Date) return value.toISOString()
|
|
204
|
+
if (typeof value === "bigint") return (value as bigint).toString()
|
|
205
|
+
|
|
206
|
+
// The column's kind decides the encoding, and only the manifest knows it: a decimal
|
|
207
|
+
// sent as a JSON number round-trips through a double and stops being the value the
|
|
208
|
+
// column holds.
|
|
209
|
+
if (typeof value === "number" && entry.numeric.includes(key)) {
|
|
210
|
+
return { $numeric: String(value) } satisfies NumericValue
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === "string" && entry.numeric.includes(key)) {
|
|
213
|
+
return { $numeric: value } satisfies NumericValue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(value)) return value.map((item) => this.encodeLoose(item))
|
|
217
|
+
return value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A relation's input, with its rows encoded against the model they will be written to.
|
|
222
|
+
*
|
|
223
|
+
* Returns `undefined` when the value is not one of the relation shapes, so an ordinary
|
|
224
|
+
* value on a field that happens to share a relation's name is left alone.
|
|
225
|
+
*/
|
|
226
|
+
private nested(target: string, value: SeedValue): SeedValue | undefined {
|
|
227
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined
|
|
228
|
+
const shape = value as Record<string, unknown>
|
|
229
|
+
const entry = this.byModel.get(target)
|
|
230
|
+
|
|
231
|
+
if ("connect" in shape) {
|
|
232
|
+
const where = shape["connect"]
|
|
233
|
+
if (entry === undefined || where === null || typeof where !== "object") return undefined
|
|
234
|
+
return { connect: this.encodeRow(entry, where as SeedData) }
|
|
235
|
+
}
|
|
236
|
+
if ("create" in shape) {
|
|
237
|
+
const rows = toRows(shape["create"])
|
|
238
|
+
if (entry === undefined || rows === undefined) return undefined
|
|
239
|
+
return { create: rows.map((row) => this.encodeRow(entry, row)) }
|
|
240
|
+
}
|
|
241
|
+
if ("createMany" in shape) {
|
|
242
|
+
const rows = toRows(shape["createMany"])
|
|
243
|
+
if (entry === undefined || rows === undefined) return undefined
|
|
244
|
+
return { createMany: rows.map((row) => this.encodeRow(entry, row)) }
|
|
245
|
+
}
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A value with no column to consult, such as an element of an array. */
|
|
250
|
+
private encodeLoose(value: SeedValue): SeedValue {
|
|
251
|
+
if (isPassThrough(value)) return value
|
|
252
|
+
if (value instanceof Date) return value.toISOString()
|
|
253
|
+
if (typeof value === "bigint") return (value as bigint).toString()
|
|
254
|
+
if (Array.isArray(value)) return value.map((item) => this.encodeLoose(item))
|
|
255
|
+
return value
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A `hasOne` nests one row and a `hasMany` nests several; the IR carries a list either way.
|
|
261
|
+
*
|
|
262
|
+
* Normalised here rather than in the generated builder, so six more generators do not each
|
|
263
|
+
* have to remember it.
|
|
264
|
+
*/
|
|
265
|
+
function toRows(value: unknown): SeedData[] | undefined {
|
|
266
|
+
if (Array.isArray(value)) return value as SeedData[]
|
|
267
|
+
if (value !== null && typeof value === "object") return [value as SeedData]
|
|
268
|
+
return undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Values that are already in the shape the engine reads.
|
|
273
|
+
*
|
|
274
|
+
* `expr.now()` is literally `{ kind: "now" }` and a reference is literally `{ $ref: ... }`,
|
|
275
|
+
* so encoding them again would wrap an operand in a decimal or walk into a proxy.
|
|
276
|
+
*/
|
|
277
|
+
function isPassThrough(value: SeedValue): boolean {
|
|
278
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
279
|
+
const shape = value as Record<string, unknown>
|
|
280
|
+
return (
|
|
281
|
+
"$ref" in shape ||
|
|
282
|
+
"$numeric" in shape ||
|
|
283
|
+
"$default" in shape ||
|
|
284
|
+
typeof shape["kind"] === "string"
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The surface a seed writes, in the shape the IR carries. */
|
|
289
|
+
export function encodeCondition(condition: SeedConditionInput): SeedCondition {
|
|
290
|
+
if ("environment" in condition) {
|
|
291
|
+
return { kind: "environment", name: condition.environment }
|
|
292
|
+
}
|
|
293
|
+
if ("exists" in condition) {
|
|
294
|
+
return scoped("exists", condition.exists)
|
|
295
|
+
}
|
|
296
|
+
return scoped("notExists", condition.notExists)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function scoped(
|
|
300
|
+
kind: "exists" | "notExists",
|
|
301
|
+
target: { model: string; where?: Predicate },
|
|
302
|
+
): SeedCondition {
|
|
303
|
+
return {
|
|
304
|
+
kind,
|
|
305
|
+
model: target.model,
|
|
306
|
+
...(target.where !== undefined ? { where: target.where } : {}),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Re-exported for the runner, which has no reason to know these came from `seed.ts`. */
|
|
311
|
+
export type { DefaultExpr, Operand, RefValue, SeedIr, SeedOperation, SeedValue }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run one seed module and take the document it described.
|
|
3
|
+
*
|
|
4
|
+
* In a child process, through tsx, which is how this CLI already evaluates a project's
|
|
5
|
+
* TypeScript. Two reasons beyond consistency: a seed is the user's code and should not share
|
|
6
|
+
* a process with the tool running it, and the same route works for the standalone binary,
|
|
7
|
+
* which interprets TypeScript itself rather than resolving tsx from a `node_modules` it does
|
|
8
|
+
* not have.
|
|
9
|
+
*
|
|
10
|
+
* The document comes back through a file rather than stdout, because a seed is allowed to
|
|
11
|
+
* print. Mixing its output into the channel carrying the document would mean a `log()` call
|
|
12
|
+
* could corrupt the IR.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
|
|
16
|
+
import { tmpdir } from "node:os"
|
|
17
|
+
import { join } from "node:path"
|
|
18
|
+
import { pathToFileURL } from "node:url"
|
|
19
|
+
import { evalTsSnippet } from "./tsx-runner.js"
|
|
20
|
+
import type { SeedConfig, SeedIr } from "./seed.js"
|
|
21
|
+
|
|
22
|
+
/** What a seed module turned out to be. */
|
|
23
|
+
export type Collected =
|
|
24
|
+
| {
|
|
25
|
+
shape: "collected"
|
|
26
|
+
ir: SeedIr
|
|
27
|
+
config: SeedConfig
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
/**
|
|
31
|
+
* The old contract: no default export, so importing it did the work.
|
|
32
|
+
*
|
|
33
|
+
* Every existing project is this shape and keeps working. Nothing here removes it.
|
|
34
|
+
*/
|
|
35
|
+
shape: "legacy"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CollectOptions {
|
|
39
|
+
cwd: string
|
|
40
|
+
/** The generated builder, for the manifest and the schema fingerprint. */
|
|
41
|
+
builderPath: string
|
|
42
|
+
environment?: string | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class SeedCollectionFailed extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
readonly file: string,
|
|
48
|
+
readonly output: string,
|
|
49
|
+
) {
|
|
50
|
+
super(`${file} failed while it was being read:\n${output}`)
|
|
51
|
+
this.name = "SeedCollectionFailed"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Import `file`, run its default export against a collector, and return what it described. */
|
|
56
|
+
export function collectSeed(file: string, options: CollectOptions): { result: Collected; stdout: string } {
|
|
57
|
+
const scratch = mkdtempSync(join(tmpdir(), "supatype-seed-"))
|
|
58
|
+
const outPath = join(scratch, "ir.json")
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const run = evalTsSnippet(snippet(file, outPath, options), { cwd: options.cwd })
|
|
62
|
+
if (run.exitCode !== 0 || !existsSync(outPath)) {
|
|
63
|
+
throw new SeedCollectionFailed(file, [run.stdout, run.stderr].filter(Boolean).join("\n").trim())
|
|
64
|
+
}
|
|
65
|
+
const result = JSON.parse(readFileSync(outPath, "utf8")) as Collected
|
|
66
|
+
return { result, stdout: run.stdout }
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The program that runs in the child.
|
|
74
|
+
*
|
|
75
|
+
* Every path is interpolated as a JSON string rather than pasted in, because a Windows path
|
|
76
|
+
* is full of backslashes and a project directory is allowed to contain a quote.
|
|
77
|
+
*/
|
|
78
|
+
function snippet(file: string, outPath: string, options: CollectOptions): string {
|
|
79
|
+
const url = (path: string): string => JSON.stringify(pathToFileURL(path).href)
|
|
80
|
+
return `
|
|
81
|
+
import { writeFileSync } from "node:fs"
|
|
82
|
+
import { SeedCollector } from "./seed-ir.js"
|
|
83
|
+
import { expr } from "./seed.js"
|
|
84
|
+
|
|
85
|
+
const builder = await import(${url(options.builderPath)})
|
|
86
|
+
const module = await import(${url(file)})
|
|
87
|
+
|
|
88
|
+
if (typeof module.default !== "function") {
|
|
89
|
+
// Importing it is what ran it, which is the old contract working exactly as it did.
|
|
90
|
+
writeFileSync(${JSON.stringify(outPath)}, JSON.stringify({ shape: "legacy" }))
|
|
91
|
+
} else {
|
|
92
|
+
const collector = new SeedCollector(builder.schemaFingerprint, builder.seedModels)
|
|
93
|
+
await module.default({
|
|
94
|
+
db: collector.db(),
|
|
95
|
+
expr,
|
|
96
|
+
log: (message) => { console.log(message) },
|
|
97
|
+
environment: ${options.environment === undefined ? "undefined" : JSON.stringify(options.environment)},
|
|
98
|
+
})
|
|
99
|
+
writeFileSync(
|
|
100
|
+
${JSON.stringify(outPath)},
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
shape: "collected",
|
|
103
|
+
ir: collector.finish().ir,
|
|
104
|
+
config: module.config ?? {},
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
`
|
|
109
|
+
}
|