From dd54199d7cdabb7544542ec2fc8a0fe439c1799d Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 6 Jul 2023 18:08:50 +0100 Subject: [PATCH] feat: support features for adapters --- packages/astro/src/@types/astro.ts | 43 ++++ packages/astro/src/assets/generate.ts | 5 + .../astro/src/assets/vite-plugin-assets.ts | 14 +- packages/astro/src/core/errors/errors-data.ts | 13 ++ packages/astro/src/integrations/index.ts | 198 +++++++++++++++++- packages/astro/test/featuresSupport.test.js | 55 +++++ packages/astro/test/test-adapter.js | 11 + .../astro/test/units/integrations/api.test.js | 195 +++++++++++++++++ packages/integrations/cloudflare/src/index.ts | 16 ++ .../cloudflare/test/no-output.test.js | 2 +- packages/integrations/deno/src/index.ts | 8 + .../netlify/src/integration-functions.ts | 8 + packages/integrations/node/src/index.ts | 8 + .../integrations/vercel/src/edge/adapter.ts | 8 + .../vercel/src/serverless/adapter.ts | 8 + 15 files changed, 586 insertions(+), 6 deletions(-) create mode 100644 packages/astro/test/featuresSupport.test.js diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index fb34954ac..98dd3c8cd 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1660,12 +1660,55 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati export type Params = Record; +export type SupportsKind = 'Unsupported' | 'Stable' | 'Experimental' | 'Deprecated'; + +export type AstroAdapterFeatureMap = { + /** + * 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; + /** + * Support for emitting a SSR file per page + */ + functionPerPage?: SupportsKind; + /** + * Support when `build.ecludeMiddleware` is enabled. + */ + edgeMiddleware?: SupportsKind; + /** + * The adapter can emit static assets + */ + assets?: { + supportKind?: SupportsKind; + /** + * Whether if this adapter deploys files in an enviroment that is Node.js compatible. + * + * The default services used by Astro needs Node.js. + */ + isNodeCompatible?: boolean; + }; +}; + export interface AstroAdapter { name: string; serverEntrypoint?: string; previewEntrypoint?: string; exports?: string[]; args?: any; + /** + * List of features supported by an adapter. + * + * If the adapter is not able to handle certain configurations, Astro will throw an error. + */ + supportedFeatures?: AstroAdapterFeatureMap; } type Body = string; diff --git a/packages/astro/src/assets/generate.ts b/packages/astro/src/assets/generate.ts index d6cb02e56..04488ed8f 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 6a29d02f0..4b3b7ceba 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -26,6 +26,12 @@ export default function assets({ logging, mode, }: AstroPluginOptions & { mode: string }): vite.Plugin[] { + const imageService = settings.config.image; + if (typeof imageService === 'undefined') { + throw new Error( + "Astro hasn't set a default service for `astro:assets`. This is an internal error and you should report it." + ); + } let resolvedConfig: vite.ResolvedConfig; globalThis.astroAsset = {}; @@ -40,7 +46,7 @@ export default function assets({ const adapterName = settings.config.adapter?.name; if ( ['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes( - settings.config.image.service.entrypoint + imageService.service.entrypoint ) && adapterName && UNSUPPORTED_ADAPTERS.has(adapterName) @@ -72,7 +78,7 @@ export default function assets({ }, async resolveId(id) { if (id === VIRTUAL_SERVICE_ID) { - return await this.resolve(settings.config.image.service.entrypoint); + return await this.resolve(imageService.service.entrypoint); } if (id === VIRTUAL_MODULE_ID) { return resolvedVirtualModuleId; @@ -85,7 +91,7 @@ export default function assets({ import { getImage as getImageInternal } from "astro/assets"; export { default as Image } from "astro/components/Image.astro"; - export const imageServiceConfig = ${JSON.stringify(settings.config.image.service.config)}; + export const imageServiceConfig = ${JSON.stringify(imageService.service.config)}; export const getImage = async (options) => await getImageInternal(options, imageServiceConfig); `; } @@ -103,7 +109,7 @@ export default function assets({ >(); } - const hash = hashTransform(options, settings.config.image.service.entrypoint); + const hash = hashTransform(options, imageService.service.entrypoint); let filePath: string; if (globalThis.astroAsset.staticImages.has(hash)) { diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 52fdbed6c..1292e86dd 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1071,6 +1071,19 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati }, }, + /** + * @docs + * @message Feature not supported by the adapter. + * @description + * The adapter doesn't support certain feature enabled via configuration. + */ + FeatureNotSupportedByAdapter: { + title: 'Feature not supported by the adapter.', + message: (adapterName: string, featureName: string) => { + return `The adapter ${adapterName} doesn't support the feature ${featureName}. Please turn it off.`; + }, + }, + /** * @docs * @see diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index cf50df0e1..6b0ba223e 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -4,6 +4,7 @@ import type { AddressInfo } from 'node:net'; import { fileURLToPath } from 'node:url'; import type { InlineConfig, ViteDevServer } from 'vite'; import type { + AstroAdapterFeatureMap, AstroConfig, AstroRenderer, AstroSettings, @@ -11,13 +12,20 @@ import type { DataEntryType, HookParameters, RouteData, + SupportsKind, } from '../@types/astro.js'; 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 } from '../core/logger/core.js'; +import { error, info, type LogOptions, warn } from '../core/logger/core.js'; import { isServerLikeOutput } from '../prerender/utils.js'; +import { AstroError, AstroErrorData } from '../core/errors/index.js'; + +const STABLE = 'Stable'; +const DEPRECATED = 'Deprecated'; +const UNSUPPORTED = 'Unsupported'; +const EXPERIMENTAL = 'Experimental'; async function withTakingALongTimeMsg({ name, @@ -178,6 +186,33 @@ export async function runHookConfigDone({ `Integration "${integration.name}" conflicts with "${settings.adapter.name}". You can only configure one deployment integration.` ); } + if (!adapter.supportedFeatures) { + // 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.supportedFeatures, + settings.config, + logging + ); + for (const [featureName, valid] of Object.entries(validationResult)) { + if (!valid) { + throw new AstroError({ + ...AstroErrorData.FeatureNotSupportedByAdapter, + message: AstroErrorData.FeatureNotSupportedByAdapter.message( + adapter.name, + featureName + ), + }); + } + } + } settings.adapter = adapter; }, }), @@ -185,6 +220,167 @@ export async function runHookConfigDone({ }); } } + // Astro doesn't set a default on purpose when loading the configuration file. + // Not setting a default allows to check if the adapter doesn't support the feature in case the user overrides + // Astro's defaults. + // + // At the end of the validation, we push the correct defaults. + if (typeof settings.config.image === 'undefined') { + settings.config.image = { + service: { + entrypoint: 'astro/assets/services/squoosh', + config: {}, + }, + }; + } +} + +// NOTE: remove for Astro 4.0 +const ALL_UNSUPPORTED: Required = { + serverOutput: UNSUPPORTED, + staticOutput: UNSUPPORTED, + edgeMiddleware: UNSUPPORTED, + hybridOutput: UNSUPPORTED, + functionPerPage: UNSUPPORTED, + assets: { supportKind: UNSUPPORTED }, +}; + +type ValidationResult = { + [Property in keyof AstroAdapterFeatureMap]: 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: AstroAdapterFeatureMap = ALL_UNSUPPORTED, + config: AstroConfig, + logging: LogOptions +): ValidationResult { + const { + functionPerPage = UNSUPPORTED, + edgeMiddleware = UNSUPPORTED, + assets, + serverOutput = UNSUPPORTED, + staticOutput = UNSUPPORTED, + hybridOutput = UNSUPPORTED, + } = featureMap; + const validationResult: ValidationResult = {}; + validationResult.functionPerPage = validateSupportKind( + functionPerPage, + adapterName, + logging, + 'functionPerPage', + () => config?.build?.split === true && config?.output === 'server' + ); + + validationResult.edgeMiddleware = validateSupportKind( + edgeMiddleware, + adapterName, + logging, + 'edgeMiddleware', + () => config?.build?.excludeMiddleware === true + ); + 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, + `adapter/${adapterName}`, + `The feature ${featureName} is not supported by the adapter ${adapterName}.` + ); +} + +function featureIsExperimental(adapterName: string, logging: LogOptions) { + warn( + logging, + `adapter/${adapterName}`, + 'The feature is experimental and subject to issues or changes.' + ); +} + +function featureIsDeprecated(adapterName: string, logging: LogOptions) { + warn( + logging, + `adapter/${adapterName}`, + 'The feature is deprecated and will be moved in the next release.' + ); +} + +const NODE_BASED_SERVICES = ['astro/assets/services/sharp', 'astro/assets/services/squoosh']; +function validateAssetsFeature( + assets: { supportKind?: SupportsKind; isNodeCompatible?: boolean } = {}, + adapterName: string, + config: AstroConfig, + logging: LogOptions +): boolean { + const { supportKind = UNSUPPORTED, isNodeCompatible = false } = assets; + if (NODE_BASED_SERVICES.includes(config?.image?.service?.entrypoint) && !isNodeCompatible) { + 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 false; + } + + return validateSupportKind(supportKind, adapterName, logging, 'assets', () => true); } export async function runHookServerSetup({ diff --git a/packages/astro/test/featuresSupport.test.js b/packages/astro/test/featuresSupport.test.js new file mode 100644 index 000000000..fba8da475 --- /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 85b4d69c0..66a0120ca 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -71,6 +71,17 @@ export default function ( name: 'my-ssr-adapter', serverEntrypoint: '@my-ssr', exports: ['manifest', 'createApp'], + supportedFeatures: { + edgeMiddleware: 'Stable', + functionPerPage: 'Stable', + 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 919628da2..a902aa870 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/index.js'; +import { defaultLogging } from '../test-utils.js'; describe('Integration API', () => { it('runHookBuildSetup should work', async () => { @@ -28,3 +30,196 @@ describe('Integration API', () => { expect(updatedViteConfig).to.haveOwnProperty('define'); }); }); + +describe('Integration feature map', function () { + it('should support the feature when stable', () => { + let result = validateSupportedFeatures( + 'test', + { + edgeMiddleware: 'Stable', + }, + { + build: { + excludeMiddleware: true, + }, + }, + defaultLogging + ); + expect(result['edgeMiddleware']).to.be.true; + }); + + it('should not support the feature when not provided', () => { + let result = validateSupportedFeatures( + 'test', + undefined, + { + build: { + excludeMiddleware: true, + }, + }, + defaultLogging + ); + expect(result['edgeMiddleware']).to.be.false; + }); + + it('should not support the feature when an empty object is provided', () => { + let result = validateSupportedFeatures( + 'test', + {}, + { + build: { + excludeMiddleware: true, + }, + }, + defaultLogging + ); + expect(result['edgeMiddleware']).to.be.false; + }); + + describe('edge middleware feature', function () { + it('should be supported with the correct config', () => { + let result = validateSupportedFeatures( + 'test', + { + edgeMiddleware: 'Stable', + }, + { + build: { + excludeMiddleware: true, + }, + }, + defaultLogging + ); + expect(result['edgeMiddleware']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + let result = validateSupportedFeatures( + 'test', + { + edgeMiddleware: 'Unsupported', + }, + { + build: { + excludeMiddleware: true, + }, + }, + defaultLogging + ); + expect(result['edgeMiddleware']).to.be.false; + }); + }); + describe('function per page feature', function () { + it('should be supported with the correct config', () => { + let result = validateSupportedFeatures( + 'test', + { functionPerPage: 'Stable' }, + { + build: { + split: true, + }, + output: 'server', + }, + defaultLogging + ); + expect(result['functionPerPage']).to.be.true; + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + // the output: server is missing + let result = validateSupportedFeatures( + 'test', + { + functionPerPage: 'Unsupported', + }, + { + build: { + split: true, + }, + output: 'server', + }, + defaultLogging + ); + expect(result['functionPerPage']).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; + }); + }); +}); diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index ef452aa95..1b56c840d 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -24,11 +24,27 @@ export function getAdapter(isModeDirectory: boolean): AstroAdapter { name: '@astrojs/cloudflare', serverEntrypoint: '@astrojs/cloudflare/server.directory.js', exports: ['onRequest', 'manifest'], + supportedFeatures: { + functionPerPage: 'Experimental', + edgeMiddleware: 'Unsupported', + hybridOutput: 'Stable', + staticOutput: 'Unsupported', + serverOutput: 'Stable', + assets: 'Unsupported', + }, } : { name: '@astrojs/cloudflare', serverEntrypoint: '@astrojs/cloudflare/server.advanced.js', exports: ['default'], + supportedFeatures: { + functionPerPage: 'Experimental', + edgeMiddleware: 'Unsupported', + hybridOutput: 'Stable', + staticOutput: 'Unsupported', + serverOutput: 'Stable', + assets: 'Unsupported', + }, }; } diff --git a/packages/integrations/cloudflare/test/no-output.test.js b/packages/integrations/cloudflare/test/no-output.test.js index af4d9c2b6..18bc21a3e 100644 --- a/packages/integrations/cloudflare/test/no-output.test.js +++ b/packages/integrations/cloudflare/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(`staticOutput`); }); }); diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts index 986449b18..402f9166f 100644 --- a/packages/integrations/deno/src/index.ts +++ b/packages/integrations/deno/src/index.ts @@ -89,6 +89,14 @@ export function getAdapter(args?: Options): AstroAdapter { serverEntrypoint: '@astrojs/deno/server.js', args: args ?? {}, exports: ['stop', 'handle', 'start', 'running'], + supportedFeatures: { + functionPerPage: 'Unsupported', + edgeMiddleware: 'Unsupported', + hybridOutput: 'Stable', + staticOutput: 'Stable', + serverOutput: 'Stable', + assets: 'Unsupported', + }, }; } diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index 28f828e48..66e0c196a 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -14,6 +14,14 @@ export function getAdapter(args: Args = {}): AstroAdapter { serverEntrypoint: '@astrojs/netlify/netlify-functions.js', exports: ['handler'], args, + supportedFeatures: { + functionPerPage: 'Experimental', + edgeMiddleware: 'Experimental', + hybridOutput: 'Stable', + staticOutput: 'Stable', + serverOutput: 'Stable', + assets: 'Unsupported', + }, }; } diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index 17a8f4502..eda6c1209 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -8,6 +8,14 @@ export function getAdapter(options: Options): AstroAdapter { previewEntrypoint: '@astrojs/node/preview.js', exports: ['handler', 'startServer'], args: options, + supportedFeatures: { + functionPerPage: 'Stable', + edgeMiddleware: 'Stable', + hybridOutput: 'Stable', + staticOutput: 'Stable', + serverOutput: 'Stable', + assets: 'Experimental', + }, }; } diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 5af00dfce..9a7ffc679 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -27,6 +27,14 @@ function getAdapter(): AstroAdapter { name: PACKAGE_NAME, serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, exports: ['default'], + supportedFeatures: { + functionPerPage: 'Unsupported', + edgeMiddleware: 'Unsupported', + hybridOutput: 'Stable', + staticOutput: 'Stable', + serverOutput: 'Stable', + assets: 'Unsupported', + }, }; } diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 2d12db5ad..486635ec3 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -34,6 +34,14 @@ function getAdapter(): AstroAdapter { name: PACKAGE_NAME, serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, exports: ['default'], + supportedFeatures: { + functionPerPage: 'Experimental', + edgeMiddleware: 'Experimental', + hybridOutput: 'Stable', + staticOutput: 'Stable', + serverOutput: 'Stable', + assets: 'Stable', + }, }; }