@highstate/cli 0.9.14 → 0.9.16
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/highstate.manifest.json +1 -1
- package/dist/library-loader-CGEPTS4L.js +78 -0
- package/dist/library-loader-CGEPTS4L.js.map +1 -0
- package/dist/main.js +257 -1096
- package/dist/main.js.map +1 -1
- package/package.json +27 -6
- package/src/commands/backend/identity.ts +24 -0
- package/src/commands/build.ts +41 -5
- package/src/main.ts +2 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/library-loader.ts +113 -0
- package/src/shared/schema-transformer.spec.ts +209 -0
- package/src/shared/schema-transformer.ts +112 -11
- package/src/shared/schemas.ts +41 -0
- package/src/shared/source-hash-calculator.ts +129 -26
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
type ComponentModel,
|
|
5
|
+
type Entity,
|
|
6
|
+
isComponent,
|
|
7
|
+
isEntity,
|
|
8
|
+
isUnitModel,
|
|
9
|
+
originalCreate,
|
|
10
|
+
} from "@highstate/contract"
|
|
11
|
+
import { serializeFunction } from "@pulumi/pulumi/runtime/index.js"
|
|
12
|
+
import { Crc32, crc32 } from "@aws-crypto/crc32"
|
|
13
|
+
import { encode } from "@msgpack/msgpack"
|
|
14
|
+
import { int32ToBytes } from "@highstate/backend/shared"
|
|
15
|
+
|
|
16
|
+
export type Library = Readonly<{
|
|
17
|
+
components: Readonly<Record<string, ComponentModel>>
|
|
18
|
+
entities: Readonly<Record<string, Entity>>
|
|
19
|
+
}>
|
|
20
|
+
|
|
21
|
+
export async function loadLibrary(logger: Logger, modulePaths: string[]): Promise<Library> {
|
|
22
|
+
const modules: Record<string, unknown> = {}
|
|
23
|
+
for (const modulePath of modulePaths) {
|
|
24
|
+
try {
|
|
25
|
+
logger.debug({ modulePath }, "loading module")
|
|
26
|
+
modules[modulePath] = await import(modulePath)
|
|
27
|
+
|
|
28
|
+
logger.debug({ modulePath }, "module loaded")
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.error({ modulePath, err }, "module load failed")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const components: Record<string, ComponentModel> = {}
|
|
35
|
+
const entities: Record<string, Entity> = {}
|
|
36
|
+
|
|
37
|
+
await _loadLibrary(modules, components, entities)
|
|
38
|
+
|
|
39
|
+
logger.info(
|
|
40
|
+
{
|
|
41
|
+
componentCount: Object.keys(components).length,
|
|
42
|
+
entityCount: Object.keys(entities).length,
|
|
43
|
+
},
|
|
44
|
+
"library loaded",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger.trace({ components, entities }, "library content")
|
|
48
|
+
|
|
49
|
+
return { components, entities }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function _loadLibrary(
|
|
53
|
+
value: unknown,
|
|
54
|
+
components: Record<string, ComponentModel>,
|
|
55
|
+
entities: Record<string, Entity>,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
if (isComponent(value)) {
|
|
58
|
+
const entityHashes: number[] = []
|
|
59
|
+
for (const entity of value.entities.values()) {
|
|
60
|
+
entity.definitionHash ??= calculateEntityDefinitionHash(entity)
|
|
61
|
+
entityHashes.push(entity.definitionHash)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
components[value.model.type] = value.model
|
|
65
|
+
value.model.definitionHash = await calculateComponentDefinitionHash(value, entityHashes)
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isEntity(value)) {
|
|
71
|
+
entities[value.type] = value
|
|
72
|
+
entities[value.type].definitionHash ??= calculateEntityDefinitionHash(value)
|
|
73
|
+
|
|
74
|
+
// @ts-expect-error remove the schema since it's not needed in the designer
|
|
75
|
+
delete value.schema
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof value !== "object" || value === null) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const key in value) {
|
|
84
|
+
await _loadLibrary((value as Record<string, unknown>)[key], components, entities)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function calculateComponentDefinitionHash(
|
|
89
|
+
component: Component,
|
|
90
|
+
entityHashes: number[],
|
|
91
|
+
): Promise<number> {
|
|
92
|
+
const result = new Crc32()
|
|
93
|
+
|
|
94
|
+
// 1. include the full component model
|
|
95
|
+
result.update(encode(component.model))
|
|
96
|
+
|
|
97
|
+
if (!isUnitModel(component.model)) {
|
|
98
|
+
// 2. for composite components, include the content of the serialized create function
|
|
99
|
+
const serializedCreate = await serializeFunction(component[originalCreate])
|
|
100
|
+
result.update(Buffer.from(serializedCreate.text))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. include the hashes of all entities
|
|
104
|
+
for (const entityHash of entityHashes) {
|
|
105
|
+
result.update(int32ToBytes(entityHash))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result.digest()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function calculateEntityDefinitionHash(entity: Entity): number {
|
|
112
|
+
return crc32(encode(entity))
|
|
113
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { applySchemaTransformations } from "./schema-transformer"
|
|
3
|
+
|
|
4
|
+
describe("applySchemaTransformations", () => {
|
|
5
|
+
it("should transform simple values to entity/schema structure", async () => {
|
|
6
|
+
const input = `
|
|
7
|
+
const spec = {
|
|
8
|
+
inputs: {
|
|
9
|
+
/**
|
|
10
|
+
* The Kubernetes cluster to deploy on.
|
|
11
|
+
*/
|
|
12
|
+
cluster: clusterEntity,
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
/**
|
|
16
|
+
* The port number to use.
|
|
17
|
+
*/
|
|
18
|
+
port: Type.Number(),
|
|
19
|
+
},
|
|
20
|
+
}`
|
|
21
|
+
|
|
22
|
+
const result = await applySchemaTransformations(input)
|
|
23
|
+
|
|
24
|
+
expect(result).toContain("entity: clusterEntity")
|
|
25
|
+
expect(result).toContain("schema: Type.Number()")
|
|
26
|
+
expect(result).toContain("description: `The Kubernetes cluster to deploy on.`")
|
|
27
|
+
expect(result).toContain("description: `The port number to use.`")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("should inject description into existing meta field", async () => {
|
|
31
|
+
const input = `
|
|
32
|
+
const spec = {
|
|
33
|
+
args: {
|
|
34
|
+
/**
|
|
35
|
+
* The API token for authentication.
|
|
36
|
+
*/
|
|
37
|
+
token: {
|
|
38
|
+
schema: Type.String(),
|
|
39
|
+
meta: {
|
|
40
|
+
displayName: "API Token",
|
|
41
|
+
sensitive: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}`
|
|
46
|
+
|
|
47
|
+
const result = await applySchemaTransformations(input)
|
|
48
|
+
|
|
49
|
+
expect(result).toContain('displayName: "API Token"')
|
|
50
|
+
expect(result).toContain("sensitive: true")
|
|
51
|
+
expect(result).toContain("description: `The API token for authentication.`")
|
|
52
|
+
expect(result).not.toContain("...obj")
|
|
53
|
+
expect(result).not.toContain("((obj) =>")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should add meta field if it doesn't exist in structured object", async () => {
|
|
57
|
+
const input = `
|
|
58
|
+
const spec = {
|
|
59
|
+
inputs: {
|
|
60
|
+
/**
|
|
61
|
+
* The target endpoint.
|
|
62
|
+
*/
|
|
63
|
+
endpoint: {
|
|
64
|
+
entity: endpointEntity,
|
|
65
|
+
required: false,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}`
|
|
69
|
+
|
|
70
|
+
const result = await applySchemaTransformations(input)
|
|
71
|
+
|
|
72
|
+
expect(result).toContain("entity: endpointEntity")
|
|
73
|
+
expect(result).toContain("required: false")
|
|
74
|
+
expect(result).toContain("meta: {")
|
|
75
|
+
expect(result).toContain("description: `The target endpoint.`")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should handle $args marker function", async () => {
|
|
79
|
+
const input = `
|
|
80
|
+
const spec = {
|
|
81
|
+
args: $args({
|
|
82
|
+
/**
|
|
83
|
+
* The configuration file path.
|
|
84
|
+
*/
|
|
85
|
+
configPath: Type.String(),
|
|
86
|
+
}),
|
|
87
|
+
}`
|
|
88
|
+
|
|
89
|
+
const result = await applySchemaTransformations(input)
|
|
90
|
+
|
|
91
|
+
expect(result).toContain("schema: Type.String()")
|
|
92
|
+
expect(result).toContain("description: `The configuration file path.`")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should handle $inputs marker function", async () => {
|
|
96
|
+
const input = `
|
|
97
|
+
const spec = {
|
|
98
|
+
inputs: $inputs({
|
|
99
|
+
/**
|
|
100
|
+
* The source data.
|
|
101
|
+
*/
|
|
102
|
+
source: dataEntity,
|
|
103
|
+
}),
|
|
104
|
+
}`
|
|
105
|
+
|
|
106
|
+
const result = await applySchemaTransformations(input)
|
|
107
|
+
|
|
108
|
+
expect(result).toContain("entity: dataEntity")
|
|
109
|
+
expect(result).toContain("description: `The source data.`")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("should handle $outputs marker function", async () => {
|
|
113
|
+
const input = `
|
|
114
|
+
const spec = {
|
|
115
|
+
outputs: $outputs({
|
|
116
|
+
/**
|
|
117
|
+
* The processed result.
|
|
118
|
+
*/
|
|
119
|
+
result: resultEntity,
|
|
120
|
+
}),
|
|
121
|
+
}`
|
|
122
|
+
|
|
123
|
+
const result = await applySchemaTransformations(input)
|
|
124
|
+
|
|
125
|
+
expect(result).toContain("entity: resultEntity")
|
|
126
|
+
expect(result).toContain("description: `The processed result.`")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should handle $secrets marker function", async () => {
|
|
130
|
+
const input = `
|
|
131
|
+
const spec = {
|
|
132
|
+
secrets: $secrets({
|
|
133
|
+
/**
|
|
134
|
+
* The database password.
|
|
135
|
+
*/
|
|
136
|
+
dbPassword: Type.String(),
|
|
137
|
+
}),
|
|
138
|
+
}`
|
|
139
|
+
|
|
140
|
+
const result = await applySchemaTransformations(input)
|
|
141
|
+
|
|
142
|
+
expect(result).toContain("schema: Type.String()")
|
|
143
|
+
expect(result).toContain("description: `The database password.`")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should ignore properties without JSDoc comments", async () => {
|
|
147
|
+
const input = `
|
|
148
|
+
const spec = {
|
|
149
|
+
inputs: {
|
|
150
|
+
cluster: clusterEntity,
|
|
151
|
+
/**
|
|
152
|
+
* Only this one has a comment.
|
|
153
|
+
*/
|
|
154
|
+
endpoint: endpointEntity,
|
|
155
|
+
},
|
|
156
|
+
}`
|
|
157
|
+
|
|
158
|
+
const result = await applySchemaTransformations(input)
|
|
159
|
+
|
|
160
|
+
expect(result).toContain("cluster: clusterEntity") // unchanged
|
|
161
|
+
expect(result).toContain("entity: endpointEntity") // transformed
|
|
162
|
+
expect(result).toContain("description: `Only this one has a comment.`")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("should ignore properties not in target objects", async () => {
|
|
166
|
+
const input = `
|
|
167
|
+
const config = {
|
|
168
|
+
/**
|
|
169
|
+
* This should not be transformed.
|
|
170
|
+
*/
|
|
171
|
+
someProperty: "value",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const spec = {
|
|
175
|
+
inputs: {
|
|
176
|
+
/**
|
|
177
|
+
* This should be transformed.
|
|
178
|
+
*/
|
|
179
|
+
cluster: clusterEntity,
|
|
180
|
+
},
|
|
181
|
+
}`
|
|
182
|
+
|
|
183
|
+
const result = await applySchemaTransformations(input)
|
|
184
|
+
|
|
185
|
+
expect(result).toContain('someProperty: "value"') // unchanged
|
|
186
|
+
expect(result).toContain("entity: clusterEntity") // transformed
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("should clean JSDoc comments properly", async () => {
|
|
190
|
+
const input = `
|
|
191
|
+
const spec = {
|
|
192
|
+
args: {
|
|
193
|
+
/**
|
|
194
|
+
* This is a description with \`backticks\` and \${template} literals.
|
|
195
|
+
* It also has multiple lines.
|
|
196
|
+
*/
|
|
197
|
+
value: Type.String(),
|
|
198
|
+
},
|
|
199
|
+
}`
|
|
200
|
+
|
|
201
|
+
const result = await applySchemaTransformations(input)
|
|
202
|
+
|
|
203
|
+
expect(result).toContain("schema: Type.String()")
|
|
204
|
+
expect(result).toContain(
|
|
205
|
+
"description: `This is a description with \\`backticks\\` and \\${template} literals.",
|
|
206
|
+
)
|
|
207
|
+
expect(result).toContain("It also has multiple lines.`")
|
|
208
|
+
})
|
|
209
|
+
})
|
|
@@ -22,31 +22,135 @@ export async function applySchemaTransformations(content: string): Promise<strin
|
|
|
22
22
|
const magicString = new MagicString(content)
|
|
23
23
|
const { program, comments } = await parseAsync("file.ts", content)
|
|
24
24
|
|
|
25
|
+
const parentStack: Node[] = []
|
|
26
|
+
|
|
25
27
|
walk(program, {
|
|
26
28
|
enter(node) {
|
|
29
|
+
parentStack.push(node)
|
|
30
|
+
|
|
27
31
|
if (node.type !== "Property" || node.key.type !== "Identifier") {
|
|
28
32
|
return
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
const parentKey = getParentObjectKey(parentStack) || getMarkerFunctionName(parentStack)
|
|
36
|
+
if (!parentKey || !["inputs", "outputs", "args", "secrets"].includes(parentKey)) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
const jsdoc = comments.find(comment => isLeadingComment(content, node, comment))
|
|
32
|
-
if (!jsdoc
|
|
41
|
+
if (!jsdoc) {
|
|
33
42
|
return
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
},
|
|
42
67
|
}`,
|
|
43
|
-
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
leave() {
|
|
72
|
+
parentStack.pop()
|
|
44
73
|
},
|
|
45
74
|
})
|
|
46
75
|
|
|
47
76
|
return magicString.toString()
|
|
48
77
|
}
|
|
49
78
|
|
|
79
|
+
function injectDescriptionIntoObject(objectString: string, description: string): string {
|
|
80
|
+
const trimmed = objectString.trim()
|
|
81
|
+
|
|
82
|
+
// Check if the object already has a meta field
|
|
83
|
+
const metaRegex = /meta\s*:\s*\{/
|
|
84
|
+
|
|
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
|
+
}
|
|
99
|
+
|
|
100
|
+
const beforeBrace = trimmed.substring(0, lastBraceIndex)
|
|
101
|
+
const afterBrace = trimmed.substring(lastBraceIndex)
|
|
102
|
+
|
|
103
|
+
// Check if we need a comma before adding meta
|
|
104
|
+
const needsComma = beforeBrace.trim().length > 1 && !beforeBrace.trim().endsWith(",")
|
|
105
|
+
const comma = needsComma ? "," : ""
|
|
106
|
+
|
|
107
|
+
return `${beforeBrace}${comma}
|
|
108
|
+
meta: {
|
|
109
|
+
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
|
+
}
|
|
120
|
+
|
|
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)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getMarkerFunctionName(parentStack: Node[]): string | null {
|
|
127
|
+
// Look for marker functions like $args, $inputs, $outputs, $secrets
|
|
128
|
+
for (let i = parentStack.length - 1; i >= 0; i--) {
|
|
129
|
+
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
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getParentObjectKey(parentStack: Node[]): string | null {
|
|
144
|
+
// Walk up the parent stack to find the parent object property
|
|
145
|
+
for (let i = parentStack.length - 2; i >= 0; i--) {
|
|
146
|
+
const node = parentStack[i]
|
|
147
|
+
if (node.type === "Property" && node.key.type === "Identifier") {
|
|
148
|
+
return node.key.name
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
|
|
50
154
|
function isLeadingComment(content: string, node: Node, comment: Comment) {
|
|
51
155
|
if (comment.end > node.start) {
|
|
52
156
|
return false
|
|
@@ -63,9 +167,6 @@ function cleanJsdoc(str: string) {
|
|
|
63
167
|
// remove leading asterisks
|
|
64
168
|
.replace(/^\s*\*/gm, "")
|
|
65
169
|
|
|
66
|
-
// remove @schema tag
|
|
67
|
-
.replace("@schema", "")
|
|
68
|
-
|
|
69
170
|
// escape backticks and dollar signs
|
|
70
171
|
.replace(/\\/g, "\\\\")
|
|
71
172
|
.replace(/`/g, "\\`")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for the sourceHash configuration in package.json
|
|
5
|
+
*/
|
|
6
|
+
export const sourceHashConfigSchema = z.discriminatedUnion("mode", [
|
|
7
|
+
z.object({
|
|
8
|
+
mode: z.literal("manual"),
|
|
9
|
+
version: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
z.object({
|
|
12
|
+
mode: z.literal("auto"),
|
|
13
|
+
}),
|
|
14
|
+
z.object({
|
|
15
|
+
mode: z.literal("version"),
|
|
16
|
+
}),
|
|
17
|
+
z.object({
|
|
18
|
+
mode: z.literal("none"),
|
|
19
|
+
}),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Schema for the highstate configuration in package.json
|
|
24
|
+
*/
|
|
25
|
+
export const highstateConfigSchema = z.object({
|
|
26
|
+
type: z.enum(["source", "library", "worker"]).default("source"),
|
|
27
|
+
sourceHash: z
|
|
28
|
+
.union([sourceHashConfigSchema, z.record(z.string(), sourceHashConfigSchema)])
|
|
29
|
+
.optional(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Schema for the highstate manifest file
|
|
34
|
+
*/
|
|
35
|
+
export const highstateManifestSchema = z.object({
|
|
36
|
+
sourceHashes: z.record(z.string(), z.number()).optional(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export type SourceHashConfig = z.infer<typeof sourceHashConfigSchema>
|
|
40
|
+
export type HighstateConfig = z.infer<typeof highstateConfigSchema>
|
|
41
|
+
export type HighstateManifest = z.infer<typeof highstateManifestSchema>
|