import detectPort from 'detect-port'; import chalk from 'chalk'; import open from 'open'; import { start as startServer } from './server'; import { getUserId } from './services/user-config'; import logger from './services/logger'; import apiService, { LocalDevConfig } from './services/api'; import { loadEnv } from './services/env'; import { checkAndLogNewerVersions } from '@stackbit/dev-common'; import config from './config'; interface DevOptions { ssgPort: number; ssgHost: string; rootDir: string; runnableDir?: string; noProxy?: boolean; publicPort?: number; serverPort?: number; logLevel?: string; autoOpen?: boolean; cmsType?: string; csiEnabled?: boolean; csiWebhookUrl?: string; contentfulAccessToken?: string; contentfulSpaceIds?: [string]; contentfulPreviewTokens?: [string]; sanityToken?: string; sanityStudioPath?: string; sanityDataset?: string; sanityProjectId?: string; sanityStudioUrl?: string; cloudinaryCloudName?: string; cloudinaryApiKey?: string; aprimoTenant?: string; telemetryTrack?: (event: string, data?: any) => void; cliVersion?: string; teamId?: string; siteId?: string; netlifyAccessToken?: string; apiSecret?: string; remoteContentSourceBaseURL?: string; projectId?: string; repoUrl?: string; repoBranch?: string; repoPublishBranch?: string; deployKey?: string; } export async function start(options: DevOptions): Promise<{ serverPort: number }> { if (options.logLevel) { logger.level = options.logLevel; } if (options.cliVersion) { logger.info(`Running Stackbit Dev (CLI v${options.cliVersion})`); await checkAndLogNewerVersions({ dir: options.rootDir, knownVersions: { '@stackbit/cli': options.cliVersion }, logger }); } logger.info('Site directory: ' + options.rootDir); await loadEnv(options.rootDir, logger).catch((err) => { logger.error('Error loading env', { err }); }); const serverPort = await detectPort(options.serverPort ?? 8090); const publicPort = options.publicPort ?? serverPort; const apiSecret = process.env.STACKBIT_API_SECRET; const isDevServer = !!process.env.NETLIFY_DEV_SERVER; const projectId = process.env.STACKBIT_PROJECT_ID; let configEnv: Record = {}; if (isDevServer && apiSecret && projectId) { logger.info('Loading configuration from ' + config.apiUrl); try { const result = await apiService.getConfiguration(apiSecret, projectId); configEnv = result.data || {}; } catch (err: any) { logger.error('Failed to load configuration', { err: err?.message || err }); } } const userId = getUserId(); // Print the localId to allow developer set STACKBIT_API_KEY and use it to call /_stackbit/getCSIDocuments logger.info('localId: ' + userId); const localDetails: LocalDevConfig = { i: userId, p: publicPort, v: require('../package.json').version, cms: options.cmsType || 'git' }; options.telemetryTrack?.('Stackbit Dev Start', { ...getAnalyticsProperties(localDetails), inputDir: options.rootDir }); if (options.contentfulSpaceIds) { localDetails.spaceIds = options.contentfulSpaceIds; } if (options.sanityProjectId) { localDetails.projectId = options.sanityProjectId; } if (options.cloudinaryCloudName && options.cloudinaryApiKey) { localDetails.cloudinary = { cloudName: options.cloudinaryCloudName, apiKey: options.cloudinaryApiKey }; } if (options.aprimoTenant) { localDetails.aprimo = { tenant: options.aprimoTenant }; } logger.debug('CSI Enabled'); options.csiEnabled = true; options.csiWebhookUrl ||= process.env.CSI_WEBHOOK_URL || configEnv.CSI_WEBHOOK_URL; options.teamId ||= process.env.NETLIFY_TEAM_ID; options.siteId ||= process.env.NETLIFY_SITE_ID; options.netlifyAccessToken ||= process.env.NETLIFY_ACCESS_TOKEN; options.remoteContentSourceBaseURL ||= process.env.REMOTE_CONTENT_SOURCE_URL || config.apiUrl; options.projectId ||= projectId; options.repoUrl ||= process.env.REPO_URL || configEnv.REPO_URL; options.repoBranch ||= process.env.REPO_BRANCH || configEnv.REPO_BRANCH; options.repoPublishBranch ||= process.env.REPO_PUBLISH_BRANCH || configEnv.REPO_PUBLISH_BRANCH; options.deployKey ||= configEnv.REPO_PRIVATE_KEY; try { const runner = await startServer({ apiSecret: apiSecret ?? userId, localDetails, isDevServer, ...options, serverPort }); localDetails.csi = runner.isCsiEnabled(); } catch (err: any) { const errorMessage = 'Error starting Stackbit dev server'; if (err?.stack) { logger.error(`${errorMessage}:\n${err.stack}`); } else if (err?.message) { logger.error(`${errorMessage}: ${err.message}`); } else { logger.error(errorMessage, { err }); } options.telemetryTrack?.('Stackbit Dev Start Error', { ...getAnalyticsProperties(localDetails), err: err?.message, inputDir: options.rootDir }); // give telemetry a chance to flush, and then exit await new Promise((resolve) => setTimeout(resolve, 2000)); throw new Error(errorMessage); } logger.debug('Local details', { localDetails }); if (!isDevServer) { const studioUrl = `http://localhost:${publicPort}/_stackbit`; try { const result = await apiService.updateLocalDev(userId, localDetails); logger.debug('update local details result', { result: result?.status }); } catch (err: any) { // failure here is expected if user hasn't pressed the link yet. // in that case we defer the update to when the server starts getting accessed. logger.debug('updateLocalDev failed', { err: err.message || err }); } if (!options.noProxy) { logger.info(`Server started. Forwarding requests to: ${options.ssgHost}:${options.ssgPort}`); } logger.info(`⚡ Open${options.autoOpen ? 'ing' : ''} ${chalk.bold(studioUrl)} in your browser`); if (options.autoOpen) { open(studioUrl).catch((err) => { logger.debug('Auto open error', { err }); logger.error('Error auto opening'); }); } } return { serverPort }; } function getAnalyticsProperties(localDetails: LocalDevConfig) { return { user_id: localDetails.i, dev_version: localDetails.v, port: localDetails.p, cms: localDetails.cms }; }