@tscircuit/cli 0.1.4 → 0.1.6
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/biome.json +1 -0
- package/bun.lockb +0 -0
- package/cli/dev/DevServer.ts +158 -0
- package/cli/dev/register.ts +9 -82
- package/cli/main.ts +4 -1
- package/dist/main.js +233 -234
- package/lib/dependency-analysis/{installNodeModuleTypes.ts → installNodeModuleTypesForSnippet.ts} +1 -1
- package/lib/file-server/FileServerEvent.ts +7 -0
- package/lib/file-server/FileServerRoutes.ts +38 -0
- package/lib/index.ts +1 -1
- package/lib/server/{createServer.ts → createHttpServer.ts} +3 -3
- package/package.json +10 -5
- package/tests/fixtures/get-test-fixture.ts +25 -0
- package/tests/test1-dev-server-basic.test.ts +40 -0
- package/tsconfig.json +2 -1
package/biome.json
CHANGED
package/bun.lockb
CHANGED
|
Binary file
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import ky from "ky"
|
|
2
|
+
import type { FileServerRoutes } from "lib/file-server/FileServerRoutes"
|
|
3
|
+
import { createHttpServer } from "lib/server/createHttpServer"
|
|
4
|
+
import { EventsWatcher } from "lib/server/EventsWatcher"
|
|
5
|
+
import type http from "node:http"
|
|
6
|
+
import type { TypedKyInstance } from "typed-ky"
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import fs from "node:fs"
|
|
9
|
+
import type { FileUpdatedEvent } from "lib/file-server/FileServerEvent"
|
|
10
|
+
import * as chokidar from "chokidar"
|
|
11
|
+
|
|
12
|
+
export class DevServer {
|
|
13
|
+
port: number
|
|
14
|
+
/**
|
|
15
|
+
* The path to a component that exports a <board /> or <group /> component
|
|
16
|
+
*/
|
|
17
|
+
componentFilePath: string
|
|
18
|
+
|
|
19
|
+
projectDir: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The HTTP server that hosts the file server and event bus. You can use
|
|
23
|
+
* fsKy to communicate with the file server/event bus
|
|
24
|
+
*/
|
|
25
|
+
httpServer?: http.Server
|
|
26
|
+
/**
|
|
27
|
+
* Watches for events on the event bus by polling `api/events/list`
|
|
28
|
+
*/
|
|
29
|
+
eventsWatcher?: EventsWatcher
|
|
30
|
+
/**
|
|
31
|
+
* A ky instance that can be used to communicate with the file server and
|
|
32
|
+
* event bus
|
|
33
|
+
*/
|
|
34
|
+
fsKy: TypedKyInstance<keyof FileServerRoutes, FileServerRoutes>
|
|
35
|
+
/**
|
|
36
|
+
* A chokidar instance that watches the project directory for file changes
|
|
37
|
+
*/
|
|
38
|
+
filesystemWatcher?: chokidar.FSWatcher
|
|
39
|
+
|
|
40
|
+
constructor({
|
|
41
|
+
port,
|
|
42
|
+
componentFilePath,
|
|
43
|
+
}: {
|
|
44
|
+
port: number
|
|
45
|
+
componentFilePath: string
|
|
46
|
+
}) {
|
|
47
|
+
this.port = port
|
|
48
|
+
this.componentFilePath = componentFilePath
|
|
49
|
+
this.projectDir = path.dirname(componentFilePath)
|
|
50
|
+
this.fsKy = ky.create({
|
|
51
|
+
prefixUrl: `http://localhost:${port}`,
|
|
52
|
+
}) as any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async start() {
|
|
56
|
+
const { server } = await createHttpServer(this.port)
|
|
57
|
+
this.httpServer = server
|
|
58
|
+
|
|
59
|
+
this.eventsWatcher = new EventsWatcher(`http://localhost:${this.port}`)
|
|
60
|
+
this.eventsWatcher.start()
|
|
61
|
+
|
|
62
|
+
this.eventsWatcher.on(
|
|
63
|
+
"FILE_UPDATED",
|
|
64
|
+
this.handleFileUpdatedEventFromServer.bind(this),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
this.filesystemWatcher = chokidar.watch(this.projectDir, {
|
|
68
|
+
persistent: true,
|
|
69
|
+
ignoreInitial: true,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
this.filesystemWatcher.on("change", (filePath) =>
|
|
73
|
+
this.handleFileChangedOnFilesystem(filePath),
|
|
74
|
+
)
|
|
75
|
+
this.filesystemWatcher.on("add", (filePath) =>
|
|
76
|
+
this.handleFileChangedOnFilesystem(filePath),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
this.upsertInitialFiles()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async addEntrypoint() {
|
|
83
|
+
const relativeComponentFilePath = path.relative(
|
|
84
|
+
this.projectDir,
|
|
85
|
+
this.componentFilePath,
|
|
86
|
+
)
|
|
87
|
+
await this.fsKy.post("api/files/upsert", {
|
|
88
|
+
json: {
|
|
89
|
+
file_path: "entrypoint.tsx",
|
|
90
|
+
text_content: `
|
|
91
|
+
import MyCircuit from "./${relativeComponentFilePath}"
|
|
92
|
+
|
|
93
|
+
circuit.add(<MyCircuit />)
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async handleFileUpdatedEventFromServer(ev: FileUpdatedEvent) {
|
|
100
|
+
if (ev.initiator === "filesystem_change") return
|
|
101
|
+
|
|
102
|
+
if (ev.file_path === "manual-edits.json") {
|
|
103
|
+
console.log("Manual edits updated, updating on filesystem...")
|
|
104
|
+
const { file } = await this.fsKy
|
|
105
|
+
.get("api/files/get", {
|
|
106
|
+
searchParams: { file_path: ev.file_path },
|
|
107
|
+
})
|
|
108
|
+
.json()
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(this.projectDir, "manual-edits.json"),
|
|
111
|
+
file.text_content,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async handleFileChangedOnFilesystem(absoluteFilePath: string) {
|
|
117
|
+
const relativeFilePath = path.relative(this.projectDir, absoluteFilePath)
|
|
118
|
+
|
|
119
|
+
// We've temporarily disabled upserting manual edits from filesystem changes
|
|
120
|
+
// because it can be edited by the browser
|
|
121
|
+
if (relativeFilePath.includes("manual-edits.json")) return
|
|
122
|
+
|
|
123
|
+
await this.fsKy
|
|
124
|
+
.post("api/files/upsert", {
|
|
125
|
+
json: {
|
|
126
|
+
file_path: relativeFilePath,
|
|
127
|
+
text_content: fs.readFileSync(absoluteFilePath, "utf-8"),
|
|
128
|
+
initiator: "filesystem_change",
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
.json()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async upsertInitialFiles() {
|
|
135
|
+
// Scan project directory for all files and upsert them
|
|
136
|
+
const fileNames = fs.readdirSync(this.projectDir)
|
|
137
|
+
for (const fileName of fileNames) {
|
|
138
|
+
if (fs.statSync(path.join(this.projectDir, fileName)).isDirectory())
|
|
139
|
+
continue
|
|
140
|
+
const fileContent = fs.readFileSync(
|
|
141
|
+
path.join(this.projectDir, fileName),
|
|
142
|
+
"utf-8",
|
|
143
|
+
)
|
|
144
|
+
await this.fsKy.post("api/files/upsert", {
|
|
145
|
+
json: {
|
|
146
|
+
file_path: fileName,
|
|
147
|
+
text_content: fileContent,
|
|
148
|
+
initiator: "filesystem_change",
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async stop() {
|
|
155
|
+
this.httpServer?.close()
|
|
156
|
+
this.eventsWatcher?.stop()
|
|
157
|
+
}
|
|
158
|
+
}
|
package/cli/dev/register.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { Command } from "commander"
|
|
|
2
2
|
import * as path from "node:path"
|
|
3
3
|
import * as chokidar from "chokidar"
|
|
4
4
|
import * as fs from "node:fs"
|
|
5
|
-
import {
|
|
5
|
+
import { createHttpServer } from "lib/server/createHttpServer"
|
|
6
6
|
import { getLocalFileDependencies } from "lib/dependency-analysis/getLocalFileDependencies"
|
|
7
|
-
import {
|
|
7
|
+
import { installNodeModuleTypesForSnippet } from "../../lib/dependency-analysis/installNodeModuleTypesForSnippet"
|
|
8
8
|
import { EventsWatcher } from "../../lib/server/EventsWatcher"
|
|
9
|
+
import { DevServer } from "./DevServer"
|
|
9
10
|
|
|
10
11
|
export const registerDev = (program: Command) => {
|
|
11
12
|
program
|
|
@@ -20,92 +21,18 @@ export const registerDev = (program: Command) => {
|
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
23
|
console.log("Installing types for imported snippets...")
|
|
23
|
-
await
|
|
24
|
+
await installNodeModuleTypesForSnippet(absolutePath)
|
|
24
25
|
console.log("Types installed successfully")
|
|
25
26
|
} catch (error) {
|
|
26
27
|
console.warn("Failed to install types:", error)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const eventsWatcher = new EventsWatcher(`http://localhost:${port}`)
|
|
33
|
-
eventsWatcher.start()
|
|
34
|
-
|
|
35
|
-
await fetch(`http://localhost:${port}/api/files/upsert`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
body: JSON.stringify({
|
|
39
|
-
file_path: "entrypoint.tsx",
|
|
40
|
-
text_content: `
|
|
41
|
-
import MyCircuit from "./snippet.tsx"
|
|
42
|
-
|
|
43
|
-
circuit.add(<MyCircuit />)
|
|
44
|
-
`,
|
|
45
|
-
}),
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// Function to update file content
|
|
49
|
-
const updateFile = async (filePath: string) => {
|
|
50
|
-
try {
|
|
51
|
-
const content = await fs.promises.readFile(filePath, "utf-8")
|
|
52
|
-
const response = await fetch(
|
|
53
|
-
`http://localhost:${port}/api/files/upsert`,
|
|
54
|
-
{
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers: { "Content-Type": "application/json" },
|
|
57
|
-
body: JSON.stringify({
|
|
58
|
-
file_path: path.relative(fileDir, filePath),
|
|
59
|
-
text_content: content,
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
)
|
|
63
|
-
if (!response.ok) {
|
|
64
|
-
console.error(`Failed to update ${filePath}`)
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error(`Error updating ${filePath}:`, error)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get initial dependencies
|
|
72
|
-
const dependencies = new Set([absolutePath])
|
|
73
|
-
try {
|
|
74
|
-
const deps = getLocalFileDependencies(absolutePath)
|
|
75
|
-
deps.forEach((dep) => dependencies.add(dep))
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.warn("Failed to analyze dependencies:", error)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Watch the main file and its dependencies
|
|
81
|
-
const filesystemWatcher = chokidar.watch(Array.from(dependencies), {
|
|
82
|
-
persistent: true,
|
|
83
|
-
ignoreInitial: false,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
filesystemWatcher.on("change", async (filePath) => {
|
|
87
|
-
console.log(`File ${filePath} changed`)
|
|
88
|
-
await updateFile(filePath)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
filesystemWatcher.on("add", async (filePath) => {
|
|
92
|
-
console.log(`File ${filePath} added`)
|
|
93
|
-
await updateFile(filePath)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
eventsWatcher.on("FILE_UPDATED", async (ev) => {
|
|
97
|
-
if (ev.file_path === "manual-edits.json") {
|
|
98
|
-
console.log("Manual edits updated, updating on filesystem...")
|
|
99
|
-
const { file } = await fetch(
|
|
100
|
-
`http://localhost:${port}/api/files/get?file_path=manual-edits.json`,
|
|
101
|
-
).then((r) => r.json())
|
|
102
|
-
fs.writeFileSync(
|
|
103
|
-
path.join(fileDir, "manual-edits.json"),
|
|
104
|
-
file.text_content,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
30
|
+
const server = new DevServer({
|
|
31
|
+
port,
|
|
32
|
+
componentFilePath: absolutePath,
|
|
107
33
|
})
|
|
108
34
|
|
|
109
|
-
|
|
35
|
+
await server.start()
|
|
36
|
+
await server.addEntrypoint()
|
|
110
37
|
})
|
|
111
38
|
}
|
package/cli/main.ts
CHANGED
|
@@ -9,13 +9,16 @@ import { registerConfigPrint } from "./config/print/register"
|
|
|
9
9
|
import { registerClone } from "./clone/register"
|
|
10
10
|
import { perfectCli } from "perfect-cli"
|
|
11
11
|
import pkg from "../package.json"
|
|
12
|
+
import semver from "semver"
|
|
12
13
|
|
|
13
14
|
const program = new Command()
|
|
14
15
|
|
|
15
16
|
program
|
|
16
17
|
.name("tsci")
|
|
17
18
|
.description("CLI for developing tscircuit snippets")
|
|
18
|
-
|
|
19
|
+
// HACK: at build time the version is old, we need to
|
|
20
|
+
// fix this at some point...
|
|
21
|
+
.version(semver.inc(pkg.version, "patch") ?? pkg.version)
|
|
19
22
|
|
|
20
23
|
registerDev(program)
|
|
21
24
|
registerClone(program)
|