diff --git a/.agents/analytics.md b/.agents/analytics.md index c1ba501ac..d5757b286 100644 --- a/.agents/analytics.md +++ b/.agents/analytics.md @@ -166,7 +166,7 @@ User took an action on the generated result. Single event with `action` prop cov | `idea_used` | string | Session context | | `action` | enum | See `BuilderAction` below | | `surface` | enum | `result_panel` (main builder UI) or `deploy_dialog` | -| `provider` | string? | Deploy provider when applicable: `vercel`, `netlify`, `cloudflare` | +| `provider` | string? | Build or deploy provider when applicable: `cloudflare`, `lovable`, `netlify`, `railway`, `render`, `vercel` | | `automatic` | boolean | `true` for system-driven actions (e.g., deploy_dialog auto-redirect countdown). Filter to `false` for true user click rates. | **Important:** automatic prompt-copies that fire as a side-effect of generation do NOT emit `builder_activated`. Only user-driven actions count as activation. @@ -203,6 +203,7 @@ User took an action on the generated result. Single event with `action` prop cov | `open_codex` | Opened the result in Codex | | `open_claude` | Opened the result in Claude | | `open_cursor` | Opened the result in Cursor | +| `open_prompt_builder` | Opened the result in a partner's prompt-based builder | | `download` | Downloaded the project as a zip | | `open_advanced` | Opened the advanced builder editor | | `netlify_start` | Started a Netlify deploy from the result | diff --git a/package.json b/package.json index 0a82141e4..cbbc3c244 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@tanstack/ai-client": "^0.28.0", "@tanstack/ai-openai": "^0.22.0", "@tanstack/charts": "0.16.0", - "@tanstack/create": "^0.69.0", + "@tanstack/create": "^0.70.0", "@tanstack/highlight": "^0.0.9", "@tanstack/markdown": "^0.0.11", "@tanstack/pacer": "^0.21.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd1680b5f..829a90857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,8 @@ importers: specifier: 0.16.0 version: 0.16.0(lit@3.3.2)(octane@0.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.16(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)))(preact@10.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.12) '@tanstack/create': - specifier: ^0.69.0 - version: 0.69.0(tslib@2.8.1) + specifier: ^0.70.0 + version: 0.70.0 '@tanstack/highlight': specifier: ^0.0.9 version: 0.0.9 @@ -1573,126 +1573,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@jsonjoy.com/base64@1.1.2': - resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/base64@17.67.0': - resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/buffers@1.2.1': - resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/buffers@17.67.0': - resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/codegen@1.0.0': - resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/codegen@17.67.0': - resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-core@4.57.1': - resolution: {integrity: sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-fsa@4.57.1': - resolution: {integrity: sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-builtins@4.57.1': - resolution: {integrity: sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-to-fsa@4.57.1': - resolution: {integrity: sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-utils@4.57.1': - resolution: {integrity: sha512-vp+7ZzIB8v43G+GLXTS4oDUSQmhAsRz532QmmWBbdYA20s465JvwhkSFvX9cVTqRRAQg+vZ7zWDaIEh0lFe2gw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node@4.57.1': - resolution: {integrity: sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-print@4.57.1': - resolution: {integrity: sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-snapshot@4.57.1': - resolution: {integrity: sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pack@1.21.0': - resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pack@17.67.0': - resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pointer@1.0.2': - resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pointer@17.67.0': - resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/util@1.9.0': - resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/util@17.67.0': - resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - '@kapaai/react-sdk@0.9.10': resolution: {integrity: sha512-osQyFgBJmhNM207MpB0aZbjs2kjNHjEJbNc5YEReJuyFE72Iydn9CoZGxox2GYRgMIlQ7wljHCzgQqkvR8lXqA==} peerDependencies: @@ -3527,8 +3407,8 @@ packages: vue: optional: true - '@tanstack/create@0.69.0': - resolution: {integrity: sha512-LVbzkTWiCK+JCKa/1Qwax0JddfJt4rxwekrDyHzhDWsSeNodJrTwbUg4eqqbQsxEWcrk0kHEJSfiTVzMnmI+kQ==} + '@tanstack/create@0.70.0': + resolution: {integrity: sha512-mkWcVbimcXJuo4eyqJiIcLO4LSLvcYS4EStTe259KDPLz0pqHVWVbnEbLFJy6MrxG0kBthWB956z7l0oZTshhw==} engines: {node: '>=20'} '@tanstack/db-ivm@0.1.19': @@ -5171,12 +5051,6 @@ packages: gifenc@1.0.3: resolution: {integrity: sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==} - glob-to-regex.js@1.2.0: - resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -5294,10 +5168,6 @@ packages: engines: {node: '>=18'} hasBin: true - hyperdyperid@1.2.0: - resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} - engines: {node: '>=10.18'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5309,8 +5179,8 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.7: + resolution: {integrity: sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==} engines: {node: '>= 4'} immediate@3.0.6: @@ -5797,11 +5667,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memfs@4.57.1: - resolution: {integrity: sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==} - peerDependencies: - tslib: '2' - memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -6198,6 +6063,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -6702,12 +6572,6 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - thingies@2.6.0: - resolution: {integrity: sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==} - engines: {node: '>=10.18'} - peerDependencies: - tslib: ^2 - three-mesh-bvh@0.8.3: resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} peerDependencies: @@ -6755,12 +6619,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tree-dump@1.1.0: - resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - troika-three-text@0.52.4: resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} peerDependencies: @@ -8132,133 +7990,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/fs-core@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-fsa@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-builtins@4.57.1(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-to-fsa@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-utils@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.57.1(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-print@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-snapshot@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) - '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - hyperdyperid: 1.2.0 - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - hyperdyperid: 1.2.0 - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - '@kapaai/react-sdk@0.9.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@fingerprintjs/fingerprintjs-pro-react': 2.7.1 @@ -9857,18 +9588,15 @@ snapshots: react-dom: 19.2.3(react@19.2.3) solid-js: 1.9.12 - '@tanstack/create@0.69.0(tslib@2.8.1)': + '@tanstack/create@0.70.0': dependencies: ejs: 3.1.10 execa: 9.6.1 - ignore: 7.0.5 - memfs: 4.57.1(tslib@2.8.1) + ignore: 7.0.7 parse-gitignore: 2.0.0 - prettier: 3.8.1 + prettier: 3.9.6 rimraf: 6.1.3 zod: 3.25.76 - transitivePeerDependencies: - - tslib '@tanstack/db-ivm@0.1.19(typescript@6.0.2)': dependencies: @@ -11782,10 +11510,6 @@ snapshots: gifenc@1.0.3: {} - glob-to-regex.js@1.2.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -11890,8 +11614,6 @@ snapshots: husky@9.1.7: {} - hyperdyperid@1.2.0: {} - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -11902,7 +11624,7 @@ snapshots: ieee754@1.2.1: {} - ignore@7.0.5: {} + ignore@7.0.7: {} immediate@3.0.6: {} @@ -12341,23 +12063,6 @@ snapshots: media-typer@1.1.0: {} - memfs@4.57.1(tslib@2.8.1): - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-to-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - memorystream@0.3.1: {} merge-descriptors@2.0.0: {} @@ -12789,6 +12494,8 @@ snapshots: prettier@3.8.1: {} + prettier@3.9.6: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -13425,10 +13132,6 @@ snapshots: transitivePeerDependencies: - react-native-b4a - thingies@2.6.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - three-mesh-bvh@0.8.3(three@0.183.2): dependencies: three: 0.183.2 @@ -13474,10 +13177,6 @@ snapshots: tr46@0.0.3: {} - tree-dump@1.1.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - troika-three-text@0.52.4(three@0.183.2): dependencies: bidi-js: 1.0.3 diff --git a/src/application-starter/api/create-worker.ts b/src/application-starter/api/create-worker.ts index 8bdd95275..921f99a9d 100644 --- a/src/application-starter/api/create-worker.ts +++ b/src/application-starter/api/create-worker.ts @@ -512,6 +512,9 @@ const reactAddOnLoaders = defineAddOnLoaders({ railway: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/railway'), ), + render: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/react/add-ons/render'), + ), resume: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/resume'), ), @@ -554,6 +557,9 @@ const reactAddOnLoaders = defineAddOnLoaders({ tRPC: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/trpc'), ), + vercel: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/react/add-ons/vercel'), + ), workos: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/workos'), ), @@ -591,6 +597,9 @@ const solidAddOnLoaders = defineAddOnLoaders({ railway: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/railway'), ), + render: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/render'), + ), sentry: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/sentry'), ), @@ -616,6 +625,9 @@ const solidAddOnLoaders = defineAddOnLoaders({ '@tanstack/create/worker-manifest/frameworks/solid/add-ons/tanstack-query' ), ), + vercel: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/vercel'), + ), }) const addOnLoaders: Record< diff --git a/src/components/ApplicationStarter.tsx b/src/components/ApplicationStarter.tsx index 6565a3ac3..f2b96c872 100644 --- a/src/components/ApplicationStarter.tsx +++ b/src/components/ApplicationStarter.tsx @@ -29,9 +29,12 @@ import { } from '~/components/application-starter/prompt-parts' import { buildStarterPromptDeployUrl, + getStarterPromptBuildLabel, + type StarterPromptDeployProvider, +} from '~/components/application-starter/prompt-deploy' +import { toneClasses, type ApplicationStarterIntegration, - type StarterPromptDeployProvider, type StarterTone, } from '~/components/application-starter/prompt-shared' import { useApplicationStarter } from '~/components/application-starter/useApplicationStarter' @@ -79,7 +82,13 @@ const starterToolchains = ['biome', 'eslint'] as const const starterEyebrowClassName = 'font-ds-mono text-ds-mono-xs uppercase tracking-wider text-text-muted' -type HostingDeployPartnerId = 'cloudflare' | 'lovable' | 'netlify' | 'railway' +type HostingDeployPartnerId = + | 'cloudflare' + | 'lovable' + | 'netlify' + | 'render' + | 'railway' + | 'vercel' type StarterTransientAction = | 'claude' | 'clone' @@ -93,7 +102,9 @@ const hostingDeployPartnerLabels: Record = { cloudflare: 'Cloudflare', lovable: 'Lovable', netlify: 'Netlify', + render: 'Render', railway: 'Railway', + vercel: 'Vercel', } function getHostingDeployPartnerId( @@ -103,7 +114,9 @@ function getHostingDeployPartnerId( case 'cloudflare': case 'lovable': case 'netlify': + case 'render': case 'railway': + case 'vercel': return partnerId default: return undefined @@ -117,7 +130,10 @@ function getPromptDeployProvider( case 'lovable': case 'netlify': return partnerId + case 'vercel': + return 'v0' case 'cloudflare': + case 'render': case 'railway': return undefined } @@ -415,7 +431,9 @@ export function ApplicationStarter({ trackActivation({ action: - selectedHostingDeployPartner === 'netlify' ? 'netlify_start' : 'deploy', + selectedHostingDeployPartner === 'netlify' + ? 'netlify_start' + : 'open_prompt_builder', surface: 'result_panel', provider: selectedHostingDeployPartner, }) @@ -436,9 +454,14 @@ export function ApplicationStarter({ break case 'netlify': break + case 'render': + await openDeployDialog('render') + break case 'railway': await openDeployDialog('railway') break + case 'vercel': + break } } finally { setPendingHostingDeployPartner(null) @@ -584,6 +607,9 @@ export function ApplicationStarter({ } if (selectedPromptDeployProvider) { + const buildLabel = getStarterPromptBuildLabel( + selectedPromptDeployProvider, + ) const disabled = !canUseFinalActions || !selectedHostingDeployHref || @@ -611,7 +637,7 @@ export function ApplicationStarter({ showTransientActionFeedback('deploy') }} className={disabled ? 'pointer-events-none opacity-50' : undefined} - aria-label={`Deploy to ${hostingDeployPartnerLabels[selectedHostingDeployPartner]}`} + aria-label={buildLabel} > {isDeployFeedbackActive || waitingForHref ? ( @@ -622,7 +648,7 @@ export function ApplicationStarter({ ? 'Opening...' : waitingForHref ? 'Preparing...' - : 'Deploy'} + : buildLabel} ) } diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx index 33effcedf..aaf76ddfa 100644 --- a/src/components/LibraryLayout.tsx +++ b/src/components/LibraryLayout.tsx @@ -18,6 +18,7 @@ import { frameworkOptions } from '~/libraries/frameworks' import { fallbackLibraryIcon, libraryIcons } from '~/libraries/icons' import { twMerge } from 'tailwind-merge' import { + getPartnerHref, partners, PartnerImage, partnerTiers, @@ -57,7 +58,6 @@ const docsPartnerTierWeights: Record = { type DocRecency = 'new' | 'updated' | null type DocsPartner = { category: Partner['category'] - score: Partner['score'] tier?: Partner['tier'] id: string name: string @@ -192,11 +192,12 @@ function DocsPartnerSlotLink({ }, }) const compactImageConfig = getCompactPartnerImageConfig(partner.image) + const href = getPartnerHref(partner, 'docs_strip') const onClick = () => { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } @@ -213,7 +214,7 @@ function DocsPartnerSlotLink({ return ( { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } diff --git a/src/components/application-starter/prompt-deploy.ts b/src/components/application-starter/prompt-deploy.ts new file mode 100644 index 000000000..0cb6fa131 --- /dev/null +++ b/src/components/application-starter/prompt-deploy.ts @@ -0,0 +1,47 @@ +const starterPromptBuildProductLabels = { + lovable: 'Lovable', + netlify: 'Netlify', + v0: 'v0', +} + +export type StarterPromptDeployProvider = + keyof typeof starterPromptBuildProductLabels + +export function buildStarterPromptDeployUrl( + provider: StarterPromptDeployProvider, + prompt: string, +) { + switch (provider) { + case 'lovable': { + const url = new URL('https://lovable.dev/') + + url.searchParams.set('autosubmit', 'true') + url.searchParams.set('utm_source', 'tanstack') + url.hash = `prompt=${encodeURIComponent(prompt)}` + + return url.toString() + } + case 'netlify': { + const url = new URL('https://app.netlify.com/start') + + url.searchParams.set('prompt', prompt) + url.searchParams.set('utm_source', 'tanstack') + + return url.toString() + } + case 'v0': { + const url = new URL('https://v0.app/') + + url.searchParams.set('q', prompt) + url.searchParams.set('utm_source', 'tanstack') + + return url.toString() + } + } +} + +export function getStarterPromptBuildLabel( + provider: StarterPromptDeployProvider, +) { + return `Build with ${starterPromptBuildProductLabels[provider]}` +} diff --git a/src/components/application-starter/prompt-parts.tsx b/src/components/application-starter/prompt-parts.tsx index e1653a57e..18495f481 100644 --- a/src/components/application-starter/prompt-parts.tsx +++ b/src/components/application-starter/prompt-parts.tsx @@ -519,7 +519,7 @@ export function StarterPartnerRows({ case 6: return 'grid-cols-2 min-[480px]:grid-cols-3 min-[900px]:grid-cols-6' case 7: - return 'grid-cols-4 min-[900px]:grid-cols-7' + return 'grid-cols-2 min-[480px]:grid-cols-4 min-[900px]:grid-cols-8' default: return 'grid-cols-2 min-[480px]:grid-cols-3' } @@ -538,10 +538,12 @@ export function StarterPartnerRows({ twMerge( 'grid w-full gap-[2px] overflow-hidden rounded-xl bg-gray-950/[0.10] dark:bg-white/[0.12]', getLargeGridColumns(row.partners.length), + row.partners.length === 7 && + 'min-[900px]:overflow-visible min-[900px]:rounded-none min-[900px]:bg-transparent min-[900px]:dark:bg-transparent', ), )} > - {row.partners.map((partner) => { + {row.partners.map((partner, partnerIndex) => { const selected = selectedPartners.includes(partner.id) const muted = mutedPartnerIds.has(partner.id) const selectionIndex = selected ? selectedOrdinal++ : -1 @@ -566,6 +568,29 @@ export function StarterPartnerRows({ visuallySelected={visuallySelected} className={twMerge( size === 'large' && 'min-w-0 w-full justify-center', + size === 'large' && + row.partners.length === 7 && + 'min-[900px]:col-span-2', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 4 && + 'min-[900px]:col-start-2', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 0 && + 'min-[900px]:rounded-tl-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 3 && + 'min-[900px]:rounded-tr-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 4 && + 'min-[900px]:rounded-bl-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 6 && + 'min-[900px]:rounded-br-xl', )} /> diff --git a/src/components/application-starter/prompt-shared.ts b/src/components/application-starter/prompt-shared.ts index d75ddd80a..fdd641616 100644 --- a/src/components/application-starter/prompt-shared.ts +++ b/src/components/application-starter/prompt-shared.ts @@ -9,8 +9,11 @@ import { } from '~/utils/application-starter' export type StarterTone = 'cyan' | 'emerald' | 'violet' -export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway' -export type StarterPromptDeployProvider = 'lovable' | 'netlify' +export type StarterDeployProvider = + | 'cloudflare' + | 'netlify' + | 'railway' + | 'render' export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn' export type StarterToolchain = 'biome' | 'eslint' @@ -144,31 +147,6 @@ export const starterLoadingPhrases = [ 'Finding calmer waters...', ] -export function buildStarterPromptDeployUrl( - provider: StarterPromptDeployProvider, - prompt: string, -) { - switch (provider) { - case 'lovable': { - const url = new URL('https://lovable.dev/') - - url.searchParams.set('autosubmit', 'true') - url.searchParams.set('utm_source', 'tanstack') - url.hash = `prompt=${encodeURIComponent(prompt)}` - - return url.toString() - } - case 'netlify': { - const url = new URL('https://app.netlify.com/start') - - url.searchParams.set('prompt', prompt) - url.searchParams.set('utm_source', 'tanstack') - - return url.toString() - } - } -} - export function isPinnedStarterLibrary(libraryId: LibraryId) { return starterPinnedLibraryIds.some( (pinnedLibraryId) => pinnedLibraryId === libraryId, diff --git a/src/components/application-starter/useApplicationStarter.tsx b/src/components/application-starter/useApplicationStarter.tsx index 4975c614f..d626368c2 100644 --- a/src/components/application-starter/useApplicationStarter.tsx +++ b/src/components/application-starter/useApplicationStarter.tsx @@ -25,8 +25,8 @@ import { } from '~/utils/partners' import { usePartnerPlacementContext } from '~/utils/usePartnerPlacementContext' import type { LibraryId } from '~/libraries' +import { buildStarterPromptDeployUrl } from './prompt-deploy' import { - buildStarterPromptDeployUrl, composeStarterInput, isNextJsMigrationInput, isPinnedStarterLibrary, @@ -839,7 +839,7 @@ export function useApplicationStarter({ try { await withResolvedPrompt((nextResult) => { trackActivation({ - action: 'deploy', + action: 'open_prompt_builder', surface: 'result_panel', provider: 'lovable', }) diff --git a/src/components/deploy/shared.ts b/src/components/deploy/shared.ts index 6e210d68a..70e44db8f 100644 --- a/src/components/deploy/shared.ts +++ b/src/components/deploy/shared.ts @@ -4,7 +4,7 @@ * Common types, constants, and validation for deploy dialogs. */ -export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' +export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'render' export type DeployState = | { step: 'auth-check' } @@ -60,6 +60,20 @@ export const PROVIDER_INFO: Record = { url.searchParams.set('utm_source', 'oss') url.searchParams.set('utm_campaign', 'tanstack') + return url.toString() + }, + }, + render: { + name: 'Render', + color: '#46E3B7', + deployUrl: (owner, repo) => { + const url = new URL('https://render.com/deploy') + + url.searchParams.set('repo', `https://github.com/${owner}/${repo}`) + url.searchParams.set('utm_source', 'tanstack') + url.searchParams.set('utm_medium', 'referral') + url.searchParams.set('utm_campaign', 'gold-launch') + return url.toString() }, }, diff --git a/src/components/ds/ui/PartnerRail.tsx b/src/components/ds/ui/PartnerRail.tsx index 2f0f857ad..bc350ae9a 100644 --- a/src/components/ds/ui/PartnerRail.tsx +++ b/src/components/ds/ui/PartnerRail.tsx @@ -1,6 +1,7 @@ import { Link } from '@tanstack/react-router' import { twMerge } from 'tailwind-merge' import { + getPartnerHref, partnerTierFlares, partnerTierLabels, type PartnerTier, @@ -176,10 +177,12 @@ function PartnerRailLogo({ }, }) + const href = getPartnerHref(partner, analyticsPlacement) + return ( { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } diff --git a/src/images/render-black.svg b/src/images/render-black.svg new file mode 100644 index 000000000..931758f1a --- /dev/null +++ b/src/images/render-black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/render-white.svg b/src/images/render-white.svg new file mode 100644 index 000000000..b222057c8 --- /dev/null +++ b/src/images/render-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/vercel-dark.svg b/src/images/vercel-dark.svg index f8b16b6a9..9f67d8fec 100644 --- a/src/images/vercel-dark.svg +++ b/src/images/vercel-dark.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/images/vercel-light.svg b/src/images/vercel-light.svg index bf8d7259f..84c419ff6 100644 --- a/src/images/vercel-light.svg +++ b/src/images/vercel-light.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/utils/analytics/events.ts b/src/utils/analytics/events.ts index d69670437..0b4af009f 100644 --- a/src/utils/analytics/events.ts +++ b/src/utils/analytics/events.ts @@ -46,6 +46,7 @@ export type ApplicationStarterAction = | 'open_codex' | 'open_claude' | 'open_cursor' + | 'open_prompt_builder' | 'download' | 'open_advanced' | 'netlify_start' diff --git a/src/utils/application-starter.ts b/src/utils/application-starter.ts index f2b696edc..c048fed77 100644 --- a/src/utils/application-starter.ts +++ b/src/utils/application-starter.ts @@ -8,6 +8,7 @@ import { getApplicationStarterSelectedPartnerIds, getApplicationStarterUserBrief, hasApplicationStarterPartnerConflictWithAny, + isRenderDeploymentRequest, } from '~/utils/partners' export type ApplicationStarterContext = @@ -21,7 +22,13 @@ export type ApplicationStarterResultType = | 'scaffoldable' export interface ApplicationStarterRecipe { - deployment?: 'cloudflare' | 'netlify' | 'nitro' | 'railway' + deployment?: + | 'cloudflare' + | 'netlify' + | 'nitro' + | 'railway' + | 'render' + | 'vercel' featureOptions: Record> features: Array framework: FrameworkId @@ -759,6 +766,10 @@ function applyPartnerOverrides( recipe.deployment = 'netlify' } else if (partnerIds.has('railway')) { recipe.deployment = 'railway' + } else if (partnerIds.has('render')) { + recipe.deployment = 'render' + } else if (partnerIds.has('vercel')) { + recipe.deployment = 'vercel' } if (partnerIds.has('workos')) { @@ -1069,6 +1080,12 @@ function detectDeployment(input: string) { if (/\brailway\b/i.test(input)) { return 'railway' as const } + if (isRenderDeploymentRequest(input)) { + return 'render' + } + if (/\b(vercel|v0)\b/i.test(input)) { + return 'vercel' + } if (/\bnitro\b/i.test(input)) { return 'nitro' as const } diff --git a/src/utils/partner-placement.ts b/src/utils/partner-placement.ts index 6f5931e01..1223a99ae 100644 --- a/src/utils/partner-placement.ts +++ b/src/utils/partner-placement.ts @@ -24,7 +24,7 @@ export type PartnerPlacementAnalyticsMetadata = { type PartnerForPlacement = Pick< Partner, - 'category' | 'id' | 'name' | 'score' | 'tier' + 'category' | 'id' | 'name' | 'tier' > & { placementWeight?: number } @@ -240,12 +240,6 @@ function compareLegacyPartnerPriority( left: TPartner, right: TPartner, ) { - const scoreComparison = right.score - left.score - - if (scoreComparison !== 0) { - return scoreComparison - } - return comparePartnerIdentity(left, right) } diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index a8f1a4ab6..2676966a5 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -41,6 +41,8 @@ import powersyncBlackSvg from '~/images/powersync-black.svg' import powersyncWhiteSvg from '~/images/powersync-white.svg' import railwayBlackSvg from '~/images/railway-black.svg' import railwayWhiteSvg from '~/images/railway-white.svg' +import renderBlackSvg from '~/images/render-black.svg' +import renderWhiteSvg from '~/images/render-white.svg' import openrouterBlackSvg from '~/images/openrouter-black.svg' import openrouterWhiteSvg from '~/images/openrouter-white.svg' import { @@ -48,6 +50,7 @@ import { getPartnersForPlacement, type PartnerPlacementContext, } from '~/utils/partner-placement' +import type { PartnerPlacement } from '~/utils/analytics' function LearnMoreButton() { return ( @@ -268,7 +271,6 @@ export type RailPartner = { id: string name: string href: string - score: number tier?: PartnerTier image: PartnerImageConfig } @@ -313,7 +315,6 @@ type PartnerBase = { llmDescription: string category: PartnerCategory lastReviewedAt?: string - score: number uniqueConstraints?: ReadonlyArray brandColor?: string // Primary brand color for game elements tagline?: string // Short tagline for game info cards @@ -432,7 +433,6 @@ const neon = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: 'Apr 2026', - score: 0.297, href, brandColor: '#00E599', tagline: 'Serverless Postgres', @@ -467,7 +467,6 @@ const convex = ((): Partner => { status: 'inactive' as const, startDate: 'May 2024', endDate: 'Mar 2026', - score: 0.286, href, brandColor: '#F3A712', tagline: 'Real-time Database', @@ -515,7 +514,6 @@ const clerk = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.286, tier: 'silver' as const, uniqueConstraints: [ 'auth-provider', @@ -572,7 +570,6 @@ const workos = ((): Partner => { relatedProducts: ['start', 'router'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.314, tier: 'silver' as const, uniqueConstraints: [ 'auth-provider', @@ -614,7 +611,6 @@ const agGrid = ((): Partner => { relatedProducts: ['table'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.497, tier: 'silver' as const, href, canonicalHref: 'https://www.ag-grid.com/', @@ -681,7 +677,6 @@ const netlify = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.343, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, href, @@ -749,7 +744,6 @@ const cloudflare = ((): Partner => { relatedProducts: ['start'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.857, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, startDate: 'Sep 2025', @@ -798,7 +792,6 @@ const lovable = ((): Partner => { relatedProducts: ['start', 'router'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.714, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, brandColor: '#FF7EB0', @@ -806,7 +799,7 @@ const lovable = ((): Partner => { applicationStarterPromptInstructions: [ 'Treat Lovable as the AI app-building and hosting path, not as a TanStack CLI deployment flag or npm package.', 'Keep the generated app portable: start with the TanStack CLI output, preserve GitHub/project ownership notes, and call out any Lovable Cloud setup that cannot be automated from code.', - 'When Lovable is selected, do not add a separate Cloudflare, Netlify, or Railway deployment target unless the user explicitly asks for a handoff path.', + 'When Lovable is selected, do not add a separate Cloudflare, Netlify, Railway, Render, or Vercel deployment target unless the user explicitly asks for a handoff path.', ], image: { light: lovableBlackSvg, @@ -840,7 +833,6 @@ const sentry = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.229, tier: 'bronze' as const, href, canonicalHref: 'https://sentry.io/', @@ -884,7 +876,6 @@ const fireship = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.014, href, tagline: 'Dev Education', image: { @@ -942,7 +933,6 @@ const nozzle = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.014, tagline: 'Enterprise SEO', image: { src: nozzleImage, @@ -976,7 +966,6 @@ const speakeasy = ((): Partner => { status: 'inactive' as const, startDate: 'Feb 2025', endDate: 'Jul 2025', - score: 0, image: { light: speakeasyLightSvg, dark: speakeasyDarkSvg, @@ -1009,7 +998,6 @@ const unkey = ((): Partner => { relatedProducts: ['start'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.051, tier: 'bronze' as const, href, canonicalHref: 'https://www.unkey.com/', @@ -1063,7 +1051,6 @@ const serpApi = ((): Partner => { relatedProducts: ['start', 'ai', 'mcp'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.41, tier: 'silver' as const, href, canonicalHref: 'https://serpapi.com/', @@ -1121,7 +1108,6 @@ const electric = ((): Partner => { relatedProducts: ['db'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.283, tier: 'bronze' as const, href, canonicalHref: 'https://electric.ax/', @@ -1162,32 +1148,47 @@ const electric = ((): Partner => { })() const vercel = ((): Partner => { - const href = 'https://vercel.com?utm_source=tanstack' + const href = + 'https://vercel.com?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch' return { name: 'Vercel', id: 'vercel', href, - relatedProducts: ['start', 'router'] as const, - status: 'inactive' as const, - startDate: 'May 2024', - endDate: 'Oct 2024', - score: 0, - uniqueConstraints: ['hosting'] satisfies Array, + canonicalHref: 'https://vercel.com/', + resources: [ + { + kind: 'documentation', + label: 'TanStack Start hosting guide', + href: '/start/latest/docs/framework/react/guide/hosting', + }, + ], + relatedProducts: ['start', 'router'], + status: 'active', + lastReviewedAt: currentPartnerReviewDate, + tier: 'gold', + uniqueConstraints: ['hosting'], + brandColor: '#000000', + tagline: 'Agentic Infrastructure', + applicationStarterIcon: { + mode: 'contain', + src: vercelLightSvg, + }, image: { light: vercelLightSvg, dark: vercelDarkSvg, }, llmDescription: - 'Cloud platform for deploying and scaling web applications with Git-based workflows, preview environments, global delivery, and Vercel Functions.', + 'Agentic infrastructure for building, deploying, and running apps and agents, with Git-based workflows, preview deployments, global delivery, serverless compute, and AI tooling.', category: 'deployment', content: ( <>
- Vercel provides Git-based deployments, preview - environments, global delivery, and server-side compute through Vercel - Functions. That makes it a familiar deployment option for TanStack - Start and Router teams building full-stack apps. + Vercel provides agentic infrastructure for building, + deploying, and running apps and agents, with Git-based workflows, + preview deployments, global delivery, serverless compute, and AI + tooling. It is a familiar deployment option for TanStack Start and + Router teams shipping full-stack applications.
@@ -1214,7 +1215,6 @@ const prisma = ((): Partner => { lastReviewedAt: currentPartnerReviewDate, relatedProducts: ['db', 'start'] as const, startDate: 'Aug 2025', - score: 0.143, tier: 'bronze' as const, brandColor: '#2D3748', tagline: 'Database ORM', @@ -1259,7 +1259,6 @@ const codeRabbit = ((): Partner => { lastReviewedAt: currentPartnerReviewDate, relatedProducts: [], startDate: 'Aug 2025', - score: 1, tier: 'gold' as const, brandColor: '#FF6B2B', tagline: 'AI Code Review', @@ -1299,7 +1298,6 @@ const strapi = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.069, tier: 'bronze' as const, href, brandColor: '#4945FF', @@ -1337,7 +1335,6 @@ const powerSync = ((): Partner => { status: 'inactive' as const, startDate: 'Jan 2026', endDate: 'Jun 2026', - score: 0.143, tier: 'bronze' as const, href, canonicalHref: 'https://www.powersync.com/', @@ -1391,7 +1388,6 @@ const railway = ((): Partner => { relatedProducts: ['start'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.145, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, href, @@ -1427,6 +1423,55 @@ const railway = ((): Partner => { } })() +const render = ((): Partner => { + const href = + 'https://render.com?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch' + + return { + name: 'Render', + id: 'render', + relatedProducts: ['start'], + status: 'active', + lastReviewedAt: currentPartnerReviewDate, + tier: 'gold', + uniqueConstraints: ['hosting'], + href, + canonicalHref: 'https://render.com/', + resources: [ + { + kind: 'documentation', + label: 'TanStack Start hosting guide', + href: '/start/latest/docs/framework/react/guide/hosting', + }, + ], + brandColor: '#000000', + tagline: 'Intuitive Cloud Infrastructure', + applicationStarterIcon: { + mode: 'contain', + src: renderBlackSvg, + }, + image: { + light: renderBlackSvg, + dark: renderWhiteSvg, + }, + llmDescription: + 'Cloud platform for deploying and scaling apps and agents with intuitive infrastructure, managed databases, autoscaling, pull request previews, and zero-ops deployment from GitHub.', + category: 'deployment', + content: ( + <> +
+ Render provides intuitive cloud infrastructure for + deploying apps, APIs, databases, and background services. With + autoscaling, PR previews, and managed infrastructure, it is a + practical fit for TanStack teams that want to ship without ops + overhead. +
+ + + ), + } +})() + const openRouter = ((): Partner => { const href = 'https://openrouter.ai?utm_source=tanstack' @@ -1451,7 +1496,6 @@ const openRouter = ((): Partner => { status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, startDate: 'Mar 2026', - score: 0.344, tier: 'silver' as const, brandColor: '#7C3AED', tagline: 'Unified LLM API', @@ -1498,6 +1542,8 @@ export const partners = [ codeRabbit, cloudflare, lovable, + vercel, + render, agGrid, serpApi, netlify, @@ -1515,7 +1561,6 @@ export const partners = [ unkey, fireship, nozzle, - vercel, speakeasy, ] satisfies Array @@ -1690,6 +1735,18 @@ export function hasApplicationStarterPartnerUniqueConstraint( return partner?.uniqueConstraints.includes(uniqueConstraint) ?? false } +const renderDeploymentPatterns = [ + /\brender\.com\b/i, + /\brender\s+blueprints?\b/i, + /\b(?:deploy|deploying|deployment|host|hosting)\b[^.!?\n]{0,40}\b(?:to|on|with|via)\s+render\b(?=\s*(?:$|[,.!?;:]|\b(?:hosting|cloud|platform)\b))/i, + /\brender\s+(?:hosting|deployment)\b/i, + /\buse\s+render\s+(?:for\s+)?(?:hosting|deployment)\b/i, +] + +export function isRenderDeploymentRequest(input: string) { + return renderDeploymentPatterns.some((pattern) => pattern.test(input)) +} + const applicationStarterInferenceRules: Array<{ partnerId: string patterns: Array @@ -1722,6 +1779,14 @@ const applicationStarterInferenceRules: Array<{ partnerId: 'railway', patterns: [/\brailway\b/i], }, + { + partnerId: 'render', + patterns: renderDeploymentPatterns, + }, + { + partnerId: 'vercel', + patterns: [/\b(vercel|v0)\b/i], + }, { partnerId: 'sentry', patterns: [/\bsentry\b/i], @@ -2044,3 +2109,26 @@ export function composeApplicationStarterInput( export function getPartnerById(partnerId: string) { return partners.find((partner) => partner.id === partnerId) } + +const renderPlacementUtmContent: Partial> = { + home_grid: 'home_grid', + library_grid: 'library_grid', + docs_rail: 'docs_rail', + docs_strip: 'docs_strip', +} + +export function getPartnerHref( + partner: Pick, + placement?: PartnerPlacement, +): string { + if (partner.id !== 'render' || !placement) { + return partner.href + } + + const utmContent = renderPlacementUtmContent[placement] + if (!utmContent) { + return partner.href + } + + return `https://render.com/?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch&utm_content=${utmContent}` +} diff --git a/src/utils/useDeploymentProviderPlacement.ts b/src/utils/useDeploymentProviderPlacement.ts index 847c938e2..51f383097 100644 --- a/src/utils/useDeploymentProviderPlacement.ts +++ b/src/utils/useDeploymentProviderPlacement.ts @@ -13,7 +13,7 @@ export const deploymentProviderIds: ReadonlyArray = [ type DeploymentProviderPlacementPartner = Pick< Partner, - 'category' | 'id' | 'name' | 'score' | 'tier' + 'category' | 'id' | 'name' | 'tier' > & { provider: DeploymentProviderId } @@ -41,7 +41,6 @@ function getDeploymentProviderPlacementPartner( id: partner.id, name: partner.name, provider, - score: partner.score, tier: partner.tier, } } diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 56516e73a..e7b95e904 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import { existsSync } from 'node:fs' import { createRequire } from 'node:module' import { test } from 'node:test' +import type { PartnerPlacement } from '../src/utils/analytics' const require = createRequire(import.meta.url) const loadAsset: NodeJS.RequireExtensions[string] = (module, filename) => { @@ -17,6 +18,7 @@ const { const { composeApplicationStarterInput, getInferredApplicationStarterPartnerIdsFromUserInput, + getPartnerHref, partners, }: typeof import('../src/utils/partners') = require('../src/utils/partners') const { @@ -357,3 +359,126 @@ test('OpenRouter guidance prefers the TanStack AI adapter', async () => { assert.match(result.prompt, /@tanstack\/ai-openrouter/) }) + +test('selected Vercel partner uses the Vercel deployment target', async () => { + const input = composeApplicationStarterInput( + 'Build a full-stack app.', + ['vercel'], + [], + ) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(result.recipe.deployment, 'vercel') + assert.match(result.cliCommand, /--deployment vercel/) +}) + +test('selected Render partner uses the Render deployment target', async () => { + const input = composeApplicationStarterInput( + 'Build a full-stack app.', + ['render'], + [], + ) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(result.recipe.deployment, 'render') + assert.match(result.cliCommand, /--deployment render/) +}) + +test('hosting names infer their matching partner and deployment target', async () => { + for (const hosting of [ + { id: 'render', name: 'Render' }, + { id: 'vercel', name: 'Vercel' }, + ]) { + const input = `Build a full-stack app and deploy to ${hosting.name}.` + const inferredPartnerIds = + getInferredApplicationStarterPartnerIdsFromUserInput(input, []) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.ok(inferredPartnerIds.includes(hosting.id)) + assert.equal(result.recipe.deployment, hosting.id) + assert.match(result.cliCommand, new RegExp(`--deployment ${hosting.id}`)) + } +}) + +test('ordinary rendering language does not select Render hosting', async () => { + for (const input of [ + 'Render a chart with server data.', + 'Server render this page before hydration.', + 'Deploy a canvas app that uses WebGL to render charts.', + ]) { + const inferredPartnerIds = + getInferredApplicationStarterPartnerIdsFromUserInput(input, []) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(inferredPartnerIds.includes('render'), false) + assert.notEqual(result.recipe.deployment, 'render') + assert.doesNotMatch(result.cliCommand, /--deployment render/) + } +}) + +test('Render uses per-placement UTM content for approved surfaces', () => { + const renderPartner = partners.find((p) => p.id === 'render') + assert.ok(renderPartner, 'Render partner should exist') + + const placements: PartnerPlacement[] = [ + 'home_grid', + 'library_grid', + 'docs_rail', + 'docs_strip', + ] + for (const placement of placements) { + const href = getPartnerHref(renderPartner, placement) + assert.match( + href, + new RegExp(`utm_content=${placement}`), + `Render href for ${placement} should include utm_content=${placement}`, + ) + assert.match(href, /render\.com/, 'Should point to render.com') + assert.match(href, /utm_source=tanstack/, 'Should include utm_source') + assert.match( + href, + /utm_campaign=gold-launch/, + 'Should include utm_campaign', + ) + } + + const defaultHref = getPartnerHref(renderPartner, 'directory') + assert.doesNotMatch( + defaultHref, + /utm_content/, + 'Render href for other placements should not include utm_content', + ) +}) + +test('other partners use their default href regardless of placement', () => { + const vercel = partners.find((p) => p.id === 'vercel') + assert.ok(vercel, 'Vercel partner should exist') + + const placements: PartnerPlacement[] = [ + 'home_grid', + 'library_grid', + 'docs_rail', + 'docs_strip', + 'directory', + ] + for (const placement of placements) { + const href = getPartnerHref(vercel, placement) + assert.equal( + href, + vercel.href, + `Vercel href should be unchanged for ${placement}`, + ) + } +}) diff --git a/tests/application-starter-prompt-deploy.test.ts b/tests/application-starter-prompt-deploy.test.ts new file mode 100644 index 000000000..f53a2381b --- /dev/null +++ b/tests/application-starter-prompt-deploy.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { + buildStarterPromptDeployUrl, + getStarterPromptBuildLabel, +} from '../src/components/application-starter/prompt-deploy' + +test('prompt handoffs use build labels', () => { + assert.equal(getStarterPromptBuildLabel('lovable'), 'Build with Lovable') + assert.equal(getStarterPromptBuildLabel('netlify'), 'Build with Netlify') + assert.equal(getStarterPromptBuildLabel('v0'), 'Build with v0') +}) + +test('v0 handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('v0', prompt)) + + assert.equal(url.origin, 'https://v0.app') + assert.equal(url.pathname, '/') + assert.equal(url.searchParams.get('q'), prompt) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') +}) + +test('Netlify handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('netlify', prompt)) + + assert.equal(url.origin, 'https://app.netlify.com') + assert.equal(url.pathname, '/start') + assert.equal(url.searchParams.get('prompt'), prompt) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') +}) + +test('Lovable handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('lovable', prompt)) + const hash = new URLSearchParams(url.hash.slice(1)) + + assert.equal(url.origin, 'https://lovable.dev') + assert.equal(url.pathname, '/') + assert.equal(url.searchParams.get('autosubmit'), 'true') + assert.equal(url.searchParams.get('utm_source'), 'tanstack') + assert.equal(hash.get('prompt'), prompt) +}) diff --git a/tests/create-worker.test.ts b/tests/create-worker.test.ts index 60f565501..f455a065d 100644 --- a/tests/create-worker.test.ts +++ b/tests/create-worker.test.ts @@ -17,9 +17,6 @@ assert.deepEqual(workos.partner, { assert.deepEqual(workos.packageAdditions?.engines, { node: '>=22.11.0', }) -assert.deepEqual(sentry.packageAdditions?.pnpm, { - onlyBuiltDependencies: ['@sentry/cli'], -}) const [materializedWorkos] = await create.finalizeAddOns(react, 'file-router', [ 'workos', @@ -29,4 +26,57 @@ assert.deepEqual(materializedWorkos?.partner, { tier: 'silver', }) +const [materializedSentry] = await create.finalizeAddOns(react, 'file-router', [ + 'sentry', +]) +assert.match(materializedSentry?.packageTemplate ?? '', /addOnEnabled\.vercel/) +assert.match( + materializedSentry?.packageTemplate ?? '', + /"onlyBuiltDependencies"/, +) + +for (const frameworkId of ['react', 'solid']) { + const framework = await create.getFrameworkById(frameworkId) + if (!framework) throw new Error(`${frameworkId} framework not found`) + + const frameworkAddOns = create.getAllAddOns(framework, 'file-router') + const codeRouterAddOnIds = create + .getAllAddOns(framework, 'code-router') + .map((addOn) => addOn.id) + + for (const deployment of ['render', 'vercel']) { + const addOn = frameworkAddOns.find( + (candidate) => candidate.id === deployment, + ) + + if (!addOn) { + throw new Error(`${frameworkId} ${deployment} add-on not found`) + } + assert.deepEqual(addOn.partner, { + id: deployment, + tier: 'gold', + }) + assert.deepEqual(addOn.modes, ['file-router']) + assert.equal(codeRouterAddOnIds.includes(deployment), false) + + const [materializedDeployment] = await create.finalizeAddOns( + framework, + 'file-router', + [deployment], + ) + + if (deployment === 'render') { + assert.match( + materializedDeployment?.files['render.yaml.ejs'] ?? '', + /BUN_VERSION/, + ) + } else { + assert.match( + materializedDeployment?.files['vercel.json'] ?? '', + /"framework": "tanstack-start"/, + ) + } + } +} + console.log('create worker tests passed') diff --git a/tests/deploy-provider.test.ts b/tests/deploy-provider.test.ts new file mode 100644 index 000000000..ad25cf2b4 --- /dev/null +++ b/tests/deploy-provider.test.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { PROVIDER_INFO } from '../src/components/deploy/shared' + +test('Render deployment opens the repository as a Blueprint', () => { + const url = new URL(PROVIDER_INFO.render.deployUrl('tanstack', 'books')) + + assert.equal(url.origin, 'https://render.com') + assert.equal(url.pathname, '/deploy') + assert.equal( + url.searchParams.get('repo'), + 'https://github.com/tanstack/books', + ) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') + assert.equal(url.searchParams.get('utm_medium'), 'referral') + assert.equal(url.searchParams.get('utm_campaign'), 'gold-launch') +})