diff --git a/.changeset/fluffy-jars-live.md b/.changeset/fluffy-jars-live.md new file mode 100644 index 000000000000..59549a4a2131 --- /dev/null +++ b/.changeset/fluffy-jars-live.md @@ -0,0 +1,7 @@ +--- +'astro': major +--- + +Removes the `assets` property on `supportedAstroFeatures` for adapters, as it did not reflect reality properly in many cases. + +Now, relating to assets, only a single `sharpImageService` property is available, determining if the adapter is compatible with the built-in sharp image service. diff --git a/.changeset/slimy-queens-hang.md b/.changeset/slimy-queens-hang.md new file mode 100644 index 000000000000..936ed273158c --- /dev/null +++ b/.changeset/slimy-queens-hang.md @@ -0,0 +1,7 @@ +--- +'astro': minor +--- + +The value of the different properties on `supportedAstroFeatures` for adapters can now be objects, with a `support` and `message` properties. The content of the `message` property will be shown in the Astro CLI when the adapter is not compatible with the feature, allowing one to give a better informational message to the user. + +This is notably useful with the new `limited` value, to explain to the user why support is limited. diff --git a/.changeset/unlucky-bobcats-sit.md b/.changeset/unlucky-bobcats-sit.md new file mode 100644 index 000000000000..e815c8acdc51 --- /dev/null +++ b/.changeset/unlucky-bobcats-sit.md @@ -0,0 +1,5 @@ +--- +'astro': minor +--- + +Adds a new `limited` value for the different properties of `supportedAstroFeatures` for adapters, which indicates that the adapter is compatible with the feature, but with some limitations. This is useful for adapters that support a feature, but not in all cases or with all options. diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 79bd3b44d3c4..df3c8c81184a 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -5,6 +5,7 @@ import type { Plugin as VitePlugin } from 'vite'; import { getAssetsPrefix } from '../../../assets/utils/getAssetsPrefix.js'; import { normalizeTheLocale } from '../../../i18n/index.js'; import { toFallbackType, toRoutingStrategy } from '../../../i18n/utils.js'; +import { unwrapSupportKind } from '../../../integrations/features-validation.js'; import { runHookBuildSsr } from '../../../integrations/hooks.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; import type { @@ -273,6 +274,7 @@ function buildManifest( serverIslandNameMap: Array.from(settings.serverIslandNameMap), key: encodedKey, envGetSecretEnabled: - (settings.adapter?.supportedAstroFeatures.envGetSecret ?? 'unsupported') !== 'unsupported', + (unwrapSupportKind(settings.adapter?.supportedAstroFeatures.envGetSecret) ?? + 'unsupported') !== 'unsupported', }; } diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index 51ebd9325b06..05566b258902 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -33,6 +33,7 @@ export type LoggerLabel = | 'assets' | 'env' | 'update' + | 'adapter' // SKIP_FORMAT: A special label that tells the logger not to apply any formatting. // Useful for messages that are already formatted, like the server start message. | 'SKIP_FORMAT'; diff --git a/packages/astro/src/integrations/features-validation.ts b/packages/astro/src/integrations/features-validation.ts index 77db47e0a2ed..9383c76b2e7c 100644 --- a/packages/astro/src/integrations/features-validation.ts +++ b/packages/astro/src/integrations/features-validation.ts @@ -1,22 +1,18 @@ import type { Logger } from '../core/logger/core.js'; import type { AstroSettings } from '../types/astro.js'; -import type { AstroConfig } from '../types/public/config.js'; import type { + AdapterSupport, AdapterSupportsKind, AstroAdapterFeatureMap, - AstroAdapterFeatures, - AstroAssetsFeature, } from '../types/public/integrations.js'; -const STABLE = 'stable'; -const DEPRECATED = 'deprecated'; -const UNSUPPORTED = 'unsupported'; -const EXPERIMENTAL = 'experimental'; - -const UNSUPPORTED_ASSETS_FEATURE: AstroAssetsFeature = { - supportKind: UNSUPPORTED, - isSharpCompatible: false, -}; +export const AdapterFeatureStability = { + STABLE: 'stable', + DEPRECATED: 'deprecated', + UNSUPPORTED: 'unsupported', + EXPERIMENTAL: 'experimental', + LIMITED: 'limited', +} as const; type ValidationResult = { [Property in keyof AstroAdapterFeatureMap]: boolean; @@ -33,16 +29,15 @@ export function validateSupportedFeatures( adapterName: string, featureMap: AstroAdapterFeatureMap, settings: AstroSettings, - adapterFeatures: AstroAdapterFeatures | undefined, logger: Logger, ): ValidationResult { const { - assets = UNSUPPORTED_ASSETS_FEATURE, - serverOutput = UNSUPPORTED, - staticOutput = UNSUPPORTED, - hybridOutput = UNSUPPORTED, - i18nDomains = UNSUPPORTED, - envGetSecret = UNSUPPORTED, + serverOutput = AdapterFeatureStability.UNSUPPORTED, + staticOutput = AdapterFeatureStability.UNSUPPORTED, + hybridOutput = AdapterFeatureStability.UNSUPPORTED, + i18nDomains = AdapterFeatureStability.UNSUPPORTED, + envGetSecret = AdapterFeatureStability.UNSUPPORTED, + sharpImageService = AdapterFeatureStability.UNSUPPORTED, } = featureMap; const validationResult: ValidationResult = {}; @@ -67,9 +62,8 @@ export function validateSupportedFeatures( adapterName, logger, 'serverOutput', - () => settings.config?.output === 'server', + () => settings.config?.output === 'server' || settings.buildOutput === 'server', ); - validationResult.assets = validateAssetsFeature(assets, adapterName, settings.config, logger); if (settings.config.i18n?.domains) { validationResult.i18nDomains = validateSupportKind( @@ -91,71 +85,93 @@ export function validateSupportedFeatures( () => Object.keys(settings.config?.env?.schema ?? {}).length !== 0, ); + validationResult.sharpImageService = validateSupportKind( + sharpImageService, + adapterName, + logger, + 'sharp', + () => settings.config?.image?.service?.entrypoint === 'astro/assets/services/sharp', + ); + return validationResult; } +export function unwrapSupportKind(supportKind?: AdapterSupport): AdapterSupportsKind | undefined { + if (!supportKind) { + return undefined; + } + + return typeof supportKind === 'object' ? supportKind.support : supportKind; +} + +export function getSupportMessage(supportKind: AdapterSupport): string | undefined { + return typeof supportKind === 'object' ? supportKind.message : undefined; +} + function validateSupportKind( - supportKind: AdapterSupportsKind, + supportKind: AdapterSupport, adapterName: string, logger: Logger, featureName: string, hasCorrectConfig: () => boolean, ): boolean { - if (supportKind === STABLE) { - return true; - } else if (supportKind === DEPRECATED) { - featureIsDeprecated(adapterName, logger, featureName); - } else if (supportKind === EXPERIMENTAL) { - featureIsExperimental(adapterName, logger, featureName); - } + const supportValue = unwrapSupportKind(supportKind); + const message = getSupportMessage(supportKind); - if (hasCorrectConfig() && supportKind === UNSUPPORTED) { - featureIsUnsupported(adapterName, logger, featureName); + if (!supportValue) { return false; - } else { - return true; } -} -function featureIsUnsupported(adapterName: string, logger: Logger, featureName: string) { - logger.error( - 'config', - `The adapter ${adapterName} doesn't currently support the feature "${featureName}".`, - ); -} + if (supportValue === AdapterFeatureStability.STABLE) { + return true; + } else if (hasCorrectConfig()) { + // If the user has the relevant configuration, but the adapter doesn't support it, warn the user + logFeatureSupport(adapterName, logger, featureName, supportValue, message); + } -function featureIsExperimental(adapterName: string, logger: Logger, featureName: string) { - logger.warn( - 'config', - `The adapter ${adapterName} provides experimental support for "${featureName}". You may experience issues or breaking changes until this feature is fully supported by the adapter.`, - ); + return false; } -function featureIsDeprecated(adapterName: string, logger: Logger, featureName: string) { - logger.warn( - 'config', - `The adapter ${adapterName} has deprecated its support for "${featureName}", and future compatibility is not guaranteed. The adapter may completely remove support for this feature without warning.`, - ); -} - -const SHARP_SERVICE = 'astro/assets/services/sharp'; - -function validateAssetsFeature( - assets: AstroAssetsFeature, +function logFeatureSupport( adapterName: string, - config: AstroConfig, logger: Logger, -): boolean { - const { supportKind = UNSUPPORTED, isSharpCompatible = false } = assets; - if (config?.image?.service?.entrypoint === SHARP_SERVICE && !isSharpCompatible) { - logger.warn( - null, - `The currently selected adapter \`${adapterName}\` is not compatible with the image service "Sharp".`, - ); - return false; + featureName: string, + supportKind: AdapterSupport, + adapterMessage?: string, +) { + switch (supportKind) { + case AdapterFeatureStability.STABLE: + break; + case AdapterFeatureStability.DEPRECATED: + logger.warn( + 'config', + `The adapter ${adapterName} has deprecated its support for "${featureName}", and future compatibility is not guaranteed. The adapter may completely remove support for this feature without warning.`, + ); + break; + case AdapterFeatureStability.EXPERIMENTAL: + logger.warn( + 'config', + `The adapter ${adapterName} provides experimental support for "${featureName}". You may experience issues or breaking changes until this feature is fully supported by the adapter.`, + ); + break; + case AdapterFeatureStability.LIMITED: + logger.warn( + 'config', + `The adapter ${adapterName} has limited support for "${featureName}". Certain features may not work as expected.`, + ); + break; + case AdapterFeatureStability.UNSUPPORTED: + logger.error( + 'config', + `The adapter ${adapterName} does not currently support the feature "${featureName}". Your project may not build correctly.`, + ); + break; } - return validateSupportKind(supportKind, adapterName, logger, 'assets', () => true); + // If the adapter specified a custom message, log it after the default message + if (adapterMessage) { + logger.warn('adapter', adapterMessage); + } } export function getAdapterStaticRecommendation(adapterName: string): string | undefined { diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 0c40e60cdb96..f241448590a2 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -322,26 +322,12 @@ export async function runHookConfigDone({ `The adapter ${adapter.name} doesn't provide a feature map. It is required in Astro 4.0.`, ); } else { - const validationResult = validateSupportedFeatures( + validateSupportedFeatures( adapter.name, adapter.supportedAstroFeatures, settings, - // SAFETY: we checked before if it's not present, and we throw an error - adapter.adapterFeatures, logger, ); - for (const [featureName, supported] of Object.entries(validationResult)) { - // If `supported` / `validationResult[featureName]` only allows boolean, - // in theory 'assets' false, doesn't mean that the feature is not supported, but rather that the chosen image service is unsupported - // in this case we should not show an error, that the featrue is not supported - // if we would refactor the validation to support more than boolean, we could still be able to differentiate between the two cases - if (!supported && featureName !== 'assets') { - logger.error( - null, - `The adapter ${adapter.name} doesn't support the feature ${featureName}. Your project won't be built. You should not use it.`, - ); - } - } } settings.adapter = adapter; }, diff --git a/packages/astro/src/types/public/integrations.ts b/packages/astro/src/types/public/integrations.ts index 0836bdc9e9bb..a067c2b87402 100644 --- a/packages/astro/src/types/public/integrations.ts +++ b/packages/astro/src/types/public/integrations.ts @@ -3,6 +3,7 @@ import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite'; import type { SerializedSSRManifest } from '../../core/app/types.js'; import type { PageBuildData } from '../../core/build/types.js'; import type { AstroIntegrationLogger } from '../../core/logger/core.js'; +import type { AdapterFeatureStability } from '../../integrations/features-validation.js'; import type { getToolbarServerCommunicationHelpers } from '../../integrations/hooks.js'; import type { DeepPartial } from '../../type-utils.js'; import type { AstroConfig } from './config.js'; @@ -60,7 +61,15 @@ export interface AstroRenderer { serverEntrypoint: string; } -export type AdapterSupportsKind = 'unsupported' | 'stable' | 'experimental' | 'deprecated'; +export type AdapterSupportsKind = + (typeof AdapterFeatureStability)[keyof typeof AdapterFeatureStability]; + +export type AdapterSupportWithMessage = { + support: Exclude; + message: string; +}; + +export type AdapterSupport = AdapterSupportsKind | AdapterSupportWithMessage; export interface AstroAdapterFeatures { /** @@ -92,45 +101,33 @@ export type AstroAdapterFeatureMap = { /** * The adapter is able serve static pages */ - staticOutput?: AdapterSupportsKind; + staticOutput?: AdapterSupport; + /** * The adapter is able to serve pages that are static or rendered via server */ - hybridOutput?: AdapterSupportsKind; + hybridOutput?: AdapterSupport; + /** * The adapter is able to serve SSR pages */ - serverOutput?: AdapterSupportsKind; - /** - * The adapter can emit static assets - */ - assets?: AstroAssetsFeature; + serverOutput?: AdapterSupport; /** - * List of features that orbit around the i18n routing + * The adapter is able to support i18n domains */ - i18nDomains?: AdapterSupportsKind; + i18nDomains?: AdapterSupport; /** * The adapter is able to support `getSecret` exported from `astro:env/server` */ - envGetSecret?: AdapterSupportsKind; -}; + envGetSecret?: AdapterSupport; -export interface AstroAssetsFeature { - supportKind?: AdapterSupportsKind; /** - * Whether if this adapter deploys files in an environment that is compatible with the library `sharp` + * The adapter supports image transformation using the built-in Sharp image service */ - isSharpCompatible?: boolean; -} - -export interface AstroInternationalizationFeature { - /** - * The adapter should be able to create the proper redirects - */ - domains?: AdapterSupportsKind; -} + sharpImageService?: AdapterSupport; +}; /** * IDs for different stages of JS script injection: diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js index 4aef593be3ed..0a0233215061 100644 --- a/packages/astro/test/units/integrations/api.test.js +++ b/packages/astro/test/units/integrations/api.test.js @@ -153,7 +153,6 @@ describe('Astro feature map', function () { buildOutput: 'server', config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['hybridOutput'], false); @@ -167,7 +166,6 @@ describe('Astro feature map', function () { buildOutput: 'server', config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['hybridOutput'], false); @@ -181,7 +179,6 @@ describe('Astro feature map', function () { { config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['staticOutput'], true); @@ -195,7 +192,6 @@ describe('Astro feature map', function () { buildOutput: 'static', config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['staticOutput'], false); @@ -209,7 +205,6 @@ describe('Astro feature map', function () { { config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['hybridOutput'], true); @@ -225,7 +220,6 @@ describe('Astro feature map', function () { buildOutput: 'server', config: { output: 'static' }, }, - {}, defaultLogger, ); assert.equal(result['hybridOutput'], false); @@ -239,7 +233,6 @@ describe('Astro feature map', function () { { config: { output: 'server' }, }, - {}, defaultLogger, ); assert.equal(result['serverOutput'], true); @@ -254,62 +247,11 @@ describe('Astro feature map', function () { { config: { output: 'server' }, }, - {}, defaultLogger, ); assert.equal(result['serverOutput'], false); }); }); - - describe('assets', function () { - it('should be supported when it is sharp compatible', () => { - let result = validateSupportedFeatures( - 'test', - { - assets: { - supportKind: 'stable', - isSharpCompatible: true, - }, - }, - { - config: { - image: { - service: { - entrypoint: 'astro/assets/services/sharp', - }, - }, - }, - }, - {}, - defaultLogger, - ); - assert.equal(result['assets'], 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, - }, - }, - { - config: { - image: { - service: { - entrypoint: 'astro/assets/services/sharp', - }, - }, - }, - }, - {}, - defaultLogger, - ); - assert.equal(result['assets'], false); - }); - }); }); describe('normalizeInjectedTypeFilename', () => {