diff --git a/packages/core/installMachine/index.ts b/packages/core/installMachine/index.ts index 06eb048..f111b01 100644 --- a/packages/core/installMachine/index.ts +++ b/packages/core/installMachine/index.ts @@ -398,7 +398,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { deployVercelProjectActor: createStepMachine( fromPromise(async ({ input }) => { try { - await deployVercelProject(); + await deployVercelProject(input.stateData.options.usePayload); input.stateData.stepsCompleted.deployVercelProject = true; saveStateToRcFile(input.stateData, input.projectDir); } catch (error) { diff --git a/packages/core/installMachine/installSteps/payload/createMigration.ts b/packages/core/installMachine/installSteps/payload/createMigration.ts new file mode 100644 index 0000000..fcef87c --- /dev/null +++ b/packages/core/installMachine/installSteps/payload/createMigration.ts @@ -0,0 +1,16 @@ +import { execAsync } from '../../../utils/execAsync'; +import { logger } from '../../../utils/logger'; + +export const createMigration = async () => { + await logger.withSpinner('payload', 'Creating migration...', async (spinner) => { + try { + await execAsync('mkdir migrations'); + await execAsync('npx payload migrate:create'); + spinner.succeed('Migration created.'); + } catch (error) { + spinner.fail('Failed to create migration.'); + console.error(error); + process.exit(1); + } + }); +}; diff --git a/packages/core/installMachine/installSteps/payload/install.ts b/packages/core/installMachine/installSteps/payload/install.ts index 9557779..ba7ac97 100644 --- a/packages/core/installMachine/installSteps/payload/install.ts +++ b/packages/core/installMachine/installSteps/payload/install.ts @@ -1,9 +1,11 @@ -import { preparePayloadConfig } from './preparePayloadConfig'; import { prepareTsConfig } from './prepareTsConfig'; -import { removeTurboFlag } from './removeTurboFlag'; import { updatePackages } from './updatePackages'; import { moveFilesToAppDir } from './moveFilesToAppDir'; import { runInstallCommand } from './runInstallCommand'; +import { updatePackageJson } from './updatePackageJson'; +import { preparePayloadConfig } from './preparePayloadConfig'; +import { createMigration } from './createMigration'; +import { updateTurboJson } from './updateTurboJson'; export const preparePayload = async () => { process.chdir('./apps/web/'); @@ -12,8 +14,11 @@ export const preparePayload = async () => { await updatePackages(); await moveFilesToAppDir(); await runInstallCommand(); - await removeTurboFlag(); + await updatePackageJson(); await preparePayloadConfig(); + await createMigration(); process.chdir('../../'); + + await updateTurboJson(); }; diff --git a/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts b/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts index e21654a..7930d7b 100644 --- a/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts +++ b/packages/core/installMachine/installSteps/payload/preparePayloadConfig.ts @@ -1,4 +1,4 @@ -import { existsSync, type PathLike } from 'fs'; +import { existsSync } from 'fs'; import fs from 'fs/promises'; import { logger } from '../../../utils/logger'; import { join } from 'path'; @@ -14,19 +14,40 @@ export const preparePayloadConfig = async () => { await logger.withSpinner('payload', 'Preparing config...', async (spinner) => { try { // Read the payload.config.ts file - const data = await fs.readFile(payloadConfigPath, 'utf8'); + let data = await fs.readFile(payloadConfigPath, 'utf8'); - // Use regex to find the "pool" object and append "schemaName: 'payload'" to the pool configuration - const updatedConfig = data.replace(/pool:\s*{([^}]*)connectionString[^}]*}/, (match, group1) => { - if (match.includes('schemaName')) { - return match; // If "schemaName" already exists, return the match unchanged - } - // Append schemaName to the existing pool configuration (avoiding the extra comma) - return match.replace(group1.trimEnd(), `${group1.trimEnd()} schemaName: 'payload',\n`); - }); + const postgresAdapterImport = `import { postgresAdapter } from '@payloadcms/db-postgres'`; + const vercelPostgresAdapterImport = `import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'`; + + // Add the vercelPostgresAdapter import after postgresAdapter if it's not already present + if (!data.includes(vercelPostgresAdapterImport)) { + data = data.replace(postgresAdapterImport, `${postgresAdapterImport}\n${vercelPostgresAdapterImport}`); + } else { + console.log('vercelPostgresAdapter import is already present.'); + } + + // Step 2: Replace the db configuration with conditional configuration + const newDbConfig = `db: process.env.POSTGRES_URL + ? vercelPostgresAdapter({ + schemaName: "payload", + pool: { + connectionString: process.env.POSTGRES_URL || "", + }, + }) + : postgresAdapter({ + schemaName: "payload", + pool: { + connectionString: process.env.DATABASE_URI || "", + }, + })`; + + data = data.replace( + /db:\s*postgresAdapter\(\{[\s\S]*?pool:\s*\{[\s\S]*?connectionString:[\s\S]*?\}[\s\S]*?\}\)/m, + newDbConfig, + ); // Write the updated payload.config.ts back to the file - await fs.writeFile(payloadConfigPath, updatedConfig); + await fs.writeFile(payloadConfigPath, data); spinner.succeed('Config prepared.'); } catch (err) { diff --git a/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts b/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts deleted file mode 100644 index f8dde30..0000000 --- a/packages/core/installMachine/installSteps/payload/removeTurboFlag.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { logger } from '../../../utils/logger'; -import { promisify } from 'util'; - -const readFileAsync = promisify(fs.readFile); -const writeFileAsync = promisify(fs.writeFile); - -export const removeTurboFlag = async () => { - await logger.withSpinner('payload', 'Removing --turbo flag from dev script...', async (spinner) => { - const packageJsonPath = path.join(process.cwd(), 'package.json'); - - try { - // Read the package.json file - const data = await readFileAsync(packageJsonPath, 'utf8'); - - // Parse the JSON data - const packageJson = JSON.parse(data); - - // Remove '--turbo' flag from the "dev" script - if (packageJson.scripts && packageJson.scripts.dev) { - packageJson.scripts.dev = packageJson.scripts.dev.replace('--turbo', '').trim(); - } - - // Write the updated package.json back to the file - await writeFileAsync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - spinner.succeed('Removed --turbo flag from dev script.'); - } catch (err) { - spinner.fail('Failed to remove --turbo flag from dev script'); - console.error('Error:', err); - } - }); -}; diff --git a/packages/core/installMachine/installSteps/payload/runInstallCommand.ts b/packages/core/installMachine/installSteps/payload/runInstallCommand.ts index 300cab3..d12bad8 100644 --- a/packages/core/installMachine/installSteps/payload/runInstallCommand.ts +++ b/packages/core/installMachine/installSteps/payload/runInstallCommand.ts @@ -1,8 +1,10 @@ import { execAsync } from '../../../utils/execAsync'; import { logger } from '../../../utils/logger'; +import { loadEnvFile } from './utils/loadEnvFile'; export const runInstallCommand = async () => { await logger.withSpinner('payload', 'Installing to Next.js...', async (spinner) => { + loadEnvFile('../../supabase/.env'); try { await execAsync( `echo y | npx create-payload-app@beta --db postgres --db-connection-string ${process.env.DB_URL}`, diff --git a/packages/core/installMachine/installSteps/payload/updatePackageJson.ts b/packages/core/installMachine/installSteps/payload/updatePackageJson.ts new file mode 100644 index 0000000..0d8103a --- /dev/null +++ b/packages/core/installMachine/installSteps/payload/updatePackageJson.ts @@ -0,0 +1,32 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { logger } from '../../../utils/logger'; + +export const updatePackageJson = async () => { + const packageJsonPath = path.resolve('package.json'); + logger.withSpinner('payload', 'Updating package.json...', async (spinner) => { + try { + // Read and parse package.json + const packageData = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); + + // Add db script to package.json + packageData.scripts = { + ...packageData.scripts, + 'db:migrate': 'npx payload migrate', + }; + + // Payload doesn't work with Turbopack yet + // Remove '--turbo' flag from the "dev" script + if (packageData.scripts && packageData.scripts.dev) { + packageData.scripts.dev = packageData.scripts.dev.replace('--turbo', '').trim(); + } + + // Write the modified package.json + await fs.writeFile(packageJsonPath, JSON.stringify(packageData, null, 2)); + spinner.succeed('Updated package.json'); + } catch (error) { + spinner.fail('Failed to update package.json'); + console.error('Error updating files:', error); + } + }); +}; diff --git a/packages/core/installMachine/installSteps/payload/updatePackages.ts b/packages/core/installMachine/installSteps/payload/updatePackages.ts index cf7191d..6607c36 100644 --- a/packages/core/installMachine/installSteps/payload/updatePackages.ts +++ b/packages/core/installMachine/installSteps/payload/updatePackages.ts @@ -18,7 +18,7 @@ export const updatePackages = async () => { await logger.withSpinner('payload', 'Installing necessary packages...', async (spinner) => { try { - await execAsync(`pnpm i pg sharp --reporter silent`); + await execAsync(`pnpm i pg sharp @payloadcms/db-vercel-postgres --reporter silent`); spinner.succeed('Installed necessary packages!'); } catch (error) { spinner.fail('Failed to install necessary packages!'); diff --git a/packages/core/installMachine/installSteps/payload/updateTurboJson.ts b/packages/core/installMachine/installSteps/payload/updateTurboJson.ts new file mode 100644 index 0000000..6d4d719 --- /dev/null +++ b/packages/core/installMachine/installSteps/payload/updateTurboJson.ts @@ -0,0 +1,44 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { logger } from '../../../utils/logger'; + +export const updateTurboJson = async () => { + const turboJsonPath = path.resolve('turbo.json'); + await logger.withSpinner('payload', 'Updating turbo.json...', async (spinner) => { + try { + // Read and parse turbo.json + const turboData = JSON.parse(await fs.readFile(turboJsonPath, 'utf8')); + + // Update turbo.json with the new structure + turboData.tasks = { + ...turboData.tasks, + 'web#db:migrate': { + cache: false, + }, + build: { + dependsOn: ['^web#db:migrate', '^build'], + outputs: ['dist/**'], + }, + 'build:core': { + outputs: ['dist/**'], + }, + }; + + turboData.globalEnv = [ + 'SUPABASE_JWT_SECRET', + 'POSTGRES_URL', + 'PAYLOAD_SECRET', + 'SUPABASE_SERVICE_ROLE_KEY', + 'NEXT_PUBLIC_*', + 'PORT', + ]; + + // Write the modified turbo.json + await fs.writeFile(turboJsonPath, JSON.stringify(turboData, null, 2)); + spinner.succeed('turbo.json updated.'); + } catch (error) { + spinner.fail('Failed to update turbo.json.'); + console.error('Error updating files:', error); + } + }); +}; diff --git a/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts b/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts new file mode 100644 index 0000000..cfe5af6 --- /dev/null +++ b/packages/core/installMachine/installSteps/supabase/initializeSupabaseProject.ts @@ -0,0 +1,22 @@ +import { execAsync } from "../../../utils/execAsync"; +import { logger } from "../../../utils/logger"; + +export const initializeSupabaseProject = async () => { + await logger.withSpinner('supabase', 'Initializing project...', async (spinner) => { + try { + await execAsync(`npx supabase init`); + spinner.succeed('Project initialized.'); + } catch (error: any) { + const errorMessage = error.stderr; + if (errorMessage.includes('file exists')) { + spinner.succeed('Configuration file already exists.'); + } else { + spinner.fail('Failed to initialize project.'); + console.error( + 'Please review the error message below, follow the initialization instructions, and try running "create-stapler-app" again.', + ); + process.exit(1); + } + } + }); +}; diff --git a/packages/core/installMachine/installSteps/supabase/install.ts b/packages/core/installMachine/installSteps/supabase/install.ts index 4a20d12..d7e42d7 100644 --- a/packages/core/installMachine/installSteps/supabase/install.ts +++ b/packages/core/installMachine/installSteps/supabase/install.ts @@ -6,44 +6,9 @@ import { templateGenerator } from '../../../utils/generator/generator'; import { getTemplateDirectory } from '../../../utils/getTemplateDirectory'; import { logger } from '../../../utils/logger'; import { execAsync } from '../../../utils/execAsync'; - -const supabaseLogin = async () => { - await logger.withSpinner('supabase', 'Logging in...', async (spinner) => { - try { - await execAsync('npx supabase projects list'); - spinner.succeed('Already logged in.'); - } catch (error) { - try { - await execAsync('npx supabase login'); - spinner.succeed('Logged in successfully.'); - } catch { - spinner.fail('Failed to log in to Supabase.'); - console.error('Please log in manually with "supabase login" and re-run "create-stapler-app".'); - process.exit(1); - } - } - }); -}; - -const initializeSupabaseProject = async () => { - await logger.withSpinner('supabase', 'Initializing project...', async (spinner) => { - try { - await execAsync(`npx supabase init`); - spinner.succeed('Project initialized.'); - } catch (error: any) { - const errorMessage = error.stderr; - if (errorMessage.includes('file exists')) { - spinner.succeed('Configuration file already exists.'); - } else { - spinner.fail('Failed to initialize project.'); - console.error( - 'Please review the error message below, follow the initialization instructions, and try running "create-stapler-app" again.', - ); - process.exit(1); - } - } - }); -}; +import { initializeSupabaseProject } from './initializeSupabaseProject'; +import { modifySupabaseConfig } from './modifySupabaseConfig'; +import { supabaseLogin } from './supabaseLogin'; export const installSupabase = async (destinationDirectory: string) => { try { @@ -69,6 +34,9 @@ export const installSupabase = async (destinationDirectory: string) => { spinner.succeed('Files added.'); }); + // Modify supabase/config.toml to enable db.pooler + await modifySupabaseConfig(destinationDirectory); + process.chdir('supabase'); await logger.withSpinner('supabase', 'Installing dependencies...', async (spinner) => { diff --git a/packages/core/installMachine/installSteps/supabase/modifySupabaseConfig.ts b/packages/core/installMachine/installSteps/supabase/modifySupabaseConfig.ts new file mode 100644 index 0000000..f104cca --- /dev/null +++ b/packages/core/installMachine/installSteps/supabase/modifySupabaseConfig.ts @@ -0,0 +1,33 @@ +import path from 'path'; +import fs from 'fs'; +import { logger } from '../../../utils/logger'; + +export const modifySupabaseConfig = async (destinationDirectory: string) => { + const configPath = path.join(destinationDirectory, 'supabase', 'config.toml'); + await logger.withSpinner('supabase', 'Modifying config.toml...', async (spinner) => { + if (!fs.existsSync(configPath)) { + console.error(`config.toml file not found at ${configPath}`); + process.exit(1); + } + + try { + const configContent = fs.readFileSync(configPath, 'utf-8'); + + // Modify [db.pooler] enabled = false to enabled = true + let modifiedContent; + if (configContent.includes('[db.pooler]')) { + modifiedContent = configContent.replace(/\[db\.pooler\]\s+enabled\s*=\s*false/, '[db.pooler]\nenabled = true'); + } else { + // Append the [db.pooler] section at the end if it doesn't exist + modifiedContent = `${configContent}\n[db.pooler]\nenabled = true\n`; + } + + fs.writeFileSync(configPath, modifiedContent, 'utf-8'); + spinner.succeed('config.toml modified.'); + } catch (error) { + spinner.fail('Failed to modify config.toml.'); + console.error(error); + process.exit(1); + } + }); +}; diff --git a/packages/core/installMachine/installSteps/supabase/supabaseLogin.ts b/packages/core/installMachine/installSteps/supabase/supabaseLogin.ts new file mode 100644 index 0000000..d6c7136 --- /dev/null +++ b/packages/core/installMachine/installSteps/supabase/supabaseLogin.ts @@ -0,0 +1,20 @@ +import { execAsync } from "../../../utils/execAsync"; +import { logger } from "../../../utils/logger"; + +export const supabaseLogin = async () => { + await logger.withSpinner('supabase', 'Logging in...', async (spinner) => { + try { + await execAsync('npx supabase projects list'); + spinner.succeed('Already logged in.'); + } catch (error) { + try { + await execAsync('npx supabase login'); + spinner.succeed('Logged in successfully.'); + } catch { + spinner.fail('Failed to log in to Supabase.'); + console.error('Please log in manually with "supabase login" and re-run "create-stapler-app".'); + process.exit(1); + } + } + }); +}; diff --git a/packages/core/installMachine/installSteps/vercel/deploy.ts b/packages/core/installMachine/installSteps/vercel/deploy.ts index 6bead4d..54824b5 100644 --- a/packages/core/installMachine/installSteps/vercel/deploy.ts +++ b/packages/core/installMachine/installSteps/vercel/deploy.ts @@ -1,8 +1,9 @@ import { execSync } from 'child_process'; import { execAsync } from '../../../utils/execAsync'; import { logger } from '../../../utils/logger'; +import crypto from 'crypto'; -export const deployVercelProject = async () => { +export const deployVercelProject = async (usePayload: boolean) => { await logger.withSpinner('vercel', 'Connecting Vercel to Git...', async (spinner) => { try { // Execute 'vercel git connect' and capture the output @@ -22,6 +23,20 @@ export const deployVercelProject = async () => { encoding: 'utf8', }); + if (usePayload) { + await logger.withSpinner('vercel', 'Setting up environment variables...', async (spinner) => { + try { + // Generate payload secret + const payloadSecret = crypto.randomBytes(256).toString('hex'); + await execAsync(`echo '${payloadSecret}' | vercel env add PAYLOAD_SECRET production --sensitive`); + spinner.succeed('Environment variables set up successfully.'); + } catch (error) { + spinner.fail('Failed to set up environment variables.'); + console.error(error); + } + }); + } + if (productionUrl) { logger.log('vercel', `You can access your production deployment at: \x1b[36m${productionUrl}\x1b[0m`); } else {