@highstate/cli 0.9.18 → 0.9.20

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
 
@@ -18,276 +24,240 @@ export const schemaTransformerPlugin: Plugin = {
18
24
  },
19
25
  }
20
26
 
21
- type Transformation = {
22
- start: number
23
- end: number
24
- newValue: string
25
- type: "zod-meta" | "schema-structure" | "import"
27
+ export async function applySchemaTransformations(content: string): Promise<string> {
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)
33
+
34
+ // third pass: apply define function meta transformations
35
+ result = await applyDefineFunctionMetaTransformations(result)
36
+
37
+ return result
26
38
  }
27
39
 
28
- export async function applySchemaTransformations(content: string): Promise<string> {
40
+ async function applyZodMetaTransformations(content: string): Promise<string> {
29
41
  const { program, comments } = await parseAsync("file.ts", content)
30
-
31
- const transformations: Transformation[] = []
32
42
  const parentStack: Node[] = []
43
+ let hasTransformations = false
44
+ const result = new MagicString(content)
33
45
 
34
46
  walk(program, {
35
47
  enter(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))
48
+ parentStack.push(node)
49
+
50
+ // handle zod object patterns
51
+ if (isZodObjectProperty(node, parentStack)) {
52
+ const jsdoc = findLeadingComment(content, node, comments)
43
53
  if (jsdoc) {
44
54
  const description = cleanJsdoc(jsdoc.value)
45
- const fieldName = node.key.name
55
+ const fieldName =
56
+ "name" in node.key && typeof node.key.name === "string" ? node.key.name : "unknown"
46
57
  const originalValue = content.substring(node.value.start, node.value.end)
47
58
 
48
- // Check if the field already has .meta() call
49
59
  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
- })
60
+ const newValue = `${originalValue}.meta({ title: __camelCaseToHumanReadable("${fieldName}"), description: \`${description}\` })`
61
+ result.update(node.value.start, node.value.end, newValue)
62
+ hasTransformations = true
56
63
  }
57
64
  }
58
- return
59
- }
60
-
61
- if (node.type !== "Property" || node.key.type !== "Identifier") {
62
- return
63
- }
64
-
65
- const parentKey = getParentObjectKey(parentStack) || getMarkerFunctionName(parentStack)
66
- if (!parentKey || !["inputs", "outputs", "args", "secrets"].includes(parentKey)) {
67
- return
68
- }
69
-
70
- const jsdoc = comments.find(comment => isLeadingComment(content, node, comment))
71
- if (!jsdoc) {
72
- return
73
- }
74
-
75
- const description = cleanJsdoc(jsdoc.value)
76
- const originalValue = content.substring(node.value.start, node.value.end)
77
-
78
- const entityField = ["inputs", "outputs"].includes(parentKey) ? "entity" : "schema"
79
-
80
- // Check if the value already has entity/schema structure
81
- const isAlreadyStructured = isStructuredValue(originalValue, entityField)
82
-
83
- if (isAlreadyStructured) {
84
- // For already structured values, inject description directly into the object
85
- const modifiedValue = injectDescriptionIntoObject(originalValue, description)
86
- transformations.push({
87
- start: node.value.start,
88
- end: node.value.end,
89
- newValue: modifiedValue,
90
- type: "schema-structure",
91
- })
92
- } else {
93
- // Transform to new structure
94
- transformations.push({
95
- start: node.value.start,
96
- end: node.value.end,
97
- newValue: `{
98
- ${entityField}: ${originalValue},
99
- meta: {
100
- description: \`${description}\`,
101
- },
102
- }`,
103
- type: "schema-structure",
104
- })
105
65
  }
106
66
  },
107
67
  leave() {
108
68
  parentStack.pop()
109
69
  },
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
70
  })
174
71
 
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
- })
180
- })
72
+ let finalResult = result.toString()
181
73
 
182
- // Combine the transformations
183
- const finalTransformations = [
184
- ...independentZodMetas,
185
- ...processedSchemaTransformations.map(({ containsZodMeta, ...rest }) => rest), // eslint-disable-line @typescript-eslint/no-unused-vars
186
- ]
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
+ }
187
80
 
188
- // Check if we need to add the import for camelCaseToHumanReadable
189
- const hasZodMetaTransformations = zodMetaTransformations.length > 0
190
- const needsImport = hasZodMetaTransformations && !content.includes("__camelCaseToHumanReadable")
81
+ return finalResult
82
+ }
191
83
 
192
- let result = content
193
- const magicString = new MagicString(result)
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)
194
89
 
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
- })
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
+ }
121
+ }
122
+ },
123
+ leave() {
124
+ parentStack.pop()
125
+ },
126
+ })
201
127
 
202
- result = magicString.toString()
128
+ let finalResult = result.toString()
203
129
 
204
- // Add import at the beginning if needed
205
- if (needsImport) {
206
- result =
207
- 'import { camelCaseToHumanReadable as __camelCaseToHumanReadable } from "@highstate/contract"\n' +
208
- result
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
209
135
  }
210
136
 
211
- return result
137
+ return finalResult
212
138
  }
213
139
 
214
- function injectDescriptionIntoObject(objectString: string, description: string): string {
215
- const trimmed = objectString.trim()
216
-
217
- // Check if the object already has a meta field
218
- const metaRegex = /meta\s*:\s*\{/
219
-
220
- if (metaRegex.test(trimmed)) {
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: {
232
- description: \`${description}\`,`,
233
- )
234
- }
235
- } else {
236
- // Add meta field at the end of the object (before the closing brace)
237
- const lastBraceIndex = trimmed.lastIndexOf("}")
238
- if (lastBraceIndex === -1) {
239
- // Invalid object structure, return as is
240
- return trimmed
241
- }
242
-
243
- const beforeBrace = trimmed.substring(0, lastBraceIndex)
244
- const afterBrace = trimmed.substring(lastBraceIndex)
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)
245
144
 
246
- // Check if we need a comma before adding meta
247
- const needsComma = beforeBrace.trim().length > 1 && !beforeBrace.trim().endsWith(",")
248
- const comma = needsComma ? "," : ""
145
+ walk(program, {
146
+ enter(node) {
147
+ parentStack.push(node)
148
+
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]
166
+
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
177
+
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 = `,
249
192
 
250
- return `${beforeBrace}${comma}
251
193
  meta: {
252
194
  description: \`${description}\`,
253
- },
254
- ${afterBrace}`
255
- }
256
- }
257
-
258
- function isStructuredValue(value: string, expectedField: string): boolean {
259
- const trimmed = value.trim()
260
- if (!trimmed.startsWith("{")) {
261
- return false
262
- }
195
+ }`
196
+ result.appendLeft(insertPos, newMetaProperty)
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ },
204
+ leave() {
205
+ parentStack.pop()
206
+ },
207
+ })
263
208
 
264
- // Check if it contains the expected field (entity or schema) at the top level
265
- const fieldPattern = new RegExp(`^\\s*{[^}]*\\b${expectedField}\\s*:`, "s")
266
- return fieldPattern.test(trimmed)
209
+ return result.toString()
267
210
  }
268
211
 
269
- function getMarkerFunctionName(parentStack: Node[]): string | null {
270
- // 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
271
218
  for (let i = parentStack.length - 1; i >= 0; i--) {
272
219
  const node = parentStack[i]
273
- if (node.type === "CallExpression" && node.callee.type === "Identifier") {
274
- const functionName = node.callee.name
275
- if (
276
- functionName.startsWith("$") &&
277
- ["$args", "$inputs", "$outputs", "$secrets"].includes(functionName)
278
- ) {
279
- return functionName.substring(1) // Remove the $ prefix
280
- }
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
281
237
  }
282
238
  }
239
+
283
240
  return null
284
241
  }
285
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
+
286
256
  function getParentObjectKey(parentStack: Node[]): string | null {
287
- // Walk up the parent stack to find the parent object property
257
+ // walk up the parent stack to find the parent object property
288
258
  for (let i = parentStack.length - 2; i >= 0; i--) {
289
259
  const node = parentStack[i]
290
- if (node.type === "Property" && node.key.type === "Identifier") {
260
+ if (node.type === "Property" && "key" in node && node.key.type === "Identifier") {
291
261
  return node.key.name
292
262
  }
293
263
  }
@@ -318,12 +288,37 @@ function cleanJsdoc(str: string) {
318
288
  )
319
289
  }
320
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
+
321
315
  function isInsideZodObject(parentStack: Node[]): boolean {
322
- // Look for z.object() call expression in the parent stack
316
+ // look for z.object() call expression in the parent stack
323
317
  for (let i = parentStack.length - 1; i >= 0; i--) {
324
318
  const node = parentStack[i]
325
319
  if (
326
320
  node.type === "CallExpression" &&
321
+ "callee" in node &&
327
322
  node.callee.type === "MemberExpression" &&
328
323
  isZodObjectCall(node.callee)
329
324
  ) {
@@ -334,52 +329,63 @@ function isInsideZodObject(parentStack: Node[]): boolean {
334
329
  }
335
330
 
336
331
  function isZodObjectCall(memberExpression: Node): boolean {
337
- if (memberExpression.type !== "MemberExpression") {
332
+ if (
333
+ memberExpression.type !== "MemberExpression" ||
334
+ !("object" in memberExpression) ||
335
+ !("property" in memberExpression)
336
+ ) {
338
337
  return false
339
338
  }
340
339
 
341
- // Handle direct z.object() calls
340
+ const member = memberExpression as MemberExpression
341
+
342
+ // handle direct z.object() calls
342
343
  if (
343
- memberExpression.object.type === "Identifier" &&
344
- memberExpression.object.name === "z" &&
345
- memberExpression.property.type === "Identifier" &&
346
- memberExpression.property.name === "object"
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"
347
349
  ) {
348
350
  return true
349
351
  }
350
352
 
351
- // Handle chained calls like z.discriminatedUnion().default().object()
353
+ // handle chained calls like z.discriminatedUnion().default().object()
352
354
  // or any other chained Zod methods that end with .object()
353
355
  if (
354
- memberExpression.property.type === "Identifier" &&
355
- memberExpression.property.name === "object" &&
356
- memberExpression.object.type === "CallExpression" &&
357
- memberExpression.object.callee.type === "MemberExpression"
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"
358
361
  ) {
359
- // Recursively check if this is part of a z.* chain
360
- return startsWithZodCall(memberExpression.object)
362
+ // recursively check if this is part of a z.* chain
363
+ return startsWithZodCall(member.object)
361
364
  }
362
365
 
363
366
  return false
364
367
  }
365
368
 
366
- function startsWithZodCall(callExpression: Node): boolean {
369
+ function startsWithZodCall(callExpression: CallExpression): boolean {
367
370
  if (!callExpression || callExpression.type !== "CallExpression") {
368
371
  return false
369
372
  }
370
373
 
371
374
  if (callExpression.callee.type === "MemberExpression") {
372
- // Check if this is a direct z.* call
375
+ const callee = callExpression.callee
376
+
377
+ // check if this is a direct z.* call
373
378
  if (
374
- callExpression.callee.object.type === "Identifier" &&
375
- callExpression.callee.object.name === "z"
379
+ callee.object.type === "Identifier" &&
380
+ "name" in callee.object &&
381
+ callee.object.name === "z"
376
382
  ) {
377
383
  return true
378
384
  }
379
385
 
380
- // Recursively check nested calls
381
- if (callExpression.callee.object.type === "CallExpression") {
382
- return startsWithZodCall(callExpression.callee.object)
386
+ // recursively check nested calls
387
+ if (callee.object.type === "CallExpression") {
388
+ return startsWithZodCall(callee.object)
383
389
  }
384
390
  }
385
391
 
@@ -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 => ({