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:
parent
8ca284b080
commit
d46f8fb14d
4 changed files with 46 additions and 11 deletions
16
.changeset/unlucky-eyes-attend.md
Normal file
16
.changeset/unlucky-eyes-attend.md
Normal 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
|
||||||
|
]
|
||||||
|
```
|
|
@ -21,7 +21,19 @@ export async function runHookConfigSetup({
|
||||||
|
|
||||||
let updatedConfig: AstroConfig = { ..._config };
|
let updatedConfig: AstroConfig = { ..._config };
|
||||||
for (const integration of _config.integrations) {
|
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']({
|
await integration.hooks['astro:config:setup']({
|
||||||
config: updatedConfig,
|
config: updatedConfig,
|
||||||
command,
|
command,
|
||||||
|
@ -42,7 +54,7 @@ export async function runHookConfigSetup({
|
||||||
|
|
||||||
export async function runHookConfigDone({ config }: { config: AstroConfig }) {
|
export async function runHookConfigDone({ config }: { config: AstroConfig }) {
|
||||||
for (const integration of config.integrations) {
|
for (const integration of config.integrations) {
|
||||||
if (integration.hooks['astro:config:done']) {
|
if (integration?.hooks?.['astro:config:done']) {
|
||||||
await integration.hooks['astro:config:done']({
|
await integration.hooks['astro:config:done']({
|
||||||
config,
|
config,
|
||||||
setAdapter(adapter) {
|
setAdapter(adapter) {
|
||||||
|
@ -60,7 +72,7 @@ export async function runHookConfigDone({ config }: { config: AstroConfig }) {
|
||||||
if (!config._ctx.adapter) {
|
if (!config._ctx.adapter) {
|
||||||
const integration = ssgAdapter();
|
const integration = ssgAdapter();
|
||||||
config.integrations.push(integration);
|
config.integrations.push(integration);
|
||||||
if (integration.hooks['astro:config:done']) {
|
if (integration?.hooks?.['astro:config:done']) {
|
||||||
await integration.hooks['astro:config:done']({
|
await integration.hooks['astro:config:done']({
|
||||||
config,
|
config,
|
||||||
setAdapter(adapter) {
|
setAdapter(adapter) {
|
||||||
|
@ -79,7 +91,7 @@ export async function runHookServerSetup({
|
||||||
server: ViteDevServer;
|
server: ViteDevServer;
|
||||||
}) {
|
}) {
|
||||||
for (const integration of config.integrations) {
|
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 });
|
await integration.hooks['astro:server:setup']({ server });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +105,7 @@ export async function runHookServerStart({
|
||||||
address: AddressInfo;
|
address: AddressInfo;
|
||||||
}) {
|
}) {
|
||||||
for (const integration of config.integrations) {
|
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 });
|
await integration.hooks['astro:server:start']({ address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +113,7 @@ export async function runHookServerStart({
|
||||||
|
|
||||||
export async function runHookServerDone({ config }: { config: AstroConfig }) {
|
export async function runHookServerDone({ config }: { config: AstroConfig }) {
|
||||||
for (const integration of config.integrations) {
|
for (const integration of config.integrations) {
|
||||||
if (integration.hooks['astro:server:done']) {
|
if (integration?.hooks?.['astro:server:done']) {
|
||||||
await integration.hooks['astro:server:done']();
|
await integration.hooks['astro:server:done']();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,7 +127,7 @@ export async function runHookBuildStart({
|
||||||
buildConfig: BuildConfig;
|
buildConfig: BuildConfig;
|
||||||
}) {
|
}) {
|
||||||
for (const integration of config.integrations) {
|
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 });
|
await integration.hooks['astro:build:start']({ buildConfig });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +145,7 @@ export async function runHookBuildSetup({
|
||||||
target: 'server' | 'client';
|
target: 'server' | 'client';
|
||||||
}) {
|
}) {
|
||||||
for (const integration of config.integrations) {
|
for (const integration of config.integrations) {
|
||||||
if (integration.hooks['astro:build:setup']) {
|
if (integration?.hooks?.['astro:build:setup']) {
|
||||||
await integration.hooks['astro:build:setup']({
|
await integration.hooks['astro:build:setup']({
|
||||||
vite,
|
vite,
|
||||||
pages,
|
pages,
|
||||||
|
@ -154,7 +166,7 @@ export async function runHookBuildSsr({
|
||||||
manifest: SerializedSSRManifest;
|
manifest: SerializedSSRManifest;
|
||||||
}) {
|
}) {
|
||||||
for (const integration of config.integrations) {
|
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 });
|
await integration.hooks['astro:build:ssr']({ manifest });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,7 +186,7 @@ export async function runHookBuildDone({
|
||||||
const dir = isBuildingToSSR(config) ? buildConfig.client : config.outDir;
|
const dir = isBuildingToSSR(config) ? buildConfig.client : config.outDir;
|
||||||
|
|
||||||
for (const integration of config.integrations) {
|
for (const integration of config.integrations) {
|
||||||
if (integration.hooks['astro:build:done']) {
|
if (integration?.hooks?.['astro:build:done']) {
|
||||||
await integration.hooks['astro:build:done']({
|
await integration.hooks['astro:build:done']({
|
||||||
pages: pages.map((p) => ({ pathname: p })),
|
pages: pages.map((p) => ({ pathname: p })),
|
||||||
dir,
|
dir,
|
||||||
|
|
|
@ -69,6 +69,13 @@ describe('Config Validation', () => {
|
||||||
expect(configError).to.be.instanceOf(Error);
|
expect(configError).to.be.instanceOf(Error);
|
||||||
expect(configError.message).to.include('Astro integrations are still experimental.');
|
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 () => {
|
it('allows third-party "integration" values with the --experimental-integrations flag', async () => {
|
||||||
await validateConfig(
|
await validateConfig(
|
||||||
{ integrations: [{ name: '@my-plugin/a' }], experimental: { integrations: true } },
|
{ integrations: [{ name: '@my-plugin/a' }], experimental: { integrations: true } },
|
||||||
|
|
2
packages/webapi/mod.d.ts
vendored
2
packages/webapi/mod.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
export { pathToPosix } from './lib/utils';
|
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: {
|
export declare const polyfill: {
|
||||||
(target: any, options?: PolyfillOptions): any;
|
(target: any, options?: PolyfillOptions): any;
|
||||||
internals(target: any, name: string): any;
|
internals(target: any, name: string): any;
|
||||||
|
|
Loading…
Reference in a new issue