@highstate/cli 0.9.16 → 0.9.18
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/dist/chunk-CMECLVT7.js +11 -0
- package/dist/chunk-CMECLVT7.js.map +1 -0
- package/dist/highstate.manifest.json +1 -1
- package/dist/{library-loader-CGEPTS4L.js → library-loader-ZABUULFB.js} +28 -23
- package/dist/library-loader-ZABUULFB.js.map +1 -0
- package/dist/main.js +189 -88
- package/dist/main.js.map +1 -1
- package/package.json +7 -10
- package/src/commands/build.ts +1 -0
- package/src/shared/library-loader.ts +30 -14
- package/src/shared/schema-transformer.spec.ts +280 -0
- package/src/shared/schema-transformer.ts +225 -14
- package/src/shared/source-hash-calculator.ts +1 -1
- package/src/shared/utils.ts +6 -0
- package/dist/library-loader-CGEPTS4L.js.map +0 -1
|
@@ -206,4 +206,284 @@ const spec = {
|
|
|
206
206
|
)
|
|
207
207
|
expect(result).toContain("It also has multiple lines.`")
|
|
208
208
|
})
|
|
209
|
+
|
|
210
|
+
it("should add .meta() to z.object fields with JSDoc comments", async () => {
|
|
211
|
+
const input = `
|
|
212
|
+
const userSchema = z.object({
|
|
213
|
+
/**
|
|
214
|
+
* The user's unique identifier.
|
|
215
|
+
*/
|
|
216
|
+
id: z.string(),
|
|
217
|
+
/**
|
|
218
|
+
* The user's email address.
|
|
219
|
+
*/
|
|
220
|
+
email: z.string().email(),
|
|
221
|
+
name: z.string(), // no comment, should not be transformed
|
|
222
|
+
})`
|
|
223
|
+
|
|
224
|
+
const result = await applySchemaTransformations(input)
|
|
225
|
+
|
|
226
|
+
// Should add import at the top
|
|
227
|
+
expect(result).toContain(
|
|
228
|
+
'import { camelCaseToHumanReadable as __camelCaseToHumanReadable } from "@highstate/contract"',
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// Should add .meta() with both title and description
|
|
232
|
+
expect(result).toContain(
|
|
233
|
+
'id: z.string().meta({ title: __camelCaseToHumanReadable("id"), description: `The user\'s unique identifier.` })',
|
|
234
|
+
)
|
|
235
|
+
expect(result).toContain(
|
|
236
|
+
'email: z.string().email().meta({ title: __camelCaseToHumanReadable("email"), description: `The user\'s email address.` })',
|
|
237
|
+
)
|
|
238
|
+
expect(result).toContain("name: z.string(), // no comment") // unchanged
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("should not add .meta() if already present in z.object fields", async () => {
|
|
242
|
+
const input = `
|
|
243
|
+
const userSchema = z.object({
|
|
244
|
+
/**
|
|
245
|
+
* The user's identifier.
|
|
246
|
+
*/
|
|
247
|
+
id: z.string().meta({ displayName: "ID" }),
|
|
248
|
+
})`
|
|
249
|
+
|
|
250
|
+
const result = await applySchemaTransformations(input)
|
|
251
|
+
|
|
252
|
+
// Should not modify the field that already has .meta()
|
|
253
|
+
expect(result).toContain('id: z.string().meta({ displayName: "ID" })')
|
|
254
|
+
expect(result).not.toContain("description:")
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("should handle nested z.object patterns", async () => {
|
|
258
|
+
const input = `
|
|
259
|
+
const schema = z.object({
|
|
260
|
+
user: z.object({
|
|
261
|
+
/**
|
|
262
|
+
* The user's name.
|
|
263
|
+
*/
|
|
264
|
+
name: z.string(),
|
|
265
|
+
profile: z.object({
|
|
266
|
+
/**
|
|
267
|
+
* The user's age.
|
|
268
|
+
*/
|
|
269
|
+
age: z.number(),
|
|
270
|
+
}),
|
|
271
|
+
}),
|
|
272
|
+
})`
|
|
273
|
+
|
|
274
|
+
const result = await applySchemaTransformations(input)
|
|
275
|
+
|
|
276
|
+
expect(result).toContain(
|
|
277
|
+
'name: z.string().meta({ title: __camelCaseToHumanReadable("name"), description: `The user\'s name.` })',
|
|
278
|
+
)
|
|
279
|
+
expect(result).toContain(
|
|
280
|
+
'age: z.number().meta({ title: __camelCaseToHumanReadable("age"), description: `The user\'s age.` })',
|
|
281
|
+
)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it("should handle multiple JSDoc-commented fields in the same z.object without conflicts", async () => {
|
|
285
|
+
const input = `
|
|
286
|
+
const networkSchema = z.discriminatedUnion("type", [
|
|
287
|
+
z.object({
|
|
288
|
+
type: z.literal("dhcp"),
|
|
289
|
+
}),
|
|
290
|
+
z.object({
|
|
291
|
+
type: z.literal("static"),
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The IPv4 address to assign to the virtual machine.
|
|
295
|
+
*/
|
|
296
|
+
address: z.string().optional(),
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The CIDR prefix for the IPv4 address.
|
|
300
|
+
*
|
|
301
|
+
* By default, this is set to 24.
|
|
302
|
+
*/
|
|
303
|
+
prefix: z.number().default(24),
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The IPv4 gateway for the virtual machine.
|
|
307
|
+
*
|
|
308
|
+
* If not specified, will be set to the first address in the subnet.
|
|
309
|
+
*/
|
|
310
|
+
gateway: z.string().optional(),
|
|
311
|
+
}),
|
|
312
|
+
]).default({ type: "dhcp" })`
|
|
313
|
+
|
|
314
|
+
const result = await applySchemaTransformations(input)
|
|
315
|
+
|
|
316
|
+
expect(result).toContain(
|
|
317
|
+
'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
|
|
318
|
+
)
|
|
319
|
+
expect(result).toContain(
|
|
320
|
+
'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.',
|
|
321
|
+
)
|
|
322
|
+
expect(result).toContain("By default, this is set to 24.` })")
|
|
323
|
+
expect(result).toContain(
|
|
324
|
+
'gateway: z.string().optional().meta({ title: __camelCaseToHumanReadable("gateway"), description: `The IPv4 gateway for the virtual machine.',
|
|
325
|
+
)
|
|
326
|
+
expect(result).toContain(
|
|
327
|
+
"If not specified, will be set to the first address in the subnet.` })",
|
|
328
|
+
)
|
|
329
|
+
expect(result).toContain('type: z.literal("static"),') // unchanged, no comment
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("should handle z.object fields inside z.discriminatedUnion", async () => {
|
|
333
|
+
const input = `
|
|
334
|
+
const schema = z.discriminatedUnion("type", [
|
|
335
|
+
z.object({
|
|
336
|
+
type: z.literal("dhcp"),
|
|
337
|
+
}),
|
|
338
|
+
z.object({
|
|
339
|
+
type: z.literal("static"),
|
|
340
|
+
/**
|
|
341
|
+
* The IPv4 address to assign to the virtual machine.
|
|
342
|
+
*/
|
|
343
|
+
address: z.string().optional(),
|
|
344
|
+
/**
|
|
345
|
+
* The CIDR prefix for the IPv4 address.
|
|
346
|
+
*/
|
|
347
|
+
prefix: z.number().default(24),
|
|
348
|
+
}),
|
|
349
|
+
]).default({ type: "dhcp" })`
|
|
350
|
+
|
|
351
|
+
const result = await applySchemaTransformations(input)
|
|
352
|
+
|
|
353
|
+
expect(result).toContain(
|
|
354
|
+
'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
|
|
355
|
+
)
|
|
356
|
+
expect(result).toContain(
|
|
357
|
+
'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.` })',
|
|
358
|
+
)
|
|
359
|
+
expect(result).toContain('type: z.literal("dhcp"),') // unchanged, no comment
|
|
360
|
+
expect(result).toContain('type: z.literal("static"),') // unchanged, no comment
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it("should handle the full defineUnit case with discriminated union and mixed transformations", async () => {
|
|
364
|
+
const input = `
|
|
365
|
+
var virtualMachine = defineUnit({
|
|
366
|
+
type: "proxmox.virtual-machine",
|
|
367
|
+
args: {
|
|
368
|
+
nodeName: z.string().optional(),
|
|
369
|
+
cpuType: z.string().default("host"),
|
|
370
|
+
cores: z.number().default(1),
|
|
371
|
+
sockets: z.number().default(1),
|
|
372
|
+
memory: z.number().default(512),
|
|
373
|
+
/**
|
|
374
|
+
* The IPv4 address configuration for the virtual machine.
|
|
375
|
+
*/
|
|
376
|
+
ipv4: {
|
|
377
|
+
schema: z.discriminatedUnion("type", [
|
|
378
|
+
z.object({
|
|
379
|
+
type: z.literal("dhcp")
|
|
380
|
+
}),
|
|
381
|
+
z.object({
|
|
382
|
+
type: z.literal("static"),
|
|
383
|
+
/**
|
|
384
|
+
* The IPv4 address to assign to the virtual machine.
|
|
385
|
+
*/
|
|
386
|
+
address: z.string().optional(),
|
|
387
|
+
/**
|
|
388
|
+
* The CIDR prefix for the IPv4 address.
|
|
389
|
+
*
|
|
390
|
+
* By default, this is set to 24.
|
|
391
|
+
*/
|
|
392
|
+
prefix: z.number().default(24),
|
|
393
|
+
/**
|
|
394
|
+
* The IPv4 gateway for the virtual machine.
|
|
395
|
+
*
|
|
396
|
+
* If not specified, will be set to the first address in the subnet.
|
|
397
|
+
*/
|
|
398
|
+
gateway: z.string().optional()
|
|
399
|
+
})
|
|
400
|
+
]).default({ type: "dhcp" })
|
|
401
|
+
},
|
|
402
|
+
dns: z.string().array().optional(),
|
|
403
|
+
datastoreId: z.string().optional(),
|
|
404
|
+
diskSize: z.number().default(8),
|
|
405
|
+
bridge: z.string().default("vmbr0"),
|
|
406
|
+
sshPort: z.number().default(22),
|
|
407
|
+
sshUser: z.string().default("root"),
|
|
408
|
+
waitForAgent: z.boolean().default(true),
|
|
409
|
+
vendorData: z.string().optional()
|
|
410
|
+
},
|
|
411
|
+
secrets: {
|
|
412
|
+
sshPassword: z.string().optional()
|
|
413
|
+
},
|
|
414
|
+
inputs: {
|
|
415
|
+
proxmoxCluster: clusterEntity,
|
|
416
|
+
image: imageEntity,
|
|
417
|
+
sshKeyPair: {
|
|
418
|
+
entity: keyPairEntity,
|
|
419
|
+
required: false
|
|
420
|
+
},
|
|
421
|
+
/**
|
|
422
|
+
* The cloud-init vendor data to use for the virtual machine.
|
|
423
|
+
*
|
|
424
|
+
* You can provide a cloud-config from the distribution component.
|
|
425
|
+
*/
|
|
426
|
+
vendorData: {
|
|
427
|
+
entity: fileEntity,
|
|
428
|
+
required: false,
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
outputs: serverOutputs,
|
|
432
|
+
meta: {
|
|
433
|
+
title: "Proxmox Virtual Machine",
|
|
434
|
+
description: "The virtual machine on a Proxmox cluster.",
|
|
435
|
+
category: "Proxmox",
|
|
436
|
+
icon: "simple-icons:proxmox",
|
|
437
|
+
iconColor: "#e56901",
|
|
438
|
+
secondaryIcon: "codicon:vm"
|
|
439
|
+
},
|
|
440
|
+
source: {
|
|
441
|
+
package: "@highstate/proxmox",
|
|
442
|
+
path: "virtual-machine"
|
|
443
|
+
}
|
|
444
|
+
});`
|
|
445
|
+
|
|
446
|
+
const result = await applySchemaTransformations(input)
|
|
447
|
+
|
|
448
|
+
// Should transform args.ipv4 to entity/schema structure (existing transformer behavior)
|
|
449
|
+
expect(result).toContain("ipv4: {")
|
|
450
|
+
expect(result).toContain("schema: z.discriminatedUnion")
|
|
451
|
+
expect(result).toContain("meta: {")
|
|
452
|
+
expect(result).toContain(
|
|
453
|
+
"description: `The IPv4 address configuration for the virtual machine.`",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
// Should add .meta() to z.object fields with JSDoc comments inside the discriminated union
|
|
457
|
+
expect(result).toContain(
|
|
458
|
+
'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
|
|
459
|
+
)
|
|
460
|
+
expect(result).toContain(
|
|
461
|
+
'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.',
|
|
462
|
+
)
|
|
463
|
+
expect(result).toContain("By default, this is set to 24.` })")
|
|
464
|
+
expect(result).toContain(
|
|
465
|
+
'gateway: z.string().optional().meta({ title: __camelCaseToHumanReadable("gateway"), description: `The IPv4 gateway for the virtual machine.',
|
|
466
|
+
)
|
|
467
|
+
expect(result).toContain(
|
|
468
|
+
"If not specified, will be set to the first address in the subnet.` })",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
// Should NOT transform fields without JSDoc comments
|
|
472
|
+
expect(result).toContain("nodeName: z.string().optional(),") // unchanged
|
|
473
|
+
expect(result).toContain('cpuType: z.string().default("host"),') // unchanged
|
|
474
|
+
expect(result).toContain("cores: z.number().default(1),") // unchanged
|
|
475
|
+
expect(result).toContain("dns: z.string().array().optional(),") // unchanged
|
|
476
|
+
expect(result).toContain('type: z.literal("dhcp")') // unchanged
|
|
477
|
+
expect(result).toContain('type: z.literal("static"),') // unchanged
|
|
478
|
+
|
|
479
|
+
// Should properly handle inputs.vendorData with existing meta (no change expected since it already has meta)
|
|
480
|
+
expect(result).toContain("vendorData: {")
|
|
481
|
+
expect(result).toContain("entity: fileEntity,")
|
|
482
|
+
expect(result).toContain("required: false,")
|
|
483
|
+
expect(result).toContain("You can provide a cloud-config from the distribution component.")
|
|
484
|
+
|
|
485
|
+
// Should NOT transform other inputs without JSDoc
|
|
486
|
+
expect(result).toContain("proxmoxCluster: clusterEntity,") // unchanged
|
|
487
|
+
expect(result).toContain("image: imageEntity,") // unchanged
|
|
488
|
+
})
|
|
209
489
|
})
|
|
@@ -18,15 +18,45 @@ export const schemaTransformerPlugin: Plugin = {
|
|
|
18
18
|
},
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
type Transformation = {
|
|
22
|
+
start: number
|
|
23
|
+
end: number
|
|
24
|
+
newValue: string
|
|
25
|
+
type: "zod-meta" | "schema-structure" | "import"
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export async function applySchemaTransformations(content: string): Promise<string> {
|
|
22
|
-
const magicString = new MagicString(content)
|
|
23
29
|
const { program, comments } = await parseAsync("file.ts", content)
|
|
24
30
|
|
|
31
|
+
const transformations: Transformation[] = []
|
|
25
32
|
const parentStack: Node[] = []
|
|
26
33
|
|
|
27
34
|
walk(program, {
|
|
28
35
|
enter(node) {
|
|
29
|
-
parentStack.push(node)
|
|
36
|
+
parentStack.push(node) // Handle z.object() patterns
|
|
37
|
+
if (
|
|
38
|
+
node.type === "Property" &&
|
|
39
|
+
node.key.type === "Identifier" &&
|
|
40
|
+
isInsideZodObject(parentStack)
|
|
41
|
+
) {
|
|
42
|
+
const jsdoc = comments.find(comment => isLeadingComment(content, node, comment))
|
|
43
|
+
if (jsdoc) {
|
|
44
|
+
const description = cleanJsdoc(jsdoc.value)
|
|
45
|
+
const fieldName = node.key.name
|
|
46
|
+
const originalValue = content.substring(node.value.start, node.value.end)
|
|
47
|
+
|
|
48
|
+
// Check if the field already has .meta() call
|
|
49
|
+
if (!originalValue.includes(".meta(")) {
|
|
50
|
+
transformations.push({
|
|
51
|
+
start: node.value.start,
|
|
52
|
+
end: node.value.end,
|
|
53
|
+
newValue: `${originalValue}.meta({ title: __camelCaseToHumanReadable("${fieldName}"), description: \`${description}\` })`,
|
|
54
|
+
type: "zod-meta",
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return
|
|
59
|
+
}
|
|
30
60
|
|
|
31
61
|
if (node.type !== "Property" || node.key.type !== "Identifier") {
|
|
32
62
|
return
|
|
@@ -53,27 +83,132 @@ export async function applySchemaTransformations(content: string): Promise<strin
|
|
|
53
83
|
if (isAlreadyStructured) {
|
|
54
84
|
// For already structured values, inject description directly into the object
|
|
55
85
|
const modifiedValue = injectDescriptionIntoObject(originalValue, description)
|
|
56
|
-
|
|
86
|
+
transformations.push({
|
|
87
|
+
start: node.value.start,
|
|
88
|
+
end: node.value.end,
|
|
89
|
+
newValue: modifiedValue,
|
|
90
|
+
type: "schema-structure",
|
|
91
|
+
})
|
|
57
92
|
} else {
|
|
58
93
|
// Transform to new structure
|
|
59
|
-
|
|
60
|
-
node.value.start,
|
|
61
|
-
node.value.end,
|
|
62
|
-
`{
|
|
94
|
+
transformations.push({
|
|
95
|
+
start: node.value.start,
|
|
96
|
+
end: node.value.end,
|
|
97
|
+
newValue: `{
|
|
63
98
|
${entityField}: ${originalValue},
|
|
64
99
|
meta: {
|
|
65
100
|
description: \`${description}\`,
|
|
66
101
|
},
|
|
67
102
|
}`,
|
|
68
|
-
|
|
103
|
+
type: "schema-structure",
|
|
104
|
+
})
|
|
69
105
|
}
|
|
70
106
|
},
|
|
71
107
|
leave() {
|
|
72
108
|
parentStack.pop()
|
|
73
109
|
},
|
|
110
|
+
}) // Handle overlapping transformations by merging them
|
|
111
|
+
const zodMetaTransformations = transformations.filter(t => t.type === "zod-meta")
|
|
112
|
+
const schemaTransformations = transformations.filter(t => t.type === "schema-structure")
|
|
113
|
+
|
|
114
|
+
// For each schema transformation, check if it contains zod-meta transformations
|
|
115
|
+
const processedSchemaTransformations = schemaTransformations.map(schemaTransform => {
|
|
116
|
+
const containedZodMetas = zodMetaTransformations.filter(zodTransform => {
|
|
117
|
+
return schemaTransform.start <= zodTransform.start && schemaTransform.end >= zodTransform.end
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (containedZodMetas.length > 0) {
|
|
121
|
+
// Apply the zod-meta transformations to the original content first
|
|
122
|
+
const originalContent = content.substring(schemaTransform.start, schemaTransform.end)
|
|
123
|
+
const tempMagicString = new MagicString(originalContent)
|
|
124
|
+
|
|
125
|
+
// Adjust positions relative to the schema transformation start
|
|
126
|
+
containedZodMetas
|
|
127
|
+
.sort((a, b) => b.start - a.start)
|
|
128
|
+
.forEach(zodTransform => {
|
|
129
|
+
const relativeStart = zodTransform.start - schemaTransform.start
|
|
130
|
+
const relativeEnd = zodTransform.end - schemaTransform.start
|
|
131
|
+
tempMagicString.update(relativeStart, relativeEnd, zodTransform.newValue)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const modifiedContent = tempMagicString.toString()
|
|
135
|
+
|
|
136
|
+
// Now create the schema transformation with the modified content
|
|
137
|
+
const entityField = schemaTransform.newValue.includes("entity:") ? "entity" : "schema"
|
|
138
|
+
const descriptionMatch = schemaTransform.newValue.match(/description: `([^`]+)`/)
|
|
139
|
+
|
|
140
|
+
if (descriptionMatch) {
|
|
141
|
+
const description = descriptionMatch[1]
|
|
142
|
+
|
|
143
|
+
// Check if the original content is already structured
|
|
144
|
+
const isAlreadyStructured = isStructuredValue(
|
|
145
|
+
content.substring(schemaTransform.start, schemaTransform.end),
|
|
146
|
+
entityField,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if (isAlreadyStructured) {
|
|
150
|
+
// Just inject the description into the existing structure with modified content
|
|
151
|
+
return {
|
|
152
|
+
...schemaTransform,
|
|
153
|
+
newValue: injectDescriptionIntoObject(modifiedContent, description),
|
|
154
|
+
containsZodMeta: true,
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Create new structure with modified content
|
|
158
|
+
return {
|
|
159
|
+
...schemaTransform,
|
|
160
|
+
newValue: `{
|
|
161
|
+
${entityField}: ${modifiedContent},
|
|
162
|
+
meta: {
|
|
163
|
+
description: \`${description}\`,
|
|
164
|
+
},
|
|
165
|
+
}`,
|
|
166
|
+
containsZodMeta: true,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { ...schemaTransform, containsZodMeta: false }
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Filter out zod-meta transformations that are already included in schema transformations
|
|
176
|
+
const independentZodMetas = zodMetaTransformations.filter(zodTransform => {
|
|
177
|
+
return !schemaTransformations.some(schemaTransform => {
|
|
178
|
+
return schemaTransform.start <= zodTransform.start && schemaTransform.end >= zodTransform.end
|
|
179
|
+
})
|
|
74
180
|
})
|
|
75
181
|
|
|
76
|
-
|
|
182
|
+
// Combine the transformations
|
|
183
|
+
const finalTransformations = [
|
|
184
|
+
...independentZodMetas,
|
|
185
|
+
...processedSchemaTransformations.map(({ containsZodMeta, ...rest }) => rest), // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
// Check if we need to add the import for camelCaseToHumanReadable
|
|
189
|
+
const hasZodMetaTransformations = zodMetaTransformations.length > 0
|
|
190
|
+
const needsImport = hasZodMetaTransformations && !content.includes("__camelCaseToHumanReadable")
|
|
191
|
+
|
|
192
|
+
let result = content
|
|
193
|
+
const magicString = new MagicString(result)
|
|
194
|
+
|
|
195
|
+
// Apply all transformations
|
|
196
|
+
finalTransformations
|
|
197
|
+
.sort((a, b) => b.start - a.start) // Sort in reverse order by start position
|
|
198
|
+
.forEach(transformation => {
|
|
199
|
+
magicString.update(transformation.start, transformation.end, transformation.newValue)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
result = magicString.toString()
|
|
203
|
+
|
|
204
|
+
// Add import at the beginning if needed
|
|
205
|
+
if (needsImport) {
|
|
206
|
+
result =
|
|
207
|
+
'import { camelCaseToHumanReadable as __camelCaseToHumanReadable } from "@highstate/contract"\n' +
|
|
208
|
+
result
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result
|
|
77
212
|
}
|
|
78
213
|
|
|
79
214
|
function injectDescriptionIntoObject(objectString: string, description: string): string {
|
|
@@ -83,12 +218,20 @@ function injectDescriptionIntoObject(objectString: string, description: string):
|
|
|
83
218
|
const metaRegex = /meta\s*:\s*\{/
|
|
84
219
|
|
|
85
220
|
if (metaRegex.test(trimmed)) {
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
221
|
+
// Check if the meta field already has a description
|
|
222
|
+
const hasDescription = /meta\s*:\s*\{[^}]*description\s*:/.test(trimmed)
|
|
223
|
+
|
|
224
|
+
if (hasDescription) {
|
|
225
|
+
// Replace existing description
|
|
226
|
+
return trimmed.replace(/description\s*:\s*`[^`]*`/, `description: \`${description}\``)
|
|
227
|
+
} else {
|
|
228
|
+
// Add description at the beginning of the meta object
|
|
229
|
+
return trimmed.replace(
|
|
230
|
+
/meta\s*:\s*\{/,
|
|
231
|
+
`meta: {
|
|
90
232
|
description: \`${description}\`,`,
|
|
91
|
-
|
|
233
|
+
)
|
|
234
|
+
}
|
|
92
235
|
} else {
|
|
93
236
|
// Add meta field at the end of the object (before the closing brace)
|
|
94
237
|
const lastBraceIndex = trimmed.lastIndexOf("}")
|
|
@@ -174,3 +317,71 @@ function cleanJsdoc(str: string) {
|
|
|
174
317
|
.trim()
|
|
175
318
|
)
|
|
176
319
|
}
|
|
320
|
+
|
|
321
|
+
function isInsideZodObject(parentStack: Node[]): boolean {
|
|
322
|
+
// Look for z.object() call expression in the parent stack
|
|
323
|
+
for (let i = parentStack.length - 1; i >= 0; i--) {
|
|
324
|
+
const node = parentStack[i]
|
|
325
|
+
if (
|
|
326
|
+
node.type === "CallExpression" &&
|
|
327
|
+
node.callee.type === "MemberExpression" &&
|
|
328
|
+
isZodObjectCall(node.callee)
|
|
329
|
+
) {
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isZodObjectCall(memberExpression: Node): boolean {
|
|
337
|
+
if (memberExpression.type !== "MemberExpression") {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Handle direct z.object() calls
|
|
342
|
+
if (
|
|
343
|
+
memberExpression.object.type === "Identifier" &&
|
|
344
|
+
memberExpression.object.name === "z" &&
|
|
345
|
+
memberExpression.property.type === "Identifier" &&
|
|
346
|
+
memberExpression.property.name === "object"
|
|
347
|
+
) {
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Handle chained calls like z.discriminatedUnion().default().object()
|
|
352
|
+
// or any other chained Zod methods that end with .object()
|
|
353
|
+
if (
|
|
354
|
+
memberExpression.property.type === "Identifier" &&
|
|
355
|
+
memberExpression.property.name === "object" &&
|
|
356
|
+
memberExpression.object.type === "CallExpression" &&
|
|
357
|
+
memberExpression.object.callee.type === "MemberExpression"
|
|
358
|
+
) {
|
|
359
|
+
// Recursively check if this is part of a z.* chain
|
|
360
|
+
return startsWithZodCall(memberExpression.object)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function startsWithZodCall(callExpression: Node): boolean {
|
|
367
|
+
if (!callExpression || callExpression.type !== "CallExpression") {
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (callExpression.callee.type === "MemberExpression") {
|
|
372
|
+
// Check if this is a direct z.* call
|
|
373
|
+
if (
|
|
374
|
+
callExpression.callee.object.type === "Identifier" &&
|
|
375
|
+
callExpression.callee.object.name === "z"
|
|
376
|
+
) {
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Recursively check nested calls
|
|
381
|
+
if (callExpression.callee.object.type === "CallExpression") {
|
|
382
|
+
return startsWithZodCall(callExpression.callee.object)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
@@ -6,7 +6,6 @@ import { readPackageJSON, resolvePackageJSON, type PackageJson } from "pkg-types
|
|
|
6
6
|
import { crc32 } from "@aws-crypto/crc32"
|
|
7
7
|
import { resolve as importMetaResolve } from "import-meta-resolve"
|
|
8
8
|
import { z } from "zod"
|
|
9
|
-
import { int32ToBytes } from "@highstate/backend/shared"
|
|
10
9
|
import {
|
|
11
10
|
type HighstateManifest,
|
|
12
11
|
type HighstateConfig,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
highstateManifestSchema,
|
|
16
15
|
sourceHashConfigSchema,
|
|
17
16
|
} from "./schemas"
|
|
17
|
+
import { int32ToBytes } from "./utils"
|
|
18
18
|
|
|
19
19
|
type FileDependency =
|
|
20
20
|
| {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/library-loader.ts"],"sourcesContent":["import type { Logger } from \"pino\"\nimport {\n type Component,\n type ComponentModel,\n type Entity,\n isComponent,\n isEntity,\n isUnitModel,\n originalCreate,\n} from \"@highstate/contract\"\nimport { serializeFunction } from \"@pulumi/pulumi/runtime/index.js\"\nimport { Crc32, crc32 } from \"@aws-crypto/crc32\"\nimport { encode } from \"@msgpack/msgpack\"\nimport { int32ToBytes } from \"@highstate/backend/shared\"\n\nexport type Library = Readonly<{\n components: Readonly<Record<string, ComponentModel>>\n entities: Readonly<Record<string, Entity>>\n}>\n\nexport async function loadLibrary(logger: Logger, modulePaths: string[]): Promise<Library> {\n const modules: Record<string, unknown> = {}\n for (const modulePath of modulePaths) {\n try {\n logger.debug({ modulePath }, \"loading module\")\n modules[modulePath] = await import(modulePath)\n\n logger.debug({ modulePath }, \"module loaded\")\n } catch (err) {\n logger.error({ modulePath, err }, \"module load failed\")\n }\n }\n\n const components: Record<string, ComponentModel> = {}\n const entities: Record<string, Entity> = {}\n\n await _loadLibrary(modules, components, entities)\n\n logger.info(\n {\n componentCount: Object.keys(components).length,\n entityCount: Object.keys(entities).length,\n },\n \"library loaded\",\n )\n\n logger.trace({ components, entities }, \"library content\")\n\n return { components, entities }\n}\n\nasync function _loadLibrary(\n value: unknown,\n components: Record<string, ComponentModel>,\n entities: Record<string, Entity>,\n): Promise<void> {\n if (isComponent(value)) {\n const entityHashes: number[] = []\n for (const entity of value.entities.values()) {\n entity.definitionHash ??= calculateEntityDefinitionHash(entity)\n entityHashes.push(entity.definitionHash)\n }\n\n components[value.model.type] = value.model\n value.model.definitionHash = await calculateComponentDefinitionHash(value, entityHashes)\n\n return\n }\n\n if (isEntity(value)) {\n entities[value.type] = value\n entities[value.type].definitionHash ??= calculateEntityDefinitionHash(value)\n\n // @ts-expect-error remove the schema since it's not needed in the designer\n delete value.schema\n return\n }\n\n if (typeof value !== \"object\" || value === null) {\n return\n }\n\n for (const key in value) {\n await _loadLibrary((value as Record<string, unknown>)[key], components, entities)\n }\n}\n\nasync function calculateComponentDefinitionHash(\n component: Component,\n entityHashes: number[],\n): Promise<number> {\n const result = new Crc32()\n\n // 1. include the full component model\n result.update(encode(component.model))\n\n if (!isUnitModel(component.model)) {\n // 2. for composite components, include the content of the serialized create function\n const serializedCreate = await serializeFunction(component[originalCreate])\n result.update(Buffer.from(serializedCreate.text))\n }\n\n // 3. include the hashes of all entities\n for (const entityHash of entityHashes) {\n result.update(int32ToBytes(entityHash))\n }\n\n return result.digest()\n}\n\nfunction calculateEntityDefinitionHash(entity: Entity): number {\n return crc32(encode(entity))\n}\n"],"mappings":";AACA;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC,SAAS,OAAO,aAAa;AAC7B,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAO7B,eAAsB,YAAY,QAAgB,aAAyC;AACzF,QAAM,UAAmC,CAAC;AAC1C,aAAW,cAAc,aAAa;AACpC,QAAI;AACF,aAAO,MAAM,EAAE,WAAW,GAAG,gBAAgB;AAC7C,cAAQ,UAAU,IAAI,MAAM,OAAO;AAEnC,aAAO,MAAM,EAAE,WAAW,GAAG,eAAe;AAAA,IAC9C,SAAS,KAAK;AACZ,aAAO,MAAM,EAAE,YAAY,IAAI,GAAG,oBAAoB;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,aAA6C,CAAC;AACpD,QAAM,WAAmC,CAAC;AAE1C,QAAM,aAAa,SAAS,YAAY,QAAQ;AAEhD,SAAO;AAAA,IACL;AAAA,MACE,gBAAgB,OAAO,KAAK,UAAU,EAAE;AAAA,MACxC,aAAa,OAAO,KAAK,QAAQ,EAAE;AAAA,IACrC;AAAA,IACA;AAAA,EACF;AAEA,SAAO,MAAM,EAAE,YAAY,SAAS,GAAG,iBAAiB;AAExD,SAAO,EAAE,YAAY,SAAS;AAChC;AAEA,eAAe,aACb,OACA,YACA,UACe;AACf,MAAI,YAAY,KAAK,GAAG;AACtB,UAAM,eAAyB,CAAC;AAChC,eAAW,UAAU,MAAM,SAAS,OAAO,GAAG;AAC5C,aAAO,mBAAmB,8BAA8B,MAAM;AAC9D,mBAAa,KAAK,OAAO,cAAc;AAAA,IACzC;AAEA,eAAW,MAAM,MAAM,IAAI,IAAI,MAAM;AACrC,UAAM,MAAM,iBAAiB,MAAM,iCAAiC,OAAO,YAAY;AAEvF;AAAA,EACF;AAEA,MAAI,SAAS,KAAK,GAAG;AACnB,aAAS,MAAM,IAAI,IAAI;AACvB,aAAS,MAAM,IAAI,EAAE,mBAAmB,8BAA8B,KAAK;AAG3E,WAAO,MAAM;AACb;AAAA,EACF;AAEA,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,EACF;AAEA,aAAW,OAAO,OAAO;AACvB,UAAM,aAAc,MAAkC,GAAG,GAAG,YAAY,QAAQ;AAAA,EAClF;AACF;AAEA,eAAe,iCACb,WACA,cACiB;AACjB,QAAM,SAAS,IAAI,MAAM;AAGzB,SAAO,OAAO,OAAO,UAAU,KAAK,CAAC;AAErC,MAAI,CAAC,YAAY,UAAU,KAAK,GAAG;AAEjC,UAAM,mBAAmB,MAAM,kBAAkB,UAAU,cAAc,CAAC;AAC1E,WAAO,OAAO,OAAO,KAAK,iBAAiB,IAAI,CAAC;AAAA,EAClD;AAGA,aAAW,cAAc,cAAc;AACrC,WAAO,OAAO,aAAa,UAAU,CAAC;AAAA,EACxC;AAEA,SAAO,OAAO,OAAO;AACvB;AAEA,SAAS,8BAA8B,QAAwB;AAC7D,SAAO,MAAM,OAAO,MAAM,CAAC;AAC7B;","names":[]}
|