Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Nate Moore
e43c2f56da refactor: make loadEnv async 2022-06-27 14:42:14 -04:00
Nate Moore
02c525ecc0 chore: update lockfile 2022-06-27 14:04:45 -04:00
Nate Moore
832ecbabb1 fix: expose loadEnv hook for config files 2022-06-27 14:04:44 -04:00
21 changed files with 165 additions and 46 deletions

View file

@ -0,0 +1,7 @@
---
'astro': patch
---
Expose `loadEnv` helper in `astro/config` to load environment variables inside of a config file.
Ensure that Vite's [built-in environment variables](https://vitejs.dev/guide/env-and-mode.html#env-variables) are always included.

View file

@ -5,3 +5,8 @@ type AstroUserConfig = import('./dist/types/@types/astro').AstroUserConfig;
* https://astro.build/config
*/
export function defineConfig(config: AstroUserConfig): AstroUserConfig;
/**
* Synchronously load environment variables from default location
*/
export function loadEnv(): Promise<Record<string, any>>;

View file

@ -1,3 +1,11 @@
export async function loadEnv() {
const { loadEnv: loadViteEnv } = await import('vite');
const { MODE } = process.env;
const PROD = MODE === 'production';
const env = loadViteEnv(MODE, process.cwd(), '');
return { ...env, MODE, DEV: !PROD, PROD };
}
export function defineConfig(config) {
return config;
}

View file

@ -64,6 +64,7 @@ export interface AstroComponentMetadata {
/** The flags supported by the Astro CLI */
export interface CLIFlags {
mode?: string;
root?: string;
site?: string;
host?: string | boolean;

View file

@ -382,6 +382,7 @@ function resolveFlags(flags: Partial<Flags>): CLIFlags {
site: typeof flags.site === 'string' ? flags.site : undefined,
port: typeof flags.port === 'number' ? flags.port : undefined,
config: typeof flags.config === 'string' ? flags.config : undefined,
mode: typeof flags.mode === 'string' ? flags.mode : undefined,
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : undefined,
@ -459,6 +460,14 @@ interface OpenConfigResult {
root: string;
}
const PRODUCTION_COMMANDS = new Set(['build', 'preview']);
function resolveMode({ cmd, flags }: Omit<LoadConfigOptions, 'flags'> & { flags: CLIFlags }): string {
const { mode } = flags;
if (mode) return mode;
if (PRODUCTION_COMMANDS.has(cmd)) return 'production';
return 'development';
}
/** Load a configuration file, returning both the userConfig and astroConfig */
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
@ -473,6 +482,14 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
);
}
// Assign built-in env variables to process.env so they're available when using `loadEnv`
// see https://vitejs.dev/guide/env-and-mode.html#env-variables
const mode = resolveMode({ ...configOptions, flags });
const PROD = mode === 'production';
const DEV = !PROD;
Object.assign(process.env, { MODE: mode, PROD: PROD || undefined, DEV: DEV || undefined })
// Automatically load config file using Proload
// If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s`
let config;
@ -501,44 +518,6 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
};
}
/**
* Attempt to load an `astro.config.mjs` file
* @deprecated
*/
export async function loadConfig(configOptions: LoadConfigOptions): Promise<AstroConfig> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
let userConfigPath: string | undefined;
if (flags?.config) {
userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`;
userConfigPath = fileURLToPath(
new URL(userConfigPath, appendForwardSlash(pathToFileURL(root).toString()))
);
}
// Automatically load config file using Proload
// If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s`
let config;
try {
config = await load('astro', {
mustExist: !!userConfigPath,
cwd: root,
filePath: userConfigPath,
});
} catch (err) {
if (err instanceof ProloadError && flags.config) {
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
}
throw err;
}
if (config) {
userConfig = config.value;
}
return resolveConfig(userConfig, root, flags, configOptions.cmd);
}
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
export async function resolveConfig(
userConfig: AstroUserConfig,

View file

@ -77,6 +77,14 @@ export default function envVitePlugin({
if (typeof privateEnv === 'undefined') {
privateEnv = getPrivateEnv(config, astroConfig);
if (privateEnv) {
// Built-in env variables
// See https://vitejs.dev/guide/env-and-mode.html#env-variables
privateEnv.MODE = `'${config.mode}'`;
privateEnv.PROD = config.mode === 'production' ? 'true' : 'false';
privateEnv.DEV = config.mode !== 'production' ? 'true' : 'false';
privateEnv.BASE_URL = astroConfig.base ? `'${astroConfig.base}'` : 'undefined';
// Astro built-in env variables
privateEnv.SITE = astroConfig.site ? `'${astroConfig.site}'` : 'undefined';
privateEnv.SSR = JSON.stringify(true);
const entries = Object.entries(privateEnv).map(([key, value]) => [

View file

@ -1,4 +1,5 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Environment Variables', () => {
@ -21,6 +22,7 @@ describe('Environment Variables', () => {
expect(indexHtml).to.include('CLUB_33');
expect(indexHtml).to.include('BLUE_BAYOU');
expect(indexHtml).to.include('production');
});
it('does render destructured public env and private env', async () => {
@ -28,6 +30,7 @@ describe('Environment Variables', () => {
expect(indexHtml).to.include('CLUB_33');
expect(indexHtml).to.include('BLUE_BAYOU');
expect(indexHtml).to.include('production');
});
it('does render builtin SITE env', async () => {
@ -62,6 +65,27 @@ describe('Environment Variables', () => {
expect(found).to.equal(true, 'found the public env variable in the JS build');
});
it('includes built-in MODE in client-side JS', async () => {
let dirs = await fixture.readdir('/');
let found = false;
// Look in all of the .js files to see if the public env is inlined.
// Testing this way prevents hardcoding expected js files.
// If we find it in any of them that's good enough to know its working.
await Promise.all(
dirs.map(async (path) => {
if (path.endsWith('.js')) {
let js = await fixture.readFile(`/${path}`);
if (js.includes('production')) {
found = true;
}
}
})
);
expect(found).to.equal(true, 'found the public env variable in the JS build');
});
it('does not include private env in client-side JS', async () => {
let dirs = await fixture.readdir('/');
let found = false;

View file

@ -3,7 +3,7 @@
import { fileURLToPath } from 'url';
import { performance } from 'perf_hooks';
import { build as astroBuild } from '#astro/build';
import { loadConfig } from '#astro/config';
import { openConfig } from '#astro/config';
import { Benchmark } from './benchmark.js';
import del from 'del';
import { Writable } from 'stream';
@ -24,7 +24,7 @@ export const errorWritable = new Writable({
let build;
async function setupBuild() {
const astroConfig = await loadConfig(fileURLToPath(snowpackExampleRoot));
const { astroConfig } = await openConfig({ cwd: fileURLToPath(snowpackExampleRoot) });
const logging = {
level: 'error',

View file

@ -0,0 +1,25 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
// NOTE: These tests use two different (but identical) fixtures!
// This ensures that Node's `require` cache doesn't break our tests
describe('config loadEnv', () => {
it('sets mode to development', async () => {
const fixture = await loadFixture({ root: './fixtures/config-env-1/' }, { cmd: 'dev' });
await fixture.build();
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('#site').text()).to.match(/development\.my-site\.com/);
});
it('sets mode to production', async () => {
const fixture = await loadFixture({ root: './fixtures/config-env-2/' }, { cmd: 'build' });
await fixture.build();
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('#site').text()).to.match(/production\.my-site\.com/);
});
});

View file

@ -2,6 +2,9 @@
<div id="client-component">
{{ PUBLIC_PLACE }}
</div>
<div id="client-mode">
{{ MODE }}
</div>
</template>
<script>
@ -10,6 +13,7 @@ export default {
return {
PUBLIC_PLACE: import.meta.env.PUBLIC_PLACE,
SECRET_PLACE: import.meta.env.SECRET_PLACE,
MODE: import.meta.env.MODE,
};
},
};

View file

@ -1,6 +1,7 @@
---
const { PUBLIC_PLACE, SECRET_PLACE, SITE } = import.meta.env;
const { PUBLIC_PLACE, SECRET_PLACE, SITE, MODE } = import.meta.env;
---
<environment-variable>{PUBLIC_PLACE}</environment-variable>
<environment-variable>{SECRET_PLACE}</environment-variable>
<environment-variable>{SITE}</environment-variable>
<environment-variable>{MODE}</environment-variable>

View file

@ -4,4 +4,5 @@ import Client from '../components/Client.vue';
<environment-variable>{import.meta.env.PUBLIC_PLACE}</environment-variable>
<environment-variable>{import.meta.env.SECRET_PLACE}</environment-variable>
<environment-variable>{import.meta.env.SITE}</environment-variable>
<environment-variable>{import.meta.env.MODE}</environment-variable>
<Client client:load />

View file

@ -0,0 +1,7 @@
import { defineConfig, loadEnv } from 'astro/config';
const { MODE } = await loadEnv();
export default defineConfig({
site: `https://${MODE}.my-site.com`
})

View file

@ -0,0 +1,8 @@
{
"name": "@test/config-env",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,5 @@
---
const { site } = Astro;
---
<div id="site">{site}</div>

View file

@ -0,0 +1,7 @@
import { defineConfig, loadEnv } from 'astro/config';
const { MODE } = await loadEnv();
export default defineConfig({
site: `https://${MODE}.my-site.com`
})

View file

@ -0,0 +1,8 @@
{
"name": "@test/config-env",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,5 @@
---
const { site } = Astro;
---
<div id="site">{site}</div>

View file

@ -2,7 +2,7 @@ import { execa } from 'execa';
import { polyfill } from '@astrojs/webapi';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { resolveConfig, loadConfig } from '../dist/core/config.js';
import { openConfig } from '../dist/core/config.js';
import dev from '../dist/core/dev/index.js';
import build from '../dist/core/build/index.js';
import preview from '../dist/core/preview/index.js';
@ -35,11 +35,15 @@ polyfill(globalThis, {
* @property {() => Promise<void>} clean
* @property {() => Promise<App>} loadTestAdapterApp
* @property {() => Promise<void>} onNextChange
*
* @typedef {Object} FixtureOpts
* @property {'dev'|'build'|undefined} cmd
*/
/**
* Load Astro fixture
* @param {AstroConfig} inlineConfig Astro config partial (note: must specify `root`)
* @param {FixtureOpts|undefined} opts Additional options for fixture
* @returns {Promise<Fixture>} The fixture. Has the following properties:
* .config - Returns the final config. Will be automatically passed to the methods below:
*
@ -57,7 +61,7 @@ polyfill(globalThis, {
* Clean-up
* .clean() - Async. Removes the projects dist folder.
*/
export async function loadFixture(inlineConfig) {
export async function loadFixture(inlineConfig, opts) {
if (!inlineConfig || !inlineConfig.root)
throw new Error("Must provide { root: './fixtures/...' }");
@ -72,7 +76,7 @@ export async function loadFixture(inlineConfig) {
}
}
// Load the config.
let config = await loadConfig({ cwd: fileURLToPath(cwd) });
let { astroConfig: config } = await openConfig({ cwd: fileURLToPath(cwd), cmd: opts?.cmd });
config = merge(config, { ...inlineConfig, root: cwd });
// Note: the inline config doesn't run through config validation where these normalizations usually occur

12
pnpm-lock.yaml generated
View file

@ -1347,6 +1347,18 @@ importers:
devDependencies:
astro: link:../../..
packages/astro/test/fixtures/config-env-1:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/config-env-2:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/config-host:
specifiers:
astro: workspace:*

View file

@ -1,7 +1,7 @@
import { fileURLToPath } from 'url';
import v8 from 'v8';
import dev from '../../packages/astro/dist/core/dev/index.js';
import { loadConfig } from '../../packages/astro/dist/core/config.js';
import { openConfig } from '../../packages/astro/dist/core/config.js';
import prettyBytes from 'pretty-bytes';
if (!global.gc) {
@ -14,7 +14,7 @@ const isCI = process.argv.includes('--ci');
/** URL directory containing the entire project. */
const projDir = new URL('./project/', import.meta.url);
let config = await loadConfig({
let { astroConfig: config } = await openConfig({
cwd: fileURLToPath(projDir),
});