Skip to content

Commit cd2d7e7

Browse files
committed
feat: astro features (#7815)
1 parent 80f1494 commit cd2d7e7

File tree

20 files changed

+598
-32
lines changed

20 files changed

+598
-32
lines changed

.changeset/dirty-lies-cover.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@astrojs/cloudflare': minor
3+
'@astrojs/netlify': minor
4+
'@astrojs/vercel': minor
5+
'@astrojs/deno': minor
6+
'@astrojs/node': minor
7+
'astro': minor
8+
---
9+
10+
Introduced the concept of feature map. A feature map is a list of features that are built-in in Astro, and an Adapter
11+
can tell Astro if it can support it.
12+
13+
```ts
14+
import {AstroIntegration} from "./astro";
15+
16+
function myIntegration(): AstroIntegration {
17+
return {
18+
name: 'astro-awesome-list',
19+
// new feature map
20+
supportedAstroFeatures: {
21+
hybridOutput: 'experimental',
22+
staticOutput: 'stable',
23+
serverOutput: 'stable',
24+
assets: {
25+
supportKind: 'stable',
26+
isSharpCompatible: false,
27+
isSquooshCompatible: false,
28+
},
29+
}
30+
}
31+
}
32+
```

packages/astro/src/@types/astro.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,13 +1685,52 @@ export type PaginateFunction = (data: any[], args?: PaginateOptions) => GetStati
16851685

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

1688+
export type SupportsKind = 'unsupported' | 'stable' | 'experimental' | 'deprecated';
1689+
1690+
export type AstroFeatureMap = {
1691+
/**
1692+
* The adapter is able serve static pages
1693+
*/
1694+
staticOutput?: SupportsKind;
1695+
/**
1696+
* The adapter is able to serve pages that are static or rendered via server
1697+
*/
1698+
hybridOutput?: SupportsKind;
1699+
/**
1700+
* The adapter is able to serve SSR pages
1701+
*/
1702+
serverOutput?: SupportsKind;
1703+
/**
1704+
* The adapter can emit static assets
1705+
*/
1706+
assets?: AstroAssetsFeature;
1707+
};
1708+
1709+
export interface AstroAssetsFeature {
1710+
supportKind?: SupportsKind;
1711+
/**
1712+
* Whether if this adapter deploys files in an enviroment that is compatible with the library `sharp`
1713+
*/
1714+
isSharpCompatible?: boolean;
1715+
/**
1716+
* Whether if this adapter deploys files in an enviroment that is compatible with the library `squoosh`
1717+
*/
1718+
isSquooshCompatible?: boolean;
1719+
}
1720+
16881721
export interface AstroAdapter {
16891722
name: string;
16901723
serverEntrypoint?: string;
16911724
previewEntrypoint?: string;
16921725
exports?: string[];
16931726
args?: any;
16941727
adapterFeatures?: AstroAdapterFeatures;
1728+
/**
1729+
* List of features supported by an adapter.
1730+
*
1731+
* If the adapter is not able to handle certain configurations, Astro will throw an error.
1732+
*/
1733+
supportedAstroFeatures?: AstroFeatureMap;
16951734
}
16961735

16971736
type Body = string;

packages/astro/src/assets/generate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export async function generateImage(
2727
options: ImageTransform,
2828
filepath: string
2929
): Promise<GenerationData | undefined> {
30+
if (typeof buildOpts.settings.config.image === 'undefined') {
31+
throw new Error(
32+
"Astro hasn't set a default service for `astro:assets`. This is an internal error and you should report it."
33+
);
34+
}
3035
if (!isESMImportedImage(options.src)) {
3136
return undefined;
3237
}

packages/astro/src/assets/vite-plugin-assets.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { bold } from 'kleur/colors';
21
import MagicString from 'magic-string';
32
import { fileURLToPath } from 'node:url';
43
import type * as vite from 'vite';
54
import { normalizePath } from 'vite';
65
import type { AstroPluginOptions, ImageTransform } from '../@types/astro';
7-
import { error } from '../core/logger/core.js';
86
import {
97
appendForwardSlash,
108
joinPaths,
@@ -23,37 +21,12 @@ const urlRE = /(\?|&)url(?:&|$)/;
2321

2422
export default function assets({
2523
settings,
26-
logging,
2724
mode,
2825
}: AstroPluginOptions & { mode: string }): vite.Plugin[] {
2926
let resolvedConfig: vite.ResolvedConfig;
3027

3128
globalThis.astroAsset = {};
3229

33-
const UNSUPPORTED_ADAPTERS = new Set([
34-
'@astrojs/cloudflare',
35-
'@astrojs/deno',
36-
'@astrojs/netlify/edge-functions',
37-
'@astrojs/vercel/edge',
38-
]);
39-
40-
const adapterName = settings.config.adapter?.name;
41-
if (
42-
['astro/assets/services/sharp', 'astro/assets/services/squoosh'].includes(
43-
settings.config.image.service.entrypoint
44-
) &&
45-
adapterName &&
46-
UNSUPPORTED_ADAPTERS.has(adapterName)
47-
) {
48-
error(
49-
logging,
50-
'assets',
51-
`The currently selected adapter \`${adapterName}\` does not run on Node, however the currently used image service depends on Node built-ins. ${bold(
52-
'Your project will NOT be able to build.'
53-
)}`
54-
);
55-
}
56-
5730
return [
5831
// Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
5932
{
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import type {
2+
AstroAssetsFeature,
3+
AstroConfig,
4+
AstroFeatureMap,
5+
SupportsKind,
6+
} from '../@types/astro';
7+
import { error, type LogOptions, warn } from '../core/logger/core.js';
8+
import { bold } from 'kleur/colors';
9+
10+
const STABLE = 'stable';
11+
const DEPRECATED = 'deprecated';
12+
const UNSUPPORTED = 'unsupported';
13+
const EXPERIMENTAL = 'experimental';
14+
15+
const UNSUPPORTED_ASSETS_FEATURE: AstroAssetsFeature = {
16+
supportKind: UNSUPPORTED,
17+
isSquooshCompatible: false,
18+
isSharpCompatible: false,
19+
};
20+
21+
// NOTE: remove for Astro 4.0
22+
const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
23+
serverOutput: UNSUPPORTED,
24+
staticOutput: UNSUPPORTED,
25+
hybridOutput: UNSUPPORTED,
26+
assets: UNSUPPORTED_ASSETS_FEATURE,
27+
};
28+
29+
type ValidationResult = {
30+
[Property in keyof AstroFeatureMap]: boolean;
31+
};
32+
33+
/**
34+
* Checks whether an adapter supports certain features that are enabled via Astro configuration.
35+
*
36+
* If a configuration is enabled and "unlocks" a feature, but the adapter doesn't support, the function
37+
* will throw a runtime error.
38+
*
39+
*/
40+
export function validateSupportedFeatures(
41+
adapterName: string,
42+
featureMap: AstroFeatureMap = ALL_UNSUPPORTED,
43+
config: AstroConfig,
44+
logging: LogOptions
45+
): ValidationResult {
46+
const {
47+
assets = UNSUPPORTED_ASSETS_FEATURE,
48+
serverOutput = UNSUPPORTED,
49+
staticOutput = UNSUPPORTED,
50+
hybridOutput = UNSUPPORTED,
51+
} = featureMap;
52+
const validationResult: ValidationResult = {};
53+
54+
validationResult.staticOutput = validateSupportKind(
55+
staticOutput,
56+
adapterName,
57+
logging,
58+
'staticOutput',
59+
() => config?.output === 'static'
60+
);
61+
62+
validationResult.hybridOutput = validateSupportKind(
63+
hybridOutput,
64+
adapterName,
65+
logging,
66+
'hybridOutput',
67+
() => config?.output === 'hybrid'
68+
);
69+
70+
validationResult.serverOutput = validateSupportKind(
71+
serverOutput,
72+
adapterName,
73+
logging,
74+
'serverOutput',
75+
() => config?.output === 'server'
76+
);
77+
validationResult.assets = validateAssetsFeature(assets, adapterName, config, logging);
78+
79+
return validationResult;
80+
}
81+
82+
function validateSupportKind(
83+
supportKind: SupportsKind,
84+
adapterName: string,
85+
logging: LogOptions,
86+
featureName: string,
87+
hasCorrectConfig: () => boolean
88+
): boolean {
89+
if (supportKind === STABLE) {
90+
return true;
91+
} else if (supportKind === DEPRECATED) {
92+
featureIsDeprecated(adapterName, logging);
93+
} else if (supportKind === EXPERIMENTAL) {
94+
featureIsExperimental(adapterName, logging);
95+
}
96+
97+
if (hasCorrectConfig() && supportKind === UNSUPPORTED) {
98+
featureIsUnsupported(adapterName, logging, featureName);
99+
return false;
100+
} else {
101+
return true;
102+
}
103+
}
104+
105+
function featureIsUnsupported(adapterName: string, logging: LogOptions, featureName: string) {
106+
error(
107+
logging,
108+
`${adapterName}`,
109+
`The feature ${featureName} is not supported by the adapter ${adapterName}.`
110+
);
111+
}
112+
113+
function featureIsExperimental(adapterName: string, logging: LogOptions) {
114+
warn(logging, `${adapterName}`, 'The feature is experimental and subject to issues or changes.');
115+
}
116+
117+
function featureIsDeprecated(adapterName: string, logging: LogOptions) {
118+
warn(
119+
logging,
120+
`${adapterName}`,
121+
'The feature is deprecated and will be moved in the next release.'
122+
);
123+
}
124+
125+
const SHARP_SERVICE = 'astro/assets/services/sharp';
126+
const SQUOOSH_SERVICE = 'astro/assets/services/squoosh';
127+
128+
function validateAssetsFeature(
129+
assets: AstroAssetsFeature,
130+
adapterName: string,
131+
config: AstroConfig,
132+
logging: LogOptions
133+
): boolean {
134+
const {
135+
supportKind = UNSUPPORTED,
136+
isSharpCompatible = false,
137+
isSquooshCompatible = false,
138+
} = assets;
139+
if (config?.image?.service?.entrypoint === SHARP_SERVICE && !isSharpCompatible) {
140+
error(
141+
logging,
142+
'astro',
143+
`The currently selected adapter \`${adapterName}\` is not compatible with the service "Sharp". ${bold(
144+
'Your project will NOT be able to build.'
145+
)}`
146+
);
147+
return false;
148+
}
149+
150+
if (config?.image?.service?.entrypoint === SQUOOSH_SERVICE && !isSquooshCompatible) {
151+
error(
152+
logging,
153+
'astro',
154+
`The currently selected adapter \`${adapterName}\` is not compatible with the service "Squoosh". ${bold(
155+
'Your project will NOT be able to build.'
156+
)}`
157+
);
158+
return false;
159+
}
160+
161+
return validateSupportKind(supportKind, adapterName, logging, 'assets', () => true);
162+
}

packages/astro/src/integrations/index.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import type { SerializedSSRManifest } from '../core/app/types';
1818
import type { PageBuildData } from '../core/build/types';
1919
import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js';
2020
import { mergeConfig } from '../core/config/index.js';
21-
import { info, type LogOptions, AstroIntegrationLogger } from '../core/logger/core.js';
21+
import { info, warn, error, type LogOptions, AstroIntegrationLogger } from '../core/logger/core.js';
2222
import { isServerLikeOutput } from '../prerender/utils.js';
23+
import { validateSupportedFeatures } from './astroFeaturesValidation.js';
2324

2425
async function withTakingALongTimeMsg<T>({
2526
name,
@@ -197,6 +198,30 @@ export async function runHookConfigDone({
197198
`Integration "${integration.name}" conflicts with "${settings.adapter.name}". You can only configure one deployment integration.`
198199
);
199200
}
201+
if (!adapter.supportedAstroFeatures) {
202+
// NOTE: throw an error in Astro 4.0
203+
warn(
204+
logging,
205+
'astro',
206+
`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.`
207+
);
208+
} else {
209+
const validationResult = validateSupportedFeatures(
210+
adapter.name,
211+
adapter.supportedAstroFeatures,
212+
settings.config,
213+
logging
214+
);
215+
for (const [featureName, supported] of Object.entries(validationResult)) {
216+
if (!supported) {
217+
error(
218+
logging,
219+
'astro',
220+
`The adapter ${adapter.name} doesn't support the feature ${featureName}. Your project won't be built. You should not use it.`
221+
);
222+
}
223+
}
224+
}
200225
settings.adapter = adapter;
201226
},
202227
logger,

0 commit comments

Comments
 (0)