@highstate/cli 0.9.18 → 0.9.20
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/assets/tsconfig.base.json +7 -2
- package/dist/highstate.manifest.json +1 -1
- package/dist/{library-loader-ZABUULFB.js → library-loader-6TJTW2HX.js} +3 -3
- package/dist/library-loader-6TJTW2HX.js.map +1 -0
- package/dist/main.js +391 -170
- package/dist/main.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/backend/identity.ts +1 -1
- package/src/commands/package/create.ts +35 -0
- package/src/commands/package/index.ts +4 -0
- package/src/commands/package/list.ts +41 -0
- package/src/commands/package/remove.ts +40 -0
- package/src/commands/package/update-references.ts +18 -0
- package/src/main.ts +10 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/library-loader.ts +1 -1
- package/src/shared/schema-transformer.spec.ts +178 -35
- package/src/shared/schema-transformer.ts +250 -244
- package/src/shared/source-hash-calculator.ts +0 -1
- package/src/shared/workspace.ts +240 -0
- package/dist/library-loader-ZABUULFB.js.map +0 -1
|
@@ -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"],"names":[],"mappings":"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","file":"library-loader-ZABUULFB.js","sourcesContent":["import type { Logger } from \"pino\"\nimport console from \"console\"\nimport {\n type Component,\n type ComponentModel,\n type Entity,\n type EntityModel,\n isComponent,\n isEntity,\n isUnitModel,\n} from \"@highstate/contract\"\nimport { Crc32, crc32 } from \"@aws-crypto/crc32\"\nimport { encode } from \"@msgpack/msgpack\"\nimport { int32ToBytes } from \"./utils\"\n\nexport type Library = Readonly<{\n components: Readonly<Record<string, ComponentModel>>\n entities: Readonly<Record<string, EntityModel>>\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 (error) {\n console.error(error)\n\n throw new Error(`Failed to load module \"${modulePath}\"`, { cause: error })\n }\n }\n\n const components: Record<string, ComponentModel> = {}\n const entities: Record<string, EntityModel> = {}\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, EntityModel>,\n): Promise<void> {\n if (isComponent(value)) {\n const entityHashes: number[] = []\n for (const entity of value.entities.values()) {\n entity.model.definitionHash ??= calculateEntityDefinitionHash(entity)\n entityHashes.push(entity.model.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.model\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.model.schema\n return\n }\n\n if (typeof value !== \"object\" || value === null) {\n return\n }\n\n if (\"_zod\" in value) {\n // this is a zod schema, we can skip it\n return\n }\n\n if (Array.isArray(value)) {\n for (const item of value) {\n await _loadLibrary(item, components, entities)\n }\n\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 const serializedCreate = { text: \"TODO: investigate why serializeFunction hangs\" }\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.model))\n}\n"]}
|