diff --git a/.changeset/dirty-lies-cover.md b/.changeset/dirty-lies-cover.md new file mode 100644 index 000000000000..ae74e348e61a --- /dev/null +++ b/.changeset/dirty-lies-cover.md @@ -0,0 +1,32 @@ +--- +'@astrojs/cloudflare': minor +'@astrojs/netlify': minor +'@astrojs/vercel': minor +'@astrojs/deno': minor +'@astrojs/node': minor +'astro': minor +--- + +Introduced the concept of feature map. A feature map is a list of features that are built-in in Astro, and an Adapter +can tell Astro if it can support it. + +```ts +import {AstroIntegration} from "./astro"; + +function myIntegration(): AstroIntegration { + return { + name: 'astro-awesome-list', + // new feature map + supportedAstroFeatures: { + hybridOutput: 'experimental', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + } + } +} +``` diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index eb19900d3e8a..dc710ec2f4d3 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1685,6 +1685,39 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati export type Params = Record; +export type SupportsKind = 'unsupported' | 'stable' | 'experimental' | 'deprecated'; + +export type AstroFeatureMap = { + /** + * The adapter is able serve static pages + */ + staticOutput?: SupportsKind; + /** + * The adapter is able to serve pages that are static or rendered via server + */ + hybridOutput?: SupportsKind; + /** + * The adapter is able to serve SSR pages + */ + serverOutput?: SupportsKind; + /** + * The adapter can emit static assets + */ + assets?: AstroAssetsFeature; +}; + +export interface AstroAssetsFeature { + supportKind?: SupportsKind; + /** + * Whether if this adapter deploys files in an enviroment that is compatible with the library `sharp` + */ + isSharpCompatible?: boolean; + /** + * Whether if this adapter deploys files in an enviroment that is compatible with the library `squoosh` + */ + isSquooshCompatible?: boolean; +} + export interface AstroAdapter { name: string; serverEntrypoint?: string; @@ -1692,6 +1725,12 @@ export interface AstroAdapter { exports?: string[]; args?: any; adapterFeatures?: AstroAdapterFeatures; + /** + * List of features supported by an adapter. + * + * If the adapter is not able to handle certain configurations, Astro will throw an error. + */ + supportedAstroFeatures?: AstroFeatureMap; } type Body = string; diff --git a/packages/astro/src/assets/generate.ts b/packages/astro/src/assets/generate.ts index d6cb02e560f3..04488ed8fe71 100644 --- a/packages/astro/src/assets/generate.ts +++ b/packages/astro/src/assets/generate.ts @@ -27,6 +27,11 @@ export async function generateImage( options: ImageTransform, filepath: string ): Promise { + if (typeof buildOpts.settings.config.image === 'undefined') { + throw new Error( + "Astro hasn't set a default service for `astro:assets`. This is an internal error and you should report it." + ); + } if (!isESMImportedImage(options.src)) { return undefined; } diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 56525300167c..2ab87b7c1ce0 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -1,10 +1,8 @@ -import { bold } from 'kleur/colors'; import MagicString from 'magic-string'; import { fileURLToPath } from 'node:url'; import type * as vite from 'vite'; import { normalizePath } from 'vite'; import type { AstroPluginOptions, ImageTransform } from '../@types/astro'; -import { error } from '../core/logger/core.js'; import { appendForwardSlash, joinPaths, @@ -23,37 +21,12 @@ const urlRE = /(\?|&)url(?:&|$)/; export default function assets({ settings, - logging, mode, }: AstroPluginOptions & { mode: string }): vite.Plugin[] { let resolvedConfig: vite.ResolvedConfig; globalThis.astroAsset = {}; - const UNSUPPORTED_ADAPTERS = new Set([ - '@astrojs/cloudflare', - '@astrojs/deno', - '@astrojs/netlify/edge-functions', - '@astrojs/vercel/edge', - ]); - - const adapterName = settings.config.adapter?.name; - if ( - ['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes( - settings.config.image.service.entrypoint - ) && - adapterName && - UNSUPPORTED_ADAPTERS.has(adapterName) - ) { - error( - logging, - 'assets', - `The currently selected adapter \`${adapterName}\` does not run on Node, however the currently used image service depends on Node built-ins. ${bold( - 'Your project will NOT be able to build.' - )}` - ); - } - return [ // Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev { diff --git a/packages/astro/src/integrations/astroFeaturesValidation.ts b/packages/astro/src/integrations/astroFeaturesValidation.ts new file mode 100644 index 000000000000..6b92813c5017 --- /dev/null +++ b/packages/astro/src/integrations/astroFeaturesValidation.ts @@ -0,0 +1,162 @@ +import type { + AstroAssetsFeature, + AstroConfig, + AstroFeatureMap, + SupportsKind, +} from '../@types/astro'; +import { error, type LogOptions, warn } from '../core/logger/core.js'; +import { bold } from 'kleur/colors'; + +const STABLE = 'stable'; +const DEPRECATED = 'deprecated'; +const UNSUPPORTED = 'unsupported'; +const EXPERIMENTAL = 'experimental'; + +const UNSUPPORTED_ASSETS_FEATURE: AstroAssetsFeature = { + supportKind: UNSUPPORTED, + isSquooshCompatible: false, + isSharpCompatible: false, +}; + +// NOTE: remove for Astro 4.0 +const ALL_UNSUPPORTED: Required = { + serverOutput: UNSUPPORTED, + staticOutput: UNSUPPORTED, + hybridOutput: UNSUPPORTED, + assets: UNSUPPORTED_ASSETS_FEATURE, +}; + +type ValidationResult = { + [Property in keyof AstroFeatureMap]: boolean; +}; + +/** + * Checks whether an adapter supports certain features that are enabled via Astro configuration. + * + * If a configuration is enabled and "unlocks" a feature, but the adapter doesn't support, the function + * will throw a runtime error. + * + */ +export function validateSupportedFeatures( + adapterName: string, + featureMap: AstroFeatureMap = ALL_UNSUPPORTED, + config: AstroConfig, + logging: LogOptions +): ValidationResult { + const { + assets = UNSUPPORTED_ASSETS_FEATURE, + serverOutput = UNSUPPORTED, + staticOutput = UNSUPPORTED, + hybridOutput = UNSUPPORTED, + } = featureMap; + const validationResult: ValidationResult = {}; + + validationResult.staticOutput = validateSupportKind( + staticOutput, + adapterName, + logging, + 'staticOutput', + () => config?.output === 'static' + ); + + validationResult.hybridOutput = validateSupportKind( + hybridOutput, + adapterName, + logging, + 'hybridOutput', + () => config?.output === 'hybrid' + ); + + validationResult.serverOutput = validateSupportKind( + serverOutput, + adapterName, + logging, + 'serverOutput', + () => config?.output === 'server' + ); + validationResult.assets = validateAssetsFeature(assets, adapterName, config, logging); + + return validationResult; +} + +function validateSupportKind( + supportKind: SupportsKind, + adapterName: string, + logging: LogOptions, + featureName: string, + hasCorrectConfig: () => boolean +): boolean { + if (supportKind === STABLE) { + return true; + } else if (supportKind === DEPRECATED) { + featureIsDeprecated(adapterName, logging); + } else if (supportKind === EXPERIMENTAL) { + featureIsExperimental(adapterName, logging); + } + + if (hasCorrectConfig() && supportKind === UNSUPPORTED) { + featureIsUnsupported(adapterName, logging, featureName); + return false; + } else { + return true; + } +} + +function featureIsUnsupported(adapterName: string, logging: LogOptions, featureName: string) { + error( + logging, + `${adapterName}`, + `The feature ${featureName} is not supported by the adapter ${adapterName}.` + ); +} + +function featureIsExperimental(adapterName: string, logging: LogOptions) { + warn(logging, `${adapterName}`, 'The feature is experimental and subject to issues or changes.'); +} + +function featureIsDeprecated(adapterName: string, logging: LogOptions) { + warn( + logging, + `${adapterName}`, + 'The feature is deprecated and will be moved in the next release.' + ); +} + +const SHARP_SERVICE = 'astro/assets/services/sharp'; +const SQUOOSH_SERVICE = 'astro/assets/services/squoosh'; + +function validateAssetsFeature( + assets: AstroAssetsFeature, + adapterName: string, + config: AstroConfig, + logging: LogOptions +): boolean { + const { + supportKind = UNSUPPORTED, + isSharpCompatible = false, + isSquooshCompatible = false, + } = assets; + if (config?.image?.service?.entrypoint === SHARP_SERVICE && !isSharpCompatible) { + error( + logging, + 'astro', + `The currently selected adapter \`${adapterName}\` is not compatible with the service "Sharp". ${bold( + 'Your project will NOT be able to build.' + )}` + ); + return false; + } + + if (config?.image?.service?.entrypoint === SQUOOSH_SERVICE && !isSquooshCompatible) { + error( + logging, + 'astro', + `The currently selected adapter \`${adapterName}\` is not compatible with the service "Squoosh". ${bold( + 'Your project will NOT be able to build.' + )}` + ); + return false; + } + + return validateSupportKind(supportKind, adapterName, logging, 'assets', () => true); +} diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index ab9898523aac..75971fa533a4 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -18,8 +18,9 @@ import type { SerializedSSRManifest } from '../core/app/types'; import type { PageBuildData } from '../core/build/types'; import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; import { mergeConfig } from '../core/config/index.js'; -import { info, type LogOptions, AstroIntegrationLogger } from '../core/logger/core.js'; +import { info, warn, error, type LogOptions, AstroIntegrationLogger } from '../core/logger/core.js'; import { isServerLikeOutput } from '../prerender/utils.js'; +import { validateSupportedFeatures } from './astroFeaturesValidation.js'; async function withTakingALongTimeMsg({ name, @@ -197,6 +198,30 @@ export async function runHookConfigDone({ `Integration "${integration.name}" conflicts with "${settings.adapter.name}". You can only configure one deployment integration.` ); } + if (!adapter.supportedAstroFeatures) { + // NOTE: throw an error in Astro 4.0 + warn( + logging, + 'astro', + `The adapter ${adapter.name} doesn't provide a feature map. From Astro 3.0, an adapter can provide a feature map. Not providing a feature map will cause an error in Astro 4.0.` + ); + } else { + const validationResult = validateSupportedFeatures( + adapter.name, + adapter.supportedAstroFeatures, + settings.config, + logging + ); + for (const [featureName, supported] of Object.entries(validationResult)) { + if (!supported) { + error( + logging, + 'astro', + `The adapter ${adapter.name} doesn't support the feature ${featureName}. Your project won't be built. You should not use it.` + ); + } + } + } settings.adapter = adapter; }, logger, diff --git a/packages/astro/test/featuresSupport.test.js b/packages/astro/test/featuresSupport.test.js new file mode 100644 index 000000000000..fba8da47557d --- /dev/null +++ b/packages/astro/test/featuresSupport.test.js @@ -0,0 +1,55 @@ +import { loadFixture } from './test-utils.js'; +import { expect } from 'chai'; +import testAdapter from './test-adapter.js'; + +describe('Adapter', () => { + let fixture; + + it("should error if the adapter doesn't support edge middleware", async () => { + try { + fixture = await loadFixture({ + root: './fixtures/middleware-dev/', + output: 'server', + build: { + excludeMiddleware: true, + }, + adapter: testAdapter({ + extendAdapter: { + supportsFeatures: { + edgeMiddleware: 'Unsupported', + }, + }, + }), + }); + await fixture.build(); + } catch (e) { + expect(e.toString()).to.contain( + "The adapter my-ssr-adapter doesn't support the feature build.excludeMiddleware." + ); + } + }); + + it("should error if the adapter doesn't support split build", async () => { + try { + fixture = await loadFixture({ + root: './fixtures/middleware-dev/', + output: 'server', + build: { + split: true, + }, + adapter: testAdapter({ + extendAdapter: { + supportsFeatures: { + functionPerPage: 'Unsupported', + }, + }, + }), + }); + await fixture.build(); + } catch (e) { + expect(e.toString()).to.contain( + "The adapter my-ssr-adapter doesn't support the feature build.split." + ); + } + }); +}); diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 85b4d69c0a0e..67058023ddd6 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -71,6 +71,15 @@ export default function ( name: 'my-ssr-adapter', serverEntrypoint: '@my-ssr', exports: ['manifest', 'createApp'], + supportedFeatures: { + assets: { + supportKind: 'Stable', + isNodeCompatible: true, + }, + serverOutput: 'Stable', + staticOutput: 'Stable', + hybridOutput: 'Stable', + }, ...extendAdapter, }); }, diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js index 919628da2901..a420dd6c97bb 100644 --- a/packages/astro/test/units/integrations/api.test.js +++ b/packages/astro/test/units/integrations/api.test.js @@ -1,5 +1,7 @@ import { expect } from 'chai'; import { runHookBuildSetup } from '../../../dist/integrations/index.js'; +import { validateSupportedFeatures } from '../../../dist/integrations/astroFeaturesValidation.js'; +import { defaultLogging } from '../test-utils.js'; describe('Integration API', () => { it('runHookBuildSetup should work', async () => { @@ -28,3 +30,187 @@ describe('Integration API', () => { expect(updatedViteConfig).to.haveOwnProperty('define'); }); }); + +describe('Astro feature map', function () { + it('should support the feature when stable', () => { + let result = validateSupportedFeatures( + 'test', + { + hybridOutput: 'stable', + }, + { + output: 'hybrid', + }, + defaultLogging + ); + expect(result['hybridOutput']).to.be.true; + }); + + it('should not support the feature when not provided', () => { + let result = validateSupportedFeatures( + 'test', + undefined, + { + output: 'hybrid', + }, + defaultLogging + ); + expect(result['hybridOutput']).to.be.false; + }); + + it('should not support the feature when an empty object is provided', () => { + let result = validateSupportedFeatures( + 'test', + {}, + { + output: 'hybrid', + }, + defaultLogging + ); + expect(result['hybridOutput']).to.be.false; + }); + + describe('static output', function () { + it('should be supported with the correct config', () => { + let result = validateSupportedFeatures( + 'test', + { staticOutput: 'stable' }, + { + output: 'static', + }, + defaultLogging + ); + expect(result['staticOutput']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + let result = validateSupportedFeatures( + 'test', + { staticOutput: 'unsupported' }, + { + output: 'static', + }, + defaultLogging + ); + expect(result['staticOutput']).to.be.false; + }); + }); + describe('hybrid output', function () { + it('should be supported with the correct config', () => { + let result = validateSupportedFeatures( + 'test', + { hybridOutput: 'stable' }, + { + output: 'hybrid', + }, + defaultLogging + ); + expect(result['hybridOutput']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + let result = validateSupportedFeatures( + 'test', + { + hybridOutput: 'unsupported', + }, + { + output: 'hybrid', + }, + defaultLogging + ); + expect(result['hybridOutput']).to.be.false; + }); + }); + describe('server output', function () { + it('should be supported with the correct config', () => { + let result = validateSupportedFeatures( + 'test', + { serverOutput: 'stable' }, + { + output: 'server', + }, + defaultLogging + ); + expect(result['serverOutput']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + let result = validateSupportedFeatures( + 'test', + { + serverOutput: 'unsupported', + }, + { + output: 'server', + }, + defaultLogging + ); + expect(result['serverOutput']).to.be.false; + }); + }); + + describe('assets', function () { + it('should be supported when it is sharp compatible', () => { + let result = validateSupportedFeatures( + 'test', + { + assets: { + supportKind: 'stable', + isSharpCompatible: true, + }, + }, + { + image: { + service: { + entrypoint: 'astro/assets/services/sharp', + }, + }, + }, + defaultLogging + ); + expect(result['assets']).to.be.true; + }); + it('should be supported when it is squoosh compatible', () => { + let result = validateSupportedFeatures( + 'test', + { + assets: { + supportKind: 'stable', + isSquooshCompatible: true, + }, + }, + { + image: { + service: { + entrypoint: 'astro/assets/services/squoosh', + }, + }, + }, + defaultLogging + ); + expect(result['assets']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + let result = validateSupportedFeatures( + 'test', + { + assets: { + supportKind: 'unsupported', + isNodeCompatible: false, + }, + }, + { + image: { + service: { + entrypoint: 'astro/assets/services/sharp', + }, + }, + }, + defaultLogging + ); + expect(result['assets']).to.be.false; + }); + }); +}); diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index ef452aa95a1f..a3bb76fbbc1a 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -24,11 +24,31 @@ export function getAdapter(isModeDirectory: boolean): AstroAdapter { name: '@astrojs/cloudflare', serverEntrypoint: '@astrojs/cloudflare/server.directory.js', exports: ['onRequest', 'manifest'], + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'unsupported', + serverOutput: 'stable', + assets: { + supportKind: 'unsupported', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, } : { name: '@astrojs/cloudflare', serverEntrypoint: '@astrojs/cloudflare/server.advanced.js', exports: ['default'], + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'unsupported', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, }; } diff --git a/packages/integrations/cloudflare/test/fixtures/basics/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/basics/astro.config.mjs index 105247b1bded..407dc43553d2 100644 --- a/packages/integrations/cloudflare/test/fixtures/basics/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/basics/astro.config.mjs @@ -6,5 +6,5 @@ process.env.SECRET_STUFF = 'secret' export default defineConfig({ adapter: cloudflare(), - output: 'server', + output: 'server' }); diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index db645eb697d1..3b011c83064a 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -89,6 +89,16 @@ export function getAdapter(args?: Options): AstroAdapter { serverEntrypoint: '@astrojs/deno/server.js', args: args ?? {}, exports: ['stop', 'handle', 'start', 'running'], + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, }; } diff --git a/packages/integrations/deno/test/fixtures/basics/astro.config.mjs b/packages/integrations/deno/test/fixtures/basics/astro.config.mjs index b5187f89164e..cf80fdb2eea6 100644 --- a/packages/integrations/deno/test/fixtures/basics/astro.config.mjs +++ b/packages/integrations/deno/test/fixtures/basics/astro.config.mjs @@ -6,5 +6,5 @@ import mdx from '@astrojs/mdx'; export default defineConfig({ adapter: deno(), integrations: [react(), mdx()], - output: 'server', + output: 'server' }) diff --git a/packages/integrations/deno/test/fixtures/dynimport/astro.config.mjs b/packages/integrations/deno/test/fixtures/dynimport/astro.config.mjs index d670faac6720..009023113884 100644 --- a/packages/integrations/deno/test/fixtures/dynimport/astro.config.mjs +++ b/packages/integrations/deno/test/fixtures/dynimport/astro.config.mjs @@ -3,5 +3,5 @@ import deno from '@astrojs/deno'; export default defineConfig({ adapter: deno(), - output: 'server', + output: 'server' }) diff --git a/packages/integrations/netlify/src/integration-edge-functions.ts b/packages/integrations/netlify/src/integration-edge-functions.ts index ac7c124fb93e..4e8bc6aa4ce1 100644 --- a/packages/integrations/netlify/src/integration-edge-functions.ts +++ b/packages/integrations/netlify/src/integration-edge-functions.ts @@ -11,6 +11,16 @@ export function getAdapter(): AstroAdapter { name: '@astrojs/netlify/edge-functions', serverEntrypoint: '@astrojs/netlify/netlify-edge-functions.js', exports: ['default'], + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, }; } diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index 3b10e096c559..b8d56ad65df8 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -18,6 +18,16 @@ export function getAdapter({ functionPerRoute, edgeMiddleware, ...args }: Args): functionPerRoute, edgeMiddleware, }, + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: true, + isSquooshCompatible: true, + }, + }, }; } diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index 17a8f4502a43..7435a60edc32 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -8,6 +8,16 @@ export function getAdapter(options: Options): AstroAdapter { previewEntrypoint: '@astrojs/node/preview.js', exports: ['handler', 'startServer'], args: options, + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: true, + isSquooshCompatible: true, + }, + }, }; } diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index b83c9f2b729e..b613f502c9e9 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -27,6 +27,16 @@ function getAdapter(): AstroAdapter { name: PACKAGE_NAME, serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, exports: ['default'], + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, }; } diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 4fd7e22a48e6..9a8ead137c37 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -44,6 +44,16 @@ function getAdapter({ edgeMiddleware, functionPerRoute, }, + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: true, + isSquooshCompatible: true, + }, + }, }; } diff --git a/packages/integrations/vercel/test/no-output.test.js b/packages/integrations/vercel/test/no-output.test.js index af4d9c2b6639..3894ee779a9f 100644 --- a/packages/integrations/vercel/test/no-output.test.js +++ b/packages/integrations/vercel/test/no-output.test.js @@ -19,6 +19,6 @@ describe('Missing output config', () => { error = err; } expect(error).to.not.be.equal(undefined); - expect(error.message).to.include(`output: "server"`); + expect(error.message).to.include('output: "server"'); }); });