import path from 'path'; import fse from 'fs-extra'; import _ from 'lodash'; import { omitByNil } from '@stackbit/utils'; import { Import } from '@stackbit/types'; import { ConfigLoadError, StackbitConfigNotFoundError, REFER_TO_STACKBIT_CONFIG_DOCS } from './config-errors'; import { findStackbitConfigFile, loadConfigFromStackbitYaml } from './config-loader-utils'; import { Config } from './config-types'; const ROOT_PROPERTIES = [ 'nodeVersion', 'ssgName', 'ssgVersion', 'cmsName', 'postGitCloneCommand', 'preInstallCommand', 'postInstallCommand', 'installCommand', 'buildCommand', 'publishDir' ] as const; const IMPORT_PROPERTIES = [ 'uploadAssets', 'assetsDirectory', 'spaceIdEnvVar', 'accessTokenEnvVar', 'deliveryTokenEnvVar', 'previewTokenEnvVar', 'sanityStudioPath', 'deployStudio', 'deployGraphql', 'projectIdEnvVar', 'datasetEnvVar', 'tokenEnvVar' ] as const; const BOOLEAN_PROPERTIES = ['uploadAssets', 'deployStudio', 'deployGraphql', 'useESM']; export type LoadStaticStackbitConfigOptions = { dirPath: string; secondaryDirPath?: string; logger?: any; }; export type StaticConfig = Pick< Config, | 'stackbitVersion' | 'nodeVersion' | 'ssgName' | 'ssgVersion' | 'cmsName' | 'postGitCloneCommand' | 'preInstallCommand' | 'postInstallCommand' | 'installCommand' | 'import' | 'buildCommand' | 'publishDir' | 'dirPath' | 'filePath' > & { hasContentSources?: boolean; }; export type LoadStaticConfigResult = { config: StaticConfig | null; errors: (ConfigLoadError | StackbitConfigNotFoundError)[]; }; export async function loadStaticConfig({ dirPath, secondaryDirPath, logger }: LoadStaticStackbitConfigOptions): Promise { try { const lookupDirs = [dirPath, secondaryDirPath].filter((dir): dir is string => typeof dir === 'string'); const configFilePath = await findStackbitConfigFile(lookupDirs); if (!configFilePath) { return { config: null, errors: [new StackbitConfigNotFoundError()] }; } logger?.debug(`[config-loader-static] found stackbit config at ${configFilePath}`); const configExtension = path.extname(configFilePath).substring(1); if (['yaml', 'yml'].includes(configExtension)) { return loadStaticConfigFromStackbitYaml(configFilePath); } return loadStaticConfigFromStackbitJs(configFilePath); } catch (error: any) { return { config: null, errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })] }; } } async function loadStaticConfigFromStackbitYaml(configFilePath: string): Promise { const result = await loadConfigFromStackbitYaml(configFilePath); if (result.error) { return { config: null, errors: [result.error] }; } const stackbitVersion = result.config.stackbitVersion; if (!stackbitVersion) { const fileName = path.basename(configFilePath); return { config: null, errors: [new ConfigLoadError(`stackbitVersion not found in ${fileName}, ${REFER_TO_STACKBIT_CONFIG_DOCS}`)] }; } const config = { stackbitVersion: stackbitVersion, ...omitByNil( ROOT_PROPERTIES.reduce( (accum: Record, propertyName) => { accum[propertyName] = toStringOrNull(result.config[propertyName]); return accum; }, { import: result.config.import } ) ), dirPath: path.dirname(configFilePath), filePath: configFilePath }; return { config: config, errors: [] }; } function toStringOrNull(value: unknown): string | null { if (value) { return _.toString(value); } return null; } async function loadStaticConfigFromStackbitJs(configFilePath: string): Promise { const jsConfigString = await fse.readFile(configFilePath, 'utf8'); const stackbitVersion = parseInlineProperty(jsConfigString, 'stackbitVersion') as string; if (!stackbitVersion) { const fileName = path.basename(configFilePath); return { config: null, errors: [new ConfigLoadError(`stackbitVersion not found in ${fileName}, ${REFER_TO_STACKBIT_CONFIG_DOCS}`)] }; } const config = { stackbitVersion: stackbitVersion, ...omitByNil( ROOT_PROPERTIES.reduce( (accum: Record, propertyName) => { accum[propertyName] = parseInlineProperty(jsConfigString, propertyName); return accum; }, { import: parseImport(jsConfigString), hasContentSources: hasProperty(jsConfigString, 'contentSources') || hasProperty(jsConfigString, 'connectors') || null } ) ), dirPath: path.dirname(configFilePath), filePath: configFilePath }; return { config: config, errors: [] }; } function parseImport(jsConfigString: string): Import | null { const importObjectRegExp = /(["']?)import\1\s*:\s*{([^}]+)}/; const match = jsConfigString.match(importObjectRegExp); if (match) { const importObjectStr = match[2]!; const type = parseInlineProperty(importObjectStr, 'type'); const contentFile = parseInlineProperty(importObjectStr, 'contentFile'); if (!type || !contentFile) { return null; } return { type, contentFile, ...omitByNil( IMPORT_PROPERTIES.reduce((result: Record, propertyName) => { result[propertyName] = parseInlineProperty(importObjectStr, propertyName); return result; }, {}) ) } as Import; } return null; } export function parseInlineProperty(jsConfigString: string, propertyName: string): string | boolean | number | null { const propRegExp = `(["']?)${propertyName}\\1`; const singleQuotedValue = "'(.+?)(?