From 7ea806a21c61f3218a7470b497b340dc762f5b9c Mon Sep 17 00:00:00 2001 From: Connor Etherington Date: Wed, 4 Mar 2026 08:37:41 +0200 Subject: [PATCH] fix: Template referral system Implemented external-template referral support in echo-start by reading optional echo.config.json referralCode, sanitizing it, writing/updating framework-aware ECHO_REFERRAL_CODE env vars, and removin --- packages/sdk/echo-start/src/index.ts | 161 ++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index c249d50d1..55a472de0 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -14,7 +14,13 @@ import chalk from 'chalk'; import { spawn } from 'child_process'; import { Command } from 'commander'; import degit from 'degit'; -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'; +import { + existsSync, + readdirSync, + readFileSync, + unlinkSync, + writeFileSync, +} from 'fs'; import path from 'path'; const program = new Command(); @@ -202,6 +208,117 @@ function resolveTemplateRepo(template: string): string { return repo; } +interface EchoTemplateConfig { + referralCode?: string; +} + +function readTemplateConfig(projectPath: string): EchoTemplateConfig | null { + const configPath = path.join(projectPath, 'echo.config.json'); + + if (!existsSync(configPath)) { + return null; + } + + try { + const configContent = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(configContent) as EchoTemplateConfig; + return parsed; + } catch { + return null; + } +} + +function sanitizeReferralCode(code: unknown): string | null { + if (typeof code !== 'string') { + return null; + } + + const trimmedCode = code.trim(); + if (!trimmedCode) { + return null; + } + + const safePattern = /^[a-zA-Z0-9_.-]+$/; + if (!safePattern.test(trimmedCode)) { + return null; + } + + if (trimmedCode.length > 128) { + return null; + } + + return trimmedCode; +} + +function detectReferralEnvVarName(projectPath: string): string | null { + const envFiles = ['.env.local', '.env.example', '.env']; + + for (const fileName of envFiles) { + const filePath = path.join(projectPath, fileName); + if (existsSync(filePath)) { + const content = readFileSync(filePath, 'utf-8'); + const match = content.match( + /(NEXT_PUBLIC_|VITE_|REACT_APP_|NUXT_PUBLIC_)?ECHO_REFERRAL_CODE/ + ); + if (match) { + return match[0]; + } + } + } + + return null; +} + +function detectFrameworkReferralEnvVarName(projectPath: string): string { + const packageJsonPath = path.join(projectPath, 'package.json'); + + if (existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const deps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }; + + if (deps['next']) { + return 'NEXT_PUBLIC_ECHO_REFERRAL_CODE'; + } else if (deps['vite']) { + return 'VITE_ECHO_REFERRAL_CODE'; + } else if (deps['react-scripts']) { + return 'REACT_APP_ECHO_REFERRAL_CODE'; + } else if (deps['nuxt']) { + return 'NUXT_PUBLIC_ECHO_REFERRAL_CODE'; + } + } catch (e) { + // Fall through to default + console.error(e); + } + } + + return 'NEXT_PUBLIC_ECHO_REFERRAL_CODE'; +} + +function upsertEnvVar( + envContent: string, + envVarName: string, + envVarValue: string +): string { + const escapedEnvVarName = envVarName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const envVarRegex = new RegExp(`^(${escapedEnvVarName}\\s*=\\s*).*$`, 'm'); + + if (envVarRegex.test(envContent)) { + const replaceRegex = new RegExp( + `^(${escapedEnvVarName}\\s*=\\s*).*$`, + 'gm' + ); + return envContent.replace(replaceRegex, `$1${envVarValue}`); + } + + return envContent.trimEnd() + ? `${envContent.trimEnd()}\n${envVarName}=${envVarValue}\n` + : `${envVarName}=${envVarValue}\n`; +} + function detectEnvVarName(projectPath: string): string | null { const envFiles = ['.env.local', '.env.example', '.env']; @@ -414,6 +531,48 @@ async function createApp(projectDir: string, options: CreateAppOptions) { log.message(`Created .env.local with ${envVarName}`); } + if (isExternal) { + const templateConfig = readTemplateConfig(absoluteProjectPath); + const referralCode = sanitizeReferralCode(templateConfig?.referralCode); + + if (templateConfig?.referralCode && !referralCode) { + log.warning( + 'Template referral code in echo.config.json was ignored due to invalid format' + ); + } + + if (referralCode) { + const detectedReferralVarName = + detectReferralEnvVarName(absoluteProjectPath); + const referralEnvVarName = + detectedReferralVarName || + detectFrameworkReferralEnvVarName(absoluteProjectPath); + + const currentEnvContent = existsSync(envPath) + ? readFileSync(envPath, 'utf-8') + : ''; + const updatedEnvContent = upsertEnvVar( + currentEnvContent, + referralEnvVarName, + referralCode + ); + + writeFileSync(envPath, updatedEnvContent); + log.message( + `Configured ${referralEnvVarName} from external template metadata` + ); + } + + const templateConfigPath = path.join( + absoluteProjectPath, + 'echo.config.json' + ); + if (existsSync(templateConfigPath)) { + unlinkSync(templateConfigPath); + log.message('Removed template metadata file: echo.config.json'); + } + } + log.step('Project setup completed successfully'); // Auto-install dependencies unless skipped