-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
config.ts
367 lines (330 loc) · 10.9 KB
/
config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroConfig, AstroUserConfig, CLIFlags } from '../../@types/astro';
import load, { ProloadError, resolve } from '@proload/core';
import loadTypeScript from '@proload/plugin-tsm';
import fs from 'fs';
import * as colors from 'kleur/colors';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import * as vite from 'vite';
import { mergeConfig as mergeViteConfig } from 'vite';
import { LogOptions } from '../logger/core.js';
import { arraify, isObject } from '../util.js';
import { createRelativeSchema } from './schema.js';
load.use([loadTypeScript]);
export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
'projectRoot',
'src',
'pages',
'public',
'dist',
'styleOptions',
'markdownOptions',
'buildOptions',
'devOptions',
]);
/** Turn raw config values into normalized values */
export async function validateConfig(
userConfig: any,
root: string,
cmd: string,
logging: LogOptions
): Promise<AstroConfig> {
const fileProtocolRoot = pathToFileURL(root + path.sep);
// Manual deprecation checks
/* eslint-disable no-console */
if (userConfig.hasOwnProperty('renderers')) {
console.error('Astro "renderers" are now "integrations"!');
console.error('Update your configuration and install new dependencies:');
try {
const rendererKeywords = userConfig.renderers.map((r: string) =>
r.replace('@astrojs/renderer-', '')
);
const rendererImports = rendererKeywords
.map((r: string) => ` import ${r} from '@astrojs/${r === 'solid' ? 'solid-js' : r}';`)
.join('\n');
const rendererIntegrations = rendererKeywords.map((r: string) => ` ${r}(),`).join('\n');
console.error('');
console.error(colors.dim(' // astro.config.js'));
if (rendererImports.length > 0) {
console.error(colors.green(rendererImports));
}
console.error('');
console.error(colors.dim(' // ...'));
if (rendererIntegrations.length > 0) {
console.error(colors.green(' integrations: ['));
console.error(colors.green(rendererIntegrations));
console.error(colors.green(' ],'));
} else {
console.error(colors.green(' integrations: [],'));
}
console.error('');
} catch (err) {
// We tried, better to just exit.
}
process.exit(1);
}
let legacyConfigKey: string | undefined;
for (const key of Object.keys(userConfig)) {
if (LEGACY_ASTRO_CONFIG_KEYS.has(key)) {
legacyConfigKey = key;
break;
}
}
if (legacyConfigKey) {
throw new Error(
`Legacy configuration detected: "${legacyConfigKey}".\nPlease update your configuration to the new format!\nSee https://astro.build/config for more information.`
);
}
/* eslint-enable no-console */
const AstroConfigRelativeSchema = createRelativeSchema(cmd, fileProtocolRoot);
// First-Pass Validation
const result = await AstroConfigRelativeSchema.parseAsync(userConfig);
// If successful, return the result as a verified AstroConfig object.
return result;
}
/** Convert the generic "yargs" flag object into our own, custom TypeScript object. */
export function resolveFlags(flags: Partial<Flags>): CLIFlags {
return {
root: typeof flags.root === 'string' ? flags.root : undefined,
site: typeof flags.site === 'string' ? flags.site : undefined,
base: typeof flags.base === 'string' ? flags.base : undefined,
port: typeof flags.port === 'number' ? flags.port : undefined,
config: typeof flags.config === 'string' ? flags.config : undefined,
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
};
}
export function resolveRoot(cwd?: string): string {
return cwd ? path.resolve(cwd) : process.cwd();
}
/** Merge CLI flags & user config object (CLI flags take priority) */
function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: string) {
astroConfig.server = astroConfig.server || {};
astroConfig.markdown = astroConfig.markdown || {};
if (typeof flags.site === 'string') astroConfig.site = flags.site;
if (typeof flags.base === 'string') astroConfig.base = flags.base;
if (typeof flags.drafts === 'boolean') astroConfig.markdown.drafts = flags.drafts;
if (typeof flags.port === 'number') {
// @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function.
// TODO: Come back here and refactor to remove this expected error.
astroConfig.server.port = flags.port;
}
if (typeof flags.host === 'string' || typeof flags.host === 'boolean') {
// @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function.
// TODO: Come back here and refactor to remove this expected error.
astroConfig.server.host = flags.host;
}
return astroConfig;
}
interface LoadConfigOptions {
cwd?: string;
flags?: Flags;
cmd: string;
validate?: boolean;
logging: LogOptions;
/** Invalidate when reloading a previously loaded config */
isConfigReload?: boolean;
}
/**
* Resolve the file URL of the user's `astro.config.js|cjs|mjs|ts` file
* Note: currently the same as loadConfig but only returns the `filePath`
* instead of the resolved config
*/
export async function resolveConfigPath(
configOptions: Pick<LoadConfigOptions, 'cwd' | 'flags'>
): Promise<string | undefined> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
let userConfigPath: string | undefined;
if (flags?.config) {
userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`;
userConfigPath = fileURLToPath(new URL(userConfigPath, `file://${root}/`));
}
// Resolve config file path using Proload
// If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s`
try {
const configPath = await resolve('astro', {
mustExist: !!userConfigPath,
cwd: root,
filePath: userConfigPath,
});
return configPath;
} catch (e) {
if (e instanceof ProloadError && flags.config) {
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
}
throw e;
}
}
interface OpenConfigResult {
userConfig: AstroUserConfig;
astroConfig: AstroConfig;
flags: CLIFlags;
root: string;
}
/** Load a configuration file, returning both the userConfig and astroConfig */
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, flags, root);
if (config) {
userConfig = config.value;
}
const astroConfig = await resolveConfig(
userConfig,
root,
flags,
configOptions.cmd,
configOptions.logging
);
return {
astroConfig,
userConfig,
flags,
root,
};
}
interface TryLoadConfigResult {
value: Record<string, any>;
filePath?: string;
}
async function tryLoadConfig(
configOptions: LoadConfigOptions,
flags: CLIFlags,
root: string
): Promise<TryLoadConfigResult | undefined> {
let finallyCleanup = async () => {};
try {
let configPath = await resolveConfigPath({
cwd: configOptions.cwd,
flags: configOptions.flags,
});
if (!configPath) return undefined;
if (configOptions.isConfigReload) {
// Hack: Write config to temporary file at project root
// This invalidates and reloads file contents when using ESM imports or "resolve"
const tempConfigPath = path.join(
root,
`.temp.${Date.now()}.config${path.extname(configPath)}`
);
await fs.promises.writeFile(tempConfigPath, await fs.promises.readFile(configPath));
finallyCleanup = async () => {
try {
await fs.promises.unlink(tempConfigPath);
} catch {
/** file already removed */
}
};
configPath = tempConfigPath;
}
const config = await load('astro', {
mustExist: !!configPath,
cwd: root,
filePath: configPath,
});
return config as TryLoadConfigResult;
} catch (e) {
if (e instanceof ProloadError && flags.config) {
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
}
const configPath = await resolveConfigPath(configOptions);
if (!configPath) {
throw e;
}
// Fallback to use Vite DevServer
const viteServer = await vite.createServer({
server: { middlewareMode: true, hmr: false },
optimizeDeps: { entries: [] },
clearScreen: false,
appType: 'custom',
// NOTE: Vite doesn't externalize linked packages by default. During testing locally,
// these dependencies trip up Vite's dev SSR transform. In the future, we should
// avoid `vite.createServer` and use `loadConfigFromFile` instead.
ssr: {
external: ['@astrojs/mdx', '@astrojs/react'],
},
});
try {
const mod = await viteServer.ssrLoadModule(configPath);
if (mod?.default) {
return {
value: mod.default,
filePath: configPath,
};
}
} finally {
await viteServer.close();
}
} finally {
await finallyCleanup();
}
}
/**
* Attempt to load an `astro.config.mjs` file
* @deprecated
*/
export async function loadConfig(configOptions: LoadConfigOptions): Promise<AstroConfig> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, flags, root);
if (config) {
userConfig = config.value;
}
return resolveConfig(userConfig, root, flags, configOptions.cmd, configOptions.logging);
}
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
export async function resolveConfig(
userConfig: AstroUserConfig,
root: string,
flags: CLIFlags = {},
cmd: string,
logging: LogOptions
): Promise<AstroConfig> {
const mergedConfig = mergeCLIFlags(userConfig, flags, cmd);
const validatedConfig = await validateConfig(mergedConfig, root, cmd, logging);
return validatedConfig;
}
function mergeConfigRecursively(
defaults: Record<string, any>,
overrides: Record<string, any>,
rootPath: string
) {
const merged: Record<string, any> = { ...defaults };
for (const key in overrides) {
const value = overrides[key];
if (value == null) {
continue;
}
const existing = merged[key];
if (existing == null) {
merged[key] = value;
continue;
}
// fields that require special handling:
if (key === 'vite' && rootPath === '') {
merged[key] = mergeViteConfig(existing, value);
continue;
}
if (Array.isArray(existing) || Array.isArray(value)) {
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
continue;
}
if (isObject(existing) && isObject(value)) {
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
continue;
}
merged[key] = value;
}
return merged;
}
export function mergeConfig(
defaults: Record<string, any>,
overrides: Record<string, any>,
isRoot = true
): Record<string, any> {
return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.');
}