@tscircuit/cli 0.1.23 ā 0.1.25
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/cli/clone/register.ts +73 -0
- package/cli/dev/DevServer.ts +31 -0
- package/cli/main.ts +1 -1
- package/cli/push/register.ts +7 -189
- package/dist/main.js +312 -221
- package/lib/file-server/FileServerRoutes.ts +3 -1
- package/lib/server/EventsRoutes.ts +32 -0
- package/lib/shared/push-snippet.ts +198 -0
- package/package.json +1 -1
- package/tests/test5-dev-server-save-snippet.test.ts +90 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface EventsRoutes {
|
|
2
|
+
"api/events/create": {
|
|
3
|
+
POST: {
|
|
4
|
+
requestJson: {
|
|
5
|
+
event_type: string
|
|
6
|
+
}
|
|
7
|
+
responseJson: {
|
|
8
|
+
event: {
|
|
9
|
+
event_id: string
|
|
10
|
+
event_type: string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
"api/events/list": {
|
|
16
|
+
GET: {
|
|
17
|
+
responseJson: {
|
|
18
|
+
event_list: Array<{
|
|
19
|
+
event_id: string
|
|
20
|
+
event_type:
|
|
21
|
+
| "FILE_UPDATED"
|
|
22
|
+
| "FAILED_TO_SAVE_SNIPPET"
|
|
23
|
+
| "SNIPPET_SAVED"
|
|
24
|
+
| "REQUEST_TO_SAVE_SNIPPET"
|
|
25
|
+
file_path: string
|
|
26
|
+
created_at: string
|
|
27
|
+
initiator?: string
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { cliConfig } from "lib/cli-config"
|
|
2
|
+
import { getKy } from "lib/registry-api/get-ky"
|
|
3
|
+
import * as fs from "node:fs"
|
|
4
|
+
import * as path from "node:path"
|
|
5
|
+
import semver from "semver"
|
|
6
|
+
|
|
7
|
+
type PushOptions = {
|
|
8
|
+
filePath?: string
|
|
9
|
+
onExit?: (code: number) => void
|
|
10
|
+
onError?: (message: string) => void
|
|
11
|
+
onSuccess?: (message: string) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const pushSnippet = async ({
|
|
15
|
+
filePath,
|
|
16
|
+
onExit = (code) => process.exit(code),
|
|
17
|
+
onError = (message) => console.error(message),
|
|
18
|
+
onSuccess = (message) => console.log(message),
|
|
19
|
+
}: PushOptions) => {
|
|
20
|
+
const sessionToken = cliConfig.get("sessionToken")
|
|
21
|
+
if (!sessionToken) {
|
|
22
|
+
onError("You need to log in to save snippet.")
|
|
23
|
+
return onExit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let snippetFilePath: string | null = null
|
|
27
|
+
if (filePath) {
|
|
28
|
+
snippetFilePath = path.resolve(filePath)
|
|
29
|
+
} else {
|
|
30
|
+
const defaultEntrypoint = path.resolve("index.tsx")
|
|
31
|
+
if (fs.existsSync(defaultEntrypoint)) {
|
|
32
|
+
snippetFilePath = defaultEntrypoint
|
|
33
|
+
onSuccess("No file provided. Using 'index.tsx' as the entrypoint.")
|
|
34
|
+
} else {
|
|
35
|
+
onError(
|
|
36
|
+
"No entrypoint found. Run 'tsci init' to bootstrap a basic project.",
|
|
37
|
+
)
|
|
38
|
+
return onExit(1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const packageJsonPath = path.resolve(
|
|
43
|
+
path.join(path.dirname(snippetFilePath), "package.json"),
|
|
44
|
+
)
|
|
45
|
+
let packageJson: { name?: string; author?: string; version?: string } = {}
|
|
46
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
47
|
+
try {
|
|
48
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString())
|
|
49
|
+
} catch {
|
|
50
|
+
onError("Invalid package.json provided")
|
|
51
|
+
return onExit(1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(snippetFilePath)) {
|
|
56
|
+
onError(`File not found: ${snippetFilePath}`)
|
|
57
|
+
return onExit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ky = getKy()
|
|
61
|
+
const packageName = (
|
|
62
|
+
packageJson.name ?? path.parse(snippetFilePath).name
|
|
63
|
+
).replace(/^@/, "")
|
|
64
|
+
const packageAuthor =
|
|
65
|
+
packageJson.author?.split(" ")[0] ?? cliConfig.get("githubUsername")
|
|
66
|
+
const packageIdentifier = `${packageAuthor}/${packageName}`
|
|
67
|
+
|
|
68
|
+
let packageVersion =
|
|
69
|
+
packageJson.version ??
|
|
70
|
+
(await ky
|
|
71
|
+
.post<{
|
|
72
|
+
error?: { error_code: string }
|
|
73
|
+
package_releases?: { version: string; is_latest: boolean }[]
|
|
74
|
+
}>("package_releases/list", {
|
|
75
|
+
json: { package_name: packageIdentifier },
|
|
76
|
+
})
|
|
77
|
+
.json()
|
|
78
|
+
.then(
|
|
79
|
+
(response) =>
|
|
80
|
+
response.package_releases?.[response.package_releases.length - 1]
|
|
81
|
+
?.version,
|
|
82
|
+
)
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
onError("Failed to retrieve latest package version:" + error)
|
|
85
|
+
return onExit(1)
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
if (!packageVersion) {
|
|
89
|
+
onError("Failed to retrieve package version.")
|
|
90
|
+
return onExit(1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const updatePackageJsonVersion = (newVersion?: string) => {
|
|
94
|
+
if (packageJson.version) {
|
|
95
|
+
try {
|
|
96
|
+
packageJson.version = newVersion ?? `${packageVersion}`
|
|
97
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
|
98
|
+
} catch (error) {
|
|
99
|
+
onError("Failed to update package.json version:" + error)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const doesPackageExist = await ky
|
|
105
|
+
.post<{ error?: { error_code: string } }>("packages/get", {
|
|
106
|
+
json: { name: packageIdentifier },
|
|
107
|
+
throwHttpErrors: false,
|
|
108
|
+
})
|
|
109
|
+
.json()
|
|
110
|
+
.then((response) => !(response.error?.error_code === "package_not_found"))
|
|
111
|
+
|
|
112
|
+
if (!doesPackageExist) {
|
|
113
|
+
await ky
|
|
114
|
+
.post("packages/create", {
|
|
115
|
+
json: { name: packageIdentifier },
|
|
116
|
+
headers: { Authorization: `Bearer ${sessionToken}` },
|
|
117
|
+
})
|
|
118
|
+
.catch((error) => {
|
|
119
|
+
onError("Error creating package:" + error)
|
|
120
|
+
return onExit(1)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const doesReleaseExist = await ky
|
|
125
|
+
.post<{
|
|
126
|
+
error?: { error_code: string }
|
|
127
|
+
package_release?: { version: string }
|
|
128
|
+
}>("package_releases/get", {
|
|
129
|
+
json: {
|
|
130
|
+
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
|
|
131
|
+
},
|
|
132
|
+
throwHttpErrors: false,
|
|
133
|
+
})
|
|
134
|
+
.json()
|
|
135
|
+
.then((response) => {
|
|
136
|
+
if (response.package_release?.version) {
|
|
137
|
+
packageVersion = response.package_release.version
|
|
138
|
+
updatePackageJsonVersion(response.package_release.version)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
return !(response.error?.error_code === "package_release_not_found")
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (doesReleaseExist) {
|
|
145
|
+
const bumpedVersion = semver.inc(packageVersion, "patch")!
|
|
146
|
+
onSuccess(
|
|
147
|
+
`Incrementing Package Version ${packageVersion} -> ${bumpedVersion}`,
|
|
148
|
+
)
|
|
149
|
+
packageVersion = bumpedVersion
|
|
150
|
+
updatePackageJsonVersion(packageVersion)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await ky
|
|
154
|
+
.post("package_releases/create", {
|
|
155
|
+
json: {
|
|
156
|
+
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
|
|
157
|
+
},
|
|
158
|
+
throwHttpErrors: false,
|
|
159
|
+
})
|
|
160
|
+
.catch((error) => {
|
|
161
|
+
onError("Error creating release:" + error)
|
|
162
|
+
return onExit(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
onSuccess("\n")
|
|
166
|
+
|
|
167
|
+
const directoryFiles = fs.readdirSync(path.dirname(snippetFilePath))
|
|
168
|
+
for (const file of directoryFiles) {
|
|
169
|
+
const fileExtension = path.extname(file).replace(".", "")
|
|
170
|
+
if (!["json", "tsx", "ts"].includes(fileExtension)) continue
|
|
171
|
+
const fileContent =
|
|
172
|
+
fs
|
|
173
|
+
.readFileSync(path.join(path.dirname(snippetFilePath), file))
|
|
174
|
+
.toString() ?? ""
|
|
175
|
+
await ky
|
|
176
|
+
.post("package_files/create", {
|
|
177
|
+
json: {
|
|
178
|
+
file_path: file,
|
|
179
|
+
content_text: fileContent,
|
|
180
|
+
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
|
|
181
|
+
},
|
|
182
|
+
throwHttpErrors: false,
|
|
183
|
+
})
|
|
184
|
+
.then(() => {
|
|
185
|
+
onSuccess(`Uploaded file ${file} to the registry.`)
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
onError(`Error uploading file ${file}:` + error)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onSuccess(
|
|
193
|
+
[
|
|
194
|
+
`\nš Successfully pushed package ${packageIdentifier}@${packageVersion} to the registry!${Bun.color("blue", "ansi")}`,
|
|
195
|
+
`https://tscircuit.com/${packageIdentifier} \x1b[0m`,
|
|
196
|
+
].join(" "),
|
|
197
|
+
)
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { test, expect, afterAll } from "bun:test"
|
|
2
|
+
import { DevServer } from "cli/dev/DevServer"
|
|
3
|
+
import { getTestFixture } from "tests/fixtures/get-test-fixture"
|
|
4
|
+
import { EventsWatcher } from "lib/server/EventsWatcher"
|
|
5
|
+
import { getTestSnippetsServer } from "./fixtures/get-test-server"
|
|
6
|
+
import { cliConfig } from "lib/cli-config"
|
|
7
|
+
|
|
8
|
+
test("test saveSnippet via REQUEST_TO_SAVE_SNIPPET event with CLI token setup", async () => {
|
|
9
|
+
afterAll(() => {
|
|
10
|
+
eventManager.stop()
|
|
11
|
+
devServer.stop()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// Start snippets server
|
|
15
|
+
await getTestSnippetsServer()
|
|
16
|
+
|
|
17
|
+
// Set up a temporary directory with a sample snippet file
|
|
18
|
+
const { tempDirPath, devServerPort, devServerUrl } = await getTestFixture({
|
|
19
|
+
vfs: {
|
|
20
|
+
"snippet.tsx": `
|
|
21
|
+
export const MyCircuit = () => (
|
|
22
|
+
<board width="10mm" height="10mm">
|
|
23
|
+
<chip name="U1" footprint="soic8" />
|
|
24
|
+
</board>
|
|
25
|
+
)
|
|
26
|
+
`,
|
|
27
|
+
"manual-edits.json": "{}",
|
|
28
|
+
"package.json": JSON.stringify({
|
|
29
|
+
version: "0.0.1",
|
|
30
|
+
name: "snippet",
|
|
31
|
+
author: "test-author",
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Create and start the DevServer instance
|
|
37
|
+
const devServer = new DevServer({
|
|
38
|
+
port: devServerPort,
|
|
39
|
+
componentFilePath: `${tempDirPath}/snippet.tsx`,
|
|
40
|
+
})
|
|
41
|
+
await devServer.start()
|
|
42
|
+
|
|
43
|
+
// Start the EventsWatcher to listen for events
|
|
44
|
+
const eventManager = new EventsWatcher(devServerUrl)
|
|
45
|
+
await eventManager.start()
|
|
46
|
+
|
|
47
|
+
// Emit the REQUEST_TO_SAVE_SNIPPET event
|
|
48
|
+
devServer.fsKy.post("api/events/create", {
|
|
49
|
+
json: { event_type: "REQUEST_TO_SAVE_SNIPPET" },
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Promises to wait for specific events using Promise.withResolvers()
|
|
53
|
+
const {
|
|
54
|
+
promise: requestToSaveSnippetPromise,
|
|
55
|
+
resolve: resolveRequestToSaveSnippet,
|
|
56
|
+
} = Promise.withResolvers<void>()
|
|
57
|
+
eventManager.on("REQUEST_TO_SAVE_SNIPPET", async () => {
|
|
58
|
+
resolveRequestToSaveSnippet()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const sessionToken = cliConfig.get("sessionToken")
|
|
62
|
+
cliConfig.delete("sessionToken")
|
|
63
|
+
|
|
64
|
+
const { promise: snippetSavedPromise, resolve: resolveSnippetSaved } =
|
|
65
|
+
Promise.withResolvers<void>()
|
|
66
|
+
eventManager.on("SNIPPET_SAVED", () => {
|
|
67
|
+
resolveSnippetSaved()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
promise: snippetSaveFailedPromise,
|
|
72
|
+
resolve: resolveSnippetSaveFailed,
|
|
73
|
+
} = Promise.withResolvers<void>()
|
|
74
|
+
eventManager.on("FAILED_TO_SAVE_SNIPPET", () => {
|
|
75
|
+
resolveSnippetSaveFailed()
|
|
76
|
+
cliConfig.set("sessionToken", sessionToken)
|
|
77
|
+
devServer.fsKy.post("api/events/create", {
|
|
78
|
+
json: { event_type: "REQUEST_TO_SAVE_SNIPPET" },
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Wait for the REQUEST_TO_SAVE_SNIPPET event to be detected
|
|
83
|
+
expect(requestToSaveSnippetPromise).resolves.toBeUndefined()
|
|
84
|
+
|
|
85
|
+
// Wait for the FAILED_TO_SAVE_SNIPPET event to be detected
|
|
86
|
+
expect(snippetSaveFailedPromise).resolves.toBeUndefined()
|
|
87
|
+
|
|
88
|
+
// Wait for the SNIPPET_SAVED event to be detected
|
|
89
|
+
expect(snippetSavedPromise).resolves.toBeUndefined()
|
|
90
|
+
}, 20_000)
|