@stackbit/sdk
Version:
235 lines (219 loc) • 7.81 kB
text/typescript
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);
}