diff options
-rw-r--r-- | .changeset/forty-trains-notice.md | 5 | ||||
-rw-r--r-- | packages/astro/config.d.ts | 40 | ||||
-rw-r--r-- | packages/astro/config.mjs | 16 | ||||
-rw-r--r-- | packages/astro/package.json | 7 | ||||
-rw-r--r-- | packages/astro/src/config/entrypoint.ts | 30 | ||||
-rw-r--r-- | packages/astro/src/config/index.ts | 13 | ||||
-rw-r--r-- | packages/astro/src/core/errors/errors-data.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/env/config.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/types/public/config.ts | 51 | ||||
-rw-r--r-- | packages/astro/test/types/define-config.ts | 41 |
10 files changed, 125 insertions, 82 deletions
diff --git a/.changeset/forty-trains-notice.md b/.changeset/forty-trains-notice.md new file mode 100644 index 000000000..29938198d --- /dev/null +++ b/.changeset/forty-trains-notice.md @@ -0,0 +1,5 @@ +--- +'astro': minor +--- + +Improves `defineConfig` type safety. TypeScript will now error if a group of related configuration options do not have consistent types. For example, you will now see an error if your language set for `i18n.defaultLocale` is not one of the supported locales specified in `i18n.locales`. diff --git a/packages/astro/config.d.ts b/packages/astro/config.d.ts deleted file mode 100644 index de2a500c2..000000000 --- a/packages/astro/config.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -/// <reference path='./client.d.ts' /> -type ViteUserConfig = import('vite').UserConfig; -type ViteUserConfigFn = import('vite').UserConfigFn; -type AstroUserConfig = import('./dist/types/public/config.js').AstroUserConfig; -type AstroInlineConfig = import('./dist/types/public/config.js').AstroInlineConfig; -type ImageServiceConfig = import('./dist/types/public/config.js').ImageServiceConfig; -type SharpImageServiceConfig = import('./dist/assets/services/sharp.js').SharpImageServiceConfig; -type EnvField = typeof import('./dist/env/config.js').envField; - -/** - * See the full Astro Configuration API Documentation - * https://astro.build/config - */ -export function defineConfig(config: AstroUserConfig): AstroUserConfig; - -/** - * Use Astro to generate a fully resolved Vite config - */ -export function getViteConfig( - config: ViteUserConfig, - inlineAstroConfig?: AstroInlineConfig, -): ViteUserConfigFn; - -/** - * Return the configuration needed to use the Sharp-based image service - */ -export function sharpImageService(config?: SharpImageServiceConfig): ImageServiceConfig; - -/** - * Return the configuration needed to use the passthrough image service. This image services does not perform - * any image transformations, and is mainly useful when your platform does not support other image services, or you are - * not using Astro's built-in image processing. - * See: https://docs.astro.build/en/guides/images/#configure-no-op-passthrough-service - */ -export function passthroughImageService(): ImageServiceConfig; - -/** - * Return a valid env field to use in this Astro config for `experimental.env.schema`. - */ -export declare const envField: EnvField; diff --git a/packages/astro/config.mjs b/packages/astro/config.mjs deleted file mode 100644 index d117d806e..000000000 --- a/packages/astro/config.mjs +++ /dev/null @@ -1,16 +0,0 @@ -export { defineConfig, getViteConfig } from './dist/config/index.js'; -export { envField } from './dist/env/config.js'; - -export function sharpImageService(config = {}) { - return { - entrypoint: 'astro/assets/services/sharp', - config, - }; -} - -export function passthroughImageService() { - return { - entrypoint: 'astro/assets/services/noop', - config: {}, - }; -} diff --git a/packages/astro/package.json b/packages/astro/package.json index 7898e570d..663b7cd38 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -46,10 +46,7 @@ }, "./compiler-runtime": "./dist/runtime/compiler/index.js", "./runtime/*": "./dist/runtime/*", - "./config": { - "types": "./config.d.ts", - "default": "./config.mjs" - }, + "./config": "./dist/config/entrypoint.js", "./container": { "types": "./dist/container/index.d.ts", "default": "./dist/container/index.js" @@ -93,8 +90,6 @@ "types", "astro.js", "index.d.ts", - "config.d.ts", - "config.mjs", "zod.d.ts", "zod.mjs", "env.d.ts", diff --git a/packages/astro/src/config/entrypoint.ts b/packages/astro/src/config/entrypoint.ts new file mode 100644 index 000000000..4951792d6 --- /dev/null +++ b/packages/astro/src/config/entrypoint.ts @@ -0,0 +1,30 @@ +// IMPORTANT: this file is the entrypoint for "astro/config". Keep it as light as possible! + +import type { SharpImageServiceConfig } from '../assets/services/sharp.js'; +import type { ImageServiceConfig } from '../types/public/index.js'; + +export { defineConfig, getViteConfig } from './index.js'; +export { envField } from '../env/config.js'; + +/** + * Return the configuration needed to use the Sharp-based image service + */ +export function sharpImageService(config: SharpImageServiceConfig = {}): ImageServiceConfig { + return { + entrypoint: 'astro/assets/services/sharp', + config, + }; +} + +/** + * Return the configuration needed to use the passthrough image service. This image services does not perform + * any image transformations, and is mainly useful when your platform does not support other image services, or you are + * not using Astro's built-in image processing. + * See: https://docs.astro.build/en/guides/images/#configure-no-op-passthrough-service + */ +export function passthroughImageService(): ImageServiceConfig { + return { + entrypoint: 'astro/assets/services/noop', + config: {}, + }; +} diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts index 0aa421f78..62316377a 100644 --- a/packages/astro/src/config/index.ts +++ b/packages/astro/src/config/index.ts @@ -1,13 +1,22 @@ import type { UserConfig as ViteUserConfig, UserConfigFn as ViteUserConfigFn } from 'vite'; import { Logger } from '../core/logger/core.js'; import { createRouteManifest } from '../core/routing/index.js'; -import type { AstroInlineConfig, AstroUserConfig } from '../types/public/config.js'; +import type { AstroInlineConfig, AstroUserConfig, Locales } from '../types/public/config.js'; import { createDevelopmentManifest } from '../vite-plugin-astro-server/plugin.js'; -export function defineConfig(config: AstroUserConfig) { +/** + * See the full Astro Configuration API Documentation + * https://astro.build/config + */ +export function defineConfig<const TLocales extends Locales = never>( + config: AstroUserConfig<TLocales>, +) { return config; } +/** + * Use Astro to generate a fully resolved Vite config + */ export function getViteConfig( userViteConfig: ViteUserConfig, inlineAstroConfig: AstroInlineConfig = {}, diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 5ad2b939e..fb93e6e91 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1160,8 +1160,8 @@ export const UnhandledRejection = { * import { defineConfig } from 'astro' * export default defineConfig({ * i18n: { - * defaultLocale: 'en', * locales: ['en', 'fr'], + * defaultLocale: 'en', * }, * }) * ``` diff --git a/packages/astro/src/env/config.ts b/packages/astro/src/env/config.ts index 629a1fe2e..beb91b43b 100644 --- a/packages/astro/src/env/config.ts +++ b/packages/astro/src/env/config.ts @@ -10,7 +10,7 @@ import type { } from './schema.js'; /** - * Return a valid env field to use in this Astro config for `experimental.env.schema`. + * Return a valid env field to use in this Astro config for `env.schema`. */ export const envField = { string: (options: StringFieldInput): StringField => ({ diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 4aec86f33..1e24f4194 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -16,6 +16,14 @@ import type { AstroIntegration } from './integrations.js'; export type Locales = (string | { codes: string[]; path: string })[]; +type NormalizeLocales<T extends Locales> = { + [K in keyof T]: T[K] extends string + ? T[K] + : T[K] extends { codes: Array<string> } + ? T[K]['codes'][number] + : never; +}[number]; + export interface ImageServiceConfig<T extends Record<string, any> = Record<string, any>> { entrypoint: 'astro/assets/services/sharp' | (string & {}); config?: T; @@ -101,8 +109,9 @@ export interface ViteUserConfig extends OriginalViteUserConfig { /** * Astro User Config * Docs: https://docs.astro.build/reference/configuration-reference/ - */ -export interface AstroUserConfig { + * + * Generics do not follow semver and may change at any time. + */ export interface AstroUserConfig<TLocales extends Locales = never> { /** * @docs * @kind heading @@ -1205,30 +1214,31 @@ export interface AstroUserConfig { i18n?: { /** * @docs - * @name i18n.defaultLocale - * @type {string} + * @name i18n.locales + * @type {Locales} * @version 3.5.0 * @description * - * The default locale of your website/application. This is a required field. + * A list of all locales supported by the website. This is a required field. * - * No particular language format or syntax is enforced, but we suggest using lower-case and hyphens as needed (e.g. "es", "pt-br") for greatest compatibility. + * Languages can be listed either as individual codes (e.g. `['en', 'es', 'pt-br']`) or mapped to a shared `path` of codes (e.g. `{ path: "english", codes: ["en", "en-US"]}`). These codes will be used to determine the URL structure of your deployed site. + * + * No particular language code format or syntax is enforced, but your project folders containing your content files must match exactly the `locales` items in the list. In the case of multiple `codes` pointing to a custom URL path prefix, store your content files in a folder with the same name as the `path` configured. */ - defaultLocale: string; + locales: [TLocales] extends [never] ? Locales : TLocales; + /** * @docs - * @name i18n.locales - * @type {Locales} + * @name i18n.defaultLocale + * @type {string} * @version 3.5.0 * @description * - * A list of all locales supported by the website, including the `defaultLocale`. This is a required field. + * The default locale of your website/application, that is one of the specified `locales`. This is a required field. * - * Languages can be listed either as individual codes (e.g. `['en', 'es', 'pt-br']`) or mapped to a shared `path` of codes (e.g. `{ path: "english", codes: ["en", "en-US"]}`). These codes will be used to determine the URL structure of your deployed site. - * - * No particular language code format or syntax is enforced, but your project folders containing your content files must match exactly the `locales` items in the list. In the case of multiple `codes` pointing to a custom URL path prefix, store your content files in a folder with the same name as the `path` configured. + * No particular language format or syntax is enforced, but we suggest using lower-case and hyphens as needed (e.g. "es", "pt-br") for greatest compatibility. */ - locales: Locales; + defaultLocale: [TLocales] extends [never] ? string : NormalizeLocales<NoInfer<TLocales>>; /** * @docs @@ -1258,7 +1268,14 @@ export interface AstroUserConfig { * }) * ``` */ - fallback?: Record<string, string>; + fallback?: [TLocales] extends [never] + ? Record<string, string> + : { + [Locale in NormalizeLocales<NoInfer<TLocales>>]?: Exclude< + NormalizeLocales<NoInfer<TLocales>>, + Locale + >; + }; /** * @docs @@ -1444,7 +1461,9 @@ export interface AstroUserConfig { * * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains) for more details, including the limitations of this feature. */ - domains?: Record<string, string>; + domains?: [TLocales] extends [never] + ? Record<string, string> + : Partial<Record<NormalizeLocales<NoInfer<TLocales>>, string>>; }; /** ! WARNING: SUBJECT TO CHANGE */ diff --git a/packages/astro/test/types/define-config.ts b/packages/astro/test/types/define-config.ts new file mode 100644 index 000000000..9015694e2 --- /dev/null +++ b/packages/astro/test/types/define-config.ts @@ -0,0 +1,41 @@ +import { describe, it } from 'node:test'; +import { defineConfig } from '../../dist/config/index.js'; +import type { AstroUserConfig } from '../../dist/types/public/index.js'; +import { expectTypeOf } from 'expect-type'; + +describe('defineConfig()', () => { + it('Infers generics correctly', () => { + const config_0 = defineConfig({}); + expectTypeOf(config_0).toEqualTypeOf<AstroUserConfig<never>>(); + expectTypeOf(config_0.i18n!.defaultLocale).toEqualTypeOf<string>(); + + const config_1 = defineConfig({ + i18n: { + locales: ['en'], + defaultLocale: 'en', + }, + }); + expectTypeOf(config_1).toEqualTypeOf<AstroUserConfig<['en']>>(); + expectTypeOf(config_1.i18n!.defaultLocale).toEqualTypeOf<'en'>(); + + const config_2 = defineConfig({ + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'fr', + }, + }); + expectTypeOf(config_2).toEqualTypeOf<AstroUserConfig<['en', 'fr']>>(); + expectTypeOf(config_2.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr'>(); + + const config_3 = defineConfig({ + i18n: { + locales: ['en', { path: 'french', codes: ['fr', 'fr-FR'] }], + defaultLocale: 'en', + }, + }); + expectTypeOf(config_3).toEqualTypeOf< + AstroUserConfig<['en', { readonly path: 'french'; readonly codes: ['fr', 'fr-FR'] }]> + >(); + expectTypeOf(config_3.i18n!.defaultLocale).toEqualTypeOf<'en' | 'fr' | 'fr-FR'>(); + }); +}); |