feat: support features for adapters

This commit is contained in:
Emanuele Stoppa 2023-07-06 18:08:50 +01:00
parent a0a1ca3e58
commit a26ef031df
10 changed files with 229 additions and 1 deletions

View file

@ -0,0 +1,17 @@
---
'astro': minor
---
Now you can tell Astro if an adapter supports certain features.
When creating ad adapter, you can specify an object like this:
```js
// adapter.js
setAdapter({
// ...
supportsFeatures: {
edgeMiddleware: "Experimental"
}
})
```

View file

@ -0,0 +1,7 @@
---
'@astrojs/cloudflare': patch
'@astrojs/vercel': patch
'@astrojs/node': patch
---
Signal Astro the kind of support for of the new created features

View file

@ -1638,12 +1638,31 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati
export type Params = Record<string, string | undefined>; export type Params = Record<string, string | undefined>;
export type SupportsKind = 'Unsupported' | 'Stable' | 'Experimental' | 'Deprecated';
export type AstroAdapterSupportsFeatures = {
/**
* Support when `build.split` is enabled.
*/
buildSplit?: SupportsKind;
/**
* Support when `build.ecludeMiddleware` is enabled.
*/
edgeMiddleware?: SupportsKind;
};
export interface AstroAdapter { export interface AstroAdapter {
name: string; name: string;
serverEntrypoint?: string; serverEntrypoint?: string;
previewEntrypoint?: string; previewEntrypoint?: string;
exports?: string[]; exports?: string[];
args?: any; args?: any;
/**
* List of features supported by an adapter.
*
* If the adapter is not able to handle certain configurations, Astro will throw an error.
*/
supportsFeatures?: AstroAdapterSupportsFeatures;
} }
type Body = string; type Body = string;

View file

@ -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 * @docs
* @see * @see

View file

@ -4,6 +4,7 @@ import fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import type { InlineConfig, ViteDevServer } from 'vite'; import type { InlineConfig, ViteDevServer } from 'vite';
import type { import type {
AstroAdapter,
AstroConfig, AstroConfig,
AstroRenderer, AstroRenderer,
AstroSettings, AstroSettings,
@ -11,13 +12,15 @@ import type {
DataEntryType, DataEntryType,
HookParameters, HookParameters,
RouteData, RouteData,
SupportsKind,
} from '../@types/astro.js'; } from '../@types/astro.js';
import type { SerializedSSRManifest } from '../core/app/types'; import type { SerializedSSRManifest } from '../core/app/types';
import type { PageBuildData } from '../core/build/types'; import type { PageBuildData } from '../core/build/types';
import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js';
import { mergeConfig } from '../core/config/config.js'; import { mergeConfig } from '../core/config/config.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 { isServerLikeOutput } from '../prerender/utils.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
async function withTakingALongTimeMsg<T>({ async function withTakingALongTimeMsg<T>({
name, name,
@ -178,6 +181,7 @@ export async function runHookConfigDone({
`Integration "${integration.name}" conflicts with "${settings.adapter.name}". You can only configure one deployment integration.` `Integration "${integration.name}" conflicts with "${settings.adapter.name}". You can only configure one deployment integration.`
); );
} }
validateSupportedFeatures(adapter, settings.config, logging);
settings.adapter = adapter; settings.adapter = adapter;
}, },
}), }),
@ -187,6 +191,101 @@ export async function runHookConfigDone({
} }
} }
/**
* 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.
*
*/
function validateSupportedFeatures(
adapter: AstroAdapter,
config: AstroConfig,
logging: LogOptions
) {
let supportsFeatures = adapter.supportsFeatures ?? {
buildSplit: 'Unsupported',
edgeMiddleware: 'Unsupported',
};
const { buildSplit, edgeMiddleware } = supportsFeatures;
if (buildSplit) {
validateSupportKind(
buildSplit,
adapter.name,
logging,
'build.split',
() => config.build.split === true
);
}
if (edgeMiddleware) {
validateSupportKind(
edgeMiddleware,
adapter.name,
logging,
'build.excludeMiddleware',
() => config.build.excludeMiddleware === true
);
}
}
const STABLE = 'Stable';
const DEPRECATED = 'Deprecated';
const UNSUPPORTED = 'Unsupported';
const EXPERIMENTAL = 'Experimental';
function validateSupportKind(
supportKind: SupportsKind,
adapterName: string,
logging: LogOptions,
featureName: string,
validation: () => boolean
) {
switch (supportKind) {
case DEPRECATED: {
featureIsDeprecated(adapterName, logging);
}
case UNSUPPORTED: {
if (validation()) {
featureIsUnsupported(adapterName, logging, featureName);
}
}
case EXPERIMENTAL: {
featureIsExperimental(adapterName, logging);
}
case STABLE: {
}
}
}
function featureIsUnsupported(adapterName: string, logging: LogOptions, featureName: string) {
error(
logging,
`adapter/${adapterName}`,
`The feature ${featureName} is not supported by ${adapterName}.`
);
throw new AstroError({
...AstroErrorData.FeatureNotSupportedByAdapter,
message: AstroErrorData.FeatureNotSupportedByAdapter.message(adapterName, featureName),
});
}
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.'
);
}
export async function runHookServerSetup({ export async function runHookServerSetup({
config, config,
server, server,

View file

@ -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: {
buildSplit: 'Unsupported',
},
},
}),
});
await fixture.build();
} catch (e) {
expect(e.toString()).to.contain(
"The adapter my-ssr-adapter doesn't support the feature build.split."
);
}
});
});

View file

@ -71,6 +71,10 @@ export default function (
name: 'my-ssr-adapter', name: 'my-ssr-adapter',
serverEntrypoint: '@my-ssr', serverEntrypoint: '@my-ssr',
exports: ['manifest', 'createApp'], exports: ['manifest', 'createApp'],
supportsFeatures: {
edgeMiddleware: 'Stable',
buildSplit: 'Stable',
},
...extendAdapter, ...extendAdapter,
}); });
}, },

View file

@ -24,11 +24,17 @@ export function getAdapter(isModeDirectory: boolean): AstroAdapter {
name: '@astrojs/cloudflare', name: '@astrojs/cloudflare',
serverEntrypoint: '@astrojs/cloudflare/server.directory.js', serverEntrypoint: '@astrojs/cloudflare/server.directory.js',
exports: ['onRequest', 'manifest'], exports: ['onRequest', 'manifest'],
supportsFeatures: {
buildSplit: 'Experimental',
},
} }
: { : {
name: '@astrojs/cloudflare', name: '@astrojs/cloudflare',
serverEntrypoint: '@astrojs/cloudflare/server.advanced.js', serverEntrypoint: '@astrojs/cloudflare/server.advanced.js',
exports: ['default'], exports: ['default'],
supportsFeatures: {
buildSplit: 'Experimental',
},
}; };
} }

View file

@ -8,6 +8,10 @@ export function getAdapter(options: Options): AstroAdapter {
previewEntrypoint: '@astrojs/node/preview.js', previewEntrypoint: '@astrojs/node/preview.js',
exports: ['handler', 'startServer'], exports: ['handler', 'startServer'],
args: options, args: options,
supportsFeatures: {
edgeMiddleware: 'Stable',
buildSplit: 'Stable',
},
}; };
} }

View file

@ -25,6 +25,10 @@ function getAdapter(): AstroAdapter {
name: PACKAGE_NAME, name: PACKAGE_NAME,
serverEntrypoint: `${PACKAGE_NAME}/entrypoint`, serverEntrypoint: `${PACKAGE_NAME}/entrypoint`,
exports: ['default'], exports: ['default'],
supportsFeatures: {
buildSplit: 'Experimental',
edgeMiddleware: 'Experimental',
},
}; };
} }