feat: support optional and conditional integrations (#3590)

* feat(integrations): support optional integrations

By making integration optional, Astro can now ignore null or undefined Integrations instead of giving an internal error most devs can't read/won't understand.

This also enables optional integrations,
e.g.
```ts
integration: [
  // Only run `compress` integration in production environments, etc...
  import.meta.env.production ? compress() : null
]
```

* ci: add tests for optional integration

* docs: add changelog
This commit is contained in:
Okiki Ojo 2022-06-14 14:08:14 -04:00 committed by GitHub
parent 8ca284b080
commit d46f8fb14d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 46 additions and 11 deletions

View file

@ -0,0 +1,16 @@
---
'astro': patch
---
Add support for optional integrations
By making integration optional, Astro can now ignore null, undefined or other [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) "Integration" values instead of giving an internal error most devs can't and/or won't understand.
This also enables conditional integrations,
e.g.
```ts
integration: [
// Only run `compress` integration when in production environments, etc...
import.meta.env.production ? compress() : null
]
```

View file

@ -21,7 +21,19 @@ export async function runHookConfigSetup({
let updatedConfig: AstroConfig = { ..._config };
for (const integration of _config.integrations) {
if (integration.hooks['astro:config:setup']) {
/**
* By making integration hooks optional, Astro can now ignore null or undefined Integrations
* instead of giving an internal error most people can't read
*
* This also enables optional integrations, e.g.
* ```ts
* integration: [
* // Only run `compress` integration in production environments, etc...
* import.meta.env.production ? compress() : null
* ]
* ```
*/
if (integration?.hooks?.['astro:config:setup']) {
await integration.hooks['astro:config:setup']({
config: updatedConfig,
command,
@ -42,7 +54,7 @@ export async function runHookConfigSetup({
export async function runHookConfigDone({ config }: { config: AstroConfig }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:config:done']) {
if (integration?.hooks?.['astro:config:done']) {
await integration.hooks['astro:config:done']({
config,
setAdapter(adapter) {
@ -60,7 +72,7 @@ export async function runHookConfigDone({ config }: { config: AstroConfig }) {
if (!config._ctx.adapter) {
const integration = ssgAdapter();
config.integrations.push(integration);
if (integration.hooks['astro:config:done']) {
if (integration?.hooks?.['astro:config:done']) {
await integration.hooks['astro:config:done']({
config,
setAdapter(adapter) {
@ -79,7 +91,7 @@ export async function runHookServerSetup({
server: ViteDevServer;
}) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:setup']) {
if (integration?.hooks?.['astro:server:setup']) {
await integration.hooks['astro:server:setup']({ server });
}
}
@ -93,7 +105,7 @@ export async function runHookServerStart({
address: AddressInfo;
}) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:start']) {
if (integration?.hooks?.['astro:server:start']) {
await integration.hooks['astro:server:start']({ address });
}
}
@ -101,7 +113,7 @@ export async function runHookServerStart({
export async function runHookServerDone({ config }: { config: AstroConfig }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:server:done']) {
if (integration?.hooks?.['astro:server:done']) {
await integration.hooks['astro:server:done']();
}
}
@ -115,7 +127,7 @@ export async function runHookBuildStart({
buildConfig: BuildConfig;
}) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:start']) {
if (integration?.hooks?.['astro:build:start']) {
await integration.hooks['astro:build:start']({ buildConfig });
}
}
@ -133,7 +145,7 @@ export async function runHookBuildSetup({
target: 'server' | 'client';
}) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:setup']) {
if (integration?.hooks?.['astro:build:setup']) {
await integration.hooks['astro:build:setup']({
vite,
pages,
@ -154,7 +166,7 @@ export async function runHookBuildSsr({
manifest: SerializedSSRManifest;
}) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:ssr']) {
if (integration?.hooks?.['astro:build:ssr']) {
await integration.hooks['astro:build:ssr']({ manifest });
}
}
@ -174,7 +186,7 @@ export async function runHookBuildDone({
const dir = isBuildingToSSR(config) ? buildConfig.client : config.outDir;
for (const integration of config.integrations) {
if (integration.hooks['astro:build:done']) {
if (integration?.hooks?.['astro:build:done']) {
await integration.hooks['astro:build:done']({
pages: pages.map((p) => ({ pathname: p })),
dir,

View file

@ -69,6 +69,13 @@ describe('Config Validation', () => {
expect(configError).to.be.instanceOf(Error);
expect(configError.message).to.include('Astro integrations are still experimental.');
});
it('ignores null or falsy "integration" values', async () => {
const configError = await validateConfig(
{ integrations: [null, undefined, false, '', ``] },
process.cwd()
).catch((err) => err);
expect(configError).to.be.not.instanceOf(Error);
});
it('allows third-party "integration" values with the --experimental-integrations flag', async () => {
await validateConfig(
{ integrations: [{ name: '@my-plugin/a' }], experimental: { integrations: true } },

View file

@ -1,5 +1,5 @@
export { pathToPosix } from './lib/utils';
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from './mod.js';
export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, } from './mod.js';
export declare const polyfill: {
(target: any, options?: PolyfillOptions): any;
internals(target: any, name: string): any;