vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
140 lines (139 loc) • 6.99 kB
JavaScript
import '../assertEnvVite.js';
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 { lowerFirst } 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';
const PUBLIC_ENV_PREFIX = 'PUBLIC_ENV__';
const PUBLIC_ENV_ALLOWLIST = [
// https://github.com/vikejs/vike/issues/1724
'STORYBOOK',
];
// 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 skipNodeModules = '/node_modules/';
const skipIrrelevant = 'import.meta.env.';
const filterRolldown = {
id: {
exclude: `**${skipNodeModules}**`,
},
code: {
include: skipIrrelevant,
},
};
const filterFunction = (id, code) => {
if (id.includes(skipNodeModules))
return false;
if (!code.includes(skipIrrelevant))
return false;
return true;
};
// ===
function pluginReplaceConstantsEnvVars() {
let envVarsAll;
let envPrefix;
let config;
return [
{
name: '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(lowerFirst((plugin) => (plugin.name === 'vite:define' ? 1 : 0)));
},
},
transform: {
filter: filterRolldown,
handler(code, id, options) {
id = normalizeId(id);
assertPosixPath(id);
assertPosixPath(config.root);
if (!id.startsWith(config.root))
return; // skip linked dependencies
assert(filterFunction(id, 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));
});
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
}