@highstate/cli 0.9.16 → 0.9.19

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.
@@ -0,0 +1,240 @@
1
+ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises"
2
+ import { join, relative, resolve } from "node:path"
3
+ import { existsSync } from "node:fs"
4
+ import { z } from "zod"
5
+ import { highstateConfigSchema } from "./schemas"
6
+
7
+ /**
8
+ * Represents a Highstate package in the workspace
9
+ */
10
+ export interface HighstatePackage {
11
+ /** Absolute path to the package directory */
12
+ path: string
13
+ /** Relative path from workspace root */
14
+ relativePath: string
15
+ /** Package name from package.json */
16
+ name: string
17
+ /** Package type from highstate config */
18
+ type?: "source" | "library" | "worker"
19
+ }
20
+
21
+ /**
22
+ * Schema for package.json files
23
+ */
24
+ const packageJsonSchema = z.object({
25
+ name: z.string(),
26
+ highstate: highstateConfigSchema.optional(),
27
+ })
28
+
29
+ /**
30
+ * Generates tsconfig content with correct relative path based on package depth
31
+ */
32
+ function generateTsconfigContent(workspaceRoot: string, packagePath: string) {
33
+ const relativePath = relative(workspaceRoot, packagePath)
34
+ const depth = relativePath.split("/").length
35
+ const relativeNodeModules = `${"../".repeat(depth)}node_modules/@highstate/cli/assets/tsconfig.base.json`
36
+
37
+ return {
38
+ extends: relativeNodeModules,
39
+ include: ["./src/**/*.ts", "./package.json", "./assets/**/*.json"],
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Finds the workspace root by looking for package.json with workspaces
45
+ */
46
+ export async function findWorkspaceRoot(startPath: string = process.cwd()): Promise<string> {
47
+ let currentPath = resolve(startPath)
48
+
49
+ while (currentPath !== "/") {
50
+ const packageJsonPath = join(currentPath, "package.json")
51
+
52
+ if (existsSync(packageJsonPath)) {
53
+ try {
54
+ const content = await readFile(packageJsonPath, "utf-8")
55
+ const packageJson = JSON.parse(content) as { workspaces?: unknown }
56
+
57
+ if (packageJson.workspaces) {
58
+ return currentPath
59
+ }
60
+ } catch {
61
+ // ignore invalid package.json files
62
+ }
63
+ }
64
+
65
+ const parentPath = resolve(currentPath, "..")
66
+ if (parentPath === currentPath) break
67
+ currentPath = parentPath
68
+ }
69
+
70
+ throw new Error("Could not find workspace root (no package.json with workspaces found)")
71
+ }
72
+
73
+ /**
74
+ * Recursively scans for packages in the workspace
75
+ */
76
+ export async function scanWorkspacePackages(workspaceRoot: string): Promise<HighstatePackage[]> {
77
+ const packages: HighstatePackage[] = []
78
+ const packagesDir = join(workspaceRoot, "packages")
79
+
80
+ if (!existsSync(packagesDir)) {
81
+ return packages
82
+ }
83
+
84
+ async function scanDirectory(dirPath: string, depth = 0): Promise<void> {
85
+ // skip node_modules and hidden directories at any depth
86
+ const dirName = relative(packagesDir, dirPath).split("/").pop()
87
+ if (dirName?.startsWith(".") || dirName === "node_modules") {
88
+ return
89
+ }
90
+
91
+ const entries = await readdir(dirPath, { withFileTypes: true })
92
+
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory()) continue
95
+
96
+ const entryPath = join(dirPath, entry.name)
97
+
98
+ // skip node_modules and hidden directories
99
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
100
+ continue
101
+ }
102
+
103
+ const packageJsonPath = join(entryPath, "package.json")
104
+
105
+ // check if this directory has a package.json
106
+ if (existsSync(packageJsonPath)) {
107
+ try {
108
+ const content = await readFile(packageJsonPath, "utf-8")
109
+ const packageJson = packageJsonSchema.parse(JSON.parse(content))
110
+
111
+ const relativePath = relative(workspaceRoot, entryPath)
112
+ const type = packageJson.highstate?.type ?? "source"
113
+
114
+ packages.push({
115
+ path: entryPath,
116
+ relativePath,
117
+ name: packageJson.name,
118
+ type,
119
+ })
120
+ } catch {
121
+ // ignore directories with invalid package.json files
122
+ }
123
+ }
124
+
125
+ // continue scanning subdirectories, but with limited depth to avoid deep node_modules
126
+ if (depth < 3) {
127
+ await scanDirectory(entryPath, depth + 1)
128
+ }
129
+ }
130
+ }
131
+
132
+ await scanDirectory(packagesDir)
133
+ return packages.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
134
+ }
135
+
136
+ /**
137
+ * Updates the root tsconfig.json with package references
138
+ */
139
+ export async function updateTsconfigReferences(
140
+ workspaceRoot: string,
141
+ packages: HighstatePackage[],
142
+ ensureTsconfigs = false,
143
+ ): Promise<void> {
144
+ const tsconfigPath = join(workspaceRoot, "tsconfig.json")
145
+
146
+ // ensure all packages have valid tsconfig.json files
147
+ if (ensureTsconfigs) {
148
+ await ensurePackageTsconfigs(
149
+ workspaceRoot,
150
+
151
+ // only udate for Highstate-managed packages
152
+ packages.filter(pkg => pkg.type !== undefined),
153
+ )
154
+ }
155
+
156
+ // generate references array
157
+ const references = packages.map(pkg => ({
158
+ path: `./${pkg.relativePath}/tsconfig.json`,
159
+ }))
160
+
161
+ const tsconfigContent = {
162
+ files: [],
163
+ references,
164
+ }
165
+
166
+ // write the file with proper formatting
167
+ await writeFile(tsconfigPath, `${JSON.stringify(tsconfigContent, null, 2)}\n`, "utf-8")
168
+ }
169
+
170
+ /**
171
+ * Ensures all packages have valid tsconfig.json files
172
+ */
173
+ async function ensurePackageTsconfigs(
174
+ workspaceRoot: string,
175
+ packages: HighstatePackage[],
176
+ ): Promise<void> {
177
+ for (const pkg of packages) {
178
+ const tsconfigPath = join(pkg.path, "tsconfig.json")
179
+ const tsconfigContent = generateTsconfigContent(workspaceRoot, pkg.path)
180
+
181
+ await writeFile(tsconfigPath, `${JSON.stringify(tsconfigContent, null, 2)}\n`, "utf-8")
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Creates a new package of the specified type
187
+ */
188
+ export async function createPackage(
189
+ workspaceRoot: string,
190
+ name: string,
191
+ type: "source" | "library" | "worker",
192
+ ): Promise<HighstatePackage> {
193
+ const packagePath = join(workspaceRoot, "packages", name)
194
+ const srcPath = join(packagePath, "src")
195
+
196
+ // create directories
197
+ await mkdir(packagePath, { recursive: true })
198
+ await mkdir(srcPath, { recursive: true })
199
+
200
+ // create package.json
201
+ const packageJson = {
202
+ name: `@highstate/${name}`,
203
+ version: "0.0.1",
204
+ type: "module",
205
+ highstate: {
206
+ type,
207
+ },
208
+ }
209
+
210
+ await writeFile(
211
+ join(packagePath, "package.json"),
212
+ `${JSON.stringify(packageJson, null, 2)}\n`,
213
+ "utf-8",
214
+ )
215
+
216
+ // create tsconfig.json
217
+ const tsconfigContent = generateTsconfigContent(workspaceRoot, packagePath)
218
+ await writeFile(
219
+ join(packagePath, "tsconfig.json"),
220
+ `${JSON.stringify(tsconfigContent, null, 2)}\n`,
221
+ "utf-8",
222
+ )
223
+
224
+ // create CHANGELOG.md
225
+ await writeFile(
226
+ join(packagePath, "CHANGELOG.md"),
227
+ `# Changelog\n\nAll notable changes to this project will be documented in this file.\n`,
228
+ "utf-8",
229
+ )
230
+
231
+ // create basic index.ts
232
+ await writeFile(join(srcPath, "index.ts"), `// ${name} package\n`, "utf-8")
233
+
234
+ return {
235
+ path: packagePath,
236
+ relativePath: `packages/${name}`,
237
+ name: `@highstate/${name}`,
238
+ type,
239
+ }
240
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/library-loader.ts"],"sourcesContent":["import type { Logger } from \"pino\"\nimport {\n type Component,\n type ComponentModel,\n type Entity,\n isComponent,\n isEntity,\n isUnitModel,\n originalCreate,\n} from \"@highstate/contract\"\nimport { serializeFunction } from \"@pulumi/pulumi/runtime/index.js\"\nimport { Crc32, crc32 } from \"@aws-crypto/crc32\"\nimport { encode } from \"@msgpack/msgpack\"\nimport { int32ToBytes } from \"@highstate/backend/shared\"\n\nexport type Library = Readonly<{\n components: Readonly<Record<string, ComponentModel>>\n entities: Readonly<Record<string, Entity>>\n}>\n\nexport async function loadLibrary(logger: Logger, modulePaths: string[]): Promise<Library> {\n const modules: Record<string, unknown> = {}\n for (const modulePath of modulePaths) {\n try {\n logger.debug({ modulePath }, \"loading module\")\n modules[modulePath] = await import(modulePath)\n\n logger.debug({ modulePath }, \"module loaded\")\n } catch (err) {\n logger.error({ modulePath, err }, \"module load failed\")\n }\n }\n\n const components: Record<string, ComponentModel> = {}\n const entities: Record<string, Entity> = {}\n\n await _loadLibrary(modules, components, entities)\n\n logger.info(\n {\n componentCount: Object.keys(components).length,\n entityCount: Object.keys(entities).length,\n },\n \"library loaded\",\n )\n\n logger.trace({ components, entities }, \"library content\")\n\n return { components, entities }\n}\n\nasync function _loadLibrary(\n value: unknown,\n components: Record<string, ComponentModel>,\n entities: Record<string, Entity>,\n): Promise<void> {\n if (isComponent(value)) {\n const entityHashes: number[] = []\n for (const entity of value.entities.values()) {\n entity.definitionHash ??= calculateEntityDefinitionHash(entity)\n entityHashes.push(entity.definitionHash)\n }\n\n components[value.model.type] = value.model\n value.model.definitionHash = await calculateComponentDefinitionHash(value, entityHashes)\n\n return\n }\n\n if (isEntity(value)) {\n entities[value.type] = value\n entities[value.type].definitionHash ??= calculateEntityDefinitionHash(value)\n\n // @ts-expect-error remove the schema since it's not needed in the designer\n delete value.schema\n return\n }\n\n if (typeof value !== \"object\" || value === null) {\n return\n }\n\n for (const key in value) {\n await _loadLibrary((value as Record<string, unknown>)[key], components, entities)\n }\n}\n\nasync function calculateComponentDefinitionHash(\n component: Component,\n entityHashes: number[],\n): Promise<number> {\n const result = new Crc32()\n\n // 1. include the full component model\n result.update(encode(component.model))\n\n if (!isUnitModel(component.model)) {\n // 2. for composite components, include the content of the serialized create function\n const serializedCreate = await serializeFunction(component[originalCreate])\n result.update(Buffer.from(serializedCreate.text))\n }\n\n // 3. include the hashes of all entities\n for (const entityHash of entityHashes) {\n result.update(int32ToBytes(entityHash))\n }\n\n return result.digest()\n}\n\nfunction calculateEntityDefinitionHash(entity: Entity): number {\n return crc32(encode(entity))\n}\n"],"mappings":";AACA;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC,SAAS,OAAO,aAAa;AAC7B,SAAS,cAAc;AACvB,SAAS,oBAAoB;AAO7B,eAAsB,YAAY,QAAgB,aAAyC;AACzF,QAAM,UAAmC,CAAC;AAC1C,aAAW,cAAc,aAAa;AACpC,QAAI;AACF,aAAO,MAAM,EAAE,WAAW,GAAG,gBAAgB;AAC7C,cAAQ,UAAU,IAAI,MAAM,OAAO;AAEnC,aAAO,MAAM,EAAE,WAAW,GAAG,eAAe;AAAA,IAC9C,SAAS,KAAK;AACZ,aAAO,MAAM,EAAE,YAAY,IAAI,GAAG,oBAAoB;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,aAA6C,CAAC;AACpD,QAAM,WAAmC,CAAC;AAE1C,QAAM,aAAa,SAAS,YAAY,QAAQ;AAEhD,SAAO;AAAA,IACL;AAAA,MACE,gBAAgB,OAAO,KAAK,UAAU,EAAE;AAAA,MACxC,aAAa,OAAO,KAAK,QAAQ,EAAE;AAAA,IACrC;AAAA,IACA;AAAA,EACF;AAEA,SAAO,MAAM,EAAE,YAAY,SAAS,GAAG,iBAAiB;AAExD,SAAO,EAAE,YAAY,SAAS;AAChC;AAEA,eAAe,aACb,OACA,YACA,UACe;AACf,MAAI,YAAY,KAAK,GAAG;AACtB,UAAM,eAAyB,CAAC;AAChC,eAAW,UAAU,MAAM,SAAS,OAAO,GAAG;AAC5C,aAAO,mBAAmB,8BAA8B,MAAM;AAC9D,mBAAa,KAAK,OAAO,cAAc;AAAA,IACzC;AAEA,eAAW,MAAM,MAAM,IAAI,IAAI,MAAM;AACrC,UAAM,MAAM,iBAAiB,MAAM,iCAAiC,OAAO,YAAY;AAEvF;AAAA,EACF;AAEA,MAAI,SAAS,KAAK,GAAG;AACnB,aAAS,MAAM,IAAI,IAAI;AACvB,aAAS,MAAM,IAAI,EAAE,mBAAmB,8BAA8B,KAAK;AAG3E,WAAO,MAAM;AACb;AAAA,EACF;AAEA,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,EACF;AAEA,aAAW,OAAO,OAAO;AACvB,UAAM,aAAc,MAAkC,GAAG,GAAG,YAAY,QAAQ;AAAA,EAClF;AACF;AAEA,eAAe,iCACb,WACA,cACiB;AACjB,QAAM,SAAS,IAAI,MAAM;AAGzB,SAAO,OAAO,OAAO,UAAU,KAAK,CAAC;AAErC,MAAI,CAAC,YAAY,UAAU,KAAK,GAAG;AAEjC,UAAM,mBAAmB,MAAM,kBAAkB,UAAU,cAAc,CAAC;AAC1E,WAAO,OAAO,OAAO,KAAK,iBAAiB,IAAI,CAAC;AAAA,EAClD;AAGA,aAAW,cAAc,cAAc;AACrC,WAAO,OAAO,aAAa,UAAU,CAAC;AAAA,EACxC;AAEA,SAAO,OAAO,OAAO;AACvB;AAEA,SAAS,8BAA8B,QAAwB;AAC7D,SAAO,MAAM,OAAO,MAAM,CAAC;AAC7B;","names":[]}