Skip to content

Commit

Permalink
feat: tsci push snippet (#44)
Browse files Browse the repository at this point in the history
* feat: tsci push snippet

* wtf

* f

* patch: tweaks

* downgrade semver

* update lock file

* new bun lock file

* Update cli/push/register.ts

Co-authored-by: Severin Ibarluzea <[email protected]>

* change backup bumped logic

* tests: just started and f#$ked

* tests: completed + !PUSH CMD HARD CODED BECAUSE OF FAKE-SNIPPETS

* Update get-cli-test-fixture.ts

* Update get-test-server.ts

* uncommented

* tweaks

* Update get-cli-test-fixture.ts

* tests: done

---------

Co-authored-by: Severin Ibarluzea <[email protected]>
Co-authored-by: 0039dope <[email protected]>
  • Loading branch information
3 people authored Feb 4, 2025
1 parent c636f38 commit 517d5c8
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 1 deletion.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import semver from "semver"
import { registerExport } from "./export/register"
import { registerAuthPrintToken } from "./auth/print-token/register"
import { registerAuthSetToken } from "./auth/set-token/register"
import { registerPush } from "./push/register"

const program = new Command()

Expand All @@ -28,6 +29,7 @@ registerInit(program)

registerDev(program)
registerClone(program)
registerPush(program)

registerAuth(program)
registerAuthLogin(program)
Expand Down
199 changes: 199 additions & 0 deletions cli/push/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import type { Command } from "commander"
import { cliConfig } from "lib/cli-config"
import { getKy } from "lib/registry-api/get-ky"
import * as fs from "node:fs"
import * as path from "node:path"
import semver from "semver"

export const registerPush = (program: Command) => {
program
.command("push")
.description("Save snippet code to Registry API")
.argument("[file]", "Path to the snippet file")
.action(async (filePath?: string) => {
const sessionToken = cliConfig.get("sessionToken")
if (!sessionToken) {
console.error("You need to log in to save snippet.")
process.exit(1)
}

let snippetFilePath: string | null = null
if (filePath) {
snippetFilePath = path.resolve(filePath)
} else {
const defaultEntrypoint = path.resolve("index.tsx")
if (fs.existsSync(defaultEntrypoint)) {
snippetFilePath = defaultEntrypoint
console.log("No file provided. Using 'index.tsx' as the entrypoint.")
} else {
console.error(
"No entrypoint found. Run 'tsci init' to bootstrap a basic project.",
)
process.exit(1)
}
}

const packageJsonPath = path.resolve(
path.join(path.dirname(snippetFilePath), "package.json"),
)
let packageJson: { name?: string; author?: string; version?: string } = {}
if (fs.existsSync(packageJsonPath)) {
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString())
} catch {
console.error("Invalid package.json provided")
process.exit(1)
}
}

if (!fs.existsSync(snippetFilePath)) {
console.error(`File not found: ${snippetFilePath}`)
process.exit(1)
}

const ky = getKy()
const packageName = (
packageJson.name ?? path.parse(snippetFilePath).name
).replace(/^@/, "")

const packageAuthor =
packageJson.author?.split(" ")[0] ?? cliConfig.get("githubUsername")

const packageIdentifier = `${packageAuthor}/${packageName}`

let packageVersion =
packageJson.version ??
(await ky
.post<{
error?: { error_code: string }
package_releases?: { version: string; is_latest: boolean }[]
}>("package_releases/list", {
json: { package_name: packageIdentifier },
})
.json()
.then(
(response) =>
response.package_releases?.[response.package_releases.length - 1]
?.version,
)
.catch((error) => {
console.error("Failed to retrieve latest package version:", error)
process.exit(1)
}))

if (!packageVersion) {
console.log("Failed to retrieve package version.")
process.exit(1)
}

const updatePackageJsonVersion = (newVersion?: string) => {
if (packageJson.version) {
try {
packageJson.version = newVersion ?? packageVersion
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
)
} catch (error) {
console.error("Failed to update package.json version:", error)
}
}
}

const doesPackageExist = await ky
.post<{ error?: { error_code: string } }>("packages/get", {
json: { name: packageIdentifier },
throwHttpErrors: false,
})
.json()
.then(
(response) => !(response.error?.error_code === "package_not_found"),
)

if (!doesPackageExist) {
await ky
.post("packages/create", {
json: { name: packageIdentifier },
headers: { Authorization: `Bearer ${sessionToken}` },
})
.catch((error) => {
console.error("Error creating package:", error)
process.exit(1)
})
}

const doesReleaseExist = await ky
.post<{
error?: { error_code: string }
package_release?: { version: string }
}>("package_releases/get", {
json: {
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
},
throwHttpErrors: false,
})
.json()
.then((response) => {
if (response.package_release?.version) {
packageVersion = response.package_release.version
updatePackageJsonVersion(response.package_release.version)
return true
}
return !(response.error?.error_code === "package_release_not_found")
})

if (doesReleaseExist) {
const bumpedVersion = semver.inc(packageVersion, "patch")!
console.log(
`Incrementing Package Version ${packageVersion} -> ${bumpedVersion}`,
)
packageVersion = bumpedVersion
updatePackageJsonVersion(packageVersion)
}

await ky
.post("package_releases/create", {
json: {
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
},
throwHttpErrors: false,
})
.catch((error) => {
console.error("Error creating release:", error)
process.exit(1)
})

console.log("\n")

const directoryFiles = fs.readdirSync(path.dirname(snippetFilePath))
for (const file of directoryFiles) {
const fileExtension = path.extname(file).replace(".", "")
if (!["json", "tsx", "ts"].includes(fileExtension)) continue

const fileContent =
fs
.readFileSync(path.join(path.dirname(snippetFilePath), file))
.toString() ?? ""
await ky
.post("package_files/create", {
json: {
file_path: file,
content_text: fileContent,
package_name_with_version: `${packageIdentifier}@${packageVersion}`,
},
throwHttpErrors: false,
})
.then(() => {
console.log(`Uploaded file ${file} to the registry.`)
})
.catch((error) => {
console.error(`Error uploading file ${file}:`, error)
})
}

console.log(
`\n🎉 Successfully pushed package ${packageIdentifier}@${packageVersion} to the registry!${Bun.color("blue", "ansi")}`,
`https://tscircuit.com/${packageIdentifier} \x1b[0m`,
)
})
}
2 changes: 1 addition & 1 deletion lib/registry-api/get-ky.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getRegistryApiUrl } from "lib/cli-config"
import ky, { type AfterResponseHook } from "ky"

const prettyResponseErrorHook: AfterResponseHook = async (
export const prettyResponseErrorHook: AfterResponseHook = async (
_request,
_options,
response,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@tscircuit/core": "^0.0.249",
"@tscircuit/fake-snippets": "^0.0.5",
"@types/bun": "^1.1.15",
"@types/configstore": "^6.0.2",
"@types/react": "^19.0.1",
Expand All @@ -41,6 +42,7 @@
"ky": "^1.7.4",
"make-vfs": "^1.0.15",
"perfect-cli": "^1.0.20",
"redaxios": "^0.5.1",
"semver": "^7.6.3"
}
}
3 changes: 3 additions & 0 deletions tests/fake-snippets-api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "fake-snippets-api/lib/db/db-client" {
export function createDatabase(): any
}
54 changes: 54 additions & 0 deletions tests/fixtures/get-test-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { afterAll } from "bun:test"
import { startServer } from "@tscircuit/fake-snippets/bun-tests/fake-snippets-api/fixtures/start-server"
import { DbClient } from "@tscircuit/fake-snippets/fake-snippets-api/lib/db/db-client"
import ky from "ky"
import { prettyResponseErrorHook } from "lib/registry-api/get-ky"
import { cliConfig } from "lib/cli-config"
import { seed as seedDB } from "@tscircuit/fake-snippets/fake-snippets-api/lib/db/seed"

interface TestFixture {
url: string
server: any
ky: typeof ky
db: DbClient
}

export const getTestSnippetsServer = async (): Promise<TestFixture> => {
const port = 3789
const testInstanceId = Math.random().toString(36).substring(2, 15)
const testDbName = `testdb${testInstanceId}`

const { server, db } = await startServer({
port,
testDbName,
})

const url = `http://localhost:${port}/api`
seedDB(db)
const kyInstance = ky.create({
prefixUrl: url,
hooks: {
afterResponse: [prettyResponseErrorHook],
},
headers: {
Authorization: `Bearer ${db.accounts[0].account_id}`,
},
})

cliConfig.set("sessionToken", db.accounts[0].account_id)
cliConfig.set("registryApiUrl", url)

afterAll(async () => {
if (server && typeof server.stop === "function") {
await server.stop()
}
cliConfig.clear()
})

return {
url,
server,
ky: kyInstance,
db,
}
}
85 changes: 85 additions & 0 deletions tests/test4-push-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { test, expect, beforeEach, afterEach } from "bun:test"
import { getTestSnippetsServer } from "./fixtures/get-test-server"
import { getCliTestFixture } from "./fixtures/get-cli-test-fixture"
import * as fs from "node:fs"
import * as path from "node:path"

const { ky } = await getTestSnippetsServer()
const { tmpDir, runCommand } = await getCliTestFixture()

beforeEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true })
fs.mkdirSync(tmpDir, { recursive: true })
fs.writeFileSync(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-package",
version: "1.0.0",
}),
)
})
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true })
})

test("should fail if no entrypoint file is found", async () => {
await runCommand("tsci push").catch((e) => {
expect(e.message).toInclude(
"No entrypoint found. Run 'tsci init' to bootstrap a basic project.",
)
})
})

test("should use default entrypoint if no file is provided", async () => {
const defaultEntrypoint = path.resolve(tmpDir, "index.tsx")
fs.writeFileSync(defaultEntrypoint, "// Default entrypoint!")
const { stdout } = await runCommand(`tsci push`)
expect(stdout).toContain(
"No file provided. Using 'index.tsx' as the entrypoint.",
)
})

test("should fail if package.json is missing or invalid", async () => {
const snippetFilePath = path.resolve(tmpDir, "snippet.tsx")
fs.writeFileSync(snippetFilePath, "// Snippet content")

try {
await runCommand(`tsci push ${snippetFilePath}`)
} catch (error) {
expect(console.error).toHaveBeenCalledWith(
"Failed to retrieve package version.",
)
}
})

test("should create package if it does not exist", async () => {
const snippetFilePath = path.resolve(tmpDir, "snippet.tsx")

fs.writeFileSync(snippetFilePath, "// Snippet content")

const { stdout } = await runCommand(`tsci push ${snippetFilePath}`)
expect(stdout).toContain("Successfully pushed package")
})

test("should bump version if release already exists", async () => {
const snippetFilePath = path.resolve(tmpDir, "snippet.tsx")
const packageJsonPath = path.resolve(tmpDir, "package.json")

fs.writeFileSync(snippetFilePath, "// Snippet content")
fs.writeFileSync(
packageJsonPath,
JSON.stringify({ name: "test-package", version: "1.0.0" }),
)

const { stdout } = await runCommand(`tsci push ${snippetFilePath}`)
expect(stdout).toContain("Incrementing Package Version 1.0.0 -> 1.0.1")
})

test("should upload files to the registry", async () => {
const snippetFilePath = path.resolve(tmpDir, "snippet.tsx")

fs.writeFileSync(snippetFilePath, "// Snippet content")

const { stdout } = await runCommand(`tsci push ${snippetFilePath}`)
expect(stdout).toContain("Uploaded file snippet.tsx to the registry.")
})

0 comments on commit 517d5c8

Please sign in to comment.