Skip to content

Commit

Permalink
fix(astro): astro sync and astro:env (#11326)
Browse files Browse the repository at this point in the history
  • Loading branch information
florian-lefebvre authored Jun 25, 2024
1 parent 4c4741b commit 41121fb
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-ducks-play.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion packages/astro/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function getViteConfig(
astroContentListenPlugin({ settings, logger, fs }),
],
},
{ settings, logger, mode }
{ settings, logger, mode, sync: false }
);
await runHookConfigDone({ settings, logger });
return mergeConfig(viteConfig, userViteConfig);
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class AstroBuilder {
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 });

Expand Down
5 changes: 3 additions & 2 deletions packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface CreateViteOptions {
// will be undefined when using `getViteConfig`
command?: 'dev' | 'build';
fs?: typeof nodeFs;
sync: boolean;
}

const ALWAYS_NOEXTERNAL = [
Expand Down Expand Up @@ -74,7 +75,7 @@ const ONLY_DEV_EXTERNAL = [
/** Return a base vite config as a common starting point for all Vite commands. */
export async function createVite(
commandConfig: vite.InlineConfig,
{ settings, logger, mode, command, fs = nodeFs }: CreateViteOptions
{ settings, logger, mode, command, fs = nodeFs, sync }: CreateViteOptions
): Promise<vite.InlineConfig> {
const astroPkgsConfig = await crawlFrameworkPkgs({
root: fileURLToPath(settings.config.root),
Expand Down Expand Up @@ -137,7 +138,7 @@ export async function createVite(
// the build to run very slow as the filewatcher is triggered often.
mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }),
envVitePlugin({ settings }),
astroEnv({ settings, mode, fs }),
astroEnv({ settings, mode, fs, sync }),
markdownVitePlugin({ settings, logger }),
htmlVitePlugin(),
mdxVitePlugin(),
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/dev/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export async function createContainer({
include: rendererClientEntries,
},
},
{ settings, logger, mode: 'dev', command: 'dev', fs }
{ settings, logger, mode: 'dev', command: 'dev', fs, sync: false }
);
await runHookConfigDone({ settings, logger });
const viteServer = await vite.createServer(viteConfig);
Expand Down
4 changes: 3 additions & 1 deletion packages/astro/src/core/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import type { Logger } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js';
import { ensureProcessNodeEnv } from '../util.js';
import { syncAstroEnv } from '../../env/sync.js';

export type ProcessExit = 0 | 1;

Expand Down Expand Up @@ -83,6 +84,7 @@ export default async function sync(
await dbPackage?.typegen?.(astroConfig);
const exitCode = await syncContentCollections(settings, { ...options, logger });
if (exitCode !== 0) return exitCode;
syncAstroEnv(settings, options?.fs);

logger.info(null, `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
return 0;
Expand Down Expand Up @@ -123,7 +125,7 @@ export async function syncContentCollections(
ssr: { external: [] },
logLevel: 'silent',
},
{ settings, logger, mode: 'build', command: 'build', fs }
{ settings, logger, mode: 'build', command: 'build', fs, sync: true }
)
);

Expand Down
30 changes: 30 additions & 0 deletions packages/astro/src/env/sync.ts
Original file line number Diff line number Diff line change
@@ -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');
}
108 changes: 24 additions & 84 deletions packages/astro/src/env/vite-plugin-env.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import type fsMod from 'node:fs';
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 { AstroError, AstroErrorData } from '../core/errors/index.js';
import {
ENV_TYPES_FILE,
MODULE_TEMPLATE_URL,
TYPES_TEMPLATE_URL,
VIRTUAL_MODULES_IDS,
VIRTUAL_MODULES_IDS_VALUES,
} from './constants.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
// Types should always be generated (like in types/content.d.ts). That means the client module will be empty
Expand All @@ -23,14 +21,16 @@ interface AstroEnvVirtualModPluginParams {
settings: AstroSettings;
mode: 'dev' | 'build' | string;
fs: typeof fsMod;
sync: boolean;
}

export function astroEnv({
settings,
mode,
fs,
sync,
}: AstroEnvVirtualModPluginParams): Plugin | undefined {
if (!settings.config.experimental.env) {
if (!settings.config.experimental.env || sync) {
return;
}
const schema = settings.config.experimental.env.schema ?? {};
Expand All @@ -54,23 +54,10 @@ export function astroEnv({

const validatedVariables = validatePublicVariables({ schema, loadedEnv });

const clientTemplates = getClientTemplates({ validatedVariables });
const serverTemplates = getServerTemplates({ validatedVariables, schema, fs });

templates = {
client: clientTemplates.module,
server: serverTemplates.module,
...getTemplates(schema, fs, validatedVariables),
internal: `export const schema = ${JSON.stringify(schema)};`,
};
generateDts({
settings,
fs,
content: getDts({
fs,
client: clientTemplates.types,
server: serverTemplates.types,
}),
});
},
buildEnd() {
templates = null;
Expand Down Expand Up @@ -104,19 +91,6 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
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({
schema,
loadedEnv,
Expand Down Expand Up @@ -152,71 +126,37 @@ function validatePublicVariables({
return valid;
}

function getDts({
client,
server,
fs,
}: {
client: string;
server: string;
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 = '';
function getTemplates(
schema: EnvSchema,
fs: typeof fsMod,
validatedVariables: ReturnType<typeof validatePublicVariables>
) {
let client = '';
let server = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
let onSetGetEnv = '';

for (const { key, type, value } of validatedVariables.filter((e) => e.context === 'server')) {
module += `export const ${key} = ${JSON.stringify(value)};`;
types += `export const ${key}: ${type}; \n`;
for (const { key, value, context } of validatedVariables) {
const str = `export const ${key} = ${JSON.stringify(value)};`;
if (context === 'client') {
client += str;
} else {
server += str;
}
}

for (const [key, options] of Object.entries(schema)) {
if (!(options.context === 'server' && options.access === 'secret')) {
continue;
}

types += `export const ${key}: ${getEnvFieldType(options)}; \n`;
module += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
server += `export let ${key} = _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 {
module,
types,
client,
server,
};
}
16 changes: 15 additions & 1 deletion packages/astro/test/astro-sync.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
thenFileShouldExist(path) {
Expand Down Expand Up @@ -164,6 +167,17 @@ describe('astro sync', () => {
`/// <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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }),
}
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/astro-env-required-public",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/base"
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 41121fb

Please sign in to comment.