diff --git a/packages/core/installMachine/index.ts b/packages/core/installMachine/index.ts index 414341d..f8db855 100644 --- a/packages/core/installMachine/index.ts +++ b/packages/core/installMachine/index.ts @@ -8,7 +8,8 @@ import { createSupabaseProject } from './installSteps/supabase/createProject'; import { installSupabase } from './installSteps/supabase/install'; import { createTurboRepo } from './installSteps/turbo/create'; import { deployVercelProject } from './installSteps/vercel/deploy'; -import { setupAndCreateVercelProject } from './installSteps/vercel/setupAndCreate'; +import { linkVercelProject } from './installSteps/vercel/link'; +import { updateVercelProjectSettings } from './installSteps/vercel/updateProjectSettings'; import { prepareDrink } from './installSteps/bar/prepareDrink'; import { createDocFiles } from './installSteps/docs/create'; import { pushToGitHub } from './installSteps/github/repositoryManager'; @@ -159,26 +160,40 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { always: [ { guard: isStepCompleted('createSupabaseProject'), - target: 'setupAndCreateVercelProject', + target: 'linkVercelProject', }, ], invoke: { input: ({ context }) => context, src: 'createSupabaseProjectActor', - onDone: 'setupAndCreateVercelProject', + onDone: 'linkVercelProject', onError: 'failed', }, }, - setupAndCreateVercelProject: { + linkVercelProject: { always: [ { - guard: isStepCompleted('setupAndCreateVercelProject'), + guard: isStepCompleted('linkVercelProject'), + target: 'updateVercelProjectSettings', + }, + ], + invoke: { + input: ({ context }) => context, + src: 'linkVercelProjectActor', + onDone: 'updateVercelProjectSettings', + onError: 'failed', + }, + }, + updateVercelProjectSettings: { + always: [ + { + guard: isStepCompleted('updateVercelProjectSettings'), target: 'connectSupabaseProject', }, ], invoke: { input: ({ context }) => context, - src: 'setupAndCreateVercelProjectActor', + src: 'updateVercelProjectSettingsActor', onDone: 'connectSupabaseProject', onError: 'failed', }, @@ -343,14 +358,26 @@ const createInstallMachine = (initialContext: InstallMachineContext) => { } }), ), - setupAndCreateVercelProjectActor: createStepMachine( + linkVercelProjectActor: createStepMachine( + fromPromise(async ({ input }) => { + try { + await linkVercelProject(); + input.stateData.stepsCompleted.linkVercelProject = true; + saveStateToRcFile(input.stateData, input.projectDir); + } catch (error) { + console.error('Error in linkVercelProjectActor:', error); + throw error; + } + }), + ), + updateVercelProjectSettingsActor: createStepMachine( fromPromise(async ({ input }) => { try { - await setupAndCreateVercelProject(); - input.stateData.stepsCompleted.setupAndCreateVercelProject = true; + await updateVercelProjectSettings(); + input.stateData.stepsCompleted.updateVercelProjectSettings = true; saveStateToRcFile(input.stateData, input.projectDir); } catch (error) { - console.error('Error in setupAndCreateVercelProjectActor:', error); + console.error('Error in updateVercelProjectSettingsActor:', error); throw error; } }), diff --git a/packages/core/installMachine/installSteps/vercel/link.ts b/packages/core/installMachine/installSteps/vercel/link.ts new file mode 100644 index 0000000..9fe669a --- /dev/null +++ b/packages/core/installMachine/installSteps/vercel/link.ts @@ -0,0 +1,48 @@ +import { execSync } from 'child_process'; +import chalk from 'chalk'; +import { logWithColoredPrefix } from '../../../utils/logWithColoredPrefix'; + +const getUsername = (): string | null => { + try { + const user = execSync('npx vercel whoami', { stdio: 'pipe', encoding: 'utf-8' }).trim(); + return user || null; + } catch { + return null; + } +}; + +const loginIfNeeded = () => { + logWithColoredPrefix('vercel', 'Logging in...'); + try { + execSync('npx vercel login', { stdio: 'inherit' }); + } catch (error) { + logWithColoredPrefix('vercel', [ + 'Oops! Something went wrong while logging in...', + '\nYou might already be logged in with this email in another project.', + '\nIn this case, select "Continue with Email" and enter the email you\'re already logged in with.\n', + ]); + try { + execSync('npx vercel login', { stdio: 'inherit' }); + } catch { + logWithColoredPrefix('vercel', [ + 'Please check the error above and try again.', + '\nAfter successfully logging in with "vercel login", please run create-stapler-app again.\n', + ]); + process.exit(1); + } + } +}; + +export const linkVercelProject = async () => { + let vercelUserName = getUsername(); + + if (!vercelUserName) { + loginIfNeeded(); + vercelUserName = getUsername(); // Retry getting username after login + } + + logWithColoredPrefix('vercel', `You are logged in as ${chalk.cyan(vercelUserName)}`); + + logWithColoredPrefix('vercel', 'Linking project...'); + execSync('npx vercel link --yes', { stdio: 'ignore' }); +}; diff --git a/packages/core/installMachine/installSteps/vercel/setupAndCreate.ts b/packages/core/installMachine/installSteps/vercel/setupAndCreate.ts deleted file mode 100644 index 0bca8f9..0000000 --- a/packages/core/installMachine/installSteps/vercel/setupAndCreate.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { execSync } from 'child_process'; -import { logWithColoredPrefix } from '../../../utils/logWithColoredPrefix'; -import chalk from 'chalk'; - -const getUserName = (): string | null => { - try { - const user = execSync('npx vercel whoami', { stdio: 'pipe', encoding: 'utf-8' }); - return user; - } catch { - return null; - } -}; - -export const setupAndCreateVercelProject = async () => { - const vercelUserName = getUserName(); - - if (!vercelUserName) { - logWithColoredPrefix('vercel', 'Logging in...'); - try { - execSync('npx vercel login', { stdio: 'inherit' }); - } catch (error) { - logWithColoredPrefix('vercel', [ - 'Oops! Something went wrong while logging in...', - '\nYou might already be logged in with this email in another project.', - '\nIn this case, select "Continue with Email" and enter the email you\'re already logged in with.\n', - ]); - try { - execSync('npx vercel login', { stdio: 'inherit' }); - } catch { - logWithColoredPrefix('vercel', [ - 'Please check the error above and try again.', - '\nAfter successfully logging in with "vercel login", please run create-stapler-app again.\n', - ]), - process.exit(1); - } - } - } else { - logWithColoredPrefix('vercel', `You are logged in as \x1b[36m${vercelUserName.toString().trim()}\x1b[0m`); - } - - logWithColoredPrefix('vercel', 'Linking project...'); - logWithColoredPrefix( - 'vercel', - `NOTE: You need to specify manually ${chalk.cyan('in which directory is your code located')}, should be: ${chalk.greenBright('./apps/web')}`, - ); - execSync('npx vercel link', { stdio: 'inherit' }); -}; diff --git a/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts b/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts new file mode 100644 index 0000000..2502afb --- /dev/null +++ b/packages/core/installMachine/installSteps/vercel/updateProjectSettings.ts @@ -0,0 +1,59 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { getGlobalPathConfig } from './utils/getGlobalPathConfig'; +import { logWithColoredPrefix } from '../../../utils/logWithColoredPrefix'; + +const getTokenFromAuthFile = async (filePath: string): Promise => { + try { + const data = await fs.readFile(filePath, 'utf-8'); + const jsonData = JSON.parse(data); + return jsonData.token || null; + } catch (error) { + console.error('Failed to read or parse auth.json:', `\n${error}`); + process.exit(1); + } +}; + +const getProjectIdFromVercelConfig = async (): Promise => { + const data = await fs.readFile('.vercel/project.json', 'utf-8'); + try { + const jsonData = JSON.parse(data); + return jsonData.projectId; + } catch (error) { + console.error('Failed to read or parse vercel.json:', `\n${error}`); + process.exit(1); + } +}; + +export const updateVercelProjectSettings = async () => { + logWithColoredPrefix('vercel', 'Changing project settings...'); + const globalPath = await getGlobalPathConfig(); + if (!globalPath) { + console.error('Global path not found. Cannot update project properties.'); + process.exit(1); + } + const filePath = path.join(globalPath, 'auth.json'); + + const token = await getTokenFromAuthFile(filePath); + if (!token) { + console.error('Token not found. Cannot update project properties.'); + process.exit(1); + } + + const projectId = await getProjectIdFromVercelConfig(); + + const response = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + framework: 'nextjs', + rootDirectory: 'apps/web', + }), + method: 'PATCH', + }); + + if (!response.ok) { + throw new Error(`Failed to update project properties: ${response.statusText}`); + } +}; diff --git a/packages/core/installMachine/installSteps/vercel/utils/getGlobalPathConfig.ts b/packages/core/installMachine/installSteps/vercel/utils/getGlobalPathConfig.ts new file mode 100644 index 0000000..5fbcb1e --- /dev/null +++ b/packages/core/installMachine/installSteps/vercel/utils/getGlobalPathConfig.ts @@ -0,0 +1,53 @@ +import { homedir } from 'os'; +import fs from 'fs'; +import path from 'path'; + +const getXDGPaths = (appName: string) => { + const homeDir = homedir(); + + if (process.platform === 'win32') { + // Windows paths, typically within %AppData% + return { + dataDirs: [path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), appName)], + configDirs: [path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), appName)], + cacheDir: path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), appName, 'Cache'), + }; + } else if (process.platform === 'darwin') { + // macOS paths, typically in ~/Library/Application Support + return { + dataDirs: [path.join(homeDir, 'Library', 'Application Support', appName)], + configDirs: [path.join(homeDir, 'Library', 'Application Support', appName)], + cacheDir: path.join(homeDir, 'Library', 'Caches', appName), + }; + } else { + // Linux/Unix paths, following the XDG Base Directory Specification + return { + dataDirs: [process.env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share', appName)], + configDirs: [process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config', appName)], + cacheDir: process.env.XDG_CACHE_HOME || path.join(homeDir, '.cache', appName), + }; + } +}; + +// Returns whether a directory exists +const isDirectory = (path: string): boolean => { + try { + return fs.lstatSync(path).isDirectory(); + } catch (_) { + // We don't care which kind of error occured, it isn't a directory anyway. + return false; + } +}; + +// Returns in which directory the config should be present +export const getGlobalPathConfig = async (): Promise => { + const vercelDirectories = getXDGPaths('com.vercel.cli').dataDirs; + + const possibleConfigPaths = [ + ...vercelDirectories, // latest vercel directory + path.join(homedir(), '.now'), // legacy config in user's home directory + ...getXDGPaths('now').dataDirs, // legacy XDG directory + ]; + + return possibleConfigPaths.find((configPath) => isDirectory(configPath)) || vercelDirectories[0]; +}; diff --git a/packages/core/types.ts b/packages/core/types.ts index b7cbd73..48d5380 100644 --- a/packages/core/types.ts +++ b/packages/core/types.ts @@ -13,7 +13,8 @@ export interface StepsCompleted { initializeRepository: boolean; pushToGitHub: boolean; createSupabaseProject: boolean; - setupAndCreateVercelProject: boolean; + linkVercelProject: boolean; + updateVercelProjectSettings: boolean; connectSupabaseProject: boolean; deployVercelProject: boolean; } diff --git a/packages/core/utils/rcFileManager/index.ts b/packages/core/utils/rcFileManager/index.ts index 3779acd..8319023 100644 --- a/packages/core/utils/rcFileManager/index.ts +++ b/packages/core/utils/rcFileManager/index.ts @@ -21,7 +21,8 @@ export const initializeRcFile = (projectDir: string, name: string, usePayload: b prettifyCode: false, createDocFiles: false, createSupabaseProject: false, - setupAndCreateVercelProject: false, + linkVercelProject: false, + updateVercelProjectSettings: false, connectSupabaseProject: false, deployVercelProject: false, prepareDrink: false,