@tscircuit/fake-snippets 0.0.88 → 0.0.89

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.
Files changed (69) hide show
  1. package/api/generated-index.js +96 -14
  2. package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
  3. package/bun.lock +187 -206
  4. package/dist/bundle.js +206 -100
  5. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  6. package/package.json +56 -47
  7. package/renovate.json +2 -1
  8. package/src/App.tsx +22 -3
  9. package/src/ContextProviders.tsx +2 -0
  10. package/src/build-watcher.ts +52 -0
  11. package/src/components/CmdKMenu.tsx +533 -197
  12. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  13. package/src/components/FileSidebar.tsx +11 -1
  14. package/src/components/Header2.tsx +7 -2
  15. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  16. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  17. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  18. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  19. package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
  20. package/src/components/PackageCard.tsx +66 -16
  21. package/src/components/SearchComponent.tsx +2 -2
  22. package/src/components/SuspenseRunFrame.tsx +14 -2
  23. package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
  24. package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
  25. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  26. package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
  27. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  28. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  29. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  30. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  31. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  32. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  33. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  34. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  35. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  36. package/src/components/package-port/CodeAndPreview.tsx +16 -0
  37. package/src/components/package-port/CodeEditor.tsx +113 -11
  38. package/src/components/package-port/CodeEditorHeader.tsx +39 -4
  39. package/src/components/package-port/EditorNav.tsx +41 -15
  40. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  41. package/src/components/package-port/QuickOpen.tsx +241 -0
  42. package/src/components/ui/dialog.tsx +1 -1
  43. package/src/components/ui/tree-view.tsx +1 -1
  44. package/src/global.d.ts +3 -0
  45. package/src/hooks/use-ai-review.ts +31 -0
  46. package/src/hooks/use-current-package-release.ts +5 -1
  47. package/src/hooks/use-download-zip.ts +50 -0
  48. package/src/hooks/use-hotkey.ts +116 -0
  49. package/src/hooks/use-package-by-package-id.ts +1 -0
  50. package/src/hooks/use-package-by-package-name.ts +1 -0
  51. package/src/hooks/use-package-files.ts +3 -0
  52. package/src/hooks/use-package-release.ts +5 -1
  53. package/src/hooks/use-package.ts +1 -0
  54. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  55. package/src/hooks/use-snippet.ts +1 -0
  56. package/src/hooks/useFileManagement.ts +26 -8
  57. package/src/hooks/usePackageFilesLoader.ts +3 -1
  58. package/src/index.css +11 -0
  59. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  60. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  61. package/src/lib/download-fns/download-png-utils.ts +31 -0
  62. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  63. package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
  64. package/src/lib/ts-lib-cache.ts +47 -0
  65. package/src/lib/types.ts +2 -0
  66. package/src/main.tsx +7 -0
  67. package/src/pages/dashboard.tsx +8 -5
  68. package/src/pages/view-package.tsx +15 -7
  69. package/vite.config.ts +100 -1
package/src/index.css CHANGED
@@ -67,3 +67,14 @@
67
67
  padding: 12px !important;
68
68
  margin: 0 !important;
69
69
  }
70
+
71
+ .markdown-content {
72
+ padding-top: 0.5rem;
73
+ max-width: none !important;
74
+ width: 100% !important;
75
+ }
76
+
77
+ .markdown-content .prose {
78
+ max-width: none !important;
79
+ width: 100% !important;
80
+ }
@@ -0,0 +1,17 @@
1
+ import { gunzipSync, strFromU8 } from "fflate"
2
+ import { base64ToBytes } from "./base64ToBytes"
3
+
4
+ export function decodeUrlHashToFsMap(
5
+ url: string,
6
+ ): Record<string, string> | null {
7
+ const base64Data = url.split("#data:application/gzip;base64,")[1]
8
+ if (!base64Data) return null
9
+ try {
10
+ const compressedData = base64ToBytes(base64Data)
11
+ const decompressedData = gunzipSync(compressedData)
12
+ const text = strFromU8(decompressedData)
13
+ return JSON.parse(text)
14
+ } catch {
15
+ return null
16
+ }
17
+ }
@@ -0,0 +1,88 @@
1
+ import { AnyCircuitElement } from "circuit-json"
2
+ import {
3
+ convertCircuitJsonToAssemblySvg,
4
+ convertCircuitJsonToPcbSvg,
5
+ convertCircuitJsonToSchematicSvg,
6
+ } from "circuit-to-svg"
7
+ import { saveAs } from "file-saver"
8
+
9
+ export type ImageFormat = "schematic" | "pcb" | "assembly"
10
+
11
+ interface DownloadCircuitPngOptions {
12
+ format: ImageFormat
13
+ width?: number
14
+ height?: number
15
+ }
16
+
17
+ const convertSvgToPng = async (svgString: string): Promise<Blob> => {
18
+ return new Promise((resolve, reject) => {
19
+ const canvas = document.createElement("canvas")
20
+ const ctx = canvas.getContext("2d")
21
+ if (!ctx) {
22
+ reject(new Error("Failed to get canvas context"))
23
+ return
24
+ }
25
+
26
+ const img = new Image()
27
+ img.onload = () => {
28
+ canvas.width = img.width || 800
29
+ canvas.height = img.height || 600
30
+ ctx.fillStyle = "white"
31
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
32
+ ctx.drawImage(img, 0, 0)
33
+
34
+ canvas.toBlob((blob) => {
35
+ if (blob) {
36
+ resolve(blob)
37
+ } else {
38
+ reject(new Error("Failed to convert canvas to blob"))
39
+ }
40
+ }, "image/png")
41
+ }
42
+
43
+ img.onerror = () => {
44
+ reject(new Error("Failed to load SVG image"))
45
+ }
46
+
47
+ const svgBlob = new Blob([svgString], { type: "image/svg+xml" })
48
+ const url = URL.createObjectURL(svgBlob)
49
+ img.src = url
50
+
51
+ setTimeout(() => URL.revokeObjectURL(url), 10000)
52
+ })
53
+ }
54
+
55
+ export const downloadCircuitPng = async (
56
+ circuitJson: AnyCircuitElement[],
57
+ fileName: string,
58
+ options: DownloadCircuitPngOptions = { format: "pcb" },
59
+ ) => {
60
+ try {
61
+ let blob: Blob
62
+ let svg: string
63
+
64
+ const svgOptions: any = {}
65
+ if (options.width) svgOptions.width = options.width
66
+ if (options.height) svgOptions.height = options.height
67
+
68
+ switch (options.format) {
69
+ case "schematic":
70
+ svg = convertCircuitJsonToSchematicSvg(circuitJson, svgOptions)
71
+ break
72
+ case "pcb":
73
+ svg = convertCircuitJsonToPcbSvg(circuitJson, svgOptions)
74
+ break
75
+ case "assembly":
76
+ svg = convertCircuitJsonToAssemblySvg(circuitJson, svgOptions)
77
+ break
78
+ default:
79
+ throw new Error(`Unsupported format: ${options.format}`)
80
+ }
81
+
82
+ blob = await convertSvgToPng(svg)
83
+ const downloadFileName = `${fileName}_${options.format}.png`
84
+ saveAs(blob, downloadFileName)
85
+ } catch (error) {
86
+ throw new Error(`Failed to download ${options.format} PNG: ${error}`)
87
+ }
88
+ }
@@ -0,0 +1,31 @@
1
+ import { AnyCircuitElement } from "circuit-json"
2
+ import { downloadCircuitPng, ImageFormat } from "./download-circuit-png"
3
+ import { toast } from "@/hooks/use-toast"
4
+
5
+ interface DownloadPngOptions {
6
+ circuitJson?: AnyCircuitElement[] | null
7
+ unscopedName?: string
8
+ author?: string
9
+ format: ImageFormat
10
+ }
11
+
12
+ export const downloadPngImage = async ({
13
+ circuitJson,
14
+ unscopedName,
15
+ format,
16
+ }: DownloadPngOptions) => {
17
+ try {
18
+ if (!circuitJson) {
19
+ throw new Error("Circuit JSON not available")
20
+ }
21
+ await downloadCircuitPng(circuitJson, unscopedName || "circuit", {
22
+ format,
23
+ })
24
+ } catch (error: any) {
25
+ toast({
26
+ title: `Error Downloading ${format.charAt(0).toUpperCase() + format.slice(1)} PNG`,
27
+ description: error.toString(),
28
+ variant: "destructive",
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,13 @@
1
+ import { gzipSync, strToU8 } from "fflate"
2
+ import { bytesToBase64 } from "./bytesToBase64"
3
+
4
+ export function encodeFsMapToUrlHash(
5
+ fsMap: Record<string, string>,
6
+ snippet_type?: string,
7
+ ): string {
8
+ const text = JSON.stringify(fsMap)
9
+ const compressedData = gzipSync(strToU8(text))
10
+ const base64Data = bytesToBase64(compressedData)
11
+ const typeParam = snippet_type ? `&snippet_type=${snippet_type}` : ""
12
+ return `${window.location.origin}/editor?${typeParam}#data:application/gzip;base64,${base64Data}`
13
+ }
@@ -14,7 +14,9 @@ export function populateQueryCacheWithSSRData(queryClient: QueryClient) {
14
14
 
15
15
  if (!ssrPackage || !ssrPackageRelease) return
16
16
 
17
+ // Cache lookups by package name and id
17
18
  queryClient.setQueryData(["package", ssrPackage.name], ssrPackage)
19
+ queryClient.setQueryData(["package", ssrPackage.package_id], ssrPackage)
18
20
  queryClient.setQueryData(["packages", ssrPackage.package_id], ssrPackage)
19
21
 
20
22
  queryClient.setQueryData(
@@ -29,6 +31,11 @@ export function populateQueryCacheWithSSRData(queryClient: QueryClient) {
29
31
  ssrPackageRelease,
30
32
  )
31
33
 
34
+ queryClient.setQueryData(
35
+ ["packageRelease", { is_latest: true, package_id: ssrPackage.package_id }],
36
+ ssrPackageRelease,
37
+ )
38
+
32
39
  queryClient.setQueryData(
33
40
  [
34
41
  "packageRelease",
@@ -0,0 +1,47 @@
1
+ import {
2
+ createDefaultMapFromCDN,
3
+ knownLibFilesForCompilerOptions,
4
+ } from "@typescript/vfs"
5
+ import { get, set } from "idb-keyval"
6
+ import { compressToUTF16, decompressFromUTF16 } from "lz-string"
7
+ import ts from "typescript"
8
+
9
+ const TS_LIB_VERSION = "5.6.3"
10
+ const CACHE_PREFIX = `ts-lib-${TS_LIB_VERSION}-`
11
+
12
+ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
13
+ const fsMap = new Map<string, string>()
14
+ const libs = knownLibFilesForCompilerOptions(
15
+ { target: ts.ScriptTarget.ES2022 },
16
+ ts,
17
+ )
18
+ const missing: string[] = []
19
+
20
+ for (const lib of libs) {
21
+ const cached = await get(CACHE_PREFIX + lib)
22
+ if (cached) {
23
+ fsMap.set("/" + lib, decompressFromUTF16(cached as string))
24
+ } else {
25
+ missing.push(lib)
26
+ }
27
+ }
28
+
29
+ if (missing.length > 0) {
30
+ const fetched = await createDefaultMapFromCDN(
31
+ { target: ts.ScriptTarget.ES2022 },
32
+ TS_LIB_VERSION,
33
+ true,
34
+ ts,
35
+ { compressToUTF16, decompressFromUTF16 } as any,
36
+ )
37
+ for (const [filename, content] of fetched) {
38
+ fsMap.set(filename, content)
39
+ await set(
40
+ CACHE_PREFIX + filename.replace(/^\//, ""),
41
+ compressToUTF16(content),
42
+ )
43
+ }
44
+ }
45
+
46
+ return fsMap
47
+ }
package/src/lib/types.ts CHANGED
@@ -11,6 +11,8 @@ declare global {
11
11
  format: (code: string, options: any) => string
12
12
  }
13
13
  prettierPlugins: any
14
+ /** Timestamp in milliseconds of when the application was first loaded */
15
+ __APP_LOADED_AT?: number
14
16
  }
15
17
  }
16
18
 
package/src/main.tsx CHANGED
@@ -1,7 +1,14 @@
1
1
  import { StrictMode } from "react"
2
2
  import { createRoot } from "react-dom/client"
3
3
  import App from "./App.tsx"
4
+
5
+ if (typeof window !== "undefined" && !window.__APP_LOADED_AT) {
6
+ window.__APP_LOADED_AT = Date.now()
7
+ }
4
8
  import "./index.css"
9
+ import { setupBuildWatcher } from "./build-watcher"
10
+
11
+ setupBuildWatcher()
5
12
 
6
13
  createRoot(document.getElementById("root")!).render(
7
14
  <StrictMode>
@@ -41,14 +41,17 @@ export const DashboardPage = () => {
41
41
  const response = await axios.post(`/packages/list`, {
42
42
  owner_github_username: currentUser,
43
43
  })
44
- return response.data.packages.sort(
45
- (a: any, b: any) =>
46
- new Date(b.updated_at || b.created_at).getTime() -
47
- new Date(a.updated_at || a.created_at).getTime(),
48
- )
44
+ return response.data.packages
49
45
  },
50
46
  {
51
47
  enabled: isLoggedIn,
48
+ select: (data: Package[]) => {
49
+ return [...data].sort(
50
+ (a: Package, b: Package) =>
51
+ new Date(b.updated_at || b.created_at).getTime() -
52
+ new Date(a.updated_at || a.created_at).getTime(),
53
+ )
54
+ },
52
55
  },
53
56
  )
54
57
 
@@ -21,11 +21,17 @@ export const ViewPackagePage = () => {
21
21
  data: packageRelease,
22
22
  error: packageReleaseError,
23
23
  isLoading: isLoadingPackageRelease,
24
- } = usePackageRelease({
25
- is_latest: true,
26
- package_name: `${author}/${packageName}`,
27
- include_ai_review: true,
28
- })
24
+ } = usePackageRelease(
25
+ {
26
+ is_latest: true,
27
+ package_name: `${author}/${packageName}`,
28
+ include_ai_review: true,
29
+ },
30
+ {
31
+ refetchInterval: (data) =>
32
+ data?.ai_review_requested && !data.ai_review_text ? 2000 : false,
33
+ },
34
+ )
29
35
 
30
36
  const { data: packageFiles } = usePackageFiles(
31
37
  packageRelease?.package_release_id,
@@ -46,15 +52,17 @@ export const ViewPackagePage = () => {
46
52
  </Helmet>
47
53
  <RepoPageContent
48
54
  packageFiles={packageFiles as any}
49
- packageInfo={packageInfo as any}
50
- packageRelease={packageRelease as any}
55
+ packageInfo={packageInfo}
56
+ packageRelease={packageRelease}
51
57
  importantFilePaths={["README.md", "LICENSE", "package.json"]}
52
58
  onFileClicked={(file) => {
59
+ if (!packageInfo?.package_id) return
53
60
  setLocation(
54
61
  `/editor?package_id=${packageInfo?.package_id}&file_path=${file.file_path}`,
55
62
  )
56
63
  }}
57
64
  onEditClicked={(file_path?: string) => {
65
+ if (!packageInfo?.package_id) return
58
66
  setLocation(
59
67
  `/editor?package_id=${packageInfo?.package_id}${
60
68
  file_path ? `&file_path=${file_path}` : ""
package/vite.config.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { createDatabase } from "./fake-snippets-api/lib/db/db-client"
2
2
  import { defineConfig, Plugin, UserConfig } from "vite"
3
3
  import type { PluginOption } from "vite"
4
- import path from "path"
4
+ import path, { extname } from "path"
5
+ import { readFileSync } from "fs"
6
+ import { execSync } from "child_process"
5
7
  import react from "@vitejs/plugin-react"
6
8
  import { ViteImageOptimizer } from "vite-plugin-image-optimizer"
7
9
  import { getNodeHandler } from "winterspec/adapters/node"
8
10
  import vercel from "vite-plugin-vercel"
11
+ import type { IncomingMessage, ServerResponse } from "http"
9
12
 
10
13
  // @ts-ignore
11
14
  import winterspecBundle from "./dist/bundle.js"
@@ -39,6 +42,100 @@ function apiFakePlugin(): Plugin {
39
42
  }
40
43
  }
41
44
 
45
+ function vercelSsrDevPlugin(): Plugin {
46
+ return {
47
+ name: "vercel-ssr-dev",
48
+ apply: "serve",
49
+ async configureServer(server) {
50
+ const port = server.config.server.port || 5173
51
+ process.env.TSC_DEV_SSR = "true"
52
+ process.env.TSC_BASE_URL = `http://127.0.0.1:${port}`
53
+ process.env.TSC_REGISTRY_API = `http://127.0.0.1:${port}/api`
54
+
55
+ const { default: ssrHandler, setHtmlContent } = await import(
56
+ "./api/generated-index.js"
57
+ )
58
+
59
+ server.middlewares.use(async (req, res, next) => {
60
+ const url = req.url?.split("?")[0] || ""
61
+ const accept = req.headers.accept || ""
62
+
63
+ if (url === "/" || url === "/landing.html") {
64
+ return next()
65
+ }
66
+
67
+ if (url.startsWith("/api/")) {
68
+ return next()
69
+ }
70
+
71
+ if (!accept.includes("text/html")) {
72
+ return next()
73
+ }
74
+
75
+ if (extname(url)) {
76
+ return next()
77
+ }
78
+
79
+ const patchedRes = res as ServerResponse & {
80
+ status?: (code: number) => ServerResponse
81
+ send?: (body: any) => void
82
+ }
83
+
84
+ if (!patchedRes.status) {
85
+ patchedRes.status = function (code: number) {
86
+ patchedRes.statusCode = code
87
+ return patchedRes
88
+ }
89
+ }
90
+
91
+ if (!patchedRes.send) {
92
+ patchedRes.send = function (body: any) {
93
+ if (Buffer.isBuffer(body) || typeof body === "string") {
94
+ patchedRes.end(body)
95
+ } else {
96
+ patchedRes.end(JSON.stringify(body))
97
+ }
98
+ }
99
+ }
100
+
101
+ try {
102
+ let rawHtml = readFileSync(
103
+ path.resolve(__dirname, "index.html"),
104
+ "utf-8",
105
+ )
106
+ rawHtml = rawHtml.replace(
107
+ /src="\.\/src\/main\.tsx"/,
108
+ 'src="/src/main.tsx"',
109
+ )
110
+ const transformed = await server.transformIndexHtml(url, rawHtml)
111
+ setHtmlContent(transformed)
112
+ await ssrHandler(req as IncomingMessage, patchedRes)
113
+ } catch (err) {
114
+ console.error("SSR handler error", err)
115
+ next()
116
+ }
117
+ })
118
+ },
119
+ }
120
+ }
121
+
122
+ function buildIdPlugin(): Plugin {
123
+ return {
124
+ name: "build-id",
125
+ transformIndexHtml(html) {
126
+ try {
127
+ const hash = execSync("git rev-parse --short HEAD").toString().trim()
128
+ return html.replace(
129
+ /<\/head>/i,
130
+ ` <meta name="tscircuit-build" content="${hash}"></head>`,
131
+ )
132
+ } catch {
133
+ return html
134
+ }
135
+ },
136
+ }
137
+ }
138
+
42
139
  export default defineConfig(async (): Promise<UserConfig> => {
43
140
  let proxyConfig: Record<string, any> | undefined
44
141
 
@@ -71,6 +168,8 @@ export default defineConfig(async (): Promise<UserConfig> => {
71
168
  effort: 6,
72
169
  },
73
170
  }),
171
+ vercelSsrDevPlugin(),
172
+ buildIdPlugin(),
74
173
  ]
75
174
 
76
175
  if (process.env.VITE_BUNDLE_ANALYZE === "true" || 1) {