@highstate/cli 0.9.15 → 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.
- package/dist/chunk-CMECLVT7.js +11 -0
- package/dist/chunk-CMECLVT7.js.map +1 -0
- package/dist/highstate.manifest.json +1 -1
- package/dist/library-loader-ZABUULFB.js +83 -0
- package/dist/library-loader-ZABUULFB.js.map +1 -0
- package/dist/main.js +407 -1145
- package/dist/main.js.map +1 -1
- package/package.json +24 -6
- package/src/commands/backend/identity.ts +24 -0
- package/src/commands/build.ts +42 -5
- package/src/main.ts +2 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/library-loader.ts +129 -0
- package/src/shared/schema-transformer.spec.ts +489 -0
- package/src/shared/schema-transformer.ts +325 -13
- package/src/shared/schemas.ts +41 -0
- package/src/shared/source-hash-calculator.ts +129 -26
- package/src/shared/utils.ts +6 -0
|
@@ -18,33 +18,280 @@ 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[] = []
|
|
32
|
+
const parentStack: Node[] = []
|
|
33
|
+
|
|
25
34
|
walk(program, {
|
|
26
35
|
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))
|
|
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
|
+
}
|
|
60
|
+
|
|
27
61
|
if (node.type !== "Property" || node.key.type !== "Identifier") {
|
|
28
62
|
return
|
|
29
63
|
}
|
|
30
64
|
|
|
65
|
+
const parentKey = getParentObjectKey(parentStack) || getMarkerFunctionName(parentStack)
|
|
66
|
+
if (!parentKey || !["inputs", "outputs", "args", "secrets"].includes(parentKey)) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
31
70
|
const jsdoc = comments.find(comment => isLeadingComment(content, node, comment))
|
|
32
|
-
if (!jsdoc
|
|
71
|
+
if (!jsdoc) {
|
|
33
72
|
return
|
|
34
73
|
}
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
},
|
|
42
102
|
}`,
|
|
43
|
-
|
|
103
|
+
type: "schema-structure",
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
leave() {
|
|
108
|
+
parentStack.pop()
|
|
44
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
|
+
})
|
|
45
180
|
})
|
|
46
181
|
|
|
47
|
-
|
|
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
|
|
212
|
+
}
|
|
213
|
+
|
|
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)
|
|
245
|
+
|
|
246
|
+
// Check if we need a comma before adding meta
|
|
247
|
+
const needsComma = beforeBrace.trim().length > 1 && !beforeBrace.trim().endsWith(",")
|
|
248
|
+
const comma = needsComma ? "," : ""
|
|
249
|
+
|
|
250
|
+
return `${beforeBrace}${comma}
|
|
251
|
+
meta: {
|
|
252
|
+
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
|
+
}
|
|
263
|
+
|
|
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)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getMarkerFunctionName(parentStack: Node[]): string | null {
|
|
270
|
+
// Look for marker functions like $args, $inputs, $outputs, $secrets
|
|
271
|
+
for (let i = parentStack.length - 1; i >= 0; i--) {
|
|
272
|
+
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
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getParentObjectKey(parentStack: Node[]): string | null {
|
|
287
|
+
// Walk up the parent stack to find the parent object property
|
|
288
|
+
for (let i = parentStack.length - 2; i >= 0; i--) {
|
|
289
|
+
const node = parentStack[i]
|
|
290
|
+
if (node.type === "Property" && node.key.type === "Identifier") {
|
|
291
|
+
return node.key.name
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null
|
|
48
295
|
}
|
|
49
296
|
|
|
50
297
|
function isLeadingComment(content: string, node: Node, comment: Comment) {
|
|
@@ -63,9 +310,6 @@ function cleanJsdoc(str: string) {
|
|
|
63
310
|
// remove leading asterisks
|
|
64
311
|
.replace(/^\s*\*/gm, "")
|
|
65
312
|
|
|
66
|
-
// remove @schema tag
|
|
67
|
-
.replace("@schema", "")
|
|
68
|
-
|
|
69
313
|
// escape backticks and dollar signs
|
|
70
314
|
.replace(/\\/g, "\\\\")
|
|
71
315
|
.replace(/`/g, "\\`")
|
|
@@ -73,3 +317,71 @@ function cleanJsdoc(str: string) {
|
|
|
73
317
|
.trim()
|
|
74
318
|
)
|
|
75
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
|
+
}
|
|
@@ -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>
|
|
@@ -3,12 +3,18 @@ import { dirname, relative, resolve } from "node:path"
|
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises"
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
5
|
import { readPackageJSON, resolvePackageJSON, type PackageJson } from "pkg-types"
|
|
6
|
-
import {
|
|
6
|
+
import { crc32 } from "@aws-crypto/crc32"
|
|
7
7
|
import { resolve as importMetaResolve } from "import-meta-resolve"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
import {
|
|
10
|
+
type HighstateManifest,
|
|
11
|
+
type HighstateConfig,
|
|
12
|
+
type SourceHashConfig,
|
|
13
|
+
highstateConfigSchema,
|
|
14
|
+
highstateManifestSchema,
|
|
15
|
+
sourceHashConfigSchema,
|
|
16
|
+
} from "./schemas"
|
|
17
|
+
import { int32ToBytes } from "./utils"
|
|
12
18
|
|
|
13
19
|
type FileDependency =
|
|
14
20
|
| {
|
|
@@ -23,8 +29,8 @@ type FileDependency =
|
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export class SourceHashCalculator {
|
|
26
|
-
private readonly dependencyHashes = new Map<string, Promise<
|
|
27
|
-
private readonly fileHashes = new Map<string, Promise<
|
|
32
|
+
private readonly dependencyHashes = new Map<string, Promise<number>>()
|
|
33
|
+
private readonly fileHashes = new Map<string, Promise<number>>()
|
|
28
34
|
|
|
29
35
|
constructor(
|
|
30
36
|
private readonly packageJsonPath: string,
|
|
@@ -32,21 +38,117 @@ export class SourceHashCalculator {
|
|
|
32
38
|
private readonly logger: Logger,
|
|
33
39
|
) {}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Calculates CRC32 hash of a string.
|
|
43
|
+
*/
|
|
44
|
+
private hashString(input: string): number {
|
|
45
|
+
return crc32(Buffer.from(input))
|
|
46
|
+
}
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Gets the highstate configuration from package.json with defaults.
|
|
50
|
+
*/
|
|
51
|
+
private getHighstateConfig(packageJson: PackageJson): HighstateConfig {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
53
|
+
const rawConfig = packageJson.highstate
|
|
54
|
+
if (!rawConfig) {
|
|
55
|
+
return { type: "source" }
|
|
56
|
+
}
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
58
|
+
try {
|
|
59
|
+
return highstateConfigSchema.parse(rawConfig)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger.warn(
|
|
62
|
+
{ error, packageName: packageJson.name },
|
|
63
|
+
"invalid highstate configuration, using defaults",
|
|
46
64
|
)
|
|
65
|
+
return { type: "source" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the effective source hash configuration with defaults for a specific output.
|
|
71
|
+
*/
|
|
72
|
+
private getSourceHashConfig(
|
|
73
|
+
highstateConfig: HighstateConfig,
|
|
74
|
+
exportKey?: string,
|
|
75
|
+
): SourceHashConfig {
|
|
76
|
+
if (highstateConfig.sourceHash) {
|
|
77
|
+
// Try to parse as a single config first
|
|
78
|
+
const singleConfigResult = sourceHashConfigSchema.safeParse(highstateConfig.sourceHash)
|
|
79
|
+
if (singleConfigResult.success) {
|
|
80
|
+
return singleConfigResult.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Try to parse as a record of configs
|
|
84
|
+
const recordConfigResult = z
|
|
85
|
+
.record(z.string(), sourceHashConfigSchema)
|
|
86
|
+
.safeParse(highstateConfig.sourceHash)
|
|
87
|
+
if (recordConfigResult.success && exportKey) {
|
|
88
|
+
const perOutputConfig = recordConfigResult.data[exportKey]
|
|
89
|
+
if (perOutputConfig) {
|
|
90
|
+
return perOutputConfig
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (highstateConfig.type === "library") {
|
|
96
|
+
return { mode: "none" }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { mode: "auto" }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async writeHighstateManifest(
|
|
103
|
+
distBasePath: string,
|
|
104
|
+
distPathToExportKey: Map<string, string>,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const highstateConfig = this.getHighstateConfig(this.packageJson)
|
|
107
|
+
|
|
108
|
+
const promises: Promise<{ distPath: string; hash: number }>[] = []
|
|
109
|
+
|
|
110
|
+
for (const [distPath, exportKey] of distPathToExportKey) {
|
|
111
|
+
const fullPath = resolve(distPath)
|
|
112
|
+
const sourceHashConfig = this.getSourceHashConfig(highstateConfig, exportKey)
|
|
113
|
+
|
|
114
|
+
switch (sourceHashConfig.mode) {
|
|
115
|
+
case "manual":
|
|
116
|
+
promises.push(
|
|
117
|
+
Promise.resolve({
|
|
118
|
+
distPath,
|
|
119
|
+
hash: this.hashString(sourceHashConfig.version),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
break
|
|
123
|
+
case "version":
|
|
124
|
+
promises.push(
|
|
125
|
+
Promise.resolve({
|
|
126
|
+
distPath,
|
|
127
|
+
hash: this.hashString(this.packageJson.version ?? ""),
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
break
|
|
131
|
+
case "none":
|
|
132
|
+
promises.push(
|
|
133
|
+
Promise.resolve({
|
|
134
|
+
distPath,
|
|
135
|
+
hash: 0,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
break
|
|
139
|
+
case "auto":
|
|
140
|
+
default:
|
|
141
|
+
promises.push(
|
|
142
|
+
this.getFileHash(fullPath).then(hash => ({
|
|
143
|
+
distPath,
|
|
144
|
+
hash,
|
|
145
|
+
})),
|
|
146
|
+
)
|
|
147
|
+
break
|
|
148
|
+
}
|
|
47
149
|
}
|
|
48
150
|
|
|
49
|
-
const manifest:
|
|
151
|
+
const manifest: HighstateManifest = {
|
|
50
152
|
sourceHashes: {},
|
|
51
153
|
}
|
|
52
154
|
|
|
@@ -59,7 +161,7 @@ export class SourceHashCalculator {
|
|
|
59
161
|
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8")
|
|
60
162
|
}
|
|
61
163
|
|
|
62
|
-
private async getFileHash(fullPath: string): Promise<
|
|
164
|
+
private async getFileHash(fullPath: string): Promise<number> {
|
|
63
165
|
const existingHash = this.fileHashes.get(fullPath)
|
|
64
166
|
if (existingHash) {
|
|
65
167
|
return existingHash
|
|
@@ -71,19 +173,19 @@ export class SourceHashCalculator {
|
|
|
71
173
|
return hash
|
|
72
174
|
}
|
|
73
175
|
|
|
74
|
-
private async calculateFileHash(fullPath: string): Promise<
|
|
176
|
+
private async calculateFileHash(fullPath: string): Promise<number> {
|
|
75
177
|
const content = await readFile(fullPath, "utf8")
|
|
76
178
|
const fileDeps = this.parseDependencies(fullPath, content)
|
|
77
179
|
|
|
78
180
|
const hashes = await Promise.all([
|
|
79
|
-
|
|
181
|
+
this.hashString(content),
|
|
80
182
|
...fileDeps.map(dep => this.getDependencyHash(dep)),
|
|
81
183
|
])
|
|
82
184
|
|
|
83
|
-
return
|
|
185
|
+
return crc32(Buffer.concat(hashes.map(int32ToBytes)))
|
|
84
186
|
}
|
|
85
187
|
|
|
86
|
-
getDependencyHash(dependency: FileDependency): Promise<
|
|
188
|
+
getDependencyHash(dependency: FileDependency): Promise<number> {
|
|
87
189
|
const existingHash = this.dependencyHashes.get(dependency.id)
|
|
88
190
|
if (existingHash) {
|
|
89
191
|
return existingHash
|
|
@@ -95,7 +197,7 @@ export class SourceHashCalculator {
|
|
|
95
197
|
return hash
|
|
96
198
|
}
|
|
97
199
|
|
|
98
|
-
private async calculateDependencyHash(dependency: FileDependency): Promise<
|
|
200
|
+
private async calculateDependencyHash(dependency: FileDependency): Promise<number> {
|
|
99
201
|
switch (dependency.type) {
|
|
100
202
|
case "relative": {
|
|
101
203
|
return await this.getFileHash(dependency.fullPath)
|
|
@@ -133,6 +235,7 @@ export class SourceHashCalculator {
|
|
|
133
235
|
this.logger.warn(`package "%s" is not listed in package.json dependencies`, packageName)
|
|
134
236
|
}
|
|
135
237
|
|
|
238
|
+
// try to get source hash from manifest first
|
|
136
239
|
let relativePath = relative(dirname(depPackageJsonPath), resolvedPath)
|
|
137
240
|
relativePath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`
|
|
138
241
|
|
|
@@ -142,10 +245,10 @@ export class SourceHashCalculator {
|
|
|
142
245
|
"highstate.manifest.json",
|
|
143
246
|
)
|
|
144
247
|
|
|
145
|
-
let manifest:
|
|
248
|
+
let manifest: HighstateManifest | undefined
|
|
146
249
|
try {
|
|
147
250
|
const manifestContent = await readFile(highstateManifestPath, "utf8")
|
|
148
|
-
manifest = JSON.parse(manifestContent)
|
|
251
|
+
manifest = highstateManifestSchema.parse(JSON.parse(manifestContent))
|
|
149
252
|
} catch (error) {
|
|
150
253
|
this.logger.debug(
|
|
151
254
|
{ error },
|
|
@@ -164,7 +267,7 @@ export class SourceHashCalculator {
|
|
|
164
267
|
// use the package version as a fallback hash
|
|
165
268
|
// this case will be applied for most npm packages
|
|
166
269
|
this.logger.debug(`using package version as a fallback hash for "%s"`, packageName)
|
|
167
|
-
return depPackageJson.version ?? "0.0.0"
|
|
270
|
+
return this.hashString(depPackageJson.version ?? "0.0.0")
|
|
168
271
|
}
|
|
169
272
|
}
|
|
170
273
|
}
|