Skip to content

Commit f935680

Browse files
committed
migrate to experimental astro:env, separate config and processEnv schemas
1 parent 04b27ed commit f935680

File tree

11 files changed

+1085
-880
lines changed

11 files changed

+1085
-880
lines changed

astro.config.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,33 @@ import { defineConfig } from 'astro/config';
66

77
// must use relative imports, and their entire import subtrees
88
import { remarkReadingTime } from './plugins/remark-reading-time.mjs';
9+
//
910
// all relative imports in subtree
10-
import { CONFIG } from './src/config';
11+
import { envSchema, PROCESS_ENV } from './src/env';
1112
import { expressiveCodeIntegration } from './src/libs/integrations/expressive-code';
1213
import { sitemapIntegration } from './src/libs/integrations/sitemap';
1314

14-
const { SITE_URL } = CONFIG;
15+
const { SITE_URL } = PROCESS_ENV;
1516
const remarkPlugins = [remarkReadingTime];
1617

1718
export default defineConfig({
1819
site: SITE_URL,
20+
experimental: { env: envSchema },
1921
trailingSlash: 'ignore',
2022
// default
2123
compressHTML: true,
22-
server: {
23-
port: 3000,
24-
},
25-
devToolbar: {
26-
enabled: false,
27-
},
24+
server: { port: 3000 },
25+
devToolbar: { enabled: false },
2826
integrations: [
2927
expressiveCodeIntegration(),
3028
sitemapIntegration(),
3129
react(),
32-
// applyBaseStyles: false prevents double loading of tailwind
33-
tailwind({
34-
applyBaseStyles: false,
35-
}),
3630
mdx(),
37-
icon({
38-
iconDir: 'src/assets/icons',
39-
}),
31+
// applyBaseStyles: false prevents double loading of tailwind
32+
tailwind({ applyBaseStyles: false }),
33+
icon({ iconDir: 'src/assets/icons' }),
4034
],
41-
markdown: {
42-
remarkPlugins,
43-
},
35+
markdown: { remarkPlugins },
4436
vite: {
4537
build: {
4638
sourcemap: false,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@astrojs/tailwind": "^5.1.0",
3131
"@fontsource-variable/inter": "^5.0.20",
3232
"@tailwindcss/typography": "^0.5.13",
33-
"astro": "^4.13.1",
33+
"astro": "^4.14.4",
3434
"astro-embed": "^0.7.2",
3535
"astro-expressive-code": "^0.35.6",
3636
"astro-icon": "^1.1.0",

src/components/Giscus.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const { class: className } = Astro.props;
3535

3636
import { SELECTORS } from '@/constants/dom';
3737
import { THEME_CONFIG } from '@/constants/theme';
38-
import { getCurrentMode, sendModeToGiscus } from '@/utils/theme';
38+
import { sendModeToGiscus } from '@/utils/dom';
39+
import { getCurrentMode } from '@/utils/theme';
3940

4041
import type { ChangeThemeCustomEvent } from '@/types/constants';
4142

src/config.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,20 @@
1-
// all relative imports in config subtree
2-
import dotenv from 'dotenv';
1+
import { SITE_URL } from 'astro:env/client';
2+
import { NODE_ENV, PREVIEW_MODE } from 'astro:env/server';
33

4-
import { configSchema, nodeEnvValues } from './schemas/config';
5-
import { validateConfig } from './utils/config';
4+
import { configSchema } from '@/schemas/config';
5+
import { prettyPrintObject } from '@/utils/log';
6+
import { validateData } from '@/utils/validation';
67

7-
import type { ConfigType } from './types/config';
8-
9-
/*------------------ load .env file -----------------*/
10-
11-
// import.meta.env is not available in astro.config.mjs, only after the config is loaded.
12-
// ! MUST use process.env for vars used in astro.config.mjs.
13-
// https://github.com/withastro/astro/issues?q=.env+file+not+loaded
14-
15-
const NODE_ENV = process.env.NODE_ENV;
16-
17-
if (!nodeEnvValues.includes(NODE_ENV)) {
18-
console.error('Invalid process.env.NODE_ENV: ', NODE_ENV);
19-
throw new Error('Invalid process.env.NODE_ENV');
20-
}
21-
22-
const envFileName = `.env.${NODE_ENV}`;
23-
dotenv.config({ path: envFileName });
8+
import type { ConfigType } from '@/types/config';
249

2510
/*-------------------- configData -------------------*/
2611

2712
/** SSG - all env vars are build time only. */
2813
const configData: ConfigType = {
29-
NODE_ENV: process.env.NODE_ENV,
30-
PREVIEW_MODE: process.env.PREVIEW_MODE,
14+
NODE_ENV,
15+
PREVIEW_MODE,
3116
/** all urls without '/' */
32-
SITE_URL: process.env.SITE_URL,
17+
SITE_URL,
3318
SITE_TITLE: 'Nemanja Mitic',
3419
SITE_DESCRIPTION: 'I am Nemanja, full stack developer',
3520
PAGE_SIZE_POST_CARD: 3,
@@ -47,4 +32,6 @@ const configData: ConfigType = {
4732
};
4833

4934
// todo: Config should go into import.meta.env in astro.config.ts
50-
export const CONFIG = validateConfig(configData, configSchema);
35+
export const CONFIG = validateData(configData, configSchema);
36+
37+
prettyPrintObject(CONFIG, 'parsed CONFIG');

src/env.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { envField } from 'astro/config';
2+
3+
// all relative imports in env subtree
4+
import dotenv from 'dotenv';
5+
6+
import { nodeEnvValues, processEnvSchema } from './schemas/config';
7+
import { prettyPrintObject } from './utils/log';
8+
import { validateData } from './utils/validation';
9+
10+
import type { ProcessEnvType } from './types/config';
11+
12+
/*------------------ load .env file -----------------*/
13+
14+
// import.meta.env is not available in astro.config.mjs, only after the config is loaded.
15+
// ! MUST use process.env for vars used in astro.config.mjs.
16+
// https://github.com/withastro/astro/issues?q=.env+file+not+loaded
17+
18+
const NODE_ENV = process.env.NODE_ENV;
19+
20+
if (!nodeEnvValues.includes(NODE_ENV)) {
21+
console.error('Invalid process.env.NODE_ENV: ', NODE_ENV);
22+
throw new Error('Invalid process.env.NODE_ENV');
23+
}
24+
25+
const envFileName = `.env.${NODE_ENV}`;
26+
dotenv.config({ path: envFileName });
27+
28+
/*------------------ validate processEnvData -----------------*/
29+
30+
const processEnvData: ProcessEnvType = {
31+
NODE_ENV: process.env.NODE_ENV,
32+
PREVIEW_MODE: process.env.PREVIEW_MODE,
33+
SITE_URL: process.env.SITE_URL,
34+
};
35+
36+
export const PROCESS_ENV = validateData(processEnvData, processEnvSchema);
37+
38+
prettyPrintObject(PROCESS_ENV, 'parsed PROCESS_ENV');
39+
40+
/*------------------ experimental.env.schema -----------------*/
41+
42+
export const envSchema = {
43+
schema: {
44+
// server
45+
NODE_ENV: envField.string({
46+
context: 'server',
47+
access: 'public',
48+
default: 'development',
49+
}),
50+
PREVIEW_MODE: envField.boolean({
51+
context: 'server',
52+
access: 'public',
53+
default: false,
54+
}),
55+
// client
56+
SITE_URL: envField.string({
57+
context: 'client',
58+
access: 'public',
59+
default: 'http://localhost:3000',
60+
}),
61+
},
62+
};

src/schemas/config.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ export const booleanValues = ['true', 'false', ''] as const;
66
export const modeValues = ['light', 'dark'] as const;
77
export const themeValues = ['default-light', 'default-dark', 'green-light', 'green-dark'] as const;
88

9-
export const configSchema = z.object({
9+
export const processEnvSchema = z.object({
1010
NODE_ENV: z.enum(nodeEnvValues),
1111
PREVIEW_MODE: z
1212
.enum(booleanValues)
1313
.transform((value) => value === 'true')
1414
.default('false'),
1515
// ensure no trailing slash
1616
SITE_URL: z.string().url().regex(/[^/]$/, 'SITE_URL should not end with a slash "/"'),
17-
SITE_TITLE: z.string().min(1),
18-
SITE_DESCRIPTION: z.string().min(1),
19-
PAGE_SIZE_POST_CARD: z.number(),
20-
PAGE_SIZE_POST_CARD_SMALL: z.number(),
21-
MORE_POSTS_COUNT: z.number(),
22-
DEFAULT_MODE: z.enum(modeValues), // check that theme and mode match
23-
DEFAULT_THEME: z.enum(themeValues),
24-
AUTHOR_NAME: z.string().min(1),
25-
AUTHOR_EMAIL: z.string().email(),
26-
AUTHOR_GITHUB: z.string().url(),
27-
AUTHOR_LINKEDIN: z.string().url(),
28-
AUTHOR_TWITTER: z.string().url(),
29-
AUTHOR_YOUTUBE: z.string().url(),
30-
REPO_URL: z.string().url(),
3117
});
18+
19+
export const configSchema = z
20+
.object({
21+
SITE_TITLE: z.string().min(1),
22+
SITE_DESCRIPTION: z.string().min(1),
23+
PAGE_SIZE_POST_CARD: z.number(),
24+
PAGE_SIZE_POST_CARD_SMALL: z.number(),
25+
MORE_POSTS_COUNT: z.number(),
26+
DEFAULT_MODE: z.enum(modeValues), // check that theme and mode match
27+
DEFAULT_THEME: z.enum(themeValues),
28+
AUTHOR_NAME: z.string().min(1),
29+
AUTHOR_EMAIL: z.string().email(),
30+
AUTHOR_GITHUB: z.string().url(),
31+
AUTHOR_LINKEDIN: z.string().url(),
32+
AUTHOR_TWITTER: z.string().url(),
33+
AUTHOR_YOUTUBE: z.string().url(),
34+
REPO_URL: z.string().url(),
35+
})
36+
.merge(processEnvSchema);

src/types/config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { configSchema } from '../schemas/config';
2-
31
import type { z } from 'zod';
2+
import type { configSchema, processEnvSchema } from '../schemas/config';
43

54
export type ConfigSchemaType = typeof configSchema;
65
export type ConfigType = z.infer<ConfigSchemaType>;
6+
7+
export type ProcessEnvSchemaType = typeof processEnvSchema;
8+
export type ProcessEnvType = z.infer<ProcessEnvSchemaType>;

src/utils/config.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/utils/log.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @ts-expect-error, js lib
2+
import treeify from 'object-treeify';
3+
4+
export const prettyPrintObject = (object: Record<string, unknown>, prefix = ''): void => {
5+
const stringData = treeify(object);
6+
console.log(`${prefix}:\n\n${stringData}`);
7+
};

src/utils/validation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z, ZodSchema } from 'zod';
2+
3+
export const validateData = <T extends ZodSchema>(config: z.infer<T>, schema: T): z.infer<T> => {
4+
const parsedConfig = schema.safeParse(config);
5+
6+
if (!parsedConfig.success) {
7+
console.error('Validation failed: ', parsedConfig.error.flatten().fieldErrors);
8+
throw new Error('Validation failed');
9+
}
10+
11+
const { data: parsedConfigData } = parsedConfig;
12+
13+
return parsedConfigData;
14+
};

0 commit comments

Comments
 (0)