UNPKG

@stackbit/sdk

Version:
235 lines (219 loc) 7.81 kB
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<LoadStaticConfigResult> { 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<LoadStaticConfigResult> { 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<string, any>, 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<LoadStaticConfigResult> { 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<string, any>, 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<string, any>, 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 = "'(.+?)(?<!\\\\)'"; const doubleQuotedValue = '"(.+?)(?<!\\\\)"'; const templateLiteralValue = '`([\\s\\S]+?)(?<!\\\\)`'; const nonStringValue = '([^\\s\'",}]+)'; const valueRegExp = `(?:${singleQuotedValue}|${doubleQuotedValue}|${templateLiteralValue}|${nonStringValue})`; const fullRegExp = new RegExp(`${propRegExp}\\s*:\\s*${valueRegExp}`); const match = jsConfigString.match(fullRegExp); if (match) { if (match[2]) { return match[2].replace(/\\'/g, "'"); } else if (match[3]) { return match[3].replace(/\\"/g, '"'); } else if (match[4]) { return match[4].replace(/\\`/g, '`'); } else if (typeof match[5] !== 'undefined') { if (BOOLEAN_PROPERTIES.includes(propertyName)) { return !['false', '0', 'null', 'undefined'].includes(match[5]); } else if (match[5] === 'null') { return null; } else if (match[5] === 'true') { return true; } else if (match[5] === 'false') { return false; } else { return _.toString(match[5]); } } } return null; } function hasProperty(jsConfigString: string, propertyName: string): boolean { return new RegExp(`\\b${propertyName}\\b`).test(jsConfigString); }