@highstate/cli 0.9.16 → 0.9.19

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.
@@ -1,6 +1,12 @@
1
1
  import type { Plugin } from "esbuild"
2
2
  import { readFile } from "node:fs/promises"
3
- import { parseAsync, type Comment } from "oxc-parser"
3
+ import {
4
+ parseAsync,
5
+ type Comment,
6
+ type CallExpression,
7
+ type ObjectProperty,
8
+ type MemberExpression,
9
+ } from "oxc-parser"
4
10
  import { walk, type Node } from "oxc-walker"
5
11
  import MagicString from "magic-string"
6
12
 
@@ -19,53 +25,99 @@ export const schemaTransformerPlugin: Plugin = {
19
25
  }
20
26
 
21
27
  export async function applySchemaTransformations(content: string): Promise<string> {
22
- const magicString = new MagicString(content)
23
- const { program, comments } = await parseAsync("file.ts", content)
28
+ // first pass: apply zod meta transformations
29
+ let result = await applyZodMetaTransformations(content)
30
+
31
+ // second pass: apply helper function transformations
32
+ result = await applyHelperFunctionTransformations(result)
24
33
 
34
+ // third pass: apply define function meta transformations
35
+ result = await applyDefineFunctionMetaTransformations(result)
36
+
37
+ return result
38
+ }
39
+
40
+ async function applyZodMetaTransformations(content: string): Promise<string> {
41
+ const { program, comments } = await parseAsync("file.ts", content)
25
42
  const parentStack: Node[] = []
43
+ let hasTransformations = false
44
+ const result = new MagicString(content)
26
45
 
27
46
  walk(program, {
28
47
  enter(node) {
29
48
  parentStack.push(node)
30
49
 
31
- if (node.type !== "Property" || node.key.type !== "Identifier") {
32
- return
33
- }
50
+ // handle zod object patterns
51
+ if (isZodObjectProperty(node, parentStack)) {
52
+ const jsdoc = findLeadingComment(content, node, comments)
53
+ if (jsdoc) {
54
+ const description = cleanJsdoc(jsdoc.value)
55
+ const fieldName =
56
+ "name" in node.key && typeof node.key.name === "string" ? node.key.name : "unknown"
57
+ const originalValue = content.substring(node.value.start, node.value.end)
34
58
 
35
- const parentKey = getParentObjectKey(parentStack) || getMarkerFunctionName(parentStack)
36
- if (!parentKey || !["inputs", "outputs", "args", "secrets"].includes(parentKey)) {
37
- return
59
+ if (!originalValue.includes(".meta(")) {
60
+ const newValue = `${originalValue}.meta({ title: __camelCaseToHumanReadable("${fieldName}"), description: \`${description}\` })`
61
+ result.update(node.value.start, node.value.end, newValue)
62
+ hasTransformations = true
63
+ }
64
+ }
38
65
  }
66
+ },
67
+ leave() {
68
+ parentStack.pop()
69
+ },
70
+ })
39
71
 
40
- const jsdoc = comments.find(comment => isLeadingComment(content, node, comment))
41
- if (!jsdoc) {
42
- return
43
- }
72
+ let finalResult = result.toString()
44
73
 
45
- const description = cleanJsdoc(jsdoc.value)
46
- const originalValue = content.substring(node.value.start, node.value.end)
47
-
48
- const entityField = ["inputs", "outputs"].includes(parentKey) ? "entity" : "schema"
49
-
50
- // Check if the value already has entity/schema structure
51
- const isAlreadyStructured = isStructuredValue(originalValue, entityField)
52
-
53
- if (isAlreadyStructured) {
54
- // For already structured values, inject description directly into the object
55
- const modifiedValue = injectDescriptionIntoObject(originalValue, description)
56
- magicString.update(node.value.start, node.value.end, modifiedValue)
57
- } else {
58
- // Transform to new structure
59
- magicString.update(
60
- node.value.start,
61
- node.value.end,
62
- `{
63
- ${entityField}: ${originalValue},
64
- meta: {
65
- description: \`${description}\`,
66
- },
67
- }`,
68
- )
74
+ // add import at the beginning if needed
75
+ if (hasTransformations && !content.includes("__camelCaseToHumanReadable")) {
76
+ finalResult =
77
+ 'import { camelCaseToHumanReadable as __camelCaseToHumanReadable } from "@highstate/contract"\n' +
78
+ finalResult
79
+ }
80
+
81
+ return finalResult
82
+ }
83
+
84
+ async function applyHelperFunctionTransformations(content: string): Promise<string> {
85
+ const { program, comments } = await parseAsync("file.ts", content)
86
+ const parentStack: Node[] = []
87
+ let hasTransformations = false
88
+ const result = new MagicString(content)
89
+
90
+ walk(program, {
91
+ enter(node) {
92
+ parentStack.push(node)
93
+
94
+ // handle properties in args, inputs, outputs, secrets objects
95
+ if (node.type === "Property" && "key" in node && node.key?.type === "Identifier") {
96
+ const propertyNode = node
97
+ const parentKey = getParentObjectKey(parentStack)
98
+
99
+ if (parentKey && ["inputs", "outputs", "args", "secrets"].includes(parentKey)) {
100
+ const jsdoc = findLeadingComment(content, node, comments)
101
+
102
+ if (jsdoc) {
103
+ const description = cleanJsdoc(jsdoc.value)
104
+ const originalValue = content.substring(
105
+ propertyNode.value.start,
106
+ propertyNode.value.end,
107
+ )
108
+
109
+ let helperFunction: string
110
+ if (["args", "secrets"].includes(parentKey)) {
111
+ helperFunction = "$addArgumentDescription"
112
+ } else {
113
+ helperFunction = "$addInputDescription"
114
+ }
115
+
116
+ const newValue = `${helperFunction}(${originalValue}, \`${description}\`)`
117
+ result.update(propertyNode.value.start, propertyNode.value.end, newValue)
118
+ hasTransformations = true
119
+ }
120
+ }
69
121
  }
70
122
  },
71
123
  leave() {
@@ -73,78 +125,139 @@ export async function applySchemaTransformations(content: string): Promise<strin
73
125
  },
74
126
  })
75
127
 
76
- return magicString.toString()
128
+ let finalResult = result.toString()
129
+
130
+ // add import at the beginning if needed
131
+ if (hasTransformations && !content.includes("$addArgumentDescription")) {
132
+ finalResult =
133
+ 'import { $addArgumentDescription, $addInputDescription } from "@highstate/contract"\n' +
134
+ finalResult
135
+ }
136
+
137
+ return finalResult
77
138
  }
78
139
 
79
- function injectDescriptionIntoObject(objectString: string, description: string): string {
80
- const trimmed = objectString.trim()
140
+ async function applyDefineFunctionMetaTransformations(content: string): Promise<string> {
141
+ const { program, comments } = await parseAsync("file.ts", content)
142
+ const parentStack: Node[] = []
143
+ const result = new MagicString(content)
81
144
 
82
- // Check if the object already has a meta field
83
- const metaRegex = /meta\s*:\s*\{/
145
+ walk(program, {
146
+ enter(node) {
147
+ parentStack.push(node)
84
148
 
85
- if (metaRegex.test(trimmed)) {
86
- // Find the meta field and inject description into it
87
- return trimmed.replace(
88
- /meta\s*:\s*\{/,
89
- `meta: {
90
- description: \`${description}\`,`,
91
- )
92
- } else {
93
- // Add meta field at the end of the object (before the closing brace)
94
- const lastBraceIndex = trimmed.lastIndexOf("}")
95
- if (lastBraceIndex === -1) {
96
- // Invalid object structure, return as is
97
- return trimmed
98
- }
149
+ // handle defineUnit, defineEntity, defineComponent function calls
150
+ if (node.type === "CallExpression" && "callee" in node && node.callee.type === "Identifier") {
151
+ const callNode = node
152
+ const callee = callNode.callee
153
+ const functionName =
154
+ "name" in callee && typeof callee.name === "string" ? callee.name : undefined
155
+
156
+ if (
157
+ functionName &&
158
+ ["defineUnit", "defineEntity", "defineComponent"].includes(functionName)
159
+ ) {
160
+ // look for JSDoc comment on the parent declaration/export, not the function call itself
161
+ const jsdoc = findJsdocForDefineFunction(content, parentStack, comments)
162
+
163
+ if (jsdoc && callNode.arguments && callNode.arguments.length > 0) {
164
+ const description = cleanJsdoc(jsdoc.value)
165
+ const firstArg = callNode.arguments[0]
99
166
 
100
- const beforeBrace = trimmed.substring(0, lastBraceIndex)
101
- const afterBrace = trimmed.substring(lastBraceIndex)
167
+ // find meta property in the object expression
168
+ if (firstArg.type === "ObjectExpression" && "properties" in firstArg) {
169
+ const properties = firstArg.properties
170
+ const metaProperty = properties?.find(
171
+ prop =>
172
+ prop.type === "Property" &&
173
+ "key" in prop &&
174
+ prop.key?.type === "Identifier" &&
175
+ prop.key?.name === "meta",
176
+ ) as ObjectProperty | undefined
102
177
 
103
- // Check if we need a comma before adding meta
104
- const needsComma = beforeBrace.trim().length > 1 && !beforeBrace.trim().endsWith(",")
105
- const comma = needsComma ? "," : ""
178
+ if (metaProperty && "value" in metaProperty) {
179
+ // inject description into existing meta object
180
+ const originalMetaValue = content.substring(
181
+ metaProperty.value.start,
182
+ metaProperty.value.end,
183
+ )
184
+ const newMetaValue = injectDescriptionIntoMetaObject(originalMetaValue, description)
185
+ result.update(metaProperty.value.start, metaProperty.value.end, newMetaValue)
186
+ } else if (properties && properties.length > 0) {
187
+ // add meta property with description
188
+ const lastProperty = properties[properties.length - 1] as ObjectProperty
189
+ if (lastProperty && "end" in lastProperty) {
190
+ const insertPos = lastProperty.end
191
+ const newMetaProperty = `,
106
192
 
107
- return `${beforeBrace}${comma}
108
193
  meta: {
109
194
  description: \`${description}\`,
110
- },
111
- ${afterBrace}`
112
- }
113
- }
114
-
115
- function isStructuredValue(value: string, expectedField: string): boolean {
116
- const trimmed = value.trim()
117
- if (!trimmed.startsWith("{")) {
118
- return false
119
- }
195
+ }`
196
+ result.appendLeft(insertPos, newMetaProperty)
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ },
204
+ leave() {
205
+ parentStack.pop()
206
+ },
207
+ })
120
208
 
121
- // Check if it contains the expected field (entity or schema) at the top level
122
- const fieldPattern = new RegExp(`^\\s*{[^}]*\\b${expectedField}\\s*:`, "s")
123
- return fieldPattern.test(trimmed)
209
+ return result.toString()
124
210
  }
125
211
 
126
- function getMarkerFunctionName(parentStack: Node[]): string | null {
127
- // Look for marker functions like $args, $inputs, $outputs, $secrets
212
+ function findJsdocForDefineFunction(
213
+ content: string,
214
+ parentStack: Node[],
215
+ comments: Comment[],
216
+ ): Comment | null {
217
+ // look for the variable declaration or export declaration that contains the function call
128
218
  for (let i = parentStack.length - 1; i >= 0; i--) {
129
219
  const node = parentStack[i]
130
- if (node.type === "CallExpression" && node.callee.type === "Identifier") {
131
- const functionName = node.callee.name
132
- if (
133
- functionName.startsWith("$") &&
134
- ["$args", "$inputs", "$outputs", "$secrets"].includes(functionName)
135
- ) {
136
- return functionName.substring(1) // Remove the $ prefix
137
- }
220
+
221
+ // check for variable declaration (const x = defineUnit(...))
222
+ if (node.type === "VariableDeclarator" && "id" in node && node.id?.type === "Identifier") {
223
+ const jsdoc = findLeadingComment(content, node, comments)
224
+ if (jsdoc) return jsdoc
225
+ }
226
+
227
+ // check for export variable declaration (export const x = defineUnit(...))
228
+ if (node.type === "VariableDeclaration") {
229
+ const jsdoc = findLeadingComment(content, node, comments)
230
+ if (jsdoc) return jsdoc
231
+ }
232
+
233
+ // check for export declaration (export const x = ...)
234
+ if (node.type === "ExportNamedDeclaration" && "declaration" in node && node.declaration) {
235
+ const jsdoc = findLeadingComment(content, node, comments)
236
+ if (jsdoc) return jsdoc
138
237
  }
139
238
  }
239
+
140
240
  return null
141
241
  }
142
242
 
243
+ function isZodObjectProperty(node: Node, parentStack: Node[]): node is ObjectProperty {
244
+ return (
245
+ node.type === "Property" &&
246
+ "key" in node &&
247
+ node.key?.type === "Identifier" &&
248
+ isInsideZodObject(parentStack)
249
+ )
250
+ }
251
+
252
+ function findLeadingComment(content: string, node: Node, comments: Comment[]): Comment | null {
253
+ return comments.find(comment => isLeadingComment(content, node, comment)) ?? null
254
+ }
255
+
143
256
  function getParentObjectKey(parentStack: Node[]): string | null {
144
- // Walk up the parent stack to find the parent object property
257
+ // walk up the parent stack to find the parent object property
145
258
  for (let i = parentStack.length - 2; i >= 0; i--) {
146
259
  const node = parentStack[i]
147
- if (node.type === "Property" && node.key.type === "Identifier") {
260
+ if (node.type === "Property" && "key" in node && node.key.type === "Identifier") {
148
261
  return node.key.name
149
262
  }
150
263
  }
@@ -174,3 +287,107 @@ function cleanJsdoc(str: string) {
174
287
  .trim()
175
288
  )
176
289
  }
290
+
291
+ function injectDescriptionIntoMetaObject(objectString: string, description: string): string {
292
+ const trimmed = objectString.trim()
293
+
294
+ // Check if description already exists
295
+ const hasDescription = /description\s*:/.test(trimmed)
296
+
297
+ if (hasDescription) {
298
+ // Replace existing description
299
+ return trimmed.replace(/description\s*:\s*`[^`]*`/, `description: \`${description}\``)
300
+ } else {
301
+ // Add description field at the beginning of the object
302
+ const openBraceIndex = trimmed.indexOf("{")
303
+ if (openBraceIndex === -1) {
304
+ return trimmed
305
+ }
306
+
307
+ const beforeBrace = trimmed.substring(0, openBraceIndex + 1)
308
+ const afterBrace = trimmed.substring(openBraceIndex + 1)
309
+
310
+ return `${beforeBrace}
311
+ description: \`${description}\`,${afterBrace}`
312
+ }
313
+ }
314
+
315
+ function isInsideZodObject(parentStack: Node[]): boolean {
316
+ // look for z.object() call expression in the parent stack
317
+ for (let i = parentStack.length - 1; i >= 0; i--) {
318
+ const node = parentStack[i]
319
+ if (
320
+ node.type === "CallExpression" &&
321
+ "callee" in node &&
322
+ node.callee.type === "MemberExpression" &&
323
+ isZodObjectCall(node.callee)
324
+ ) {
325
+ return true
326
+ }
327
+ }
328
+ return false
329
+ }
330
+
331
+ function isZodObjectCall(memberExpression: Node): boolean {
332
+ if (
333
+ memberExpression.type !== "MemberExpression" ||
334
+ !("object" in memberExpression) ||
335
+ !("property" in memberExpression)
336
+ ) {
337
+ return false
338
+ }
339
+
340
+ const member = memberExpression as MemberExpression
341
+
342
+ // handle direct z.object() calls
343
+ if (
344
+ member.object.type === "Identifier" &&
345
+ "name" in member.object &&
346
+ member.object.name === "z" &&
347
+ member.property.type === "Identifier" &&
348
+ member.property.name === "object"
349
+ ) {
350
+ return true
351
+ }
352
+
353
+ // handle chained calls like z.discriminatedUnion().default().object()
354
+ // or any other chained Zod methods that end with .object()
355
+ if (
356
+ member.property.type === "Identifier" &&
357
+ member.property.name === "object" &&
358
+ member.object.type === "CallExpression" &&
359
+ "callee" in member.object &&
360
+ member.object.callee.type === "MemberExpression"
361
+ ) {
362
+ // recursively check if this is part of a z.* chain
363
+ return startsWithZodCall(member.object)
364
+ }
365
+
366
+ return false
367
+ }
368
+
369
+ function startsWithZodCall(callExpression: CallExpression): boolean {
370
+ if (!callExpression || callExpression.type !== "CallExpression") {
371
+ return false
372
+ }
373
+
374
+ if (callExpression.callee.type === "MemberExpression") {
375
+ const callee = callExpression.callee
376
+
377
+ // check if this is a direct z.* call
378
+ if (
379
+ callee.object.type === "Identifier" &&
380
+ "name" in callee.object &&
381
+ callee.object.name === "z"
382
+ ) {
383
+ return true
384
+ }
385
+
386
+ // recursively check nested calls
387
+ if (callee.object.type === "CallExpression") {
388
+ return startsWithZodCall(callee.object)
389
+ }
390
+ }
391
+
392
+ return false
393
+ }
@@ -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
  | {
@@ -136,7 +136,6 @@ export class SourceHashCalculator {
136
136
  }),
137
137
  )
138
138
  break
139
- case "auto":
140
139
  default:
141
140
  promises.push(
142
141
  this.getFileHash(fullPath).then(hash => ({
@@ -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
+ }