From d46f8fb14d3c702d62cc327de23562078fca0088 Mon Sep 17 00:00:00 2001 From: Okiki Ojo Date: Tue, 14 Jun 2022 14:08:14 -0400 Subject: [PATCH] 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 --- .changeset/unlucky-eyes-attend.md | 16 +++++++++++ packages/astro/src/integrations/index.ts | 32 ++++++++++++++------- packages/astro/test/config-validate.test.js | 7 +++++ packages/webapi/mod.d.ts | 2 +- 4 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 .changeset/unlucky-eyes-attend.md diff --git a/.changeset/unlucky-eyes-attend.md b/.changeset/unlucky-eyes-attend.md new file mode 100644 index 000000000..e54c1078a --- /dev/null +++ b/.changeset/unlucky-eyes-attend.md @@ -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 +] +``` \ No newline at end of file diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index 4e53dc4e6..f2ca31a67 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -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, diff --git a/packages/astro/test/config-validate.test.js b/packages/astro/test/config-validate.test.js index e90eea69c..95c3af17c 100644 --- a/packages/astro/test/config-validate.test.js +++ b/packages/astro/test/config-validate.test.js @@ -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 } }, diff --git a/packages/webapi/mod.d.ts b/packages/webapi/mod.d.ts index a3c49dc5c..b385e82a5 100644 --- a/packages/webapi/mod.d.ts +++ b/packages/webapi/mod.d.ts @@ -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;