Skip to content

Commit

Permalink
feat: astro features (#7815)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Aug 3, 2023
1 parent 3fdf509 commit 9b4f70a
Show file tree
Hide file tree
Showing 20 changed files with 598 additions and 32 deletions.
32 changes: 32 additions & 0 deletions .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,
},
}
}
}
```
39 changes: 39 additions & 0 deletions packages/astro/src/@types/astro.ts
Expand Up @@ -1680,13 +1680,52 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati

export type Params = Record<string, string | undefined>;

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;
previewEntrypoint?: string;
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;
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/assets/generate.ts
Expand Up @@ -27,6 +27,11 @@ export async function generateImage(
options: ImageTransform,
filepath: string
): Promise<GenerationData | undefined> {
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;
}
Expand Down
27 changes: 0 additions & 27 deletions 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,
Expand All @@ -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
{
Expand Down
162 changes: 162 additions & 0 deletions 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<AstroFeatureMap> = {
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);
}
27 changes: 26 additions & 1 deletion packages/astro/src/integrations/index.ts
Expand Up @@ -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<T>({
name,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 9b4f70a

Please sign in to comment.