create-surf-app 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var args = process.argv.slice(2);
7
+ var projectName = args.find((a) => !a.startsWith("--")) || ".";
8
+ var frontendPort = getFlag("--port", "5173");
9
+ var backendPort = getFlag("--backend-port", "3001");
10
+ function getFlag(name2, fallback) {
11
+ const idx = args.indexOf(name2);
12
+ return idx >= 0 && args[idx + 1] ? args[idx + 1] : fallback;
13
+ }
14
+ var frontendPkg = {
15
+ name: "frontend",
16
+ private: true,
17
+ type: "module",
18
+ scripts: {
19
+ dev: `vite --port ${frontendPort}`,
20
+ build: "vite build",
21
+ preview: "vite preview",
22
+ lint: "eslint ."
23
+ },
24
+ dependencies: {
25
+ "@surf-ai/sdk": "latest",
26
+ "@surf-ai/theme": "latest",
27
+ "@radix-ui/react-accordion": "1.2.12",
28
+ "@radix-ui/react-aspect-ratio": "1.1.8",
29
+ "@radix-ui/react-avatar": "1.1.11",
30
+ "@radix-ui/react-checkbox": "1.3.3",
31
+ "@radix-ui/react-collapsible": "1.1.12",
32
+ "@radix-ui/react-context-menu": "2.2.16",
33
+ "@radix-ui/react-dialog": "1.1.15",
34
+ "@radix-ui/react-dropdown-menu": "2.1.16",
35
+ "@radix-ui/react-hover-card": "1.1.15",
36
+ "@radix-ui/react-label": "2.1.8",
37
+ "@radix-ui/react-menubar": "1.1.16",
38
+ "@radix-ui/react-navigation-menu": "1.2.14",
39
+ "@radix-ui/react-popover": "1.1.15",
40
+ "@radix-ui/react-progress": "1.1.8",
41
+ "@radix-ui/react-radio-group": "1.3.8",
42
+ "@radix-ui/react-scroll-area": "1.2.10",
43
+ "@radix-ui/react-select": "2.2.6",
44
+ "@radix-ui/react-separator": "1.1.8",
45
+ "@radix-ui/react-slider": "1.3.6",
46
+ "@radix-ui/react-slot": "1.2.4",
47
+ "@radix-ui/react-switch": "1.2.6",
48
+ "@radix-ui/react-tabs": "1.1.13",
49
+ "@radix-ui/react-toast": "1.2.15",
50
+ "@radix-ui/react-toggle": "1.1.10",
51
+ "@radix-ui/react-toggle-group": "1.1.11",
52
+ "@radix-ui/react-tooltip": "1.2.8",
53
+ "@tanstack/react-query": "5.94.5",
54
+ "class-variance-authority": "0.7.1",
55
+ "clsx": "2.1.1",
56
+ "cmdk": "1.1.1",
57
+ "echarts": "5.6.0",
58
+ "echarts-for-react": "3.0.6",
59
+ "embla-carousel-react": "8.6.0",
60
+ "lucide-react": "0.454.0",
61
+ "next-themes": "0.4.6",
62
+ "react": "19.2.4",
63
+ "react-dom": "19.2.4",
64
+ "sonner": "1.7.4",
65
+ "tailwind-merge": "2.6.1",
66
+ "vaul": "1.1.2",
67
+ "zod": "3.25.76"
68
+ },
69
+ devDependencies: {
70
+ "@eslint/js": "9.39.4",
71
+ "@tailwindcss/vite": "4.2.2",
72
+ "@types/node": "22.19.15",
73
+ "@types/react": "19.2.14",
74
+ "@types/react-dom": "19.2.3",
75
+ "@vitejs/plugin-react": "4.7.0",
76
+ "eslint": "9.39.4",
77
+ "eslint-plugin-react-hooks": "5.2.0",
78
+ "eslint-plugin-react-refresh": "0.4.26",
79
+ "globals": "16.5.0",
80
+ "tailwindcss": "4.2.2",
81
+ "tw-animate-css": "1.4.0",
82
+ "typescript": "5.9.3",
83
+ "typescript-eslint": "8.57.1",
84
+ "vite": "6.4.1"
85
+ }
86
+ };
87
+ var templates = {
88
+ // ── Backend ──────────────────────────────────────────────────────────────
89
+ "backend/package.json": JSON.stringify({
90
+ name: "backend",
91
+ private: true,
92
+ scripts: {
93
+ start: "node server.js",
94
+ dev: "node --watch server.js"
95
+ },
96
+ dependencies: {
97
+ "@surf-ai/sdk": "latest",
98
+ "express": "^4.22.0"
99
+ }
100
+ }, null, 2),
101
+ "backend/server.js": `const { createServer } = require('@surf-ai/sdk/server')
102
+ createServer({ port: ${backendPort} }).start()
103
+ `,
104
+ "backend/routes/.gitkeep": "",
105
+ "backend/db/schema.js": `// Define your Drizzle ORM tables here.
106
+ // Example:
107
+ // const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
108
+ // exports.users = pgTable('users', {
109
+ // id: serial('id').primaryKey(),
110
+ // name: text('name').notNull(),
111
+ // created_at: timestamp('created_at').defaultNow(),
112
+ // })
113
+ `,
114
+ // ── Frontend ─────────────────────────────────────────────────────────────
115
+ "frontend/package.json": JSON.stringify(frontendPkg, null, 2),
116
+ "frontend/vite.config.ts": `import { defineConfig, loadEnv } from 'vite'
117
+ import react from '@vitejs/plugin-react'
118
+ import tailwindcss from '@tailwindcss/vite'
119
+ import path from 'path'
120
+
121
+ const env = loadEnv('', process.cwd(), '')
122
+ const FRONTEND_PORT = parseInt(env.VITE_PORT || '${frontendPort}', 10)
123
+ const BACKEND_PORT = parseInt(env.VITE_BACKEND_PORT || '${backendPort}', 10)
124
+
125
+ export default defineConfig({
126
+ plugins: [react(), tailwindcss()],
127
+ server: {
128
+ port: FRONTEND_PORT,
129
+ host: '0.0.0.0',
130
+ proxy: {
131
+ '/proxy': { target: \`http://127.0.0.1:\${BACKEND_PORT}\`, changeOrigin: true },
132
+ '/api': { target: \`http://127.0.0.1:\${BACKEND_PORT}\`, changeOrigin: true },
133
+ },
134
+ },
135
+ resolve: {
136
+ alias: {
137
+ '@': path.resolve(__dirname, './src'),
138
+ },
139
+ },
140
+ })
141
+ `,
142
+ // index.html comes from templates/ directory (has cold-start guard script)
143
+ "frontend/tsconfig.json": JSON.stringify({
144
+ compilerOptions: {
145
+ target: "ES2020",
146
+ module: "ESNext",
147
+ moduleResolution: "bundler",
148
+ jsx: "react-jsx",
149
+ strict: true,
150
+ esModuleInterop: true,
151
+ skipLibCheck: true,
152
+ paths: { "@/*": ["./src/*"] }
153
+ },
154
+ include: ["src"]
155
+ }, null, 2),
156
+ "frontend/components.json": JSON.stringify({
157
+ "$schema": "https://ui.shadcn.com/schema.json",
158
+ "style": "default",
159
+ "rsc": false,
160
+ "tsx": true,
161
+ "tailwind": {
162
+ "config": "",
163
+ "css": "src/index.css",
164
+ "baseColor": "slate",
165
+ "cssVariables": true
166
+ },
167
+ "aliases": {
168
+ "components": "@/components",
169
+ "utils": "@surf-ai/sdk/react",
170
+ "hooks": "@/hooks"
171
+ }
172
+ }, null, 2),
173
+ // entry-client.tsx replaces main.tsx — comes from templates/ directory
174
+ "frontend/src/App.tsx": `import { useMarketPrice } from '@surf-ai/sdk/react'
175
+
176
+ export default function App() {
177
+ const { data, isLoading, error } = useMarketPrice({ symbol: 'BTC', time_range: '1d' })
178
+
179
+ return (
180
+ <div className="min-h-screen bg-background text-foreground p-10">
181
+ <h1 className="text-3xl font-bold mb-4">Surf App</h1>
182
+ {isLoading && <p className="text-muted-foreground">Loading BTC price...</p>}
183
+ {error && <p className="text-destructive">Error: {(error as Error).message}</p>}
184
+ {data?.data?.[0] && (
185
+ <p className="text-4xl font-bold">
186
+ BTC: <span className="text-primary">\${data.data[0].value?.toLocaleString()}</span>
187
+ </p>
188
+ )}
189
+ </div>
190
+ )
191
+ }
192
+ `,
193
+ "frontend/src/index.css": `@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;600;700;900&family=Roboto+Mono:wght@400;500&display=swap');
194
+ @import "tailwindcss";
195
+ @import "tw-animate-css";
196
+ @import "@surf-ai/theme";
197
+ `,
198
+ // cn() and useToast() are now in @surf-ai/sdk/react
199
+ "frontend/src/db/schema.ts": `// Database schema definition \u2014 keep in sync with backend/db/schema.js.
200
+ // This file mirrors the backend schema for TypeScript type safety in the frontend.
201
+ //
202
+ // Example:
203
+ // import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
204
+ // export const users = pgTable('users', {
205
+ // id: serial('id').primaryKey(),
206
+ // name: text('name').notNull(),
207
+ // created_at: timestamp('created_at').defaultNow(),
208
+ // })
209
+ `,
210
+ "frontend/src/vite-env.d.ts": `/// <reference types="vite/client" />
211
+ `,
212
+ // ── Root ──────────────────────────────────────────────────────────────────
213
+ "CLAUDE.md": `# Project
214
+
215
+ Built with [Surf SDK](https://github.com/cyberconnecthq/urania/tree/main/packages/sdk).
216
+
217
+ ## Imports from @surf-ai/sdk
218
+
219
+ Everything comes from \`@surf-ai/sdk\`. Do NOT create local utility files for these.
220
+
221
+ **Frontend (\`@surf-ai/sdk/react\`):**
222
+ \`\`\`tsx
223
+ import { useMarketPrice, useTokenHolders } from '@surf-ai/sdk/react' // data hooks
224
+ import { cn } from '@surf-ai/sdk/react' // Tailwind class merge
225
+ import { useToast, toast } from '@surf-ai/sdk/react' // toast notifications
226
+ \`\`\`
227
+
228
+ **Backend (\`@surf-ai/sdk/server\`):**
229
+ \`\`\`js
230
+ const { dataApi } = require('@surf-ai/sdk/server')
231
+ const data = await dataApi.market.price({ symbol: 'BTC' })
232
+ const holders = await dataApi.token.holders({ address: '0x...', chain: 'ethereum' })
233
+ // Escape hatch for new endpoints:
234
+ const raw = await dataApi.get('newcategory/endpoint', { foo: 'bar' })
235
+ \`\`\`
236
+
237
+ ## Structure
238
+
239
+ \`\`\`
240
+ frontend/src/App.tsx - build your UI here
241
+ frontend/src/components/ - add components
242
+ frontend/src/db/schema.ts - frontend DB schema mirror
243
+ backend/routes/*.js - add API routes (auto-mounted at /api/{name})
244
+ backend/db/schema.js - define database tables
245
+ \`\`\`
246
+
247
+ ## Built-in Endpoints (from @surf-ai/sdk/server)
248
+
249
+ \`createServer()\` provides these automatically \u2014 do NOT create routes for them:
250
+
251
+ | Endpoint | Method | Purpose |
252
+ |----------|--------|---------|
253
+ | \`/api/health\` | GET | Health check \u2014 \`{ status: 'ok' }\` |
254
+ | \`/api/__sync-schema\` | POST | Sync \`backend/db/schema.js\` tables to database |
255
+ | \`/api/cron\` | GET | List cron jobs with status and next run time |
256
+ | \`/api/cron\` | POST | Create a new cron task |
257
+ | \`/api/cron/:id\` | PATCH | Update a cron task (schedule, enabled, etc.) |
258
+ | \`/api/cron/:id\` | DELETE | Delete a cron task |
259
+ | \`/api/cron/:id/run\` | POST | Manually trigger a cron task |
260
+ | \`/proxy/*\` | ANY | Data API passthrough \u2014 \`/proxy/market/price\` \u2192 hermod |
261
+
262
+ Auto-registered from \`backend/routes/*.js\`:
263
+ | File | Endpoint |
264
+ |------|----------|
265
+ | \`routes/btc.js\` | \`/api/btc\` |
266
+ | \`routes/portfolio.js\` | \`/api/portfolio\` |
267
+
268
+ ## Database
269
+
270
+ Define tables in \`backend/db/schema.js\` using Drizzle ORM:
271
+ \`\`\`js
272
+ const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
273
+ exports.users = pgTable('users', {
274
+ id: serial('id').primaryKey(),
275
+ name: text('name').notNull(),
276
+ created_at: timestamp('created_at').defaultNow(),
277
+ })
278
+ \`\`\`
279
+
280
+ Tables are auto-created on startup and when \`schema.js\` changes (file watcher).
281
+ The agent can also call \`POST /api/__sync-schema\` explicitly after editing.
282
+
283
+ ## Do NOT modify
284
+
285
+ - \`vite.config.ts\` \u2014 proxy and build config
286
+ - \`backend/server.js\` \u2014 uses @surf-ai/sdk/server
287
+ - \`entry-client.tsx\` \u2014 app bootstrap with SSR hydration
288
+ - \`entry-server.tsx\` \u2014 SSR render for deploy
289
+ - \`index.html\` \u2014 cold-start guard and Surf badge
290
+ - \`eslint.config.*\` \u2014 lint rules
291
+ - \`index.css\` \u2014 only imports, do not add styles here (use Tailwind classes)
292
+
293
+ ## Rules
294
+
295
+ - Use \`@surf-ai/sdk/react\` hooks in frontend, \`@surf-ai/sdk/server\` dataApi in backend
296
+ - Use Tailwind CSS classes for styling (Surf Design System theme via \`@surf-ai/theme\`)
297
+ - Use shadcn/ui components \u2014 install with \`bunx shadcn@latest add button\`
298
+ - Use \`cn()\` from \`@surf-ai/sdk/react\` to merge Tailwind classes
299
+ - Frontend packages are pre-installed \u2014 check \`package.json\` before installing
300
+ - Dark theme is the default (configured in entry-client.tsx)
301
+ `
302
+ };
303
+ var root = path.resolve(projectName);
304
+ var name = path.basename(root);
305
+ console.log(`
306
+ Creating Surf app in ${root}
307
+ `);
308
+ fs.mkdirSync(root, { recursive: true });
309
+ for (const [relPath, content] of Object.entries(templates)) {
310
+ const fullPath = path.join(root, relPath);
311
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
312
+ fs.writeFileSync(fullPath, content);
313
+ console.log(` ${relPath}`);
314
+ }
315
+ var templatesDir = path.join(new URL(".", import.meta.url).pathname, "templates");
316
+ if (fs.existsSync(templatesDir)) {
317
+ copyDir(templatesDir, root);
318
+ }
319
+ function copyDir(src, dest) {
320
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
321
+ const srcPath = path.join(src, entry.name);
322
+ const destPath = path.join(dest, entry.name);
323
+ if (entry.isDirectory()) {
324
+ fs.mkdirSync(destPath, { recursive: true });
325
+ copyDir(srcPath, destPath);
326
+ } else {
327
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath));
328
+ console.log(` ${path.relative(root, destPath)}`);
329
+ }
330
+ }
331
+ }
332
+ console.log(`
333
+ Done! Next steps:
334
+
335
+ cd ${name}
336
+ cd backend && bun install && cd ..
337
+ cd frontend && bun install && cd ..
338
+
339
+ # Start backend
340
+ cd backend && bun dev &
341
+
342
+ # Start frontend
343
+ cd frontend && bun dev
344
+
345
+ Open http://localhost:${frontendPort}
346
+ `);
@@ -0,0 +1,21 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+
4
+ export default [
5
+ { ignores: ['node_modules'] },
6
+ {
7
+ files: ['**/*.js'],
8
+ ...js.configs.recommended,
9
+ languageOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'commonjs',
12
+ globals: globals.node,
13
+ },
14
+ rules: {
15
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
16
+ 'no-empty': 'warn',
17
+ 'eqeqeq': 'warn',
18
+ 'no-fallthrough': 'warn',
19
+ },
20
+ },
21
+ ];
@@ -0,0 +1,42 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist', 'node_modules'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{js,jsx,ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: {
15
+ ...globals.browser,
16
+ ...globals.node,
17
+ },
18
+ parserOptions: {
19
+ ecmaFeatures: { jsx: true },
20
+ },
21
+ },
22
+ plugins: {
23
+ 'react-hooks': reactHooks,
24
+ 'react-refresh': reactRefresh,
25
+ },
26
+ rules: {
27
+ ...reactHooks.configs.recommended.rules,
28
+ 'react-refresh/only-export-components': [
29
+ 'warn',
30
+ { allowConstantExport: true },
31
+ ],
32
+ 'no-unused-vars': 'off',
33
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
34
+ '@typescript-eslint/no-explicit-any': 'off',
35
+ '@typescript-eslint/no-empty-object-type': 'warn',
36
+ '@typescript-eslint/ban-ts-comment': 'warn',
37
+ '@typescript-eslint/no-require-imports': 'off',
38
+ '@typescript-eslint/no-unused-expressions': 'warn',
39
+ '@typescript-eslint/no-unsafe-function-type': 'warn',
40
+ },
41
+ },
42
+ )
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico" />
7
+ <title>App</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"><!--ssr-outlet--></div>
11
+ <script type="module" src="./src/entry-client.tsx"></script>
12
+ <script>
13
+ // Fallback: if React fails to render (e.g. Vite dep stubs during cold
14
+ // start), auto-reload until deps are ready. This fires OUTSIDE React
15
+ // so it works even when React itself is null/undefined.
16
+ (function(){
17
+ var K='__boot_reload', W=30000, M=8;
18
+ function st(){try{return JSON.parse(sessionStorage.getItem(K))||{c:0,t:0}}catch(e){return{c:0,t:0}}}
19
+ setTimeout(function(){
20
+ if(window.__reactOk)return;
21
+ var s=st(),c=(Date.now()-s.t>W)?0:s.c;
22
+ if(c<M){sessionStorage.setItem(K,JSON.stringify({c:c+1,t:Date.now()}));location.reload()}
23
+ },3000);
24
+ })();
25
+ </script>
26
+ <!-- Made by Surf badge -->
27
+ <style>
28
+ #surf-badge{position:fixed;bottom:16px;right:16px;z-index:2147483647;display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:20px;font-family:"Lato",system-ui,sans-serif;font-size:12px;font-weight:500;line-height:16px;text-decoration:none;box-shadow:0 1px 1px rgba(51,51,51,.08),0 6px 12px rgba(51,51,51,.08);opacity:.9;transition:opacity .2s}
29
+ #surf-badge:hover{opacity:1}
30
+ @media print{#surf-badge{display:none}}
31
+ </style>
32
+ <a id="surf-badge" href="https://asksurf.ai" target="_blank" rel="noopener noreferrer" aria-label="Made by Surf" style="background:var(--bg-menu,#fff);color:var(--fg-base,#212121)">
33
+ <span>Made by</span>
34
+ <svg width="40" height="13" viewBox="0 0 63 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
35
+ <path d="M15.583 13.333C15.9931 13.3331 16.3814 13.5196 16.6377 13.8398L18.7666 16.5019C18.7956 16.5382 18.8261 16.5719 18.8574 16.6035C19.2049 16.954 19.6484 17.3405 19.6484 17.834C19.6484 18.3399 19.2383 18.75 18.7324 18.75H18.4668C18.1079 18.75 17.7682 18.5868 17.5439 18.3066L16.3633 16.831C15.6565 15.9476 14.2315 16.4477 14.2314 17.5791V18.6494C14.2314 18.7061 14.1827 18.7499 14.126 18.75H12.2529C11.6074 18.75 10.997 18.4562 10.5938 17.9521L9.69629 16.831C8.98948 15.9478 7.56544 16.4478 7.56543 17.5791V18.6494C7.56542 18.7062 7.51669 18.7499 7.45996 18.75H5.58594C4.94038 18.7499 4.32999 18.4563 3.92676 17.9521L3.03027 16.831C2.67436 16.3862 2.14844 15.9311 2.14844 15.3613V13.4385C2.14851 13.3817 2.19228 13.333 2.24902 13.333C2.65919 13.333 3.04737 13.5197 3.30371 13.8398L5.43359 16.5019C6.14031 17.3853 7.56521 16.8861 7.56543 15.7549V14.6846C7.56543 13.9385 8.16998 13.333 8.91602 13.333C9.32621 13.333 9.71437 13.5196 9.9707 13.8398L12.1006 16.5019C12.8052 17.3827 14.2227 16.8887 14.2314 15.7646V14.6846C14.2314 13.9385 14.837 13.333 15.583 13.333ZM15.583 6.66698C15.9932 6.66711 16.3814 6.8535 16.6377 7.17382L18.7666 9.83593C19.1224 10.2806 19.6484 10.7352 19.6484 11.3047V13.2266C19.6483 13.2833 19.6046 13.333 19.5479 13.333C19.1375 13.333 18.7495 13.1466 18.4932 12.8262L16.3633 10.1641C15.6565 9.28087 14.2315 9.78081 14.2314 10.9121V11.9824C14.2314 12.7284 13.6269 13.333 12.8809 13.333C12.4706 13.3329 12.0825 13.1465 11.8262 12.8262L9.69629 10.1641C8.98942 9.28109 7.56544 9.78088 7.56543 10.9121V11.9824C7.56543 12.7282 6.96062 13.3327 6.21484 13.333C5.80451 13.333 5.41551 13.1466 5.15918 12.8262L3.03027 10.1641C2.67435 9.71918 2.14844 9.26406 2.14844 8.69433V6.77245C2.14851 6.71573 2.19228 6.66699 2.24902 6.66698C2.65936 6.66698 3.04738 6.8534 3.30371 7.17382L5.43359 9.83593C6.14042 10.7191 7.56543 10.2192 7.56543 9.08788V8.01757C7.56543 7.27154 8.16998 6.66698 8.91602 6.66698C9.32634 6.66701 9.71437 6.85341 9.9707 7.17382L12.1006 9.83593C12.8052 10.7162 14.2225 10.2223 14.2314 9.09863V8.01757C14.2314 7.27154 14.837 6.66698 15.583 6.66698ZM3.33008 1.24999C3.68899 1.24999 4.02863 1.41318 4.25293 1.69335L5.43359 3.16894C6.14036 4.0524 7.56543 3.55227 7.56543 2.42089V1.35058C7.56544 1.29384 7.61417 1.25007 7.6709 1.24999H9.54395C10.1895 1.24999 10.7999 1.54378 11.2031 2.04784L12.1006 3.16894C12.8052 4.04953 14.2227 3.5556 14.2314 2.43163V1.35058C14.2315 1.29384 14.2802 1.25007 14.3369 1.24999H16.2109C16.8565 1.25014 17.4669 1.54375 17.8701 2.04784L18.7666 3.16894C19.1223 3.61354 19.6481 4.06741 19.6484 4.63671V6.56054C19.6483 6.61728 19.6046 6.66698 19.5479 6.66698C19.1377 6.66698 18.7495 6.48034 18.4932 6.16015L16.3633 3.49804C15.6566 2.61465 14.2317 3.11391 14.2314 4.24511V5.3164C14.2312 6.06217 13.6267 6.66698 12.8809 6.66698C12.4707 6.66693 12.0825 6.48039 11.8262 6.16015L9.69629 3.49804C8.98957 2.61465 7.56565 3.11391 7.56543 4.24511V5.3164C7.56518 6.06202 6.96046 6.66673 6.21484 6.66698C5.80453 6.66698 5.41551 6.48054 5.15918 6.16015L3.03027 3.49804C3.00109 3.46156 2.97093 3.42724 2.93945 3.3955C2.59217 3.04543 2.14867 2.65906 2.14844 2.16601C2.14844 1.66032 2.55877 1.24999 3.06445 1.24999H3.33008Z"/>
36
+ <path d="M35.1083 6.97044C34.7154 6.97044 34.5223 6.77734 34.3692 6.39782C33.9963 5.49228 33.0042 4.97958 31.6925 4.97958C30.0146 4.97958 28.9559 5.83851 28.9559 6.99707C28.9493 8.30211 30.3675 8.80149 31.4328 9.07449L32.7645 9.42072C34.4957 9.84686 36.6863 10.799 36.6863 13.2559C36.6863 15.5664 34.8419 17.251 31.6392 17.251C28.8827 17.251 27.0649 16.0125 26.6321 13.9617C26.5389 13.5156 26.7986 13.2559 27.2514 13.2559H27.9971C28.3966 13.2559 28.5897 13.4557 28.7229 13.8485C29.1024 14.9272 30.2676 15.4466 31.6259 15.4466C33.3771 15.4466 34.6488 14.5676 34.6488 13.236C34.6488 12.0241 33.5236 11.5447 32.0654 11.1586L30.454 10.7191C28.2634 10.1265 26.9051 9.01456 26.9051 7.11692C26.9051 4.76651 29.0025 3.20179 31.7391 3.20179C34.2027 3.20179 36.0005 4.45357 36.4133 6.27131C36.5132 6.71076 36.2335 6.97044 35.7874 6.97044H35.1083Z"/>
37
+ <path d="M45.0887 7.46316C45.0887 7.03036 45.3217 6.79732 45.7545 6.79732H46.4204C46.8532 6.79732 47.0862 7.03036 47.0862 7.46316V16.3588C47.0862 16.7915 46.8532 17.0246 46.4204 17.0246H45.7945C45.3617 17.0246 45.1286 16.7915 45.1286 16.3588V15.2535H45.0221C44.5494 16.3454 43.504 17.1578 41.9659 17.1578C40.0017 17.1578 38.6101 15.8394 38.6101 13.3026V7.46316C38.6101 7.03036 38.8431 6.79732 39.2759 6.79732H39.9351C40.3679 6.79732 40.601 7.03036 40.601 7.46316V13.0629C40.601 14.4545 41.4532 15.3467 42.685 15.3467C43.8036 15.3467 45.0954 14.521 45.0887 12.7832V7.46316Z"/>
38
+ <path d="M49.3862 7.46316C49.3862 7.03036 49.6193 6.79732 50.0521 6.79732H50.6447C51.0774 6.79732 51.3105 7.03036 51.3105 7.46316V8.42196H51.417C51.7899 7.32333 52.8153 6.64418 54.0138 6.64418C54.0737 6.64418 54.147 6.64418 54.2202 6.65084C54.6397 6.65749 54.8594 6.91717 54.8594 7.33665V7.88264C54.8594 8.33541 54.673 8.54182 54.3467 8.50852C54.1803 8.48855 53.9938 8.47523 53.8207 8.47523C52.4091 8.47523 51.3771 9.4407 51.3771 10.779V16.3588C51.3771 16.7915 51.144 17.0246 50.7112 17.0246H50.0521C49.6193 17.0246 49.3862 16.7915 49.3862 16.3588V7.46316Z"/>
39
+ <path d="M61.6788 7.72949C61.6788 8.16229 61.4457 8.39533 61.013 8.39533H59.4749V16.3588C59.4749 16.7915 59.2418 17.0246 58.809 17.0246H58.1432C57.7104 17.0246 57.4774 16.7915 57.4774 16.3588V8.39533H56.5652C56.1324 8.39533 55.8993 8.16229 55.8993 7.72949V7.46316C55.8993 7.03036 56.1324 6.79732 56.5652 6.79732H57.4774V5.61213C57.4774 3.69451 58.8956 2.74902 60.5136 2.74902C60.9131 2.74902 61.2593 2.78897 61.539 2.84224C61.9651 2.9288 62.1116 3.2484 61.9917 3.66122L61.8919 4.01412C61.7787 4.42028 61.5656 4.57342 61.2726 4.52015C61.1395 4.49352 60.993 4.4802 60.8199 4.4802C59.8477 4.4802 59.4749 4.97292 59.4749 5.87846V6.79732H61.013C61.4457 6.79732 61.6788 7.03036 61.6788 7.46316V7.72949Z"/>
40
+ </svg>
41
+ </a>
42
+ </body>
43
+ </html>
@@ -0,0 +1,109 @@
1
+ import './index.css'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Cold-start guard: verify React is real BEFORE rendering.
5
+ //
6
+ // During Vite dep-optimization (cold start or after `npm install <pkg>`),
7
+ // Vite may serve placeholder "stub" modules where all React exports are
8
+ // undefined. Rendering with stub hooks causes "Cannot read properties of
9
+ // null (reading 'useState')".
10
+ //
11
+ // Strategy:
12
+ // 1. Dynamic-import React and check if useState is a function.
13
+ // 2. If stub → show a loading banner, schedule a reload with increasing
14
+ // delay, and do NOT mount any React tree (avoids the error entirely).
15
+ // 3. If real → dynamic-import App/ErrorBoundary and render normally.
16
+ // 4. Use createElement (not JSX) in this file so Vite doesn't inject a
17
+ // static `import { jsx } from 'react/jsx-dev-runtime'` which would
18
+ // itself be a stub and crash before our guard runs.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const RELOAD_KEY = '__dep_reload'
22
+ const MAX_RELOADS = 6
23
+ const RELOAD_WINDOW = 60_000 // 1 minute
24
+
25
+ function getReloads(): { c: number; t: number } {
26
+ try {
27
+ return JSON.parse(sessionStorage.getItem(RELOAD_KEY) || '{}') as { c: number; t: number }
28
+ } catch { return { c: 0, t: 0 } }
29
+ }
30
+
31
+ async function boot() {
32
+ const root = document.getElementById('root')!
33
+
34
+ try {
35
+ const React = await import('react')
36
+
37
+ // If useState is not a function, React is a dep-optimization stub
38
+ if (typeof React.useState !== 'function') throw new Error('dep-stub')
39
+
40
+ const { createElement } = React
41
+ const { createRoot, hydrateRoot } = await import('react-dom/client')
42
+ const { QueryClient, QueryClientProvider } = await import('@tanstack/react-query')
43
+ const { default: App } = await import('./App')
44
+
45
+ const queryClient = new QueryClient({
46
+ defaultOptions: { queries: {
47
+ refetchOnWindowFocus: false,
48
+ retry: 3,
49
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000),
50
+ staleTime: 30_000,
51
+ } },
52
+ })
53
+
54
+ const children = createElement(QueryClientProvider, { client: queryClient },
55
+ createElement(App)
56
+ )
57
+
58
+ // Use hydrateRoot only when SSR rendered real app content. The scaffold's
59
+ // server entry intentionally returns a lightweight placeholder shell so
60
+ // SSR-incompatible libraries (for example echarts-for-react) cannot crash
61
+ // deploy-time render. Placeholder markup must be client-rendered, not hydrated.
62
+ const hasPlaceholder = !!root.querySelector('[data-surf-placeholder]')
63
+ if (root.childNodes.length > 0 && root.innerHTML !== '<!--ssr-outlet-->' && !hasPlaceholder) {
64
+ hydrateRoot(root, children)
65
+ } else {
66
+ root.innerHTML = ''
67
+ createRoot(root).render(children)
68
+ }
69
+
70
+ // React rendered successfully — signal to index.html fallback & reset counter
71
+ ;(window as any).__reactOk = true
72
+ sessionStorage.removeItem(RELOAD_KEY)
73
+
74
+ // Notify parent frame when real app content renders (not the placeholder).
75
+ // DO NOT REMOVE — the hosting app uses this to dismiss the loading overlay.
76
+ function notifyParentReady() {
77
+ if (!document.querySelector('[data-surf-placeholder]')) {
78
+ try { window.parent.postMessage({ type: 'surf-app-ready' }, '*') } catch { /* cross-origin — ignore */ }
79
+ }
80
+ }
81
+ notifyParentReady()
82
+ new MutationObserver(notifyParentReady).observe(root, { childList: true, subtree: true })
83
+ } catch {
84
+ // React is not ready — show loading banner and schedule reload
85
+ const prev = getReloads()
86
+ const count = (Date.now() - prev.t > RELOAD_WINDOW) ? 0 : prev.c
87
+
88
+ if (count < MAX_RELOADS) {
89
+ root.innerHTML = [
90
+ '<div style=\"padding:24px;text-align:center;font-family:system-ui,sans-serif\">',
91
+ '<p style=\"color:#3b82f6;font-weight:600;margin:0 0 4px\">Loading dependencies...</p>',
92
+ '<p style=\"color:#3b82f6;opacity:0.7;font-size:12px;margin:0\">Reloading automatically</p>',
93
+ '</div>',
94
+ ].join('')
95
+ sessionStorage.setItem(RELOAD_KEY, JSON.stringify({ c: count + 1, t: Date.now() }))
96
+ // Increasing delay: 3s, 4s, 5s, ... gives Vite more time to finish
97
+ setTimeout(() => location.reload(), 3000 + count * 1000)
98
+ } else {
99
+ root.innerHTML = [
100
+ '<div style=\"padding:24px;text-align:center;font-family:system-ui,sans-serif\">',
101
+ '<p style=\"color:#c0392b;font-weight:600;margin:0 0 4px\">Failed to load dependencies</p>',
102
+ '<p style=\"color:#c0392b;opacity:0.8;font-size:12px;margin:0\">Please refresh the page</p>',
103
+ '</div>',
104
+ ].join('')
105
+ }
106
+ }
107
+ }
108
+
109
+ boot()
@@ -0,0 +1,13 @@
1
+ import { renderToString } from 'react-dom/server'
2
+
3
+ export function render() {
4
+ return renderToString(
5
+ <div
6
+ data-surf-placeholder
7
+ style={{
8
+ minHeight: '100vh',
9
+ background: '#ffffff',
10
+ }}
11
+ />
12
+ )
13
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "create-surf-app",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-surf-app": "./dist/index.js"
8
+ },
9
+ "files": ["dist", "templates"],
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --clean && rm -rf dist/templates && cp -r templates dist/templates",
12
+ "dev": "tsup src/index.ts --format esm --watch"
13
+ },
14
+ "devDependencies": {
15
+ "tsup": "^8.0.0",
16
+ "typescript": "^5.9.0"
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+
4
+ export default [
5
+ { ignores: ['node_modules'] },
6
+ {
7
+ files: ['**/*.js'],
8
+ ...js.configs.recommended,
9
+ languageOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'commonjs',
12
+ globals: globals.node,
13
+ },
14
+ rules: {
15
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
16
+ 'no-empty': 'warn',
17
+ 'eqeqeq': 'warn',
18
+ 'no-fallthrough': 'warn',
19
+ },
20
+ },
21
+ ];
@@ -0,0 +1,42 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist', 'node_modules'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{js,jsx,ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: {
15
+ ...globals.browser,
16
+ ...globals.node,
17
+ },
18
+ parserOptions: {
19
+ ecmaFeatures: { jsx: true },
20
+ },
21
+ },
22
+ plugins: {
23
+ 'react-hooks': reactHooks,
24
+ 'react-refresh': reactRefresh,
25
+ },
26
+ rules: {
27
+ ...reactHooks.configs.recommended.rules,
28
+ 'react-refresh/only-export-components': [
29
+ 'warn',
30
+ { allowConstantExport: true },
31
+ ],
32
+ 'no-unused-vars': 'off',
33
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
34
+ '@typescript-eslint/no-explicit-any': 'off',
35
+ '@typescript-eslint/no-empty-object-type': 'warn',
36
+ '@typescript-eslint/ban-ts-comment': 'warn',
37
+ '@typescript-eslint/no-require-imports': 'off',
38
+ '@typescript-eslint/no-unused-expressions': 'warn',
39
+ '@typescript-eslint/no-unsafe-function-type': 'warn',
40
+ },
41
+ },
42
+ )
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico" />
7
+ <title>App</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"><!--ssr-outlet--></div>
11
+ <script type="module" src="./src/entry-client.tsx"></script>
12
+ <script>
13
+ // Fallback: if React fails to render (e.g. Vite dep stubs during cold
14
+ // start), auto-reload until deps are ready. This fires OUTSIDE React
15
+ // so it works even when React itself is null/undefined.
16
+ (function(){
17
+ var K='__boot_reload', W=30000, M=8;
18
+ function st(){try{return JSON.parse(sessionStorage.getItem(K))||{c:0,t:0}}catch(e){return{c:0,t:0}}}
19
+ setTimeout(function(){
20
+ if(window.__reactOk)return;
21
+ var s=st(),c=(Date.now()-s.t>W)?0:s.c;
22
+ if(c<M){sessionStorage.setItem(K,JSON.stringify({c:c+1,t:Date.now()}));location.reload()}
23
+ },3000);
24
+ })();
25
+ </script>
26
+ <!-- Made by Surf badge -->
27
+ <style>
28
+ #surf-badge{position:fixed;bottom:16px;right:16px;z-index:2147483647;display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:20px;font-family:"Lato",system-ui,sans-serif;font-size:12px;font-weight:500;line-height:16px;text-decoration:none;box-shadow:0 1px 1px rgba(51,51,51,.08),0 6px 12px rgba(51,51,51,.08);opacity:.9;transition:opacity .2s}
29
+ #surf-badge:hover{opacity:1}
30
+ @media print{#surf-badge{display:none}}
31
+ </style>
32
+ <a id="surf-badge" href="https://asksurf.ai" target="_blank" rel="noopener noreferrer" aria-label="Made by Surf" style="background:var(--bg-menu,#fff);color:var(--fg-base,#212121)">
33
+ <span>Made by</span>
34
+ <svg width="40" height="13" viewBox="0 0 63 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
35
+ <path d="M15.583 13.333C15.9931 13.3331 16.3814 13.5196 16.6377 13.8398L18.7666 16.5019C18.7956 16.5382 18.8261 16.5719 18.8574 16.6035C19.2049 16.954 19.6484 17.3405 19.6484 17.834C19.6484 18.3399 19.2383 18.75 18.7324 18.75H18.4668C18.1079 18.75 17.7682 18.5868 17.5439 18.3066L16.3633 16.831C15.6565 15.9476 14.2315 16.4477 14.2314 17.5791V18.6494C14.2314 18.7061 14.1827 18.7499 14.126 18.75H12.2529C11.6074 18.75 10.997 18.4562 10.5938 17.9521L9.69629 16.831C8.98948 15.9478 7.56544 16.4478 7.56543 17.5791V18.6494C7.56542 18.7062 7.51669 18.7499 7.45996 18.75H5.58594C4.94038 18.7499 4.32999 18.4563 3.92676 17.9521L3.03027 16.831C2.67436 16.3862 2.14844 15.9311 2.14844 15.3613V13.4385C2.14851 13.3817 2.19228 13.333 2.24902 13.333C2.65919 13.333 3.04737 13.5197 3.30371 13.8398L5.43359 16.5019C6.14031 17.3853 7.56521 16.8861 7.56543 15.7549V14.6846C7.56543 13.9385 8.16998 13.333 8.91602 13.333C9.32621 13.333 9.71437 13.5196 9.9707 13.8398L12.1006 16.5019C12.8052 17.3827 14.2227 16.8887 14.2314 15.7646V14.6846C14.2314 13.9385 14.837 13.333 15.583 13.333ZM15.583 6.66698C15.9932 6.66711 16.3814 6.8535 16.6377 7.17382L18.7666 9.83593C19.1224 10.2806 19.6484 10.7352 19.6484 11.3047V13.2266C19.6483 13.2833 19.6046 13.333 19.5479 13.333C19.1375 13.333 18.7495 13.1466 18.4932 12.8262L16.3633 10.1641C15.6565 9.28087 14.2315 9.78081 14.2314 10.9121V11.9824C14.2314 12.7284 13.6269 13.333 12.8809 13.333C12.4706 13.3329 12.0825 13.1465 11.8262 12.8262L9.69629 10.1641C8.98942 9.28109 7.56544 9.78088 7.56543 10.9121V11.9824C7.56543 12.7282 6.96062 13.3327 6.21484 13.333C5.80451 13.333 5.41551 13.1466 5.15918 12.8262L3.03027 10.1641C2.67435 9.71918 2.14844 9.26406 2.14844 8.69433V6.77245C2.14851 6.71573 2.19228 6.66699 2.24902 6.66698C2.65936 6.66698 3.04738 6.8534 3.30371 7.17382L5.43359 9.83593C6.14042 10.7191 7.56543 10.2192 7.56543 9.08788V8.01757C7.56543 7.27154 8.16998 6.66698 8.91602 6.66698C9.32634 6.66701 9.71437 6.85341 9.9707 7.17382L12.1006 9.83593C12.8052 10.7162 14.2225 10.2223 14.2314 9.09863V8.01757C14.2314 7.27154 14.837 6.66698 15.583 6.66698ZM3.33008 1.24999C3.68899 1.24999 4.02863 1.41318 4.25293 1.69335L5.43359 3.16894C6.14036 4.0524 7.56543 3.55227 7.56543 2.42089V1.35058C7.56544 1.29384 7.61417 1.25007 7.6709 1.24999H9.54395C10.1895 1.24999 10.7999 1.54378 11.2031 2.04784L12.1006 3.16894C12.8052 4.04953 14.2227 3.5556 14.2314 2.43163V1.35058C14.2315 1.29384 14.2802 1.25007 14.3369 1.24999H16.2109C16.8565 1.25014 17.4669 1.54375 17.8701 2.04784L18.7666 3.16894C19.1223 3.61354 19.6481 4.06741 19.6484 4.63671V6.56054C19.6483 6.61728 19.6046 6.66698 19.5479 6.66698C19.1377 6.66698 18.7495 6.48034 18.4932 6.16015L16.3633 3.49804C15.6566 2.61465 14.2317 3.11391 14.2314 4.24511V5.3164C14.2312 6.06217 13.6267 6.66698 12.8809 6.66698C12.4707 6.66693 12.0825 6.48039 11.8262 6.16015L9.69629 3.49804C8.98957 2.61465 7.56565 3.11391 7.56543 4.24511V5.3164C7.56518 6.06202 6.96046 6.66673 6.21484 6.66698C5.80453 6.66698 5.41551 6.48054 5.15918 6.16015L3.03027 3.49804C3.00109 3.46156 2.97093 3.42724 2.93945 3.3955C2.59217 3.04543 2.14867 2.65906 2.14844 2.16601C2.14844 1.66032 2.55877 1.24999 3.06445 1.24999H3.33008Z"/>
36
+ <path d="M35.1083 6.97044C34.7154 6.97044 34.5223 6.77734 34.3692 6.39782C33.9963 5.49228 33.0042 4.97958 31.6925 4.97958C30.0146 4.97958 28.9559 5.83851 28.9559 6.99707C28.9493 8.30211 30.3675 8.80149 31.4328 9.07449L32.7645 9.42072C34.4957 9.84686 36.6863 10.799 36.6863 13.2559C36.6863 15.5664 34.8419 17.251 31.6392 17.251C28.8827 17.251 27.0649 16.0125 26.6321 13.9617C26.5389 13.5156 26.7986 13.2559 27.2514 13.2559H27.9971C28.3966 13.2559 28.5897 13.4557 28.7229 13.8485C29.1024 14.9272 30.2676 15.4466 31.6259 15.4466C33.3771 15.4466 34.6488 14.5676 34.6488 13.236C34.6488 12.0241 33.5236 11.5447 32.0654 11.1586L30.454 10.7191C28.2634 10.1265 26.9051 9.01456 26.9051 7.11692C26.9051 4.76651 29.0025 3.20179 31.7391 3.20179C34.2027 3.20179 36.0005 4.45357 36.4133 6.27131C36.5132 6.71076 36.2335 6.97044 35.7874 6.97044H35.1083Z"/>
37
+ <path d="M45.0887 7.46316C45.0887 7.03036 45.3217 6.79732 45.7545 6.79732H46.4204C46.8532 6.79732 47.0862 7.03036 47.0862 7.46316V16.3588C47.0862 16.7915 46.8532 17.0246 46.4204 17.0246H45.7945C45.3617 17.0246 45.1286 16.7915 45.1286 16.3588V15.2535H45.0221C44.5494 16.3454 43.504 17.1578 41.9659 17.1578C40.0017 17.1578 38.6101 15.8394 38.6101 13.3026V7.46316C38.6101 7.03036 38.8431 6.79732 39.2759 6.79732H39.9351C40.3679 6.79732 40.601 7.03036 40.601 7.46316V13.0629C40.601 14.4545 41.4532 15.3467 42.685 15.3467C43.8036 15.3467 45.0954 14.521 45.0887 12.7832V7.46316Z"/>
38
+ <path d="M49.3862 7.46316C49.3862 7.03036 49.6193 6.79732 50.0521 6.79732H50.6447C51.0774 6.79732 51.3105 7.03036 51.3105 7.46316V8.42196H51.417C51.7899 7.32333 52.8153 6.64418 54.0138 6.64418C54.0737 6.64418 54.147 6.64418 54.2202 6.65084C54.6397 6.65749 54.8594 6.91717 54.8594 7.33665V7.88264C54.8594 8.33541 54.673 8.54182 54.3467 8.50852C54.1803 8.48855 53.9938 8.47523 53.8207 8.47523C52.4091 8.47523 51.3771 9.4407 51.3771 10.779V16.3588C51.3771 16.7915 51.144 17.0246 50.7112 17.0246H50.0521C49.6193 17.0246 49.3862 16.7915 49.3862 16.3588V7.46316Z"/>
39
+ <path d="M61.6788 7.72949C61.6788 8.16229 61.4457 8.39533 61.013 8.39533H59.4749V16.3588C59.4749 16.7915 59.2418 17.0246 58.809 17.0246H58.1432C57.7104 17.0246 57.4774 16.7915 57.4774 16.3588V8.39533H56.5652C56.1324 8.39533 55.8993 8.16229 55.8993 7.72949V7.46316C55.8993 7.03036 56.1324 6.79732 56.5652 6.79732H57.4774V5.61213C57.4774 3.69451 58.8956 2.74902 60.5136 2.74902C60.9131 2.74902 61.2593 2.78897 61.539 2.84224C61.9651 2.9288 62.1116 3.2484 61.9917 3.66122L61.8919 4.01412C61.7787 4.42028 61.5656 4.57342 61.2726 4.52015C61.1395 4.49352 60.993 4.4802 60.8199 4.4802C59.8477 4.4802 59.4749 4.97292 59.4749 5.87846V6.79732H61.013C61.4457 6.79732 61.6788 7.03036 61.6788 7.46316V7.72949Z"/>
40
+ </svg>
41
+ </a>
42
+ </body>
43
+ </html>
@@ -0,0 +1,109 @@
1
+ import './index.css'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Cold-start guard: verify React is real BEFORE rendering.
5
+ //
6
+ // During Vite dep-optimization (cold start or after `npm install <pkg>`),
7
+ // Vite may serve placeholder "stub" modules where all React exports are
8
+ // undefined. Rendering with stub hooks causes "Cannot read properties of
9
+ // null (reading 'useState')".
10
+ //
11
+ // Strategy:
12
+ // 1. Dynamic-import React and check if useState is a function.
13
+ // 2. If stub → show a loading banner, schedule a reload with increasing
14
+ // delay, and do NOT mount any React tree (avoids the error entirely).
15
+ // 3. If real → dynamic-import App/ErrorBoundary and render normally.
16
+ // 4. Use createElement (not JSX) in this file so Vite doesn't inject a
17
+ // static `import { jsx } from 'react/jsx-dev-runtime'` which would
18
+ // itself be a stub and crash before our guard runs.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const RELOAD_KEY = '__dep_reload'
22
+ const MAX_RELOADS = 6
23
+ const RELOAD_WINDOW = 60_000 // 1 minute
24
+
25
+ function getReloads(): { c: number; t: number } {
26
+ try {
27
+ return JSON.parse(sessionStorage.getItem(RELOAD_KEY) || '{}') as { c: number; t: number }
28
+ } catch { return { c: 0, t: 0 } }
29
+ }
30
+
31
+ async function boot() {
32
+ const root = document.getElementById('root')!
33
+
34
+ try {
35
+ const React = await import('react')
36
+
37
+ // If useState is not a function, React is a dep-optimization stub
38
+ if (typeof React.useState !== 'function') throw new Error('dep-stub')
39
+
40
+ const { createElement } = React
41
+ const { createRoot, hydrateRoot } = await import('react-dom/client')
42
+ const { QueryClient, QueryClientProvider } = await import('@tanstack/react-query')
43
+ const { default: App } = await import('./App')
44
+
45
+ const queryClient = new QueryClient({
46
+ defaultOptions: { queries: {
47
+ refetchOnWindowFocus: false,
48
+ retry: 3,
49
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000),
50
+ staleTime: 30_000,
51
+ } },
52
+ })
53
+
54
+ const children = createElement(QueryClientProvider, { client: queryClient },
55
+ createElement(App)
56
+ )
57
+
58
+ // Use hydrateRoot only when SSR rendered real app content. The scaffold's
59
+ // server entry intentionally returns a lightweight placeholder shell so
60
+ // SSR-incompatible libraries (for example echarts-for-react) cannot crash
61
+ // deploy-time render. Placeholder markup must be client-rendered, not hydrated.
62
+ const hasPlaceholder = !!root.querySelector('[data-surf-placeholder]')
63
+ if (root.childNodes.length > 0 && root.innerHTML !== '<!--ssr-outlet-->' && !hasPlaceholder) {
64
+ hydrateRoot(root, children)
65
+ } else {
66
+ root.innerHTML = ''
67
+ createRoot(root).render(children)
68
+ }
69
+
70
+ // React rendered successfully — signal to index.html fallback & reset counter
71
+ ;(window as any).__reactOk = true
72
+ sessionStorage.removeItem(RELOAD_KEY)
73
+
74
+ // Notify parent frame when real app content renders (not the placeholder).
75
+ // DO NOT REMOVE — the hosting app uses this to dismiss the loading overlay.
76
+ function notifyParentReady() {
77
+ if (!document.querySelector('[data-surf-placeholder]')) {
78
+ try { window.parent.postMessage({ type: 'surf-app-ready' }, '*') } catch { /* cross-origin — ignore */ }
79
+ }
80
+ }
81
+ notifyParentReady()
82
+ new MutationObserver(notifyParentReady).observe(root, { childList: true, subtree: true })
83
+ } catch {
84
+ // React is not ready — show loading banner and schedule reload
85
+ const prev = getReloads()
86
+ const count = (Date.now() - prev.t > RELOAD_WINDOW) ? 0 : prev.c
87
+
88
+ if (count < MAX_RELOADS) {
89
+ root.innerHTML = [
90
+ '<div style=\"padding:24px;text-align:center;font-family:system-ui,sans-serif\">',
91
+ '<p style=\"color:#3b82f6;font-weight:600;margin:0 0 4px\">Loading dependencies...</p>',
92
+ '<p style=\"color:#3b82f6;opacity:0.7;font-size:12px;margin:0\">Reloading automatically</p>',
93
+ '</div>',
94
+ ].join('')
95
+ sessionStorage.setItem(RELOAD_KEY, JSON.stringify({ c: count + 1, t: Date.now() }))
96
+ // Increasing delay: 3s, 4s, 5s, ... gives Vite more time to finish
97
+ setTimeout(() => location.reload(), 3000 + count * 1000)
98
+ } else {
99
+ root.innerHTML = [
100
+ '<div style=\"padding:24px;text-align:center;font-family:system-ui,sans-serif\">',
101
+ '<p style=\"color:#c0392b;font-weight:600;margin:0 0 4px\">Failed to load dependencies</p>',
102
+ '<p style=\"color:#c0392b;opacity:0.8;font-size:12px;margin:0\">Please refresh the page</p>',
103
+ '</div>',
104
+ ].join('')
105
+ }
106
+ }
107
+ }
108
+
109
+ boot()
@@ -0,0 +1,13 @@
1
+ import { renderToString } from 'react-dom/server'
2
+
3
+ export function render() {
4
+ return renderToString(
5
+ <div
6
+ data-surf-placeholder
7
+ style={{
8
+ minHeight: '100vh',
9
+ background: '#ffffff',
10
+ }}
11
+ />
12
+ )
13
+ }