@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.
@@ -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
- magicString.update(node.value.start, node.value.end, modifiedValue)
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
- magicString.update(
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
- return magicString.toString()
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
- // Find the meta field and inject description into it
87
- return trimmed.replace(
88
- /meta\s*:\s*\{/,
89
- `meta: {
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
  | {
@@ -0,0 +1,6 @@
1
+ export function int32ToBytes(value: number): Uint8Array {
2
+ const buffer = new ArrayBuffer(4)
3
+ const view = new DataView(buffer)
4
+ view.setInt32(0, value, true) // true for little-endian
5
+ return new Uint8Array(buffer)
6
+ }
@@ -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":[]}