UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

291 lines (290 loc) 13.2 kB
export { serializeConfigValues }; export { getConfigValuesBase }; export { isJsonValue }; import { assertIsNotProductionRuntime } from '../../../utils/assertSetup.js'; import { assert, assertUsage } from '../../../utils/assert.js'; import { assertIsNotBrowser } from '../../../utils/assertIsNotBrowser.js'; import { assertPosixPath } from '../../../utils/path.js'; import { deepEqual } from '../../../utils/deepEqual.js'; import { getPropAccessNotation } from '../../../utils/getPropAccessNotation.js'; import { isImportPathRelative } from '../../../utils/isImportPath.js'; import { parsePointerImportData } from '../../../node/vite/shared/resolveVikeConfigInternal/pointerImports.js'; import { getConfigValueFilePathToShowToUser } from '../helpers.js'; import { stringify } from '@brillout/json-serializer/stringify'; import pc from '@brillout/picocolors'; import { getConfigValueSourcesRelevant, isRuntimeEnvMatch, } from '../../../node/vite/plugins/pluginVirtualFiles/getConfigValueSourcesRelevant.js'; const stringifyOptions = { forbidReactElements: true }; const REPLACE_ME_BEFORE = '__VIKE__REPLACE_ME_BEFORE__'; const REPLACE_ME_AFTER = '__VIKE__REPLACE_ME_AFTER__'; // This file is never loaded on the client-side but we save it under the vike/shared/ directory in order to collocate it with parsePageConfigsSerialized() // - vike/shared/page-configs/serialize/parsePageConfigsSerialized.ts // - parsePageConfigsSerialized() is loaded on both the client- and server-side. assertIsNotBrowser(); assertIsNotProductionRuntime(); function serializeConfigValues(pageConfig, importStatements, filesEnv, runtimeEnv, tabspace, isEager) { const lines = []; tabspace += ' '; getConfigValuesBase(pageConfig, runtimeEnv, isEager).forEach((entry) => { if (entry.configValueBase.type === 'computed') { assert('value' in entry); // Help TS const { configValueBase, value, configName, configEnv } = entry; const valueData = getValueSerializedWithJson(value, configName, configValueBase.definedAtData, importStatements, filesEnv, configEnv); serializeConfigValue(configValueBase, valueData, configName, lines, tabspace); } if (entry.configValueBase.type === 'standard') { assert('sourceRelevant' in entry); // Help TS const { configValueBase, sourceRelevant, configName } = entry; const valueData = getValueSerializedFromSource(sourceRelevant, configName, importStatements, filesEnv); serializeConfigValue(configValueBase, valueData, configName, lines, tabspace); } if (entry.configValueBase.type === 'cumulative') { assert('sourcesRelevant' in entry); // Help TS const { configValueBase, sourcesRelevant, configName } = entry; const valueDataList = []; sourcesRelevant.forEach((source) => { const valueData = getValueSerializedFromSource(source, configName, importStatements, filesEnv); valueDataList.push(valueData); }); serializeConfigValue(configValueBase, valueDataList, configName, lines, tabspace); } }); return lines; } function getValueSerializedFromSource(configValueSource, configName, importStatements, filesEnv) { let valueData; if (configValueSource.valueIsLoaded && !configValueSource.valueIsLoadedWithImport) { valueData = getValueSerializedWithJson(configValueSource.value, configName, configValueSource.definedAt, importStatements, filesEnv, configValueSource.configEnv); } else { valueData = getValueSerializedWithImport(configValueSource, importStatements, filesEnv, configName); } return valueData; } function serializeConfigValue(configValueBase, valueData, configName, lines, tabspace) { lineAdd(`[${JSON.stringify(configName)}]: {`); { tab(); lineAdd(`type: "${configValueBase.type}",`); lineAdd(`definedAtData: ${JSON.stringify(configValueBase.definedAtData)},`); lineAdd(`valueSerialized:`); if (!Array.isArray(valueData)) { serializeValueData(valueData); } else { lineAppend(' ['); valueData.forEach(serializeValueData); lineAppend(` ],`); } untab(); } lineAdd('},'); return; function serializeValueData(valueData) { lineAppend(` {`); tab(); lineAdd(`type: "${valueData.type}",`); const valueProp = valueData.type !== 'plus-file' ? 'value' : 'exportValues'; lineAdd(`${valueProp}: ${valueData.valueAsJsCode},`); untab(); lineAdd(`},`); } function lineAppend(str) { const i = lines.length - 1; lines[i] = lines[i] += str; } function lineAdd(str) { lines.push(`${tabspace}${str}`); } function tab() { tabspace += ' '; } function untab() { tabspace = tabspace.slice(2); } } function getValueSerializedWithImport(configValueSource, importStatements, filesEnv, configName) { assert(!configValueSource.valueIsFilePath); const { valueIsDefinedByPlusValueFile, definedAt, configEnv } = configValueSource; assert(!definedAt.definedBy); const { filePathAbsoluteVite, fileExportName } = definedAt; if (valueIsDefinedByPlusValueFile) assert(fileExportName === undefined); const { importName } = addImportStatement(importStatements, filePathAbsoluteVite, fileExportName || '*', filesEnv, configEnv, configName); return { type: valueIsDefinedByPlusValueFile ? 'plus-file' : 'pointer-import', valueAsJsCode: importName, }; } function getValueSerializedWithJson(value, configName, definedAtData, importStatements, filesEnv, configEnv) { const valueAsJsCode = valueToJson(value, configName, definedAtData, importStatements, filesEnv, configEnv); return { type: 'js-serialized', valueAsJsCode, }; } function valueToJson(value, configName, definedAtData, importStatements, filesEnv, configEnv) { const valueName = `config${getPropAccessNotation(configName)}`; let configValueSerialized; try { configValueSerialized = stringify(value, { valueName, ...stringifyOptions, // Replace import strings with import variables. // - We don't need this anymore and could remove it. // - We temporarily needed it for nested document configs (`config.document.{title,description,favicon}`), but we finally decided to go for flat document configs instead (`config.{title,description,favicon}`). // - https://github.com/vikejs/vike-react/pull/113 replacer(_, value) { if (typeof value === 'string') { const importData = parsePointerImportData(value); if (importData) { const { importName } = addImportStatement(importStatements, importData.importPath, importData.exportName, filesEnv, configEnv, configName); const replacement = [REPLACE_ME_BEFORE, importName, REPLACE_ME_AFTER].join(''); return { replacement }; } } }, }); } catch (err) { logJsonSerializeError(err, configName, definedAtData); assert(false); } configValueSerialized = configValueSerialized.replaceAll(`"${REPLACE_ME_BEFORE}`, ''); configValueSerialized = configValueSerialized.replaceAll(`${REPLACE_ME_AFTER}"`, ''); assert(!configValueSerialized.includes(REPLACE_ME_BEFORE)); assert(!configValueSerialized.includes(REPLACE_ME_AFTER)); return configValueSerialized; } function isJsonValue(value) { try { stringify(value, stringifyOptions); } catch (err) { return false; } return true; } function logJsonSerializeError(_err, configName, definedAtData) { /* // import { isJsonSerializerError } from '@brillout/json-serializer/stringify' let serializationErrMsg = '' if (isJsonSerializerError(_err)) { serializationErrMsg = _err.messageCore } else { // When a property getter throws an error console.error('Serialization error:') console.error(_err) serializationErrMsg = 'see serialization error printed above' } //*/ const configValueFilePathToShowToUser = getConfigValueFilePathToShowToUser(definedAtData); assert(configValueFilePathToShowToUser); assertUsage(false, `${pc.cyan(configName)} defined by ${configValueFilePathToShowToUser} must be defined using a separate file ${pc.bold(`+${configName}.js`)}, see https://vike.dev/error/runtime-in-config`); } function getConfigValuesBase(pageConfig, runtimeEnv, isEager) { const fromComputed = Object.entries(pageConfig.configValuesComputed ?? {}).map(([configName, valueInfo]) => { const { configEnv, value } = valueInfo; if (!isRuntimeEnvMatch(configEnv, runtimeEnv)) return 'SKIP'; // AFAICT this should never happen: I ain't aware of a use case for overriding computed values. If there is a use case, then configValueSources has higher precedence. if (pageConfig.configValueSources[configName]) return 'SKIP'; const configValueBase = { type: 'computed', definedAtData: null, }; return { configValueBase, value, configName, configEnv }; }); const fromSources = Object.entries(pageConfig.configValueSources).map(([configName]) => { const configDef = pageConfig.configDefinitions[configName]; assert(configDef); if (isEager !== null && isEager !== !!configDef.eager) return 'SKIP'; if (!configDef.cumulative) { const sourcesRelevant = getConfigValueSourcesRelevant(configName, runtimeEnv, pageConfig); const source = sourcesRelevant[0]; if (!source) return 'SKIP'; assert(sourcesRelevant.length === 1); const definedAtFile = getDefinedAtFileSource(source); const configValueBase = { type: 'standard', definedAtData: definedAtFile, }; return { configValueBase, sourceRelevant: source, configName }; } else { const sourcesRelevant = getConfigValueSourcesRelevant(configName, runtimeEnv, pageConfig); if (sourcesRelevant.length === 0) return 'SKIP'; const definedAtData = []; sourcesRelevant.forEach((source) => { const definedAtFile = getDefinedAtFileSource(source); definedAtData.push(definedAtFile); }); const configValueBase = { type: 'cumulative', definedAtData, }; return { configValueBase, sourcesRelevant, configName }; } }); return [...fromComputed, ...fromSources].filter((r) => r !== 'SKIP'); } function getDefinedAtFileSource(source) { const { definedAt } = source; if (definedAt.definedBy) return definedAt; const definedAtFile = { filePathToShowToUser: definedAt.filePathToShowToUser, fileExportPathToShowToUser: definedAt.fileExportPathToShowToUser, }; return definedAtFile; } /* * Naming: * `import { someExport as someImport } from './some-file'` * <=> * `{` * `importPath: './some-file',` * `exportName: 'someExport',` * `importName: 'someImport',` * `}` */ function addImportStatement(importStatements, importPath, exportName, filesEnv, configEnv, configName) { const importCounter = importStatements.length + 1; const importName = `import${importCounter}`; const importLiteral = (() => { if (exportName === '*') { return `* as ${importName}`; } if (exportName === 'default') { return importName; } return `{ ${exportName} as ${importName} }`; })(); const importStatement = `import ${importLiteral} from '${importPath}';`; importStatements.push(importStatement); assertFileEnv(importPath, configEnv, configName, filesEnv); return { importName }; } function assertFileEnv(importPath, configEnv, configName, filesEnv) { assert(!isImportPathRelative(importPath)); const key = importPath; assert(key); assertPosixPath(key); if (!filesEnv.has(key)) { filesEnv.set(key, []); } const fileEnvs = filesEnv.get(key); const fileEnvNew = { configEnv, configName }; fileEnvs.push(fileEnvNew); const fileEnvDiff = fileEnvs.filter((c) => !deepEqual(c.configEnv, configEnv))[0]; if (fileEnvDiff) { assertUsage(false, [ `${importPath} defines the value of configs living in different environments:`, ...[fileEnvDiff, fileEnvNew].map((c) => ` - config ${pc.code(c.configName)} which value lives in environment ${pc.code(JSON.stringify(c.configEnv))}`), 'Defining config values in the same file is allowed only if they live in the same environment, see https://vike.dev/config#pointer-imports', ].join('\n')); } }