@highstate/cli 0.16.0 → 0.17.0
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-VNBLDKUT.js → chunk-SA46IMPG.js} +106 -34
- package/dist/chunk-SA46IMPG.js.map +1 -0
- package/dist/commands/index.js +1 -1
- package/dist/highstate.manifest.json +2 -2
- package/dist/main.js +1 -1
- package/package.json +3 -3
- package/src/shared/schema-transformer.spec.ts +27 -0
- package/src/shared/schema-transformer.ts +117 -11
- package/src/shared/source-hash-calculator.test.ts +73 -0
- package/src/shared/source-hash-calculator.ts +39 -39
- package/dist/chunk-VNBLDKUT.js.map +0 -1
|
@@ -50,17 +50,44 @@ async function applyZodMetaTransformations(content: string): Promise<string> {
|
|
|
50
50
|
// handle zod object patterns
|
|
51
51
|
if (isZodObjectProperty(node, parentStack)) {
|
|
52
52
|
const jsdoc = findLeadingComment(content, node, comments)
|
|
53
|
-
if (jsdoc) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
if (!jsdoc) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const description = cleanJsdoc(jsdoc.value)
|
|
58
|
+
const fieldName =
|
|
59
|
+
"name" in node.key && typeof node.key.name === "string" ? node.key.name : "unknown"
|
|
60
|
+
|
|
61
|
+
// Getter properties (get foo() { return ... }) need special handling:
|
|
62
|
+
// we inject `.meta(...)` into the returned schema expression.
|
|
63
|
+
if (isGetterProperty(node)) {
|
|
64
|
+
const returnArgument = findFirstReturnArgument(node)
|
|
65
|
+
if (!returnArgument) {
|
|
66
|
+
return
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
const originalReturnValue = content.substring(returnArgument.start, returnArgument.end)
|
|
70
|
+
if (originalReturnValue.includes(".meta(")) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newReturnValue = `${originalReturnValue}.meta({ title: __camelCaseToHumanReadable("${fieldName}"), description: \`${description}\` })`
|
|
75
|
+
result.update(returnArgument.start, returnArgument.end, newReturnValue)
|
|
76
|
+
hasTransformations = true
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Standard properties (foo: z.string())
|
|
81
|
+
if (!hasValue(node) || !hasSourceRange(node.value)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const originalValue = content.substring(node.value.start, node.value.end)
|
|
86
|
+
|
|
87
|
+
if (!originalValue.includes(".meta(")) {
|
|
88
|
+
const newValue = `${originalValue}.meta({ title: __camelCaseToHumanReadable("${fieldName}"), description: \`${description}\` })`
|
|
89
|
+
result.update(node.value.start, node.value.end, newValue)
|
|
90
|
+
hasTransformations = true
|
|
64
91
|
}
|
|
65
92
|
}
|
|
66
93
|
},
|
|
@@ -101,11 +128,36 @@ async function applyHelperFunctionTransformations(content: string): Promise<stri
|
|
|
101
128
|
|
|
102
129
|
if (jsdoc) {
|
|
103
130
|
const description = cleanJsdoc(jsdoc.value)
|
|
131
|
+
// Getter properties (get foo() { return ... }) need special handling:
|
|
132
|
+
// inject the helper call into the returned expression.
|
|
133
|
+
if (isGetterProperty(propertyNode)) {
|
|
134
|
+
const returnArgument = findFirstReturnArgument(propertyNode)
|
|
135
|
+
if (!returnArgument) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const originalReturnValue = content.substring(
|
|
140
|
+
returnArgument.start,
|
|
141
|
+
returnArgument.end,
|
|
142
|
+
)
|
|
143
|
+
if (originalReturnValue.trimStart().startsWith(`${helperFunction}(`)) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const newReturnValue = `${helperFunction}(${originalReturnValue}, \`${description}\`)`
|
|
148
|
+
result.update(returnArgument.start, returnArgument.end, newReturnValue)
|
|
149
|
+
hasTransformations = true
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!hasValue(propertyNode) || !hasSourceRange(propertyNode.value)) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
const originalValue = content.substring(
|
|
105
158
|
propertyNode.value.start,
|
|
106
159
|
propertyNode.value.end,
|
|
107
160
|
)
|
|
108
|
-
|
|
109
161
|
const newValue = `${helperFunction}(${originalValue}, \`${description}\`)`
|
|
110
162
|
result.update(propertyNode.value.start, propertyNode.value.end, newValue)
|
|
111
163
|
hasTransformations = true
|
|
@@ -242,6 +294,60 @@ function isZodObjectProperty(node: Node, parentStack: Node[]): node is ObjectPro
|
|
|
242
294
|
)
|
|
243
295
|
}
|
|
244
296
|
|
|
297
|
+
type GetterObjectProperty = ObjectProperty & {
|
|
298
|
+
kind: "get"
|
|
299
|
+
value: Node & { type: "FunctionExpression" }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isGetterProperty(node: Node): node is GetterObjectProperty {
|
|
303
|
+
if (node.type !== "Property") {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!hasValue(node)) {
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return "kind" in node && node.kind === "get" && node.value.type === "FunctionExpression"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function findFirstReturnArgument(node: ObjectProperty): Node | null {
|
|
315
|
+
if (!hasValue(node) || node.value.type !== "FunctionExpression") {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const body = node.value.body
|
|
320
|
+
if (!body || body.type !== "BlockStatement") {
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const statement of body.body) {
|
|
325
|
+
if (statement.type !== "ReturnStatement") {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const returnArgument = "argument" in statement ? statement.argument : null
|
|
330
|
+
if (returnArgument && hasSourceRange(returnArgument)) {
|
|
331
|
+
return returnArgument
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function hasValue(node: Node): node is Node & { value: Node } {
|
|
339
|
+
return "value" in node && !!node.value
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function hasSourceRange(node: Node): node is Node & { start: number; end: number } {
|
|
343
|
+
return (
|
|
344
|
+
"start" in node &&
|
|
345
|
+
typeof node.start === "number" &&
|
|
346
|
+
"end" in node &&
|
|
347
|
+
typeof node.end === "number"
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
245
351
|
function findLeadingComment(content: string, node: Node, comments: Comment[]): Comment | null {
|
|
246
352
|
return comments.find(comment => isLeadingComment(content, node, comment)) ?? null
|
|
247
353
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest"
|
|
2
|
+
import { parseFileDependencies } from "./source-hash-calculator"
|
|
3
|
+
|
|
4
|
+
describe("parseFileDependencies", () => {
|
|
5
|
+
test("parses relative imports", () => {
|
|
6
|
+
const filePath = "/repo/src/main.ts"
|
|
7
|
+
const content = "import { x } from './utils'\n"
|
|
8
|
+
|
|
9
|
+
const deps = parseFileDependencies(filePath, content)
|
|
10
|
+
|
|
11
|
+
expect(deps).toEqual([
|
|
12
|
+
{
|
|
13
|
+
type: "relative",
|
|
14
|
+
id: "relative:/repo/src/utils",
|
|
15
|
+
fullPath: "/repo/src/utils",
|
|
16
|
+
},
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("parses npm imports", () => {
|
|
21
|
+
const filePath = "/repo/src/main.ts"
|
|
22
|
+
const content = "import { mapValues } from 'remeda'\n"
|
|
23
|
+
|
|
24
|
+
const deps = parseFileDependencies(filePath, content)
|
|
25
|
+
|
|
26
|
+
expect(deps).toEqual([
|
|
27
|
+
{
|
|
28
|
+
type: "npm",
|
|
29
|
+
id: "npm:remeda",
|
|
30
|
+
package: "remeda",
|
|
31
|
+
},
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("ignores node: built-in imports", () => {
|
|
36
|
+
const filePath = "/repo/src/main.ts"
|
|
37
|
+
const content = "import { readFile } from 'node:fs/promises'\n"
|
|
38
|
+
|
|
39
|
+
const deps = parseFileDependencies(filePath, content)
|
|
40
|
+
|
|
41
|
+
expect(deps).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("supports multi-line imports", () => {
|
|
45
|
+
const filePath = "/repo/src/main.ts"
|
|
46
|
+
const content = 'import {\n a,\n b,\n} from "pkg-types"\n'
|
|
47
|
+
|
|
48
|
+
const deps = parseFileDependencies(filePath, content)
|
|
49
|
+
|
|
50
|
+
expect(deps).toEqual([
|
|
51
|
+
{
|
|
52
|
+
type: "npm",
|
|
53
|
+
id: "npm:pkg-types",
|
|
54
|
+
package: "pkg-types",
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("does not match identifiers like importBasePath or template strings containing from", () => {
|
|
60
|
+
const filePath = "/repo/src/main.ts"
|
|
61
|
+
const content = [
|
|
62
|
+
"const x = {",
|
|
63
|
+
" importBasePath: '/some/path',",
|
|
64
|
+
"}",
|
|
65
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: false positive
|
|
66
|
+
'throw new Error(`Cannot use output "foo" from "${input.input.instanceId}"`)\n',
|
|
67
|
+
].join("\n")
|
|
68
|
+
|
|
69
|
+
const deps = parseFileDependencies(filePath, content)
|
|
70
|
+
|
|
71
|
+
expect(deps).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -28,6 +28,44 @@ type FileDependency =
|
|
|
28
28
|
package: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function parseFileDependencies(filePath: string, content: string): FileDependency[] {
|
|
32
|
+
type DependencyMatch = {
|
|
33
|
+
relativePath?: string
|
|
34
|
+
nodeBuiltin?: string
|
|
35
|
+
npmPackage?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dependencyRegex =
|
|
39
|
+
/^[ \t]*import\b[\s\S]*?\bfrom\s*["']((?<relativePath>\.\.?\/[^"']+)|(?<nodeBuiltin>node:[^"']+)|(?<npmPackage>[^"']+))["']/gm
|
|
40
|
+
|
|
41
|
+
const matches = content.matchAll(dependencyRegex)
|
|
42
|
+
const dependencies: FileDependency[] = []
|
|
43
|
+
|
|
44
|
+
for (const match of matches) {
|
|
45
|
+
const { nodeBuiltin, npmPackage, relativePath } = match.groups as DependencyMatch
|
|
46
|
+
|
|
47
|
+
if (relativePath) {
|
|
48
|
+
const fullPath = resolve(dirname(filePath), relativePath)
|
|
49
|
+
|
|
50
|
+
dependencies.push({
|
|
51
|
+
type: "relative",
|
|
52
|
+
id: `relative:${fullPath}`,
|
|
53
|
+
fullPath,
|
|
54
|
+
})
|
|
55
|
+
} else if (npmPackage) {
|
|
56
|
+
dependencies.push({
|
|
57
|
+
type: "npm",
|
|
58
|
+
id: `npm:${npmPackage}`,
|
|
59
|
+
package: npmPackage,
|
|
60
|
+
})
|
|
61
|
+
} else if (nodeBuiltin) {
|
|
62
|
+
// ignore node built-in modules
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return dependencies
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
export class SourceHashCalculator {
|
|
32
70
|
private readonly dependencyHashes = new Map<string, Promise<number>>()
|
|
33
71
|
private readonly fileHashes = new Map<string, Promise<number>>()
|
|
@@ -174,7 +212,7 @@ export class SourceHashCalculator {
|
|
|
174
212
|
|
|
175
213
|
private async calculateFileHash(fullPath: string): Promise<number> {
|
|
176
214
|
const content = await readFile(fullPath, "utf8")
|
|
177
|
-
const fileDeps =
|
|
215
|
+
const fileDeps = parseFileDependencies(fullPath, content)
|
|
178
216
|
|
|
179
217
|
const hashes = await Promise.all([
|
|
180
218
|
this.hashString(content),
|
|
@@ -284,42 +322,4 @@ export class SourceHashCalculator {
|
|
|
284
322
|
basePath = resolve(dirname(basePath), "..")
|
|
285
323
|
}
|
|
286
324
|
}
|
|
287
|
-
|
|
288
|
-
private parseDependencies(filePath: string, content: string): FileDependency[] {
|
|
289
|
-
type DependencyMatch = {
|
|
290
|
-
relativePath?: string
|
|
291
|
-
nodeBuiltin?: string
|
|
292
|
-
npmPackage?: string
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const dependencyRegex =
|
|
296
|
-
/^[ \t]*import[\s\S]*?\bfrom\s*["']((?<relativePath>\.\.?\/[^"']+)|(?<nodeBuiltin>node:[^"']+)|(?<npmPackage>[^"']+))["']/gm
|
|
297
|
-
|
|
298
|
-
const matches = content.matchAll(dependencyRegex)
|
|
299
|
-
const dependencies: FileDependency[] = []
|
|
300
|
-
|
|
301
|
-
for (const match of matches) {
|
|
302
|
-
const { nodeBuiltin, npmPackage, relativePath } = match.groups as DependencyMatch
|
|
303
|
-
|
|
304
|
-
if (relativePath) {
|
|
305
|
-
const fullPath = resolve(dirname(filePath), relativePath)
|
|
306
|
-
|
|
307
|
-
dependencies.push({
|
|
308
|
-
type: "relative",
|
|
309
|
-
id: `relative:${fullPath}`,
|
|
310
|
-
fullPath,
|
|
311
|
-
})
|
|
312
|
-
} else if (npmPackage) {
|
|
313
|
-
dependencies.push({
|
|
314
|
-
type: "npm",
|
|
315
|
-
id: `npm:${npmPackage}`,
|
|
316
|
-
package: npmPackage,
|
|
317
|
-
})
|
|
318
|
-
} else if (nodeBuiltin) {
|
|
319
|
-
// ignore node built-in modules
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return dependencies
|
|
324
|
-
}
|
|
325
325
|
}
|