@tscircuit/fake-snippets 0.0.109 → 0.0.111

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 (185) hide show
  1. package/.github/workflows/bun-formatcheck.yml +2 -2
  2. package/.github/workflows/bun-pver-release.yml +3 -3
  3. package/.github/workflows/bun-test.yml +1 -1
  4. package/.github/workflows/bun-typecheck.yml +2 -2
  5. package/.github/workflows/update-snapshots.yml +1 -1
  6. package/README.md +4 -0
  7. package/api/generated-index.js +37 -3
  8. package/biome.json +2 -1
  9. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +32 -3
  10. package/bun-tests/fake-snippets-api/fixtures/preload.ts +18 -0
  11. package/bun-tests/fake-snippets-api/routes/orgs/add_member.test.ts +26 -0
  12. package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +37 -0
  13. package/bun-tests/fake-snippets-api/routes/orgs/get.test.ts +52 -0
  14. package/bun-tests/fake-snippets-api/routes/orgs/list.test.ts +17 -0
  15. package/bun-tests/fake-snippets-api/routes/orgs/list_members.test.ts +23 -0
  16. package/bun-tests/fake-snippets-api/routes/orgs/remove_member.test.ts +81 -0
  17. package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +151 -0
  18. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +1 -1
  19. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +15 -13
  20. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -24
  21. package/bun-tests/fake-snippets-api/routes/package_files/delete.test.ts +9 -9
  22. package/bun-tests/fake-snippets-api/routes/package_files/download.test.ts +4 -4
  23. package/bun-tests/fake-snippets-api/routes/package_files/get.test.ts +38 -28
  24. package/bun-tests/fake-snippets-api/routes/package_files/list.test.ts +23 -15
  25. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +33 -0
  26. package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +4 -4
  27. package/bun-tests/fake-snippets-api/routes/package_releases/get_image_generation_fields.test.ts +38 -0
  28. package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +19 -0
  29. package/bun-tests/fake-snippets-api/routes/packages/fork.test.ts +3 -4
  30. package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +30 -0
  31. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +4 -2
  32. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +34 -0
  33. package/bun.lock +361 -453
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1313 -639
  36. package/dist/index.d.ts +313 -6
  37. package/dist/index.js +328 -24
  38. package/dist/schema.d.ts +290 -1
  39. package/dist/schema.js +54 -1
  40. package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
  41. package/fake-snippets-api/lib/db/db-client.ts +219 -4
  42. package/fake-snippets-api/lib/db/schema.ts +63 -1
  43. package/fake-snippets-api/lib/db/seed.ts +100 -0
  44. package/fake-snippets-api/lib/middleware/with-session-auth.ts +60 -8
  45. package/fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor.ts +2 -2
  46. package/fake-snippets-api/lib/public-mapping/public-map-org.ts +33 -0
  47. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +10 -0
  48. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +17 -0
  49. package/fake-snippets-api/routes/api/orgs/add_member.ts +52 -0
  50. package/fake-snippets-api/routes/api/orgs/create.ts +48 -0
  51. package/fake-snippets-api/routes/api/orgs/get.ts +39 -0
  52. package/fake-snippets-api/routes/api/orgs/list.ts +31 -0
  53. package/fake-snippets-api/routes/api/orgs/list_members.ts +60 -0
  54. package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
  55. package/fake-snippets-api/routes/api/orgs/update.ts +118 -0
  56. package/fake-snippets-api/routes/api/package_files/get.ts +3 -6
  57. package/fake-snippets-api/routes/api/package_files/list.ts +7 -4
  58. package/fake-snippets-api/routes/api/packages/create.ts +57 -10
  59. package/fake-snippets-api/routes/api/packages/get.ts +23 -0
  60. package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +13 -11
  61. package/fake-snippets-api/routes/api/packages/list.ts +29 -2
  62. package/fake-snippets-api/routes/api/packages/update_ai_description.ts +37 -0
  63. package/package.json +25 -19
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +27 -8
  67. package/src/ContextProviders.tsx +25 -2
  68. package/src/components/CircuitJsonImportDialog.tsx +1 -1
  69. package/src/components/CmdKMenu.tsx +281 -247
  70. package/src/components/DownloadButtonAndMenu.tsx +17 -5
  71. package/src/components/FileSidebar.tsx +11 -17
  72. package/src/components/Footer.tsx +8 -9
  73. package/src/components/Header.tsx +19 -32
  74. package/src/components/Header2.tsx +16 -32
  75. package/src/components/HeaderDropdown.tsx +13 -8
  76. package/src/components/HeaderLogin.tsx +43 -15
  77. package/src/components/NotFound.tsx +5 -5
  78. package/src/components/PackageBreadcrumb.tsx +6 -12
  79. package/src/components/PackageSearchResults.tsx +1 -1
  80. package/src/components/PrefetchPageLink.tsx +7 -1
  81. package/src/components/ProfileRouter.tsx +32 -0
  82. package/src/components/SearchComponent.tsx +12 -8
  83. package/src/components/SentryNotFoundReporter.tsx +44 -0
  84. package/src/components/UserCard.tsx +80 -0
  85. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  86. package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
  87. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
  88. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
  89. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
  90. package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
  91. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
  92. package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
  93. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  94. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  95. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  96. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  97. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  98. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  99. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  100. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  101. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  102. package/src/components/organization/OrganizationCard.tsx +206 -0
  103. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  104. package/src/components/organization/OrganizationHeader.tsx +154 -0
  105. package/src/components/organization/OrganizationMembers.tsx +146 -0
  106. package/src/components/package-port/CodeAndPreview.tsx +15 -12
  107. package/src/components/package-port/CodeEditor.tsx +4 -30
  108. package/src/components/package-port/CodeEditorHeader.tsx +123 -61
  109. package/src/components/package-port/EditorNav.tsx +32 -49
  110. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  111. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  112. package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
  113. package/src/components/ui/tree-view.tsx +6 -3
  114. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  115. package/src/hooks/use-create-org-mutation.ts +38 -0
  116. package/src/hooks/use-create-package-mutation.ts +3 -0
  117. package/src/hooks/use-current-package-release.ts +4 -3
  118. package/src/hooks/use-download-zip.ts +2 -2
  119. package/src/hooks/use-global-store.ts +6 -4
  120. package/src/hooks/use-hydration.ts +30 -0
  121. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  122. package/src/hooks/use-list-org-members.ts +27 -0
  123. package/src/hooks/use-list-user-orgs.ts +25 -0
  124. package/src/hooks/use-org-by-github-handle.ts +26 -0
  125. package/src/hooks/use-org.ts +24 -0
  126. package/src/hooks/use-organization.ts +42 -0
  127. package/src/hooks/use-package-as-snippet.ts +4 -2
  128. package/src/hooks/use-package-builds.ts +6 -2
  129. package/src/hooks/use-package-files.ts +5 -3
  130. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  131. package/src/hooks/use-package-release-images.ts +105 -0
  132. package/src/hooks/use-package-release.ts +2 -2
  133. package/src/hooks/use-package-stars.ts +80 -4
  134. package/src/hooks/use-preview-images.ts +6 -3
  135. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  136. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  137. package/src/hooks/use-update-org-mutation.ts +41 -0
  138. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  139. package/src/hooks/useFileManagement.ts +51 -22
  140. package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
  141. package/src/hooks/usePackageFilesLoader.ts +2 -2
  142. package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
  143. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
  144. package/src/lib/download-fns/download-kicad-files.ts +22 -11
  145. package/src/lib/download-fns/download-step.ts +12 -0
  146. package/src/lib/normalize-svg-for-tile.ts +50 -0
  147. package/src/lib/posthog.ts +11 -9
  148. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  149. package/src/lib/sentry.ts +14 -0
  150. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  151. package/src/lib/ts-lib-cache.ts +122 -7
  152. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  153. package/src/lib/utils/findTargetFile.ts +45 -10
  154. package/src/lib/utils/isComponentExported.ts +2 -1
  155. package/src/main.tsx +2 -1
  156. package/src/pages/create-organization.tsx +169 -0
  157. package/src/pages/dashboard.tsx +38 -6
  158. package/src/pages/datasheet.tsx +1 -1
  159. package/src/pages/datasheets.tsx +3 -3
  160. package/src/pages/editor.tsx +4 -6
  161. package/src/pages/landing.tsx +6 -6
  162. package/src/pages/latest.tsx +3 -0
  163. package/src/pages/organization-profile.tsx +199 -0
  164. package/src/pages/organization-settings.tsx +569 -0
  165. package/src/pages/package-editor.tsx +21 -21
  166. package/src/pages/preview-release.tsx +75 -145
  167. package/src/pages/quickstart.tsx +159 -123
  168. package/src/pages/release-detail.tsx +119 -31
  169. package/src/pages/search.tsx +197 -57
  170. package/src/pages/settings-redirect.tsx +44 -0
  171. package/src/pages/trending.tsx +29 -20
  172. package/src/pages/user-profile.tsx +58 -7
  173. package/src/pages/user-settings.tsx +161 -0
  174. package/src/pages/view-package.tsx +30 -16
  175. package/vite.config.ts +9 -0
  176. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  177. package/src/components/JLCPCBImportDialog.tsx +0 -280
  178. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  179. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
  180. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
  181. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  182. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  183. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  184. package/src/components/PageSearchComponent.tsx +0 -148
  185. package/src/pages/package-builds.tsx +0 -33
@@ -0,0 +1,148 @@
1
+ import type { QueryKey } from "react-query"
2
+ import { posthog } from "./posthog"
3
+
4
+ const TARGET_HOSTNAMES = new Set(["api.tscircuit.com"])
5
+
6
+ type FailureContext = {
7
+ operationType: "query" | "mutation"
8
+ queryKey?: QueryKey
9
+ mutationKey?: unknown
10
+ }
11
+
12
+ type RedaxiosConfig = {
13
+ url?: string
14
+ baseURL?: string
15
+ method?: string
16
+ }
17
+
18
+ type ResponseLike = {
19
+ url?: string
20
+ status?: number
21
+ statusText?: string
22
+ config?: RedaxiosConfig
23
+ }
24
+
25
+ const isPosthogLoaded = () => Boolean((posthog as any)?.__loaded)
26
+
27
+ const toUpperCaseMethod = (method?: string) => method?.toUpperCase() ?? "GET"
28
+
29
+ const resolveAbsoluteUrl = (url?: string, baseURL?: string) => {
30
+ if (!url) return undefined
31
+
32
+ try {
33
+ if (baseURL) {
34
+ return new URL(url, baseURL).toString()
35
+ }
36
+
37
+ if (typeof window !== "undefined") {
38
+ return new URL(url, window.location.origin).toString()
39
+ }
40
+
41
+ return new URL(url).toString()
42
+ } catch (error) {
43
+ console.warn("Failed to resolve API failure URL", error)
44
+ return undefined
45
+ }
46
+ }
47
+
48
+ const shouldTrackUrl = (resolvedUrl?: string) => {
49
+ if (!resolvedUrl) return false
50
+
51
+ try {
52
+ const { hostname } = new URL(resolvedUrl)
53
+ return TARGET_HOSTNAMES.has(hostname)
54
+ } catch (error) {
55
+ console.warn("Failed to parse URL for API failure tracking", error)
56
+ return false
57
+ }
58
+ }
59
+
60
+ const serializeKey = (key?: unknown) => {
61
+ if (!key) return undefined
62
+
63
+ try {
64
+ return JSON.stringify(key)
65
+ } catch {
66
+ return String(key)
67
+ }
68
+ }
69
+
70
+ const extractFromResponse = (error: unknown): ResponseLike | null => {
71
+ if (!error || typeof error !== "object") return null
72
+
73
+ const maybeResponse = error as Partial<ResponseLike>
74
+ if (typeof maybeResponse.status !== "number" || !("url" in maybeResponse)) {
75
+ return null
76
+ }
77
+
78
+ return {
79
+ url: maybeResponse.url,
80
+ status: maybeResponse.status,
81
+ statusText: maybeResponse.statusText,
82
+ config: maybeResponse.config,
83
+ }
84
+ }
85
+
86
+ const extractFromAxiosError = (error: unknown): ResponseLike | null => {
87
+ if (!error || typeof error !== "object") return null
88
+
89
+ const maybeAxiosError = error as {
90
+ response?: ResponseLike
91
+ config?: RedaxiosConfig
92
+ message?: string
93
+ }
94
+
95
+ if (!maybeAxiosError.response && !maybeAxiosError.config) {
96
+ return null
97
+ }
98
+
99
+ return {
100
+ url: maybeAxiosError.response?.url,
101
+ status: maybeAxiosError.response?.status,
102
+ statusText: maybeAxiosError.response?.statusText,
103
+ config: maybeAxiosError.response?.config ?? maybeAxiosError.config,
104
+ }
105
+ }
106
+
107
+ const captureApiFailure = (
108
+ response: ResponseLike,
109
+ error: unknown,
110
+ context: FailureContext,
111
+ ) => {
112
+ if (!isPosthogLoaded()) return
113
+
114
+ const resolvedUrl =
115
+ resolveAbsoluteUrl(response.url, response.config?.baseURL) ??
116
+ resolveAbsoluteUrl(response.config?.url, response.config?.baseURL)
117
+ if (!shouldTrackUrl(resolvedUrl)) return
118
+
119
+ const errorMessage =
120
+ error instanceof Error
121
+ ? error.message
122
+ : typeof error === "string"
123
+ ? error
124
+ : undefined
125
+
126
+ posthog.capture("api_request_failed", {
127
+ url: resolvedUrl,
128
+ method: toUpperCaseMethod(response.config?.method),
129
+ status: response.status,
130
+ statusText: response.statusText,
131
+ errorMessage,
132
+ operationType: context.operationType,
133
+ queryKey: serializeKey(context.queryKey),
134
+ mutationKey: serializeKey(context.mutationKey),
135
+ environment:
136
+ typeof window !== "undefined" ? window.location.hostname : undefined,
137
+ })
138
+ }
139
+
140
+ export const trackReactQueryApiFailure = (
141
+ error: unknown,
142
+ context: FailureContext,
143
+ ) => {
144
+ const response = extractFromResponse(error) ?? extractFromAxiosError(error)
145
+ if (!response) return
146
+
147
+ captureApiFailure(response, error, context)
148
+ }
@@ -0,0 +1,14 @@
1
+ import * as Sentry from "@sentry/react"
2
+
3
+ if (
4
+ typeof window !== "undefined" &&
5
+ import.meta.env.VITE_SENTRY_DSN &&
6
+ !window.location.hostname.includes("localhost") &&
7
+ !window.location.hostname.includes("127.0.0.1")
8
+ ) {
9
+ Sentry.init({
10
+ dsn: import.meta.env.VITE_SENTRY_DSN,
11
+ })
12
+ }
13
+
14
+ export { Sentry }
@@ -7,15 +7,11 @@ export default () => (
7
7
  resistance="1k"
8
8
  footprint="0402"
9
9
  name="R1"
10
- schX={3}
11
- pcbX={3}
12
10
  />
13
11
  <capacitor
14
12
  capacitance="1000pF"
15
13
  footprint="0402"
16
14
  name="C1"
17
- schX={-3}
18
- pcbX={-3}
19
15
  />
20
16
  <trace from=".R1 > .pin1" to=".C1 > .pin1" />
21
17
  </board>
@@ -8,6 +8,7 @@ import ts from "typescript"
8
8
 
9
9
  const TS_LIB_VERSION = "5.6.3"
10
10
  const CACHE_PREFIX = `ts-lib-${TS_LIB_VERSION}-`
11
+ const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 // 7 days
11
12
 
12
13
  export async function loadDefaultLibMap(): Promise<Map<string, string>> {
13
14
  const fsMap = new Map<string, string>()
@@ -18,9 +19,23 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
18
19
  const missing: string[] = []
19
20
 
20
21
  for (const lib of libs) {
21
- const cached = await get(CACHE_PREFIX + lib)
22
- if (cached) {
23
- fsMap.set("/" + lib, decompressFromUTF16(cached as string))
22
+ const cacheKey = CACHE_PREFIX + lib
23
+ const cached = await get(cacheKey)
24
+ if (
25
+ cached &&
26
+ typeof cached === "object" &&
27
+ "compressedFileContent" in cached &&
28
+ "timestamp" in cached
29
+ ) {
30
+ const { compressedFileContent, timestamp } = cached as {
31
+ compressedFileContent: string
32
+ timestamp: number
33
+ }
34
+ if (Date.now() - timestamp < CACHE_TTL) {
35
+ fsMap.set("/" + lib, decompressFromUTF16(compressedFileContent))
36
+ } else {
37
+ missing.push(lib)
38
+ }
24
39
  } else {
25
40
  missing.push(lib)
26
41
  }
@@ -36,12 +51,112 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
36
51
  )
37
52
  for (const [filename, content] of fetched) {
38
53
  fsMap.set(filename, content)
39
- await set(
40
- CACHE_PREFIX + filename.replace(/^\//, ""),
41
- compressToUTF16(content),
42
- )
54
+ const cacheKey = CACHE_PREFIX + filename.replace(/^\//, "")
55
+ const compressed = compressToUTF16(content)
56
+ await set(cacheKey, {
57
+ compressedFileContent: compressed,
58
+ timestamp: Date.now(),
59
+ }).catch(() => {})
43
60
  }
44
61
  }
45
62
 
46
63
  return fsMap
47
64
  }
65
+
66
+ export async function fetchWithPackageCaching(
67
+ input: RequestInfo | URL,
68
+ init?: RequestInit,
69
+ ): Promise<Response> {
70
+ const url = typeof input === "string" ? input : input.toString()
71
+
72
+ // Only cache GET requests for packages
73
+ if (init?.method && init.method !== "GET") {
74
+ return fetch(input, init)
75
+ }
76
+
77
+ // Check if this should be cached
78
+ const shouldCache =
79
+ url.includes("jsdelivr.net") ||
80
+ url.includes("unpkg.com") ||
81
+ url.includes("@types/") ||
82
+ url.includes("@tsci/")
83
+
84
+ if (!shouldCache) {
85
+ return fetch(input, init)
86
+ }
87
+
88
+ const cacheKey = `package-cache-${url}`
89
+
90
+ // Check cache
91
+ const cached = await get(cacheKey).catch(() => null)
92
+ if (
93
+ cached &&
94
+ typeof cached === "object" &&
95
+ "compressedFileContent" in cached &&
96
+ "timestamp" in cached
97
+ ) {
98
+ const { compressedFileContent, timestamp } = cached as {
99
+ compressedFileContent: string
100
+ timestamp: number
101
+ }
102
+ if (Date.now() - timestamp < CACHE_TTL) {
103
+ return new Response(decompressFromUTF16(compressedFileContent), {
104
+ status: 200,
105
+ statusText: "OK",
106
+ })
107
+ }
108
+ }
109
+
110
+ // Handle @tsci packages
111
+ let fetchUrl = url
112
+ if (
113
+ url.includes("@tsci/") &&
114
+ (url.includes("jsdelivr.net") || url.includes("data.jsdelivr.com"))
115
+ ) {
116
+ let packagePath = ""
117
+ if (url.includes("jsdelivr.net")) {
118
+ packagePath = url.replace("https://cdn.jsdelivr.net/npm/@tsci/", "")
119
+ } else if (url.includes("/v1/package/resolve/npm/@tsci/")) {
120
+ const resolveIndex = url.indexOf("/v1/package/resolve/npm/@tsci/")
121
+ packagePath = url.substring(
122
+ resolveIndex + "/v1/package/resolve/npm/@tsci/".length,
123
+ )
124
+ } else if (url.includes("/v1/package/npm/@tsci/")) {
125
+ const npmIndex = url.indexOf("/v1/package/npm/@tsci/")
126
+ packagePath = url.substring(npmIndex + "/v1/package/npm/@tsci/".length)
127
+ }
128
+
129
+ if (packagePath) {
130
+ // Convert dots to slashes in the package name part (like original logic)
131
+ const parts = packagePath.split("/")
132
+ if (parts.length > 0) {
133
+ parts[0] = parts[0].replace(/\./, "/")
134
+ }
135
+ const transformedPackagePath = parts.join("/")
136
+
137
+ const apiUrl = import.meta.env.VITE_SNIPPETS_API_URL ?? "/api"
138
+ const isResolve = url.includes("/resolve/")
139
+ fetchUrl = `${apiUrl}/snippets/download?jsdelivr_resolve=${isResolve}&jsdelivr_path=${encodeURIComponent(
140
+ transformedPackagePath,
141
+ )}`
142
+ }
143
+ }
144
+
145
+ // Fetch and cache
146
+ const response = await fetch(fetchUrl, init)
147
+ if (response.ok) {
148
+ const text = await response.text()
149
+ const compressed = compressToUTF16(text)
150
+ await set(cacheKey, {
151
+ compressedFileContent: compressed,
152
+ timestamp: Date.now(),
153
+ }).catch(() => {})
154
+ return new Response(text, {
155
+ status: response.status,
156
+ statusText: response.statusText,
157
+ headers: response.headers,
158
+ })
159
+ }
160
+
161
+ return response
162
+ }
@@ -5,10 +5,10 @@ export const checkIfManualEditsImported = (
5
5
  file: string = "index.tsx",
6
6
  ) => {
7
7
  if (!files[file]) return false
8
- const targetFile = findTargetFile(
9
- Object.keys(files).map((f) => ({ path: f, content: files[f] })),
10
- null,
11
- )
8
+ const targetFile = findTargetFile({
9
+ files: Object.keys(files).map((f) => ({ path: f, content: files[f] })),
10
+ filePathFromUrl: null,
11
+ })
12
12
  if (targetFile && file !== targetFile.path) {
13
13
  return false
14
14
  }
@@ -1,4 +1,6 @@
1
+ import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
1
2
  import { PackageFile } from "@/types/package"
3
+ import { isComponentExported } from "./isComponentExported"
2
4
 
3
5
  export const findMainEntrypointFileFromTscircuitConfig = (
4
6
  files: PackageFile[],
@@ -24,37 +26,70 @@ export const findMainEntrypointFileFromTscircuitConfig = (
24
26
  return null
25
27
  }
26
28
 
27
- export const findTargetFile = (
28
- files: PackageFile[],
29
- filePathFromUrl: string | null,
30
- ): PackageFile | null => {
29
+ export const findTargetFile = ({
30
+ files,
31
+ filePathFromUrl,
32
+ fallbackToAnyFile = true,
33
+ }: {
34
+ files: PackageFile[]
35
+ filePathFromUrl: string | null
36
+ fallbackToAnyFile?: boolean
37
+ }): PackageFile | null => {
31
38
  if (files.length === 0) {
32
39
  return null
33
40
  }
34
41
 
35
- let targetFile: PackageFile | null = null
42
+ let targetFile: PackageFile | undefined | null = null
43
+ if (!filePathFromUrl) {
44
+ files = files.filter((x) => !isHiddenFile(x.path))
45
+ }
36
46
 
37
47
  if (filePathFromUrl) {
38
- targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
48
+ const file = files.find((file) => file.path === filePathFromUrl)?.path
49
+ if (
50
+ file &&
51
+ !file.endsWith(".ts") &&
52
+ !file.endsWith(".tsx") &&
53
+ fallbackToAnyFile
54
+ ) {
55
+ targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
56
+ } else {
57
+ const _isComponentExported = isComponentExported(
58
+ files.find((file) => file.path === filePathFromUrl)?.content || "",
59
+ )
60
+ if (_isComponentExported) {
61
+ targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
62
+ }
63
+ }
39
64
  }
40
65
 
41
66
  if (!targetFile) {
42
67
  targetFile = findMainEntrypointFileFromTscircuitConfig(files)
43
68
  }
69
+ if (!targetFile) {
70
+ targetFile =
71
+ files.find(
72
+ (file) => file.path === "index.tsx" || file.path === "index.ts",
73
+ ) ?? null
74
+ }
44
75
 
45
76
  if (!targetFile) {
46
- targetFile = files.find((file) => file.path === "index.tsx") ?? null
77
+ targetFile =
78
+ files.find((file) => file.path.endsWith(".circuit.tsx")) ?? null
47
79
  }
48
80
 
49
81
  if (!targetFile) {
50
- targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
82
+ targetFile =
83
+ files.find(
84
+ (file) => file.path === "main.tsx" || file.path === "main.ts",
85
+ ) ?? null
51
86
  }
52
87
 
53
88
  if (!targetFile) {
54
- targetFile = files.find((file) => file.path === "index.ts") ?? null
89
+ targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
55
90
  }
56
91
 
57
- if (!targetFile && files[0]) {
92
+ if (!targetFile && files[0] && fallbackToAnyFile) {
58
93
  targetFile = files[0]
59
94
  }
60
95
 
@@ -4,6 +4,7 @@ export const isComponentExported = (code: string) => {
4
4
  /export const\s+\w+\s*=/.test(code) ||
5
5
  /export default\s+\w+/.test(code) ||
6
6
  /export default\s+function\s*(\w*)\s*\(/.test(code) ||
7
- /export default\s*\(\s*\)\s*=>/.test(code)
7
+ /export default\s*\(\s*\)\s*=>/.test(code) ||
8
+ /export default\s*\(.*?\)\s*=>/.test(code)
8
9
  )
9
10
  }
package/src/main.tsx CHANGED
@@ -1,11 +1,12 @@
1
1
  import { StrictMode } from "react"
2
2
  import { createRoot } from "react-dom/client"
3
3
  import App from "./App.tsx"
4
+ import "./lib/sentry"
5
+ import "./index.css"
4
6
 
5
7
  if (typeof window !== "undefined" && !window.__APP_LOADED_AT) {
6
8
  window.__APP_LOADED_AT = Date.now()
7
9
  }
8
- import "./index.css"
9
10
 
10
11
  createRoot(document.getElementById("root")!).render(
11
12
  <StrictMode>
@@ -0,0 +1,169 @@
1
+ import React, { useState } from "react"
2
+ import { useLocation } from "wouter"
3
+ import { Helmet } from "react-helmet-async"
4
+ import Header from "@/components/Header"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Label } from "@/components/ui/label"
8
+ import toast from "react-hot-toast"
9
+ import { useCreateOrgMutation } from "@/hooks/use-create-org-mutation"
10
+
11
+ interface FormErrors {
12
+ name?: string
13
+ }
14
+
15
+ export const CreateOrganizationPage = () => {
16
+ const [, setLocation] = useLocation()
17
+
18
+ const [formData, setFormData] = useState({
19
+ name: "",
20
+ })
21
+
22
+ const [errors, setErrors] = useState<FormErrors>({})
23
+ const [isLoading, setIsLoading] = useState(false)
24
+
25
+ const { mutate: createOrganization, isLoading: isMutating } =
26
+ useCreateOrgMutation({
27
+ onSuccess: (newOrganization) => {
28
+ toast.success(
29
+ `Organization "${newOrganization.name || formData.name}" created successfully!`,
30
+ )
31
+ setLocation(`/${newOrganization.name || formData.name}`)
32
+ setIsLoading(false)
33
+ },
34
+ })
35
+
36
+ const validateForm = (): boolean => {
37
+ const newErrors: FormErrors = {}
38
+
39
+ if (!formData.name) {
40
+ newErrors.name = "Organization name is required"
41
+ } else if (formData.name.length > 30) {
42
+ newErrors.name = "Organization name must be less than 30 characters"
43
+ } else if (!/^[a-zA-Z0-9-_]+$/.test(formData.name)) {
44
+ newErrors.name =
45
+ "Organization name can only contain letters, numbers, hyphens, and underscores"
46
+ }
47
+ setErrors(newErrors)
48
+ return Object.keys(newErrors).length === 0
49
+ }
50
+
51
+ const handleSubmit = async (e: React.FormEvent) => {
52
+ e.preventDefault()
53
+
54
+ if (!validateForm()) {
55
+ return
56
+ }
57
+
58
+ setIsLoading(true)
59
+ createOrganization(
60
+ { name: formData.name },
61
+ {
62
+ onError: (error: any) => {
63
+ console.error("Failed to create organization:", error)
64
+ toast.error(
65
+ error?.response?.data?.error?.message ||
66
+ error?.data?.error?.message ||
67
+ "Failed to create organization. Please try again.",
68
+ )
69
+ setIsLoading(false)
70
+ },
71
+ },
72
+ )
73
+ }
74
+
75
+ const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
76
+ const name = e.target.value
77
+ setFormData((prev) => ({ ...prev, name }))
78
+ if (errors.name) {
79
+ setErrors({})
80
+ }
81
+ }
82
+
83
+ const handleCancel = () => {
84
+ setLocation("/dashboard")
85
+ }
86
+
87
+ return (
88
+ <div className="min-h-screen bg-white">
89
+ <Helmet>
90
+ <title>Create Organization - tscircuit</title>
91
+ <meta
92
+ name="description"
93
+ content="Create a new organization to collaborate with others and manage shared projects."
94
+ />
95
+ </Helmet>
96
+
97
+ <Header />
98
+
99
+ <div className="flex items-center justify-center min-h-[calc(100vh-80px)] px-4 sm:px-6 lg:px-8">
100
+ <div className="w-full max-w-lg mx-auto">
101
+ <div className="text-center mb-8">
102
+ <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-2">
103
+ Set up your organization
104
+ </h1>
105
+ <p className="text-gray-600 text-sm">
106
+ Tell us about your organization
107
+ </p>
108
+ </div>
109
+
110
+ <form onSubmit={handleSubmit} className="space-y-6">
111
+ <div className="space-y-2">
112
+ <Label
113
+ htmlFor="org-name"
114
+ className="text-sm font-semibold text-gray-900"
115
+ >
116
+ Organization Name
117
+ <span className="text-red-500">*</span>
118
+ </Label>
119
+ <Input
120
+ spellCheck={false}
121
+ id="org-handle"
122
+ type="text"
123
+ placeholder="tscircuit"
124
+ value={formData.name}
125
+ onChange={handleNameChange}
126
+ className={`h-10 sm:h-11 ${
127
+ errors.name
128
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
129
+ : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
130
+ }`}
131
+ disabled={isLoading || isMutating}
132
+ />
133
+ {errors.name && (
134
+ <p className="text-sm text-red-600">{errors.name}</p>
135
+ )}
136
+ <p className="text-xs text-gray-500">
137
+ This will be the name of your organization on tscircuit.
138
+ <br />
139
+ Your URL will be:{" "}
140
+ <span className="font-mono text-gray-700">
141
+ tscircuit.com/{formData.name || "orgname"}
142
+ </span>
143
+ </p>
144
+ </div>
145
+
146
+ <div>
147
+ <Button
148
+ type="submit"
149
+ disabled={isLoading || isMutating || !formData.name}
150
+ className="w-full h-10 sm:h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm sm:text-base"
151
+ >
152
+ {isLoading || isMutating ? (
153
+ <>
154
+ <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
155
+ Creating organization...
156
+ </>
157
+ ) : (
158
+ "Create organization"
159
+ )}
160
+ </Button>
161
+ </div>
162
+ </form>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ export default CreateOrganizationPage