vike
Version:
(Replaces Next.js/Nuxt) 🔨 Composable framework to build advanced applications with flexibility and stability.
155 lines (154 loc) • 8.76 kB
JavaScript
export { pluginReplaceConstantsEnvVars };
import { loadEnv } from 'vite';
import { escapeRegex } from '../../../utils/escapeRegex.js';
import { isNotNullish } from '../../../utils/isNullish.js';
import { assert, assertUsage, assertWarning } from '../../../utils/assert.js';
import { isArray } from '../../../utils/isArray.js';
import { makeLast } from '../../../utils/sorter.js';
import { assertPosixPath } from '../../../utils/path.js';
import { getFilePathToShowToUserModule } from '../shared/getFilePath.js';
import { normalizeId } from '../shared/normalizeId.js';
import { isViteServerSide_extraSafe } from '../shared/isViteServerSide.js';
import { getMagicString } from '../shared/getMagicString.js';
import pc from '@brillout/picocolors';
import '../assertEnvVite.js';
const PUBLIC_ENV_PREFIX = 'PUBLIC_ENV__';
const PUBLIC_ENV_ALLOWLIST = [
// https://github.com/vikejs/vike/issues/1724
'STORYBOOK',
// https://github.com/vikejs/vike/pull/3069
'DEBUG',
];
// TO-DO/eventually:
// - Make import.meta.env work inside +config.js
// - For it to work, we'll probably need the user to define the settings (e.g. `envDir`) for loadEnv() inside vike.config.js instead of vite.config.js
// - Or stop using Vite's `mode` implementation and have Vike implement its own `mode` feature? (So that the only dependencies are `$ vike build --mode staging` and `$ MODE=staging vike build`.)
// === Rolldown filter
const skipIrrelevant = 'import.meta.env';
const filterRolldown = {
/* We don't do that, because vike-react-sentry uses import.meta.env.PUBLIC_ENV__SENTRY_DSN
id: {
exclude: `**${'/node_modules/'}**`,
},
*/
code: {
include: skipIrrelevant,
},
};
const filterFunction = (code) => {
if (!code.includes(skipIrrelevant))
return false;
return true;
};
// ===
function pluginReplaceConstantsEnvVars() {
let envVarsAll;
let envPrefix;
let config;
return [
{
name: 'vike:pluginReplaceConstantsEnvVars',
// Correct order:
// 1. @vitejs/plugin-vue
// 2. vike:pluginExtractAssets and vike:pluginExtractExportNames [needs to be applied after @vitejs/plugin-vue]
// 3. vike:pluginReplaceConstantsEnvVars [needs to be applied after vike:pluginExtractAssets and vike:pluginExtractExportNames]
// 4. vite:define (Vite built-in plugin) [needs to be applied after vike:pluginReplaceConstantsEnvVars]
enforce: 'post',
configResolved: {
handler(config_) {
config = config_;
envVarsAll = loadEnv(config.mode, config.envDir || config.root, '');
// Add process.env values defined by .env files
Object.entries(envVarsAll).forEach(([key, val]) => { var _a; return ((_a = process.env)[key] ?? (_a[key] = val)); });
envPrefix = getEnvPrefix(config);
config.plugins.sort(makeLast((plugin) => plugin.name === 'vite:define'));
},
},
transform: {
filter: filterRolldown,
handler(code, id, options) {
id = normalizeId(id);
assertPosixPath(id);
assert(filterFunction(code));
const isBuild = config.command === 'build';
const isClientSide = !isViteServerSide_extraSafe(config, this.environment, options);
const { magicString, getMagicStringResult } = getMagicString(code, id);
// Get regex operations
const replacements = Object.entries(envVarsAll)
// Skip env vars that start with [`config.envPrefix`](https://vite.dev/config/shared-options.html#envprefix) — they are already handled by Vite
.filter(([envName]) => !envPrefix.some((prefix) => envName.startsWith(prefix)))
// Skip constants like import.meta.env.DEV which are already handled by Vite
.filter(([envName]) => !['DEV', 'PROD', 'SSR', 'MODE', 'BASE_URL'].includes(envName))
.map(([envName, envVal]) => {
const envStatement = `import.meta.env.${envName}`;
const envStatementRegExpStr = escapeRegex(envStatement) + '\\b';
// Show error (warning in dev) if client code contains a private environment variable (one that doesn't start with PUBLIC_ENV__ and that isn't included in `PUBLIC_ENV_ALLOWLIST`).
if (isClientSide) {
const skip = assertNoClientSideLeak({
envName,
envStatement,
envStatementRegExpStr,
code,
id,
config,
isBuild,
});
if (skip)
return null;
}
return { regExpStr: envStatementRegExpStr, replacement: envVal };
})
.filter(isNotNullish);
// Apply regex operations
replacements.forEach(({ regExpStr, replacement }) => {
magicString.replaceAll(new RegExp(regExpStr, 'g'), JSON.stringify(replacement));
});
// Replace bare `import.meta.env` expression with `null` in the user-land.
// - Otherwise Vite replaces it with an object missing PUBLIC_ENV__ variables which is confusing for users.
// - We purposely don't support replacing `import.meta.env` with an object to incentivize users to write tree-shaking friendly code.
// - `define: { 'import.meta.env': JSON.stringify(null) }` doesn't work because it also replaces `import.meta.env` inside `import.meta.env.SONE_ENV`
const bareImportMetaEnvRegex = /\bimport\.meta\.env(?!\.)/g;
const isUserLand = !id.includes('node_modules') && id.startsWith(config.root); // skip node_modules/ as well as linked dependencies
if (isUserLand && bareImportMetaEnvRegex.test(code)) {
assertWarning(false, `The bare ${pc.cyan('import.meta.env')} expression in ${getFilePathToShowToUserModule(id, config)} is replaced with ${pc.cyan('null')} — use ${pc.cyan('import.meta.env.SONE_ENV')} instead ${pc.underline('https://vike.dev/env')}`, { onlyOnce: true });
bareImportMetaEnvRegex.lastIndex = 0; // Reset state after .test() since the /g flag makes the RegExp stateful
magicString.replaceAll(bareImportMetaEnvRegex, JSON.stringify(null));
}
return getMagicStringResult();
},
},
},
];
}
function getEnvPrefix(config) {
const { envPrefix } = config;
if (!envPrefix)
return [];
if (!isArray(envPrefix))
return [envPrefix];
return envPrefix;
}
function assertNoClientSideLeak({ envName, envStatement, envStatementRegExpStr, code, id, config, isBuild, }) {
const isPrivate = !envName.startsWith(PUBLIC_ENV_PREFIX) && !PUBLIC_ENV_ALLOWLIST.includes(envName);
// ✅ All good
if (!isPrivate)
return;
if (!new RegExp(envStatementRegExpStr).test(code))
return true;
// ❌ Security leak!
// - Warning in dev
// - assertUsage() and abort when building for production
const modulePath = getFilePathToShowToUserModule(id, config);
const errMsgAddendum = isBuild ? '' : ' (Vike will prevent your app from building for production)';
const envNameFixed = `${PUBLIC_ENV_PREFIX}${envName}`;
const errMsg = `${envStatement} is used in client-side file ${modulePath} which means that the environment variable ${envName} will be included in client-side bundles and, therefore, ${envName} will be publicly exposed which can be a security leak${errMsgAddendum}. Use ${envStatement} only in server-side files, or rename ${envName} to ${envNameFixed}, see https://vike.dev/env`;
if (isBuild) {
assertUsage(false, errMsg);
}
else {
// - Only a warning for faster development DX (e.g. when user toggles `ssr: boolean` or `onBeforeRenderIsomorph: boolean`).
// - Although only showing a warning can be confusing: https://github.com/vikejs/vike/issues/1641
assertWarning(false, errMsg, { onlyOnce: true });
}
assert(!isBuild); // we should abort if building for production
}