Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Loading