@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.
@@ -1,13 +1,17 @@
1
1
  import type { Plugin } from "esbuild"
2
+ import { resolve } from "node:path"
3
+ import { writeFile } from "node:fs/promises"
2
4
  import { Command, Option } from "clipanion"
3
5
  import { readPackageJSON, resolvePackageJSON } from "pkg-types"
4
6
  import { mapKeys, mapValues, pipe } from "remeda"
5
7
  import { build } from "tsup"
8
+ import { encode } from "@msgpack/msgpack"
6
9
  import {
7
10
  createBinTransformerPlugin,
8
11
  logger,
9
12
  schemaTransformerPlugin,
10
13
  SourceHashCalculator,
14
+ highstateConfigSchema,
11
15
  } from "../shared"
12
16
 
13
17
  export class BuildCommand extends Command {
@@ -21,9 +25,20 @@ export class BuildCommand extends Command {
21
25
  watch = Option.Boolean("--watch", false)
22
26
  library = Option.Boolean("--library", false)
23
27
  silent = Option.Boolean("--silent", true)
28
+ noSourceHash = Option.Boolean("--no-source-hash", false)
24
29
 
25
30
  async execute(): Promise<void> {
26
31
  const packageJson = await readPackageJSON()
32
+
33
+ const highstateConfig = highstateConfigSchema.parse(packageJson.highstate ?? {})
34
+ if (highstateConfig.type === "library") {
35
+ this.library = true
36
+ }
37
+
38
+ if (highstateConfig.type === "worker") {
39
+ this.noSourceHash = true
40
+ }
41
+
27
42
  const exports = packageJson.exports
28
43
  let bin = packageJson.bin
29
44
 
@@ -87,6 +102,7 @@ export class BuildCommand extends Command {
87
102
  targetName,
88
103
  distPath,
89
104
  isBin,
105
+ key,
90
106
  }
91
107
  }),
92
108
  mapKeys((_, value) => value.targetName),
@@ -113,25 +129,46 @@ export class BuildCommand extends Command {
113
129
  sourcemap: true,
114
130
  clean: true,
115
131
  format: "esm",
116
- target: "esnext",
132
+ target: "es2024",
133
+ platform: "node",
117
134
  external: ["@pulumi/pulumi"],
118
135
  esbuildPlugins,
136
+ treeshake: true,
137
+ removeNodeProtocol: false,
119
138
  silent: this.silent || ["warn", "error", "fatal"].includes(logger.level),
120
139
  })
121
140
 
122
141
  const packageJsonPath = await resolvePackageJSON()
123
142
  const upToDatePackageJson = await readPackageJSON()
124
143
 
125
- // do not write highstate manifest for library packages since their changes are tracked more granularly
126
- if (!this.library) {
144
+ if (!this.noSourceHash) {
127
145
  const sourceHashCalculator = new SourceHashCalculator(
128
146
  packageJsonPath,
129
147
  upToDatePackageJson,
130
148
  logger,
131
149
  )
132
- const distPaths = Object.values(entry).map(value => value.distPath)
133
150
 
134
- await sourceHashCalculator.writeHighstateManifest("./dist", distPaths)
151
+ const distPathToExportKey = new Map<string, string>()
152
+ for (const value of Object.values(entry)) {
153
+ distPathToExportKey.set(value.distPath, value.key)
154
+ }
155
+
156
+ await sourceHashCalculator.writeHighstateManifest("./dist", distPathToExportKey)
135
157
  }
158
+
159
+ // write the "highstate.library.json" file if the library flag is set
160
+ if (this.library) {
161
+ const { loadLibrary } = await import("../shared/library-loader.js")
162
+ const fullModulePaths = Object.values(entry).map(value => resolve(value.distPath))
163
+
164
+ logger.info("evaluating library components from modules: %s", fullModulePaths.join(", "))
165
+
166
+ const library = await loadLibrary(logger, fullModulePaths)
167
+ const libraryPath = resolve("./dist", "highstate.library.msgpack")
168
+
169
+ await writeFile(libraryPath, encode(library), "utf8")
170
+ }
171
+
172
+ logger.info("build completed successfully")
136
173
  }
137
174
  }
package/src/main.ts CHANGED
@@ -2,6 +2,7 @@ import { Builtins, Cli } from "clipanion"
2
2
  import { version } from "../package.json"
3
3
  import { DesignerCommand } from "./commands/designer"
4
4
  import { BuildCommand } from "./commands/build"
5
+ import { BackendIdentityCommand } from "./commands/backend/identity"
5
6
 
6
7
  const cli = new Cli({
7
8
  binaryName: "highstate",
@@ -11,6 +12,7 @@ const cli = new Cli({
11
12
 
12
13
  cli.register(BuildCommand)
13
14
  cli.register(DesignerCommand)
15
+ cli.register(BackendIdentityCommand)
14
16
  cli.register(Builtins.HelpCommand)
15
17
  cli.register(Builtins.VersionCommand)
16
18
 
@@ -3,3 +3,4 @@ export * from "./logger"
3
3
  export * from "./schema-transformer"
4
4
  export * from "./source-hash-calculator"
5
5
  export * from "./bin-transformer"
6
+ export * from "./schemas"
@@ -0,0 +1,129 @@
1
+ import type { Logger } from "pino"
2
+ import console from "console"
3
+ import {
4
+ type Component,
5
+ type ComponentModel,
6
+ type Entity,
7
+ type EntityModel,
8
+ isComponent,
9
+ isEntity,
10
+ isUnitModel,
11
+ } from "@highstate/contract"
12
+ import { Crc32, crc32 } from "@aws-crypto/crc32"
13
+ import { encode } from "@msgpack/msgpack"
14
+ import { int32ToBytes } from "./utils"
15
+
16
+ export type Library = Readonly<{
17
+ components: Readonly<Record<string, ComponentModel>>
18
+ entities: Readonly<Record<string, EntityModel>>
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 (error) {
30
+ console.error(error)
31
+
32
+ throw new Error(`Failed to load module "${modulePath}"`, { cause: error })
33
+ }
34
+ }
35
+
36
+ const components: Record<string, ComponentModel> = {}
37
+ const entities: Record<string, EntityModel> = {}
38
+
39
+ await _loadLibrary(modules, components, entities)
40
+
41
+ logger.info(
42
+ {
43
+ componentCount: Object.keys(components).length,
44
+ entityCount: Object.keys(entities).length,
45
+ },
46
+ "library loaded",
47
+ )
48
+
49
+ logger.trace({ components, entities }, "library content")
50
+
51
+ return { components, entities }
52
+ }
53
+
54
+ async function _loadLibrary(
55
+ value: unknown,
56
+ components: Record<string, ComponentModel>,
57
+ entities: Record<string, EntityModel>,
58
+ ): Promise<void> {
59
+ if (isComponent(value)) {
60
+ const entityHashes: number[] = []
61
+ for (const entity of value.entities.values()) {
62
+ entity.model.definitionHash ??= calculateEntityDefinitionHash(entity)
63
+ entityHashes.push(entity.model.definitionHash)
64
+ }
65
+
66
+ components[value.model.type] = value.model
67
+ value.model.definitionHash = await calculateComponentDefinitionHash(value, entityHashes)
68
+
69
+ return
70
+ }
71
+
72
+ if (isEntity(value)) {
73
+ entities[value.type] = value.model
74
+ entities[value.type].definitionHash ??= calculateEntityDefinitionHash(value)
75
+
76
+ // @ts-expect-error remove the schema since it's not needed in the designer
77
+ delete value.model.schema
78
+ return
79
+ }
80
+
81
+ if (typeof value !== "object" || value === null) {
82
+ return
83
+ }
84
+
85
+ if ("_zod" in value) {
86
+ // this is a zod schema, we can skip it
87
+ return
88
+ }
89
+
90
+ if (Array.isArray(value)) {
91
+ for (const item of value) {
92
+ await _loadLibrary(item, components, entities)
93
+ }
94
+
95
+ return
96
+ }
97
+
98
+ for (const key in value) {
99
+ await _loadLibrary((value as Record<string, unknown>)[key], components, entities)
100
+ }
101
+ }
102
+
103
+ async function calculateComponentDefinitionHash(
104
+ component: Component,
105
+ entityHashes: number[],
106
+ ): Promise<number> {
107
+ const result = new Crc32()
108
+
109
+ // 1. include the full component model
110
+ result.update(encode(component.model))
111
+
112
+ if (!isUnitModel(component.model)) {
113
+ // 2. for composite components, include the content of the serialized create function
114
+ // const serializedCreate = await serializeFunction(component[originalCreate])
115
+ const serializedCreate = { text: "TODO: investigate why serializeFunction hangs" }
116
+ result.update(Buffer.from(serializedCreate.text))
117
+ }
118
+
119
+ // 3. include the hashes of all entities
120
+ for (const entityHash of entityHashes) {
121
+ result.update(int32ToBytes(entityHash))
122
+ }
123
+
124
+ return result.digest()
125
+ }
126
+
127
+ function calculateEntityDefinitionHash(entity: Entity): number {
128
+ return crc32(encode(entity.model))
129
+ }