0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-13 22:11:20 -05:00

fix(astro): astro sync and astro:env (#11326)

This commit is contained in:
Florian Lefebvre 2024-06-25 14:51:55 +02:00 committed by GitHub
parent 4c4741b42d
commit 41121fbe00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 113 additions and 91 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a case where running `astro sync` when using the experimental `astro:env` feature would fail if environment variables were missing

View file

@ -48,7 +48,7 @@ export function getViteConfig(
astroContentListenPlugin({ settings, logger, fs }), astroContentListenPlugin({ settings, logger, fs }),
], ],
}, },
{ settings, logger, mode } { settings, logger, mode, sync: false }
); );
await runHookConfigDone({ settings, logger }); await runHookConfigDone({ settings, logger });
return mergeConfig(viteConfig, userViteConfig); return mergeConfig(viteConfig, userViteConfig);

View file

@ -139,7 +139,7 @@ class AstroBuilder {
middlewareMode: true, middlewareMode: true,
}, },
}, },
{ settings: this.settings, logger: this.logger, mode: 'build', command: 'build' } { settings: this.settings, logger: this.logger, mode: 'build', command: 'build', sync: false }
); );
await runHookConfigDone({ settings: this.settings, logger: logger }); await runHookConfigDone({ settings: this.settings, logger: logger });

View file

@ -47,6 +47,7 @@ interface CreateViteOptions {
// will be undefined when using `getViteConfig` // will be undefined when using `getViteConfig`
command?: 'dev' | 'build'; command?: 'dev' | 'build';
fs?: typeof nodeFs; fs?: typeof nodeFs;
sync: boolean;
} }
const ALWAYS_NOEXTERNAL = [ const ALWAYS_NOEXTERNAL = [
@ -74,7 +75,7 @@ const ONLY_DEV_EXTERNAL = [
/** Return a base vite config as a common starting point for all Vite commands. */ /** Return a base vite config as a common starting point for all Vite commands. */
export async function createVite( export async function createVite(
commandConfig: vite.InlineConfig, commandConfig: vite.InlineConfig,
{ settings, logger, mode, command, fs = nodeFs }: CreateViteOptions { settings, logger, mode, command, fs = nodeFs, sync }: CreateViteOptions
): Promise<vite.InlineConfig> { ): Promise<vite.InlineConfig> {
const astroPkgsConfig = await crawlFrameworkPkgs({ const astroPkgsConfig = await crawlFrameworkPkgs({
root: fileURLToPath(settings.config.root), root: fileURLToPath(settings.config.root),
@ -137,7 +138,7 @@ export async function createVite(
// the build to run very slow as the filewatcher is triggered often. // the build to run very slow as the filewatcher is triggered often.
mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }), mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }),
envVitePlugin({ settings }), envVitePlugin({ settings }),
astroEnv({ settings, mode, fs }), astroEnv({ settings, mode, fs, sync }),
markdownVitePlugin({ settings, logger }), markdownVitePlugin({ settings, logger }),
htmlVitePlugin(), htmlVitePlugin(),
mdxVitePlugin(), mdxVitePlugin(),

View file

@ -74,7 +74,7 @@ export async function createContainer({
include: rendererClientEntries, include: rendererClientEntries,
}, },
}, },
{ settings, logger, mode: 'dev', command: 'dev', fs } { settings, logger, mode: 'dev', command: 'dev', fs, sync: false }
); );
await runHookConfigDone({ settings, logger }); await runHookConfigDone({ settings, logger });
const viteServer = await vite.createServer(viteConfig); const viteServer = await vite.createServer(viteConfig);

View file

@ -27,6 +27,7 @@ import {
import type { Logger } from '../logger/core.js'; import type { Logger } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js'; import { formatErrorMessage } from '../messages.js';
import { ensureProcessNodeEnv } from '../util.js'; import { ensureProcessNodeEnv } from '../util.js';
import { syncAstroEnv } from '../../env/sync.js';
export type ProcessExit = 0 | 1; export type ProcessExit = 0 | 1;
@ -83,6 +84,7 @@ export default async function sync(
await dbPackage?.typegen?.(astroConfig); await dbPackage?.typegen?.(astroConfig);
const exitCode = await syncContentCollections(settings, { ...options, logger }); const exitCode = await syncContentCollections(settings, { ...options, logger });
if (exitCode !== 0) return exitCode; if (exitCode !== 0) return exitCode;
syncAstroEnv(settings, options?.fs);
logger.info(null, `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`); logger.info(null, `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
return 0; return 0;
@ -123,7 +125,7 @@ export async function syncContentCollections(
ssr: { external: [] }, ssr: { external: [] },
logLevel: 'silent', logLevel: 'silent',
}, },
{ settings, logger, mode: 'build', command: 'build', fs } { settings, logger, mode: 'build', command: 'build', fs, sync: true }
) )
); );

30
packages/astro/src/env/sync.ts vendored Normal file
View file

@ -0,0 +1,30 @@
import type { AstroSettings } from '../@types/astro.js';
import fsMod from 'node:fs';
import { getEnvFieldType } from './validators.js';
import { ENV_TYPES_FILE, TYPES_TEMPLATE_URL } from './constants.js';
export function syncAstroEnv(settings: AstroSettings, fs = fsMod) {
if (!settings.config.experimental.env) {
return;
}
const schema = settings.config.experimental.env.schema ?? {};
let client = '';
let server = '';
for (const [key, options] of Object.entries(schema)) {
const str = `export const ${key}: ${getEnvFieldType(options)}; \n`;
if (options.context === 'client') {
client += str;
} else {
server += str;
}
}
const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8');
const dts = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server);
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), dts, 'utf-8');
}

View file

@ -1,17 +1,15 @@
import type fsMod from 'node:fs'; import type fsMod from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { type Plugin, loadEnv } from 'vite'; import { loadEnv, type Plugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js'; import type { AstroSettings } from '../@types/astro.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { import {
ENV_TYPES_FILE,
MODULE_TEMPLATE_URL, MODULE_TEMPLATE_URL,
TYPES_TEMPLATE_URL,
VIRTUAL_MODULES_IDS, VIRTUAL_MODULES_IDS,
VIRTUAL_MODULES_IDS_VALUES, VIRTUAL_MODULES_IDS_VALUES,
} from './constants.js'; } from './constants.js';
import type { EnvSchema } from './schema.js'; import type { EnvSchema } from './schema.js';
import { getEnvFieldType, validateEnvVariable } from './validators.js'; import { validateEnvVariable } from './validators.js';
// TODO: reminders for when astro:env comes out of experimental // TODO: reminders for when astro:env comes out of experimental
// Types should always be generated (like in types/content.d.ts). That means the client module will be empty // Types should always be generated (like in types/content.d.ts). That means the client module will be empty
@ -23,14 +21,16 @@ interface AstroEnvVirtualModPluginParams {
settings: AstroSettings; settings: AstroSettings;
mode: 'dev' | 'build' | string; mode: 'dev' | 'build' | string;
fs: typeof fsMod; fs: typeof fsMod;
sync: boolean;
} }
export function astroEnv({ export function astroEnv({
settings, settings,
mode, mode,
fs, fs,
sync,
}: AstroEnvVirtualModPluginParams): Plugin | undefined { }: AstroEnvVirtualModPluginParams): Plugin | undefined {
if (!settings.config.experimental.env) { if (!settings.config.experimental.env || sync) {
return; return;
} }
const schema = settings.config.experimental.env.schema ?? {}; const schema = settings.config.experimental.env.schema ?? {};
@ -54,23 +54,10 @@ export function astroEnv({
const validatedVariables = validatePublicVariables({ schema, loadedEnv }); const validatedVariables = validatePublicVariables({ schema, loadedEnv });
const clientTemplates = getClientTemplates({ validatedVariables });
const serverTemplates = getServerTemplates({ validatedVariables, schema, fs });
templates = { templates = {
client: clientTemplates.module, ...getTemplates(schema, fs, validatedVariables),
server: serverTemplates.module,
internal: `export const schema = ${JSON.stringify(schema)};`, internal: `export const schema = ${JSON.stringify(schema)};`,
}; };
generateDts({
settings,
fs,
content: getDts({
fs,
client: clientTemplates.types,
server: serverTemplates.types,
}),
});
}, },
buildEnd() { buildEnd() {
templates = null; templates = null;
@ -104,19 +91,6 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
return `\0${id}`; return `\0${id}`;
} }
function generateDts({
content,
settings,
fs,
}: {
content: string;
settings: AstroSettings;
fs: typeof fsMod;
}) {
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), content, 'utf-8');
}
function validatePublicVariables({ function validatePublicVariables({
schema, schema,
loadedEnv, loadedEnv,
@ -152,55 +126,22 @@ function validatePublicVariables({
return valid; return valid;
} }
function getDts({ function getTemplates(
client, schema: EnvSchema,
server, fs: typeof fsMod,
fs, validatedVariables: ReturnType<typeof validatePublicVariables>
}: { ) {
client: string; let client = '';
server: string; let server = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
fs: typeof fsMod;
}) {
const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8');
return template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server);
}
function getClientTemplates({
validatedVariables,
}: {
validatedVariables: ReturnType<typeof validatePublicVariables>;
}) {
let module = '';
let types = '';
for (const { key, type, value } of validatedVariables.filter((e) => e.context === 'client')) {
module += `export const ${key} = ${JSON.stringify(value)};`;
types += `export const ${key}: ${type}; \n`;
}
return {
module,
types,
};
}
function getServerTemplates({
validatedVariables,
schema,
fs,
}: {
validatedVariables: ReturnType<typeof validatePublicVariables>;
schema: EnvSchema;
fs: typeof fsMod;
}) {
let module = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
let types = '';
let onSetGetEnv = ''; let onSetGetEnv = '';
for (const { key, type, value } of validatedVariables.filter((e) => e.context === 'server')) { for (const { key, value, context } of validatedVariables) {
module += `export const ${key} = ${JSON.stringify(value)};`; const str = `export const ${key} = ${JSON.stringify(value)};`;
types += `export const ${key}: ${type}; \n`; if (context === 'client') {
client += str;
} else {
server += str;
}
} }
for (const [key, options] of Object.entries(schema)) { for (const [key, options] of Object.entries(schema)) {
@ -208,15 +149,14 @@ function getServerTemplates({
continue; continue;
} }
types += `export const ${key}: ${getEnvFieldType(options)}; \n`; server += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
module += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`; onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`;
} }
module = module.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv); server = server.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv);
return { return {
module, client,
types, server,
}; };
} }

View file

@ -47,7 +47,10 @@ const createFixture = () => {
}, },
}; };
await astroFixture.sync({}, { fs: fsMock }); const code = await astroFixture.sync({}, { fs: fsMock });
if (code !== 0) {
throw new Error(`Process error code ${code}`);
}
}, },
/** @param {string} path */ /** @param {string} path */
thenFileShouldExist(path) { thenFileShouldExist(path) {
@ -164,6 +167,17 @@ describe('astro sync', () => {
`/// <reference path="../.astro/env.d.ts" />` `/// <reference path="../.astro/env.d.ts" />`
); );
}); });
it('Does not throw if a public variable is required', async () => {
let error = null;
try {
await fixture.whenSyncing('./fixtures/astro-env-required-public/');
} catch (e) {
error = e;
}
assert.equal(error, null, 'Syncing should not throw astro:env validation errors');
});
}); });
describe('Astro Actions', () => { describe('Astro Actions', () => {

View file

@ -0,0 +1,13 @@
import { defineConfig, envField } from 'astro/config';
// https://astro.build/config
export default defineConfig({
experimental: {
env: {
schema: {
FOO: envField.string({ context: "client", access: "public" }),
BAR: envField.number({ context: "server", access: "public" }),
}
}
}
});

View file

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

View file

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/base"
}

6
pnpm-lock.yaml generated
View file

@ -2121,6 +2121,12 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../.. version: link:../../..
packages/astro/test/fixtures/astro-env-required-public:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/astro-env-server-fail: packages/astro/test/fixtures/astro-env-server-fail:
dependencies: dependencies:
astro: astro: