@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.
@@ -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 || !jsdoc.value.includes("@schema")) {
71
+ if (!jsdoc) {
33
72
  return
34
73
  }
35
74
 
36
- magicString.update(
37
- node.value.start,
38
- node.value.end,
39
- `{
40
- ...${content.substring(node.value.start, node.value.end)},
41
- description: \`${cleanJsdoc(jsdoc.value)}\`,
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
- return magicString.toString()
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 { sha256 } from "crypto-hash"
6
+ import { crc32 } from "@aws-crypto/crc32"
7
7
  import { resolve as importMetaResolve } from "import-meta-resolve"
8
-
9
- export type HighstateManifestJson = {
10
- sourceHashes?: Record<string, string>
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<string>>()
27
- private readonly fileHashes = new Map<string, Promise<string>>()
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
- async writeHighstateManifest(distBasePath: string, distPaths: string[]): Promise<void> {
36
- const promises: Promise<{ distPath: string; hash: string }>[] = []
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
- for (const distPath of distPaths) {
39
- const fullPath = resolve(distPath)
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
- promises.push(
42
- this.getFileHash(fullPath).then(hash => ({
43
- distPath,
44
- hash,
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: HighstateManifestJson = {
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<string> {
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<string> {
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
- sha256(content),
181
+ this.hashString(content),
80
182
  ...fileDeps.map(dep => this.getDependencyHash(dep)),
81
183
  ])
82
184
 
83
- return await sha256(hashes.join(""))
185
+ return crc32(Buffer.concat(hashes.map(int32ToBytes)))
84
186
  }
85
187
 
86
- getDependencyHash(dependency: FileDependency): Promise<string> {
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<string> {
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: HighstateManifestJson | undefined
248
+ let manifest: HighstateManifest | undefined
146
249
  try {
147
250
  const manifestContent = await readFile(highstateManifestPath, "utf8")
148
- manifest = JSON.parse(manifestContent) as HighstateManifestJson
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
  }
@@ -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
+ }