diff --git a/.changeset/honest-partner-paths.md b/.changeset/honest-partner-paths.md new file mode 100644 index 00000000..36aefea2 --- /dev/null +++ b/.changeset/honest-partner-paths.md @@ -0,0 +1,5 @@ +--- +'@tanstack/create': patch +--- + +Fix pnpm 11 build approvals for Prisma and Netlify projects, make generated Prisma MySQL apps use their configured `DATABASE_URL`, and update the Prisma demo to current server-function and package-script APIs. diff --git a/packages/create/src/frameworks/react/add-ons/prisma/assets/prisma/seed.ts.ejs b/packages/create/src/frameworks/react/add-ons/prisma/assets/prisma/seed.ts.ejs index f5971ced..f6728488 100644 --- a/packages/create/src/frameworks/react/add-ons/prisma/assets/prisma/seed.ts.ejs +++ b/packages/create/src/frameworks/react/add-ons/prisma/assets/prisma/seed.ts.ejs @@ -1,19 +1,18 @@ import { PrismaClient } from "../src/generated/prisma/client.js"; +<% if (addOnOption.prisma.database !== 'sqlite') { %> +import { getDatabaseUrl } from '../src/database-url.js' +<% } %> <% if (addOnOption.prisma.database === 'postgres') { %> import { PrismaPg } from '@prisma/adapter-pg'; const adapter = new PrismaPg({ - connectionString: process.env.DATABASE_URL!, + connectionString: getDatabaseUrl(), });<% } %> <% if (addOnOption.prisma.database === 'mysql') { %> import { PrismaMariaDb } from '@prisma/adapter-mariadb'; -const adapter = new PrismaMariaDb({ - host: "localhost", - port: 3306, - connectionLimit: 5 -});<% } %> +const adapter = new PrismaMariaDb(getDatabaseUrl());<% } %> <% if (addOnOption.prisma.database === 'sqlite') { %> import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; diff --git a/packages/create/src/frameworks/react/add-ons/prisma/assets/src/database-url.ts.ejs b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/database-url.ts.ejs new file mode 100644 index 00000000..bf719dc1 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/database-url.ts.ejs @@ -0,0 +1,10 @@ +<% if (addOnOption.prisma.database === 'sqlite') { ignoreFile(); return; } -%> +export function getDatabaseUrl() { + const databaseUrl = process.env.DATABASE_URL + + if (!databaseUrl) { + throw new Error('DATABASE_URL is required') + } + + return databaseUrl +} diff --git a/packages/create/src/frameworks/react/add-ons/prisma/assets/src/db.ts.ejs b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/db.ts.ejs index 316d53dd..146eba2b 100644 --- a/packages/create/src/frameworks/react/add-ons/prisma/assets/src/db.ts.ejs +++ b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/db.ts.ejs @@ -1,19 +1,18 @@ import { PrismaClient } from './generated/prisma/client.js' +<% if (addOnOption.prisma.database !== 'sqlite') { %> +import { getDatabaseUrl } from './database-url.js' +<% } %> <% if (addOnOption.prisma.database === 'postgres') { %> import { PrismaPg } from '@prisma/adapter-pg'; const adapter = new PrismaPg({ - connectionString: process.env.DATABASE_URL!, + connectionString: getDatabaseUrl(), });<% } %> <% if (addOnOption.prisma.database === 'mysql') { %> import { PrismaMariaDb } from '@prisma/adapter-mariadb'; -const adapter = new PrismaMariaDb({ - host: "localhost", - port: 3306, - connectionLimit: 5 -});<% } %> +const adapter = new PrismaMariaDb(getDatabaseUrl());<% } %> <% if (addOnOption.prisma.database === 'sqlite') { %> import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; diff --git a/packages/create/src/frameworks/react/add-ons/prisma/assets/src/routes/demo/prisma.tsx.ejs b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/routes/demo/prisma.tsx.ejs index eb7bd938..3ecc4908 100644 --- a/packages/create/src/frameworks/react/add-ons/prisma/assets/src/routes/demo/prisma.tsx.ejs +++ b/packages/create/src/frameworks/react/add-ons/prisma/assets/src/routes/demo/prisma.tsx.ejs @@ -13,7 +13,7 @@ const getTodos = createServerFn({ const createTodo = createServerFn({ method: 'POST', }) - .inputValidator((data: { title: string }) => data) + .validator((data: { title: string }) => data) .handler(async ({ data }) => { return await prisma.todo.create({ data, @@ -31,15 +31,15 @@ function DemoPrisma() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - const formData = new FormData(e.target as HTMLFormElement) - const title = formData.get('title') as string + const form = e.currentTarget + const title = new FormData(form).get('title') - if (!title) return + if (typeof title !== 'string' || !title) return try { await createTodo({ data: { title } }) router.invalidate() - ;(e.target as HTMLFormElement).reset() + form.reset() } catch (error) { console.error('Failed to create todo:', error) } @@ -99,7 +99,7 @@ function DemoPrisma() { Powered by Prisma ORM

- Next-generation ORM for Node.js & TypeScript with PostgreSQL + Next-generation ORM for Node.js & TypeScript with <%= addOnOption.prisma.database === 'postgres' ? 'PostgreSQL' : addOnOption.prisma.database === 'mysql' ? 'MySQL' : 'SQLite' %>

Setup Instructions:

@@ -112,19 +112,19 @@ function DemoPrisma() {
  • Run:{' '} - <%- getPackageManagerExecuteScript('prisma', ['generate']) %> + <%- getPackageManagerRunScript('db:generate') %>
  • Run:{' '} - <%- getPackageManagerExecuteScript('prisma', ['db', 'push']) %> + <%- getPackageManagerRunScript('db:push') %>
  • Optional:{' '} - <%- getPackageManagerExecuteScript('prisma', ['studio']) %> + <%- getPackageManagerRunScript('db:studio') %>
  • diff --git a/packages/create/src/frameworks/react/add-ons/prisma/package.json.ejs b/packages/create/src/frameworks/react/add-ons/prisma/package.json.ejs index 6443912a..4e7ca7ca 100644 --- a/packages/create/src/frameworks/react/add-ons/prisma/package.json.ejs +++ b/packages/create/src/frameworks/react/add-ons/prisma/package.json.ejs @@ -17,9 +17,12 @@ "db:migrate": "dotenv -e .env.local -- prisma migrate dev", "db:studio": "dotenv -e .env.local -- prisma studio", "db:seed": "dotenv -e .env.local -- prisma db seed" - }<% if (addOnOption.prisma.database === 'sqlite') { %>, + }, "pnpm": { - "onlyBuiltDependencies": ["better-sqlite3"] + "onlyBuiltDependencies": [ + "@prisma/engines", + "prisma"<% if (addOnOption.prisma.database === 'sqlite') { %>, + "better-sqlite3"<% } %> + ] } - <% } %> } diff --git a/packages/create/src/frameworks/react/hosts/netlify/package.json b/packages/create/src/frameworks/react/hosts/netlify/package.json index 4df1a99e..70a27e56 100644 --- a/packages/create/src/frameworks/react/hosts/netlify/package.json +++ b/packages/create/src/frameworks/react/hosts/netlify/package.json @@ -1,5 +1,10 @@ { "devDependencies": { "@netlify/vite-plugin-tanstack-start": "^1.2.15" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp" + ] } } diff --git a/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs b/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs index 7c8dcf63..e5e85b8b 100644 --- a/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs +++ b/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs @@ -5,10 +5,16 @@ allowBuilds: <% if (addOnEnabled.sentry) { -%> '@sentry/cli': true <% } -%> -<% if (addOnEnabled.cloudflare) { -%> +<% if (addOnEnabled.cloudflare || addOnEnabled.netlify) { -%> sharp: true +<% } -%> +<% if (addOnEnabled.cloudflare) { -%> workerd: true <% } -%> -<% if (addOnEnabled.drizzle || addOnEnabled.prisma) { -%> +<% if (addOnEnabled.prisma) { -%> + '@prisma/engines': true + prisma: true +<% } -%> +<% if (addOnEnabled.drizzle || (addOnEnabled.prisma && addOnOption.prisma.database === 'sqlite')) { -%> better-sqlite3: true <% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/netlify/package.json b/packages/create/src/frameworks/solid/hosts/netlify/package.json index 4df1a99e..70a27e56 100644 --- a/packages/create/src/frameworks/solid/hosts/netlify/package.json +++ b/packages/create/src/frameworks/solid/hosts/netlify/package.json @@ -1,5 +1,10 @@ { "devDependencies": { "@netlify/vite-plugin-tanstack-start": "^1.2.15" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp" + ] } } diff --git a/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs b/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs index c31a713e..6d539e4c 100644 --- a/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs +++ b/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs @@ -2,7 +2,9 @@ allowBuilds: esbuild: true lightningcss: true -<% if (addOnEnabled.cloudflare) { -%> +<% if (addOnEnabled.cloudflare || addOnEnabled.netlify) { -%> sharp: true +<% } -%> +<% if (addOnEnabled.cloudflare) { -%> workerd: true <% } -%> diff --git a/packages/create/tests/partner-addons.test.ts b/packages/create/tests/partner-addons.test.ts index 5eb11ffc..9ec7f446 100644 --- a/packages/create/tests/partner-addons.test.ts +++ b/packages/create/tests/partner-addons.test.ts @@ -23,9 +23,11 @@ function frameworkFromDefinition(definition: FrameworkDefinition): Framework { async function generateApp( definition: FrameworkDefinition, addOnIds: Array, + addOnOptions: Options['addOnOptions'] = {}, ) { const framework = frameworkFromDefinition(definition) const chosenAddOns = await finalizeAddOns(framework, 'file-router', addOnIds) + const defaultAddOnOptions = populateAddOnOptionsDefaults(chosenAddOns) const targetDir = '/partner-app' const { environment, output } = createMemoryEnvironment(targetDir) @@ -41,7 +43,10 @@ async function generateApp( install: false, intent: false, chosenAddOns, - addOnOptions: populateAddOnOptionsDefaults(chosenAddOns), + addOnOptions: { + ...defaultAddOnOptions, + ...addOnOptions, + }, includeExamples: true, } satisfies Options) @@ -176,6 +181,78 @@ describe('partner add-on scaffolds', () => { }, ) + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + 'approves Netlify build dependencies for pnpm in %s projects', + async (_name, createFrameworkDefinition) => { + const output = await generateApp(createFrameworkDefinition(), ['netlify']) + const packageJSON = JSON.parse(output.files['package.json']) + const pnpmWorkspace = output.files['pnpm-workspace.yaml'] + + expect(packageJSON.pnpm.onlyBuiltDependencies).toContain('sharp') + expect(pnpmWorkspace).toContain('sharp: true') + expect(pnpmWorkspace).not.toContain('workerd: true') + }, + ) + + it.each(['postgres', 'mysql', 'sqlite'])( + 'approves Prisma build dependencies for pnpm with %s', + async (database) => { + const output = await generateApp( + createReactFrameworkDefinition(), + ['prisma'], + { prisma: { database } }, + ) + const packageJSON = JSON.parse(output.files['package.json']) + const pnpmWorkspace = output.files['pnpm-workspace.yaml'] + + expect(packageJSON.pnpm.onlyBuiltDependencies).toEqual( + expect.arrayContaining(['@prisma/engines', 'prisma']), + ) + expect(pnpmWorkspace).toContain("'@prisma/engines': true") + expect(pnpmWorkspace).toContain('prisma: true') + + if (database === 'sqlite') { + expect(packageJSON.pnpm.onlyBuiltDependencies).toContain( + 'better-sqlite3', + ) + expect(pnpmWorkspace).toContain('better-sqlite3: true') + } else { + expect(packageJSON.pnpm.onlyBuiltDependencies).not.toContain( + 'better-sqlite3', + ) + expect(pnpmWorkspace).not.toContain('better-sqlite3: true') + } + }, + ) + + it('generates a URL-driven, env-aware Prisma MySQL setup', async () => { + const output = await generateApp( + createReactFrameworkDefinition(), + ['prisma'], + { prisma: { database: 'mysql' } }, + ) + const databaseUrl = output.files['src/database-url.ts'] + const db = output.files['src/db.ts'] + const seed = output.files['prisma/seed.ts'] + const demo = output.files['src/routes/demo/prisma.tsx'] + + expect(databaseUrl).toContain('process.env.DATABASE_URL') + expect(databaseUrl).toContain("throw new Error('DATABASE_URL is required')") + expect(db).toContain('new PrismaMariaDb(getDatabaseUrl())') + expect(seed).toContain('new PrismaMariaDb(getDatabaseUrl())') + expect(db).not.toContain('host: "localhost"') + expect(seed).not.toContain('host: "localhost"') + expect(demo).toContain('.validator(') + expect(demo).not.toContain('.inputValidator(') + expect(demo).toContain('pnpm db:generate') + expect(demo).toContain('pnpm db:push') + expect(demo).toContain('pnpm db:studio') + expect(demo).not.toContain('pnpm dlx prisma') + }) + it.each([ ['React', createReactFrameworkDefinition], ['Solid', createSolidFrameworkDefinition],