UNPKG

vike

Version:

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

1,017 lines 56 kB
// Internal usage export { getVikeConfigInternal }; export { getVikeConfigInternalOptional }; export { getVikeConfigInternalSync }; export { setVikeConfigContext }; export { reloadVikeConfig }; export { isV1Design }; export { getConfVal }; export { getConfigDefinitionOptional }; export { getVikeConfigFromCliOrEnv }; export { isOverridden }; // Public usage export { getVikeConfig }; import { assertPosixPath, assert, isObject, assertUsage, assertWarning, objectEntries, hasProp, includes, assertIsNotProductionRuntime, getMostSimilar, joinEnglish, assertKeys, objectKeys, objectFromEntries, unique, isCallable, makeFirst, lowerFirst, makeLast, assertIsSingleModuleInstance, genPromise, } from '../utils.js'; import { configDefinitionsBuiltIn, } from './resolveVikeConfigInternal/configDefinitionsBuiltIn.js'; import { getLocationId, getFilesystemRouteString, getFilesystemRouteDefinedBy, isInherited, sortAfterInheritanceOrder, applyFilesystemRoutingRootEffect, } from './resolveVikeConfigInternal/filesystemRouting.js'; import { getViteDevServer } from '../../runtime/globalContext.js'; import { logConfigError, logConfigErrorRecover } from './loggerNotProd.js'; import { removeSuperfluousViteLog_enable, removeSuperfluousViteLog_disable, } from './loggerVite/removeSuperfluousViteLog.js'; import pc from '@brillout/picocolors'; import { getConfigDefinedAt, getDefinedByString } from '../../../shared/page-configs/getConfigDefinedAt.js'; import { loadPointerImport, loadValueFile } from './resolveVikeConfigInternal/loadFileAtConfigTime.js'; import { resolvePointerImport } from './resolveVikeConfigInternal/resolvePointerImport.js'; import { getFilePathResolved } from './getFilePath.js'; import { getConfigValueBuildTime } from '../../../shared/page-configs/getConfigValueBuildTime.js'; import { resolveVikeConfigPublicGlobal, resolveVikeConfigPublicPageEager, } from '../../../shared/page-configs/resolveVikeConfigPublic.js'; import { getConfigValuesBase, isJsonValue } from '../../../shared/page-configs/serialize/serializeConfigValues.js'; import { getPlusFilesAll, } from './resolveVikeConfigInternal/getPlusFilesAll.js'; import { getEnvVarObject } from './getEnvVarObject.js'; import { getApiOperation } from '../../api/context.js'; import { getCliOptions } from '../../cli/context.js'; import { resolvePrerenderConfigGlobal } from '../../prerender/resolvePrerenderConfig.js'; import { getProxyForPublicUsage } from '../../../shared/getProxyForPublicUsage.js'; import { setVikeConfigError } from '../../shared/getVikeConfigError.js'; assertIsNotProductionRuntime(); // We can simply use global variables since Vike's config is: // - global // - independent of Vite (therefore we don't need to tie Vike's config with Vite's `config` object) assertIsSingleModuleInstance('v1-design/getVikeConfig.ts'); let restartVite = false; let vikeConfigHasBuildError = null; let isV1Design_ = null; let vikeConfigPromise = null; // TODO/v1-release: remove let vikeConfigSync = null; let vikeConfigCtx = null; // Information provided by Vite's `config` and Vike's CLI. We could, if we want or need to, completely remove the dependency on Vite. let prerenderContext; function reloadVikeConfig() { assert(vikeConfigCtx); const { userRootDir, vikeVitePluginOptions } = vikeConfigCtx; assert(vikeVitePluginOptions); resolveVikeConfigInternal_withErrorHandling(userRootDir, true, vikeVitePluginOptions); } async function getVikeConfigInternal( // I don't remember the logic behind it — neither why we restart Vite's dev server, nor why we sometimes don't. // TO-DO: eventually rethink all that. Some + settings are expected to influence Vite's config (restarting Vite's dev server is needed) while some don't. doNotRestartViteOnError = false) { assert(vikeConfigCtx); const { userRootDir, isDev, vikeVitePluginOptions } = vikeConfigCtx; const vikeConfig = await getOrResolveVikeConfig(userRootDir, isDev, vikeVitePluginOptions, doNotRestartViteOnError); return vikeConfig; } // TODO/v1-release: remove function getVikeConfigInternalSync() { assert(vikeConfigSync); return vikeConfigSync; } // TO-DO/eventually: this maybe(/probably?) isn't safe against race conditions upon file changes in development, thus: // - Like getGlobalContext() and getGlobalContextSync() — make getVikeConfig() async and provide a getVikeConfigSync() while discourage using it // Public usage /** * Get all the information Vike knows about the app in your Vite plugin. * * https://vike.dev/getVikeConfig */ function getVikeConfig( // TO-DO/eventually: remove unused arguments (older versions used it and we didn't remove it yet to avoid a TypeScript breaking change) // - No rush: we can do it later since it's getVikeConfig() is a beta feature as documented at https://vike.dev/getVikeConfig config) { const vikeConfig = getVikeConfigInternalSync(); assertUsage(vikeConfig, 'getVikeConfig() can only be used when Vite is loaded (i.e. during development or build) — Vite is never loaded in production.'); const vikeConfigPublic = getProxyForPublicUsage(vikeConfig, 'vikeConfig'); return vikeConfigPublic; } function setVikeConfigContext(vikeConfigCtx_) { // If the user changes Vite's `config.root` => Vite completely reloads itself => setVikeConfigContext() is called again vikeConfigCtx = vikeConfigCtx_; } async function getOrResolveVikeConfig(userRootDir, isDev, vikeVitePluginOptions, doNotRestartViteOnError) { if (!vikeConfigPromise) { resolveVikeConfigInternal_withErrorHandling(userRootDir, isDev, vikeVitePluginOptions, doNotRestartViteOnError); } assert(vikeConfigPromise); const vikeConfig = await vikeConfigPromise; return vikeConfig; } async function getVikeConfigInternalOptional() { if (!vikeConfigPromise) return null; const vikeConfig = await vikeConfigPromise; return vikeConfig; } function isV1Design() { assert(typeof isV1Design_ === 'boolean'); return isV1Design_; } async function resolveVikeConfigInternal_withErrorHandling(userRootDir, isDev, vikeVitePluginOptions, doNotRestartViteOnError) { const { promise, resolve, reject } = genPromise(); vikeConfigPromise = promise; const esbuildCache = { transpileCache: {}, vikeConfigDependencies: new Set(), }; let hasError = false; let ret; let err; try { ret = await resolveVikeConfigInternal(userRootDir, vikeVitePluginOptions, esbuildCache); } catch (err_) { hasError = true; err = err_; } // There is a newer call — let the new call supersede the old one. // We deliberately swallow the intermetidate state (including any potential error) — it's now outdated and has existed only for a very short period of time. if (vikeConfigPromise !== promise) { // vikeConfigPromise.then(resolve).catch(reject) try { resolve(await vikeConfigPromise); } catch (err) { reject(err); } return; } if (!hasError) { assert(ret); assert(err === undefined); const hadError = vikeConfigHasBuildError; vikeConfigHasBuildError = false; setVikeConfigError({ errorBuild: false }); if (hadError) { logConfigErrorRecover(); if (restartVite) { restartVite = false; restartViteDevServer(); } } resolve(ret); } else { assert(ret === undefined); assert(err); vikeConfigHasBuildError = true; setVikeConfigError({ errorBuild: { err } }); if (!doNotRestartViteOnError) restartVite = true; if (!isDev) { reject(err); } else { logConfigError(err); resolve(getVikeConfigDummy(esbuildCache)); } } } async function resolveVikeConfigInternal(userRootDir, vikeVitePluginOptions, esbuildCache) { const plusFilesAll = await getPlusFilesAll(userRootDir, esbuildCache); const configDefinitionsResolved = await resolveConfigDefinitions(plusFilesAll, userRootDir, esbuildCache); const { pageConfigGlobal, pageConfigs } = getPageConfigsBuildTime(configDefinitionsResolved, plusFilesAll, userRootDir); if (!isV1Design_) isV1Design_ = pageConfigs.length > 0; // Backwards compatibility for vike(options) in vite.config.js temp_interopVikeVitePlugin(pageConfigGlobal, vikeVitePluginOptions, userRootDir); setCliAndApiOptions(pageConfigGlobal, configDefinitionsResolved); // global const pageConfigGlobalValues = getConfigValues(pageConfigGlobal); const vikeConfigPublicGlobal = resolveVikeConfigPublicGlobal({ pageConfigGlobalValues }); // pages const vikeConfigPublicPagesEager = objectFromEntries(pageConfigs.map((pageConfig) => { const pageConfigValues = getConfigValues(pageConfig, true); return resolveVikeConfigPublicPageEager(pageConfigGlobalValues, pageConfig, pageConfigValues); })); const prerenderContext = resolvePrerenderContext({ config: vikeConfigPublicGlobal.config, _from: vikeConfigPublicGlobal._from, _pageConfigs: pageConfigs, }); const vikeConfig = { _pageConfigs: pageConfigs, _pageConfigGlobal: pageConfigGlobal, config: vikeConfigPublicGlobal.config, _from: vikeConfigPublicGlobal._from, pages: vikeConfigPublicPagesEager, prerenderContext, _vikeConfigDependencies: esbuildCache.vikeConfigDependencies, }; vikeConfigSync = vikeConfig; return vikeConfig; } async function resolveConfigDefinitions(plusFilesAll, userRootDir, esbuildCache) { const plusFilesAllOrdered = Object.values(plusFilesAll) .flat() .sort((plusFile1, plusFile2) => sortAfterInheritanceOrderGlobal(plusFile1, plusFile2, plusFilesAll, null)); const configDefinitionsGlobal = getConfigDefinitions( // We use `plusFilesAll` in order to allow local Vike extensions to create global configs, and to set the value of global configs such as `+vite` (enabling Vike extensions to add Vite plugins). plusFilesAllOrdered, (configDef) => !!configDef.global); await loadCustomConfigBuildTimeFiles(plusFilesAll, configDefinitionsGlobal, userRootDir, esbuildCache); const configDefinitionsAll = getConfigDefinitions(Object.values(plusFilesAll).flat()); const configNamesKnownAll = Object.keys(configDefinitionsAll); const configNamesKnownGlobal = Object.keys(configDefinitionsGlobal); assert(configNamesKnownGlobal.every((configName) => configNamesKnownAll.includes(configName))); const configDefinitionsLocal = {}; await Promise.all(objectEntries(plusFilesAll).map(async ([locationIdPage, plusFiles]) => { const plusFilesRelevant = objectEntries(plusFilesAll) .filter(([locationId]) => isInherited(locationId, locationIdPage)) .map(([, plusFiles]) => plusFiles) .flat() .sort((plusFile1, plusFile2) => sortAfterInheritanceOrderPage(plusFile1, plusFile2, locationIdPage, null)); const configDefinitions = getConfigDefinitions(plusFilesRelevant, (configDef) => configDef.global !== true); await loadCustomConfigBuildTimeFiles(plusFiles, configDefinitions, userRootDir, esbuildCache); const configNamesKnownLocal = unique([...Object.keys(configDefinitions), ...configNamesKnownGlobal]); assert(configNamesKnownLocal.every((configName) => configNamesKnownAll.includes(configName))); configDefinitionsLocal[locationIdPage] = { configDefinitions, plusFiles, plusFilesRelevant, configNamesKnownLocal, }; })); const configDefinitionsResolved = { configDefinitionsGlobal, configDefinitionsLocal, configDefinitionsAll, configNamesKnownAll, configNamesKnownGlobal, }; assertKnownConfigs(configDefinitionsResolved); return configDefinitionsResolved; } // Load value files (with `env.config===true`) of *custom* configs. // - The value files of *built-in* configs are already loaded at `getPlusFilesAll()`. async function loadCustomConfigBuildTimeFiles(plusFiles, configDefinitions, userRootDir, esbuildCache) { const plusFileList = Object.values(plusFiles).flat(1); await Promise.all(plusFileList.map(async (plusFile) => { if (!plusFile.isConfigFile) { await loadValueFile(plusFile, configDefinitions, userRootDir, esbuildCache); } else { await Promise.all(Object.entries(plusFile.pointerImportsByConfigName).map(async ([configName, pointerImport]) => { await loadPointerImport(pointerImport, userRootDir, configName, configDefinitions, esbuildCache); })); } })); } function getPageConfigsBuildTime(configDefinitionsResolved, plusFilesAll, userRootDir) { const pageConfigGlobal = { configDefinitions: configDefinitionsResolved.configDefinitionsGlobal, configValueSources: {}, }; objectEntries(configDefinitionsResolved.configDefinitionsGlobal).forEach(([configName, configDef]) => { const sources = resolveConfigValueSources(configName, configDef, // We use `plusFilesAll` in order to allow local Vike extensions to create global configs, and to set the value of global configs such as `+vite` (enabling Vike extensions to add Vite plugins). Object.values(plusFilesAll).flat(), userRootDir, true, plusFilesAll); if (sources.length === 0) return; pageConfigGlobal.configValueSources[configName] = sources; }); applyEffectsMetaEnv(pageConfigGlobal.configValueSources, configDefinitionsResolved.configDefinitionsGlobal); applyEffectsConfVal(pageConfigGlobal.configValueSources, configDefinitionsResolved.configDefinitionsGlobal, plusFilesAll); sortConfigValueSources(pageConfigGlobal.configValueSources, null); assertPageConfigGlobal(pageConfigGlobal, plusFilesAll); const pageConfigs = objectEntries(configDefinitionsResolved.configDefinitionsLocal) .filter(([_locationId, { plusFiles }]) => isDefiningPage(plusFiles)) .map(([locationId, { configDefinitions, plusFilesRelevant }]) => { const configDefinitionsLocal = configDefinitions; const configValueSources = {}; objectEntries(configDefinitionsLocal) .filter(([_configName, configDef]) => configDef.global !== true) .forEach(([configName, configDef]) => { const sources = resolveConfigValueSources(configName, configDef, plusFilesRelevant, userRootDir, false, plusFilesAll); if (sources.length === 0) return; configValueSources[configName] = sources; }); const pageConfigRoute = determineRouteFilesystem(locationId, configValueSources); applyEffectsMetaEnv(configValueSources, configDefinitionsLocal); applyEffectsConfVal(configValueSources, configDefinitionsLocal, plusFilesAll); sortConfigValueSources(configValueSources, locationId); const configValuesComputed = getComputed(configValueSources, configDefinitionsLocal); const pageConfig = { pageId: locationId, ...pageConfigRoute, configDefinitions: configDefinitionsLocal, plusFiles: plusFilesRelevant, configValueSources, configValuesComputed, }; return pageConfig; }); assertPageConfigs(pageConfigs); return { pageConfigs, pageConfigGlobal }; } function assertPageConfigGlobal(pageConfigGlobal, plusFilesAll) { Object.entries(pageConfigGlobal.configValueSources).forEach(([configName, sources]) => { assertGlobalConfigLocation(configName, sources, plusFilesAll, pageConfigGlobal.configDefinitions); }); } function assertGlobalConfigLocation(configName, sources, plusFilesAll, configDefinitionsGlobal) { // Determine existing global +config.js files const configFilePathsGlobal = []; const plusFilesGlobal = Object.values(objectFromEntries(objectEntries(plusFilesAll).filter(([locationId]) => isGlobalLocation(locationId, plusFilesAll)))).flat(); plusFilesGlobal .filter((i) => i.isConfigFile) .forEach((plusFile) => { const { filePathAbsoluteUserRootDir } = plusFile.filePath; if (filePathAbsoluteUserRootDir) { configFilePathsGlobal.push(filePathAbsoluteUserRootDir); } }); // Call assertWarning() sources.forEach((source) => { const { plusFile } = source; // It's `null` when the config is defined by `vike(options)` in vite.config.js assert(plusFile); const { filePathAbsoluteUserRootDir } = plusFile.filePath; // Allow local Vike extensions to set global configs (`filePathAbsoluteUserRootDir===null` for Vike extension) if (!filePathAbsoluteUserRootDir) return; assert(!plusFile.isExtensionConfig); if (!isGlobalLocation(source.locationId, plusFilesAll)) { const configDef = configDefinitionsGlobal[configName]; assert(configDef); const isConditionallyGlobal = isCallable(configDef.global); const errBeg = `${filePathAbsoluteUserRootDir} (which is a local config file) sets the config ${pc.cyan(configName)}`; const errMid = !isConditionallyGlobal ? "but it's a global config" : 'to a value that is global'; const what = isConditionallyGlobal ? 'global values' : pc.cyan(configName); const errEnd = configFilePathsGlobal.length > 0 ? `define ${what} at a global config file such as ${joinEnglish(configFilePathsGlobal.map(pc.bold), 'or')} instead` : `create a global config file (e.g. /pages/+config.js) and define ${what} there instead`; // When updating this error message => also update error message at https://vike.dev/warning/global-config const errMsg = `${errBeg} ${errMid}: ${errEnd} (https://vike.dev/warning/global-config).`; assertWarning(false, errMsg, { onlyOnce: true }); } }); } function assertPageConfigs(pageConfigs) { pageConfigs.forEach((pageConfig) => { assertOnBeforeRenderEnv(pageConfig); }); } function assertOnBeforeRenderEnv(pageConfig) { const onBeforeRenderConfig = pageConfig.configValueSources.onBeforeRender?.[0]; if (!onBeforeRenderConfig) return; const onBeforeRenderEnv = onBeforeRenderConfig.configEnv; const isClientRouting = getConfigValueBuildTime(pageConfig, 'clientRouting', 'boolean'); // When using Server Routing, loading a onBeforeRender() hook on the client-side hasn't any effect (the Server Routing's client runtime never calls it); it unnecessarily bloats client bundle sizes assertUsage(!(onBeforeRenderEnv.client && !isClientRouting), `Page ${pageConfig.pageId} has an onBeforeRender() hook with env ${pc.cyan(JSON.stringify(onBeforeRenderEnv))} which doesn't make sense because the page is using Server Routing: onBeforeRender() can be run in the client only when using Client Routing.`); } function getConfigValues(pageConfig, tolerateMissingValue) { const configValues = {}; getConfigValuesBase(pageConfig, (configEnv) => !!configEnv.config, null).forEach((entry) => { if (entry.configValueBase.type === 'computed') { assert('value' in entry); // Help TS const { configValueBase, value, configName } = entry; configValues[configName] = { ...configValueBase, value }; } if (entry.configValueBase.type === 'standard') { assert('sourceRelevant' in entry); // Help TS const { configValueBase, sourceRelevant, configName } = entry; if (!sourceRelevant.valueIsLoaded) { if (tolerateMissingValue) return; assert(false); } const { value } = sourceRelevant; configValues[configName] = { ...configValueBase, value }; } if (entry.configValueBase.type === 'cumulative') { assert('sourcesRelevant' in entry); // Help TS const { configValueBase, sourcesRelevant, configName } = entry; const values = []; sourcesRelevant.forEach((source) => { if (!source.valueIsLoaded) { if (tolerateMissingValue) return; assert(false); } values.push(source.value); }); if (values.length === 0) { if (tolerateMissingValue) return; assert(false); } configValues[configName] = { ...configValueBase, value: values }; } }); return configValues; } function temp_interopVikeVitePlugin(pageConfigGlobal, vikeVitePluginOptions, userRootDir) { assert(isObject(vikeVitePluginOptions)); assertWarning(Object.keys(vikeVitePluginOptions).length === 0, `Define Vike settings in +config.js instead of vite.config.js ${pc.underline('https://vike.dev/migration/settings')}`, { onlyOnce: true }); Object.entries(vikeVitePluginOptions).forEach(([configName, value]) => { var _a; const sources = ((_a = pageConfigGlobal.configValueSources)[configName] ?? (_a[configName] = [])); sources.push(getSourceNonConfigFile(configName, value, { ...getFilePathResolved({ userRootDir, filePathAbsoluteUserRootDir: '/vite.config.js', }), fileExportPathToShowToUser: null, })); }); } function setCliAndApiOptions(pageConfigGlobal, configDefinitionsResolved) { // Vike API — passed options [lowest precedence] const apiOperation = getApiOperation(); if (apiOperation?.options.vikeConfig) { addSources(apiOperation.options.vikeConfig, { definedBy: 'api', operation: apiOperation.operation }, false); } const { configFromCliOptions, configFromEnvVar } = getVikeConfigFromCliOrEnv(); // Vike CLI options if (configFromCliOptions) { addSources(configFromCliOptions, { definedBy: 'cli' }, true); } // VIKE_CONFIG [highest precedence] if (configFromEnvVar) { addSources(configFromEnvVar, { definedBy: 'env' }, false); } return; function addSources(configValues, definedBy, exitOnError) { Object.entries(configValues).forEach(([configName, value]) => { var _a; const sourceName = `The ${getDefinedByString(definedBy, configName)}`; assertKnownConfig(configName, configDefinitionsResolved.configNamesKnownGlobal, configDefinitionsResolved, '/', false, sourceName, exitOnError); const sources = ((_a = pageConfigGlobal.configValueSources)[configName] ?? (_a[configName] = [])); sources.unshift(getSourceNonConfigFile(configName, value, definedBy)); }); } } function getVikeConfigFromCliOrEnv() { const configFromCliOptions = getCliOptions(); const configFromEnvVar = getEnvVarObject('VIKE_CONFIG'); const vikeConfigFromCliOrEnv = { ...configFromCliOptions, // Lower precedence ...configFromEnvVar, // Higher precedence }; return { vikeConfigFromCliOrEnv, configFromCliOptions, configFromEnvVar, }; } function getSourceNonConfigFile(configName, value, definedAt) { assert(includes(objectKeys(configDefinitionsBuiltIn), configName)); const configDef = configDefinitionsBuiltIn[configName]; const source = { valueIsLoaded: true, value, configEnv: configDef.env, definedAt, locationId: '/', plusFile: null, valueIsLoadedWithImport: false, valueIsDefinedByPlusValueFile: false, }; return source; } function sortConfigValueSources(configValueSources, locationIdPage) { Object.entries(configValueSources).forEach(([configName, sources]) => { sources .sort((source1, source2) => { if (!source1.plusFile || !source2.plusFile) return 0; const isGlobal = !locationIdPage; if (isGlobal) { return sortAfterInheritanceOrderGlobal(source1.plusFile, source2.plusFile, null, configName); } else { return sortAfterInheritanceOrderPage(source1.plusFile, source2.plusFile, locationIdPage, configName); } }) // TODO/next-major: remove // Interop with vike(options) in vite.config.js — make it least precedence. .sort(makeLast((source) => !source.plusFile)); }); } function sortAfterInheritanceOrderPage(plusFile1, plusFile2, locationIdPage, configName) { { const ret = sortAfterInheritanceOrder(plusFile1.locationId, plusFile2.locationId, locationIdPage); if (ret !== 0) return ret; assert(plusFile1.locationId === plusFile2.locationId); } if (configName) { const ret = sortPlusFilesSameLocationId(plusFile1, plusFile2, configName); if (ret !== 0) return ret; } return 0; } function sortAfterInheritanceOrderGlobal(plusFile1, plusFile2, plusFilesAll, configName) { if (plusFilesAll) { const ret = makeFirst((plusFile) => isGlobalLocation(plusFile.locationId, plusFilesAll))(plusFile1, plusFile2); if (ret !== 0) return ret; } { const ret = lowerFirst((plusFile) => plusFile.locationId.split('/').length)(plusFile1, plusFile2); if (ret !== 0) return ret; } if (plusFile1.locationId !== plusFile2.locationId) { // Same as `sort()` in `['some', 'string', 'array'].sort()` return plusFile1.locationId > plusFile2.locationId ? 1 : -1; } if (configName) { assert(plusFile1.locationId === plusFile2.locationId); const ret = sortPlusFilesSameLocationId(plusFile1, plusFile2, configName); if (ret !== 0) return ret; } return 0; } function sortPlusFilesSameLocationId(plusFile1, plusFile2, configName) { assert(plusFile1.locationId === plusFile2.locationId); assert(isDefiningConfig(plusFile1, configName)); assert(isDefiningConfig(plusFile2, configName)); // Config set by extensions (lowest precedence) { const ret = makeLast((plusFile) => !!plusFile.isExtensionConfig)(plusFile1, plusFile2); if (ret !== 0) return ret; } // Config set by side-export (lower precedence) { // - For example `export { frontmatter }` of `.mdx` files. // - This only considers side-export configs that are already loaded at build-time. (E.g. it actually doesn't consider `export { frontmatter }` of .mdx files since .mdx files are loaded only at runtime.) const ret = makeLast((plusFile) => !plusFile.isConfigFile && // Is side-export plusFile.configName !== configName)(plusFile1, plusFile2); if (ret !== 0) return ret; } // Config set by +config.js { const ret = makeLast((plusFile) => plusFile.isConfigFile)(plusFile1, plusFile2); if (ret !== 0) return ret; } // Config set by +{configName}.js (highest precedence) // No need to make it deterministic: the overall order is already deterministic, see sortMakeDeterministic() at getPlusFilesAll() return 0; } function resolveConfigValueSources(configName, configDef, plusFilesRelevant, userRootDir, isGlobal, plusFilesAll) { let sources = plusFilesRelevant .filter((plusFile) => isDefiningConfig(plusFile, configName)) .map((plusFile) => getConfigValueSource(configName, plusFile, configDef, userRootDir)); // Filter hydrid global-local configs if (!isCallable(configDef.global)) { // Already filtered assert((configDef.global ?? false) === isGlobal); } else { // We cannot filter earlier assert(configDef.env.config); sources = sources.filter((source) => { assert(source.configEnv.config); assert(source.valueIsLoaded); const valueIsGlobal = resolveIsGlobalValue(configDef.global, source, plusFilesAll); return isGlobal ? valueIsGlobal : !valueIsGlobal; }); } return sources; } function isDefiningConfig(plusFile, configName) { return getConfigNamesSetByPlusFile(plusFile).includes(configName); } function getConfigValueSource(configName, plusFile, configDef, userRootDir) { const confVal = getConfVal(plusFile, configName); assert(confVal); const configValueSourceCommon = { locationId: plusFile.locationId, plusFile, }; const definedAtFilePath_ = { ...plusFile.filePath, fileExportPathToShowToUser: ['default', configName], }; // +client.js if (configDef._valueIsFilePath) { let definedAtFilePath; let valueFilePath; if (plusFile.isConfigFile) { // Defined over pointer import assert(confVal.valueIsLoaded); const pointerImport = resolvePointerImport(confVal.value, plusFile.filePath, userRootDir, configName); const configDefinedAt = getConfigDefinedAt('Config', configName, definedAtFilePath_); assertUsage(pointerImport, `${configDefinedAt} should be an import`); valueFilePath = pointerImport.fileExportPath.filePathAbsoluteVite; definedAtFilePath = pointerImport.fileExportPath; } else { // Defined by value file, i.e. +{configName}.js assert(!plusFile.isConfigFile); valueFilePath = plusFile.filePath.filePathAbsoluteVite; definedAtFilePath = { ...plusFile.filePath, fileExportPathToShowToUser: [], }; } const configValueSource = { ...configValueSourceCommon, valueIsLoaded: true, value: valueFilePath, valueIsFilePath: true, configEnv: configDef.env, valueIsLoadedWithImport: false, valueIsDefinedByPlusValueFile: false, definedAt: definedAtFilePath, }; return configValueSource; } // +config.js if (plusFile.isConfigFile) { assert(confVal.valueIsLoaded); // Defined over pointer import const pointerImport = plusFile.pointerImportsByConfigName[configName]; if (pointerImport) { const value = pointerImport.fileExportValueLoaded ? { valueIsLoaded: true, value: pointerImport.fileExportValue, } : { valueIsLoaded: false, }; const configValueSource = { ...configValueSourceCommon, ...value, configEnv: resolveConfigEnv(configDef.env, pointerImport.fileExportPath), valueIsLoadedWithImport: true, valueIsDefinedByPlusValueFile: false, definedAt: pointerImport.fileExportPath, }; return configValueSource; } // Defined inside +config.js const configValueSource = { ...configValueSourceCommon, valueIsLoaded: true, value: confVal.value, configEnv: configDef.env, valueIsLoadedWithImport: false, valueIsDefinedByPlusValueFile: false, definedAt: definedAtFilePath_, }; return configValueSource; } // Defined by value file, i.e. +{configName}.js if (!plusFile.isConfigFile) { const configEnvResolved = resolveConfigEnv(configDef.env, plusFile.filePath); assert(confVal.valueIsLoaded === !!configEnvResolved.config); const configValueSource = { ...configValueSourceCommon, ...confVal, configEnv: configEnvResolved, valueIsLoadedWithImport: !confVal.valueIsLoaded || !isJsonValue(confVal.value), valueIsDefinedByPlusValueFile: true, definedAt: { ...plusFile.filePath, fileExportPathToShowToUser: configName === plusFile.configName ? [] : // Side-effect config (e.g. `export { frontmatter }` of .md files) [configName], }, }; return configValueSource; } assert(false); } function isDefiningPage(plusFiles) { for (const plusFile of plusFiles) { const configNames = getConfigNamesSetByPlusFile(plusFile); if (configNames.some((configName) => isDefiningPageConfig(configName))) { return true; } } return false; } function isDefiningPageConfig(configName) { return ['Page', 'route'].includes(configName); } function resolveIsGlobalValue(configDefGlobal, source, plusFilesAll) { assert(source.valueIsLoaded); let isGlobal; if (isCallable(configDefGlobal)) isGlobal = configDefGlobal(source.value, { isGlobalLocation: isGlobalLocation(source.locationId, plusFilesAll), }); else isGlobal = configDefGlobal ?? false; assert(typeof isGlobal === 'boolean'); return isGlobal; } function getConfigNamesSetByPlusFile(plusFile) { if (!plusFile.isConfigFile) { return [plusFile.configName]; } else { return Object.keys(plusFile.fileExportsByConfigName); } } function getConfigDefinitions(plusFilesRelevant, filter) { let configDefinitions = { ...configDefinitionsBuiltIn }; // Add user-land meta configs plusFilesRelevant .slice() .reverse() .forEach((plusFile) => { const confVal = getConfVal(plusFile, 'meta'); if (!confVal) return; assert(confVal.valueIsLoaded); const meta = confVal.value; assertMetaUsage(meta, `Config ${pc.cyan('meta')} defined at ${plusFile.filePath.filePathToShowToUser}`); // Set configDef._userEffectDefinedAtFilePath Object.entries(meta).forEach(([configName, configDef]) => { if ('isDefinedByPeerDependency' in configDef) return; if (!configDef.effect) return; assert(plusFile.isConfigFile); configDef._userEffectDefinedAtFilePath = { ...plusFile.filePath, fileExportPathToShowToUser: ['default', 'meta', configName, 'effect'], }; }); objectEntries(meta).forEach(([configName, configDefinitionUserLand]) => { if ('isDefinedByPeerDependency' in configDefinitionUserLand) { configDefinitionUserLand = { env: { client: false, server: false, config: false }, ...configDefinitionUserLand, }; } // User can override an existing config definition configDefinitions[configName] = { ...configDefinitions[configName], ...configDefinitionUserLand, }; }); }); if (filter) { configDefinitions = Object.fromEntries(Object.entries(configDefinitions).filter(([_configName, configDef]) => filter(configDef))); } return configDefinitions; } function assertMetaUsage(metaVal, metaConfigDefinedAt) { if (!isObject(metaVal)) { assert(metaConfigDefinedAt); // We expect internal effects to return a valid meta value assertUsage(false, `${metaConfigDefinedAt} has an invalid type ${pc.cyan(typeof metaVal)}: it should be an object instead.`); } objectEntries(metaVal).forEach(([configName, def]) => { if (!isObject(def)) { assert(metaConfigDefinedAt); // We expect internal effects to return a valid meta value assertUsage(false, `${metaConfigDefinedAt} sets ${pc.cyan(`meta.${configName}`)} to a value with an invalid type ${pc.cyan(typeof def)}: it should be an object instead.`); } if (def.isDefinedByPeerDependency) return; // env let configEnv; { assert(metaConfigDefinedAt); // We expect internal effects to return a valid meta value if (!('env' in def)) { assertUsage(false, `${metaConfigDefinedAt} doesn't set ${pc.cyan(`meta.${configName}.env`)} but it's required.`); } configEnv = getConfigEnvValue(def.env, `${metaConfigDefinedAt} sets ${pc.cyan(`meta.${configName}.env`)} to`); // Overwrite deprecated value with valid value // TODO/v1-release: remove once support for the deprecated values is removed if (typeof def.env === 'string') def.env = configEnv; } // effect if ('effect' in def) { if (!hasProp(def, 'effect', 'function')) { assert(metaConfigDefinedAt); // We expect internal effects to return a valid meta value assertUsage(false, `${metaConfigDefinedAt} sets ${pc.cyan(`meta.${configName}.effect`)} to an invalid type ${pc.cyan(typeof def.effect)}: it should be a function instead`); } if (!configEnv.config) { assert(metaConfigDefinedAt); // We expect internal effects to return a valid meta value assertUsage(false, `${metaConfigDefinedAt} sets ${pc.cyan(`meta.${configName}.effect`)} but it's only supported if meta.${configName}.env has ${pc.cyan('{ config: true }')} (but it's ${pc.cyan(JSON.stringify(configEnv))} instead)`); } } }); } // Test: https://github.com/vikejs/vike/blob/441a37c4c1a3b07bb8f6efb1d1f7be297a53974a/test/playground/vite.config.ts#L39 function applyEffectsConfVal(configValueSources, configDefinitions, plusFilesAll) { objectEntries(configDefinitions).forEach(([configNameEffect, configDefEffect]) => { const sourceEffect = configValueSources[configNameEffect]?.[0]; if (!sourceEffect) return; const effect = runEffect(configNameEffect, configDefEffect, sourceEffect); if (!effect) return; const configModFromEffect = effect; applyEffectConfVal(configModFromEffect, sourceEffect, configValueSources, configNameEffect, configDefEffect, configDefinitions, plusFilesAll); }); } // Test: https://github.com/vikejs/vike/blob/441a37c4c1a3b07bb8f6efb1d1f7be297a53974a/test/playground/pages/config-meta/effect/e2e-test.ts#L16 function applyEffectsMetaEnv(configValueSources, configDefinitions) { objectEntries(configDefinitions).forEach(([configNameEffect, configDefEffect]) => { const sourceEffect = configValueSources[configNameEffect]?.[0]; if (!sourceEffect) return; const effect = runEffect(configNameEffect, configDefEffect, sourceEffect); if (!effect) return; const configModFromEffect = effect; applyEffectMetaEnv(configModFromEffect, configValueSources, configDefEffect); }); } function runEffect(configName, configDef, source) { if (!configDef.effect) return null; // The value needs to be loaded at config time, that's why we only support effect for configs that are config-only for now. assertUsage(configDef.env.config, [ `Cannot add meta.effect to ${pc.cyan(configName)} because its meta.env is ${pc.cyan(JSON.stringify(configDef.env))} but an effect can only be added to a config that has a meta.env with ${pc.cyan('{ config: true }')}.`, ].join(' ')); assert(source.valueIsLoaded); // Call effect const configModFromEffect = configDef.effect({ configValue: source.value, configDefinedAt: getConfigDefinedAt('Config', configName, source.definedAt), }); if (!configModFromEffect) return null; return configModFromEffect; } function applyEffectConfVal(configModFromEffect, sourceEffect, configValueSources, configNameEffect, configDefEffect, configDefinitions, plusFilesAll) { objectEntries(configModFromEffect).forEach(([configNameTarget, configValue]) => { if (configNameTarget === 'meta') return; const configDef = configDefinitions[configNameTarget]; assert(configDef); assert(configDefEffect._userEffectDefinedAtFilePath); const configValueSource = { definedAt: configDefEffect._userEffectDefinedAtFilePath, plusFile: sourceEffect.plusFile, locationId: sourceEffect.locationId, configEnv: configDef.env, valueIsLoadedWithImport: false, valueIsDefinedByPlusValueFile: false, valueIsLoaded: true, value: configValue, }; assert(sourceEffect.valueIsLoaded); const isValueGlobalSource = resolveIsGlobalValue(configDefEffect.global, sourceEffect, plusFilesAll); const isValueGlobalTarget = resolveIsGlobalValue(configDef.global, configValueSource, plusFilesAll); const isGlobalHumanReadable = (isGlobal) => `${isGlobal ? 'non-' : ''}global`; // The error message make it sound like it's an inherent limitation, it actually isn't (both ways can make senses). assertUsage(isValueGlobalSource === isValueGlobalTarget, `The configuration ${pc.cyan(configNameEffect)} is set to ${pc.cyan(JSON.stringify(sourceEffect.value))} which is considered ${isGlobalHumanReadable(isValueGlobalSource)}. However, it has a meta.effect that sets the configuration ${pc.cyan(configNameTarget)} to ${pc.cyan(JSON.stringify(configValue))} which is considered ${isGlobalHumanReadable(isValueGlobalTarget)}. This is contradictory: make sure the values are either both non-global or both global.`); configValueSources[configNameTarget] ?? (configValueSources[configNameTarget] = []); configValueSources[configNameTarget].push(configValueSource); }); } function applyEffectMetaEnv(configModFromEffect, configValueSources, configDefEffect) { const notSupported = `${pc.cyan('meta.effect')} currently only supports setting the value of a config, or modifying the ${pc.cyan('meta.env')} of a config.`; objectEntries(configModFromEffect).forEach(([configNameTarget, configValue]) => { if (configNameTarget !== 'meta') return; let configDefinedAt; if (configDefEffect._userEffectDefinedAtFilePath) { configDefinedAt = getConfigDefinedAt('Config', configNameTarget, configDefEffect._userEffectDefinedAtFilePath); } else { configDefinedAt = null; } assertMetaUsage(configValue, configDefinedAt); objectEntries(configValue).forEach(([configTargetName, configTargetDef]) => { assert(!('isDefinedByPeerDependency' in configTargetDef)); { const keys = Object.keys(configTargetDef); assertUsage(keys.includes('env'), notSupported); assertUsage(keys.length === 1, notSupported); } const envOverridden = configTargetDef.env; const sources = configValueSources[configTargetName]; sources?.forEach((configValueSource) => { // Apply effect configValueSource.configEnv = envOverridden; }); }); }); } function getComputed(configValueSources, configDefinitions) { const configValuesComputed = {}; objectEntries(configDefinitions).forEach(([configName, configDef]) => { if (!configDef._computed) return; const value = configDef._computed(configValueSources); if (value === undefined) return; configValuesComputed[configName] = { value, configEnv: configDef.env, }; }); return configValuesComputed; } // Show error message upon unknown config function assertKnownConfigs(configDefinitionsResolved) { objectEntries(configDefinitionsResolved.configDefinitionsLocal).forEach(([_locationId, { configNamesKnownLocal, plusFiles }]) => { plusFiles.forEach((plusFile) => { const configNames = getConfigNamesSetByPlusFile(plusFile); configNames.forEach((configName) => { const { locationId } = plusFile; const sourceName = plusFile.filePath.filePathToShowToUser; assertKnownConfig(configName, configNamesKnownLocal, configDefinitionsResolved, locationId, true, sourceName, false); }); }); }); } function assertKnownConfig(configName, configNamesKnownRelevant, configDefinitionsResolved, locationId, isPlusFile, sourceName, exitOnError) { const { configNamesKnownAll } = configDefinitionsResolved; if (configNamesKnownRelevant.includes(configName)) { assert(configNamesKnownAll.includes(configName)); return; } const configNameColored = pc.cyan(configName); // Inheritance issue: config is known but isn't defined at `locationId` if (configNamesKnownAll.includes(configName)) { assertUsage(false, `${sourceName} sets the value of the config ${configNameColored} which is a custom config that is defined with ${pc.underline('https://vike.dev/meta')} at a path that doesn't apply to ${locationId} — see ${pc.underline('https://vike.dev/config#inheritance')}`, { exitOnError }); } const errMsg = isPlusFile ? `${sourceName} sets an unknown config ${configNameColored}` : `${sourceName} sets an unknown Vike config, see ${pc.underline('https://vike.dev/cli')} for the list of CLI options`; assert(errMsg.includes(configName)); // Missing vike-{react,vue,solid} installation { const ui = ['vike-react', 'vike-vue', 'vike-solid']; const knownVikeExntensionConfigs = { description: ui, favicon: ui, Head: ui, Layout: ui, onCreateApp: ['vike-vue'], title: ui, ssr: ui, stream: ui, Wrapper: ui, }; if (configName in knownVikeExntensionConfigs) { const requiredVikeExtension = joinEnglish(knownVikeExntensionConfigs[configName].map((e) => pc.bold(e)), 'or'); const errMsgEnhanced = `${errMsg}. If you want to use the configuration ${configNameColored} documented at ${pc.underline(`https://vike.dev/${configName}`)} then make sure to install ${requiredVikeExtension}. (Alternatively, you can define ${configNameColored} yourself by using ${pc.cyan('meta')}, see ${pc.underline('https://vike.dev/meta')} for more information.)`; assertUsage(false, errMsgEnhanced, { exitOnError }); } } // Similarity hint let configNameSimilar = null; if (configName === 'page') { configNameSimilar = 'Page'; } else { configNameSimilar = getMostSimilar(configName, configNamesKnownAll); } if (configNameSimilar) { assert(configNameSimilar !== configName); let errMsgEnhanced = `${errMsg}. Did you mean ${pc.cyan(configNameSimilar)} instead?`; if (configName === 'page') { errMsgEnhanced += ` (The name of the config ${pc.cyan('Page')} starts with a capital letter ${pc.cyan('P')} because it defines a UI component: a ubiquitous JavaScript convention is that the name of UI components start with a capital letter.)`; } assertUsage(false, errMsgEnhanced, { exitOnError }); } assertUsage(false, errMsg, { exitOnError }); } function determineRouteFilesystem(locationId, configValueSources) { const configName = 'filesystemRoutingRoot'; const configFilesystemRoutingRoot = configValueSources[configName]?.[0]; let filesystemRouteString = getFilesystemRouteString(locationId); if (determineIsErrorPage(filesystemRouteString)) { return { isErrorPage: true, routeFilesystem: undefined }; } let filesystemRouteDefinedBy = getFilesystemRouteDefinedBy(locationId); // for log404() if (configFilesystemRoutingRoot) { const routingRoot = getFilesystemRoutingRootEffect(configFilesystemRoutingRoot, configName); if (routingRoot) { const { filesystemRoutingRootEffect /*, filesystemRoutingRootConfigDefinedAt*/ } = routingRoot; const debugInfo = { locationId, routeFilesystem: filesystemRouteString, configFilesystemRoutingRoot }; assert(filesystemRouteString.startsWith(filesystemRoutingRootEffect.before), debugInfo); filesystemRouteString = applyFilesystemRoutingRootEffect(filesystemRouteString, filesystemRoutingRootEffect); // filesystemRouteDefinedBy = `${filesystemRouteDefinedBy} (with ${filesystemRoutingRootConfigDefinedAt})` } } assert(filesystemRouteString.startsWith('/')); const routeFilesystem = { routeString: filesystemRouteString, definedAtLocation: filesystemRouteDefinedBy, }; return { routeFilesystem, isErrorPage: undefined }; } function getFilesystemRoutingRootEffect(configFilesystemRoutingRoot, configName) { assert(configFilesystemRoutingRoot.configEnv.config); // Eagerly loaded since it's config-only assert(configFilesystemRoutingRoot.valueIsLoaded); const { value } = configFilesystemRoutingRoot; const configDefinedAt = getConfigDefinedAt('Config', configName, configFilesystemRoutingRoot.definedAt); assertUsage(typeof value === 'string', `${configDefinedAt} should be a string`); assertUsage(value.startsWith('/'), `${configDefinedAt} is ${pc.cyan(value)} but it should start with a leading slash ${pc.cyan('/')}`); const { definedAt } = configFilesystemRoutingRoot; assert(!definedAt.definedBy); const { filePathAbsoluteUserRootDir } = definedAt; assert(filePathAbsoluteUserRootDir); const before = getFilesystemRouteString(ge