@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.
@@ -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 { int32ToBytes } from "@highstate/backend/shared"
10
+ import {
11
+ type HighstateManifest,
12
+ type HighstateConfig,
13
+ type SourceHashConfig,
14
+ highstateConfigSchema,
15
+ highstateManifestSchema,
16
+ sourceHashConfigSchema,
17
+ } from "./schemas"
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
  }