astro 3.2.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/astro.js +7 -0
- package/client.d.ts +3 -9
- package/components/Code.astro +60 -29
- package/components/Image.astro +7 -1
- package/components/Picture.astro +59 -0
- package/components/shiki.ts +46 -0
- package/dist/@types/astro.d.ts +2 -2
- package/dist/assets/consts.d.ts +1 -0
- package/dist/assets/consts.js +2 -0
- package/dist/assets/internal.js +19 -3
- package/dist/assets/services/service.d.ts +12 -0
- package/dist/assets/services/service.js +89 -16
- package/dist/assets/services/sharp.js +3 -2
- package/dist/assets/services/squoosh.js +3 -2
- package/dist/assets/types.d.ts +29 -2
- package/dist/assets/utils/transformToPath.js +1 -1
- package/dist/assets/vite-plugin-assets.js +1 -0
- package/dist/cli/add/index.js +18 -13
- package/dist/cli/build/index.js +1 -0
- package/dist/cli/flags.js +1 -0
- package/dist/config/index.js +1 -1
- package/dist/content/server-listeners.js +8 -7
- package/dist/core/build/css-asset-name.d.ts +1 -0
- package/dist/core/build/css-asset-name.js +29 -7
- package/dist/core/build/index.js +1 -1
- package/dist/core/build/plugins/plugin-css.js +1 -13
- package/dist/core/build/plugins/plugin-internals.js +0 -2
- package/dist/core/config/schema.d.ts +39 -38
- package/dist/core/config/schema.js +15 -3
- package/dist/core/config/settings.d.ts +1 -1
- package/dist/core/config/settings.js +10 -5
- package/dist/core/config/tsconfig.d.ts +24 -7
- package/dist/core/config/tsconfig.js +44 -22
- package/dist/core/constants.js +1 -1
- package/dist/core/create-vite.js +0 -2
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/dev/restart.js +2 -2
- package/dist/core/endpoint/index.js +24 -18
- package/dist/core/errors/dev/vite.js +4 -3
- package/dist/core/errors/errors-data.d.ts +13 -0
- package/dist/core/errors/errors-data.js +7 -0
- package/dist/core/messages.js +2 -2
- package/dist/core/preview/index.js +1 -1
- package/dist/core/sync/index.js +1 -1
- package/dist/runtime/server/hydration.d.ts +4 -2
- package/dist/runtime/server/hydration.js +6 -1
- package/dist/runtime/server/render/component.js +6 -3
- package/dist/runtime/server/render/page.js +3 -0
- package/dist/transitions/router.d.ts +1 -1
- package/dist/transitions/router.js +93 -53
- package/package.json +5 -5
- package/components/Shiki.js +0 -97
- package/components/shiki-languages.js +0 -176
- package/components/shiki-themes.js +0 -37
package/astro.js
CHANGED
|
@@ -32,6 +32,13 @@ async function main() {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// windows drive letters can sometimes be lowercase, which vite cannot process
|
|
36
|
+
if (process.platform === 'win32') {
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
const correctedCwd = cwd.slice(0, 1).toUpperCase() + cwd.slice(1);
|
|
39
|
+
if (correctedCwd !== cwd) process.chdir(correctedCwd);
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
return import('./dist/cli/index.js')
|
|
36
43
|
.then(({ cli }) => cli(process.argv))
|
|
37
44
|
.catch((error) => {
|
package/client.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ declare module 'astro:assets' {
|
|
|
53
53
|
imageConfig: import('./dist/@types/astro.js').AstroConfig['image'];
|
|
54
54
|
getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService;
|
|
55
55
|
Image: typeof import('./components/Image.astro').default;
|
|
56
|
+
Picture: typeof import('./components/Picture.astro').default;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
type ImgAttributes = import('./dist/type-utils.js').WithRequired<
|
|
@@ -66,17 +67,10 @@ declare module 'astro:assets' {
|
|
|
66
67
|
export type RemoteImageProps = import('./dist/type-utils.js').Simplify<
|
|
67
68
|
import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
|
|
68
69
|
>;
|
|
69
|
-
export const { getImage, getConfiguredImageService, imageConfig, Image }: AstroAssets;
|
|
70
|
+
export const { getImage, getConfiguredImageService, imageConfig, Image, Picture }: AstroAssets;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
type
|
|
73
|
-
|
|
74
|
-
interface ImageMetadata {
|
|
75
|
-
src: string;
|
|
76
|
-
width: number;
|
|
77
|
-
height: number;
|
|
78
|
-
format: InputFormat;
|
|
79
|
-
}
|
|
73
|
+
type ImageMetadata = import('./dist/assets/types.js').ImageMetadata;
|
|
80
74
|
|
|
81
75
|
declare module '*.gif' {
|
|
82
76
|
const metadata: ImageMetadata;
|
package/components/Code.astro
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
BuiltinLanguage,
|
|
4
|
+
BuiltinTheme,
|
|
5
|
+
LanguageRegistration,
|
|
6
|
+
SpecialLanguage,
|
|
7
|
+
ThemeRegistration,
|
|
8
|
+
ThemeRegistrationRaw,
|
|
9
|
+
} from 'shikiji';
|
|
10
|
+
import { visit } from 'unist-util-visit';
|
|
11
|
+
import { getCachedHighlighter, replaceCssVariables } from './shiki.js';
|
|
5
12
|
|
|
6
13
|
interface Props {
|
|
7
14
|
/** The code to highlight. Required. */
|
|
@@ -13,7 +20,7 @@ interface Props {
|
|
|
13
20
|
*
|
|
14
21
|
* @default "plaintext"
|
|
15
22
|
*/
|
|
16
|
-
lang?:
|
|
23
|
+
lang?: BuiltinLanguage | SpecialLanguage | LanguageRegistration;
|
|
17
24
|
/**
|
|
18
25
|
* The styling theme.
|
|
19
26
|
* Supports all themes listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes
|
|
@@ -21,7 +28,7 @@ interface Props {
|
|
|
21
28
|
*
|
|
22
29
|
* @default "github-dark"
|
|
23
30
|
*/
|
|
24
|
-
theme?:
|
|
31
|
+
theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw;
|
|
25
32
|
/**
|
|
26
33
|
* Enable word wrapping.
|
|
27
34
|
* - true: enabled.
|
|
@@ -47,41 +54,65 @@ const {
|
|
|
47
54
|
inline = false,
|
|
48
55
|
} = Astro.props;
|
|
49
56
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
// shiki -> shikiji compat
|
|
58
|
+
if (typeof lang === 'object') {
|
|
59
|
+
// `id` renamed to `name
|
|
60
|
+
if ((lang as any).id && !lang.name) {
|
|
61
|
+
lang.name = (lang as any).id;
|
|
62
|
+
}
|
|
63
|
+
// `grammar` flattened to lang itself
|
|
64
|
+
if ((lang as any).grammar) {
|
|
65
|
+
Object.assign(lang, (lang as any).grammar);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const highlighter = await getCachedHighlighter({
|
|
70
|
+
langs: [lang],
|
|
71
|
+
themes: [theme],
|
|
55
72
|
});
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
const html = highlighter.codeToHtml(code, {
|
|
75
|
+
lang: typeof lang === 'string' ? lang : lang.name,
|
|
76
|
+
theme,
|
|
77
|
+
transforms: {
|
|
78
|
+
pre(node) {
|
|
79
|
+
// Swap to `code` tag if inline
|
|
80
|
+
if (inline) {
|
|
81
|
+
node.tagName = 'code';
|
|
82
|
+
}
|
|
59
83
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
84
|
+
// Cast to string as shikiji will always pass them as strings instead of any other types
|
|
85
|
+
const classValue = (node.properties.class as string) ?? '';
|
|
86
|
+
const styleValue = (node.properties.style as string) ?? '';
|
|
62
87
|
|
|
63
|
-
// 4. Render the theme tokens as html
|
|
64
|
-
const html = renderToHtml(tokens, {
|
|
65
|
-
themeName: _theme.name,
|
|
66
|
-
fg: _theme.fg,
|
|
67
|
-
bg: _theme.bg,
|
|
68
|
-
elements: {
|
|
69
|
-
pre({ className, style, children }) {
|
|
70
|
-
// Swap to `code` tag if inline
|
|
71
|
-
const tag = inline ? 'code' : 'pre';
|
|
72
88
|
// Replace "shiki" class naming with "astro-code"
|
|
73
|
-
|
|
89
|
+
node.properties.class = classValue.replace(/shiki/g, 'astro-code');
|
|
90
|
+
|
|
74
91
|
// Handle code wrapping
|
|
75
92
|
// if wrap=null, do nothing.
|
|
76
93
|
if (wrap === false) {
|
|
77
|
-
style
|
|
94
|
+
node.properties.style = styleValue + '; overflow-x: auto;';
|
|
78
95
|
} else if (wrap === true) {
|
|
79
|
-
style
|
|
96
|
+
node.properties.style =
|
|
97
|
+
styleValue + '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
|
|
80
98
|
}
|
|
81
|
-
return `<${tag} class="${className}" style="${style}" tabindex="0">${children}</${tag}>`;
|
|
82
99
|
},
|
|
83
|
-
code(
|
|
84
|
-
|
|
100
|
+
code(node) {
|
|
101
|
+
if (inline) {
|
|
102
|
+
return node.children[0] as typeof node;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
root(node) {
|
|
106
|
+
// theme.id for shiki -> shikiji compat
|
|
107
|
+
const themeName = typeof theme === 'string' ? theme : theme.name;
|
|
108
|
+
if (themeName === 'css-variables') {
|
|
109
|
+
// Replace special color tokens to CSS variables
|
|
110
|
+
visit(node as any, 'element', (child) => {
|
|
111
|
+
if (child.properties?.style) {
|
|
112
|
+
child.properties.style = replaceCssVariables(child.properties.style);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
85
116
|
},
|
|
86
117
|
},
|
|
87
118
|
});
|
package/components/Image.astro
CHANGED
|
@@ -23,6 +23,12 @@ if (typeof props.height === 'string') {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const image = await getImage(props);
|
|
26
|
+
|
|
27
|
+
const additionalAttributes: Record<string, any> = {};
|
|
28
|
+
|
|
29
|
+
if (image.srcSet.values.length > 0) {
|
|
30
|
+
additionalAttributes.srcset = image.srcSet.attribute;
|
|
31
|
+
}
|
|
26
32
|
---
|
|
27
33
|
|
|
28
|
-
<img src={image.src} {...image.attributes} />
|
|
34
|
+
<img src={image.src} {...additionalAttributes} {...image.attributes} />
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
|
|
3
|
+
import type { GetImageResult, ImageOutputFormat } from '../dist/@types/astro';
|
|
4
|
+
import { isESMImportedImage } from '../dist/assets/internal';
|
|
5
|
+
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
|
|
6
|
+
import type { HTMLAttributes } from '../types';
|
|
7
|
+
|
|
8
|
+
type Props = (LocalImageProps | RemoteImageProps) & {
|
|
9
|
+
formats?: ImageOutputFormat[];
|
|
10
|
+
fallbackFormat?: ImageOutputFormat;
|
|
11
|
+
pictureAttributes?: HTMLAttributes<'picture'>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { formats = ['webp'], pictureAttributes = {}, ...props } = Astro.props;
|
|
15
|
+
|
|
16
|
+
if (props.alt === undefined || props.alt === null) {
|
|
17
|
+
throw new AstroError(AstroErrorData.ImageMissingAlt);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const optimizedImages: GetImageResult[] = await Promise.all(
|
|
21
|
+
formats.map(
|
|
22
|
+
async (format) =>
|
|
23
|
+
await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const fallbackFormat =
|
|
28
|
+
props.fallbackFormat ?? isESMImportedImage(props.src)
|
|
29
|
+
? ['svg', 'gif'].includes(props.src.format)
|
|
30
|
+
? props.src.format
|
|
31
|
+
: 'png'
|
|
32
|
+
: 'png';
|
|
33
|
+
|
|
34
|
+
const fallbackImage = await getImage({
|
|
35
|
+
...props,
|
|
36
|
+
format: fallbackFormat,
|
|
37
|
+
widths: props.widths,
|
|
38
|
+
densities: props.densities,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const additionalAttributes: Record<string, any> = {};
|
|
42
|
+
if (fallbackImage.srcSet.values.length > 0) {
|
|
43
|
+
additionalAttributes.srcset = fallbackImage.srcSet.attribute;
|
|
44
|
+
}
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<picture {...pictureAttributes}>
|
|
48
|
+
{
|
|
49
|
+
Object.entries(optimizedImages).map(([_, image]) => (
|
|
50
|
+
<source
|
|
51
|
+
srcset={`${image.src}${
|
|
52
|
+
image.srcSet.values.length > 0 ? ' , ' + image.srcSet.attribute : ''
|
|
53
|
+
}`}
|
|
54
|
+
type={'image/' + image.options.format}
|
|
55
|
+
/>
|
|
56
|
+
))
|
|
57
|
+
}
|
|
58
|
+
<img src={fallbackImage.src} {...additionalAttributes} {...fallbackImage.attributes} />
|
|
59
|
+
</picture>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Highlighter, getHighlighter } from 'shikiji';
|
|
2
|
+
|
|
3
|
+
type HighlighterOptions = NonNullable<Parameters<typeof getHighlighter>[0]>;
|
|
4
|
+
|
|
5
|
+
const ASTRO_COLOR_REPLACEMENTS: Record<string, string> = {
|
|
6
|
+
'#000001': 'var(--astro-code-color-text)',
|
|
7
|
+
'#000002': 'var(--astro-code-color-background)',
|
|
8
|
+
'#000004': 'var(--astro-code-token-constant)',
|
|
9
|
+
'#000005': 'var(--astro-code-token-string)',
|
|
10
|
+
'#000006': 'var(--astro-code-token-comment)',
|
|
11
|
+
'#000007': 'var(--astro-code-token-keyword)',
|
|
12
|
+
'#000008': 'var(--astro-code-token-parameter)',
|
|
13
|
+
'#000009': 'var(--astro-code-token-function)',
|
|
14
|
+
'#000010': 'var(--astro-code-token-string-expression)',
|
|
15
|
+
'#000011': 'var(--astro-code-token-punctuation)',
|
|
16
|
+
'#000012': 'var(--astro-code-token-link)',
|
|
17
|
+
};
|
|
18
|
+
const COLOR_REPLACEMENT_REGEX = new RegExp(
|
|
19
|
+
`(${Object.keys(ASTRO_COLOR_REPLACEMENTS).join('|')})`,
|
|
20
|
+
'g'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Caches Promise<Highlighter> for reuse when the same theme and langs are provided
|
|
24
|
+
const cachedHighlighters = new Map();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* shiki -> shikiji compat as we need to manually replace it
|
|
28
|
+
*/
|
|
29
|
+
export function replaceCssVariables(str: string) {
|
|
30
|
+
return str.replace(COLOR_REPLACEMENT_REGEX, (match) => ASTRO_COLOR_REPLACEMENTS[match] || match);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCachedHighlighter(opts: HighlighterOptions): Promise<Highlighter> {
|
|
34
|
+
// Always sort keys before stringifying to make sure objects match regardless of parameter ordering
|
|
35
|
+
const key = JSON.stringify(opts, Object.keys(opts).sort());
|
|
36
|
+
|
|
37
|
+
// Highlighter has already been requested, reuse the same instance
|
|
38
|
+
if (cachedHighlighters.has(key)) {
|
|
39
|
+
return cachedHighlighters.get(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const highlighter = getHighlighter(opts);
|
|
43
|
+
cachedHighlighters.set(key, highlighter);
|
|
44
|
+
|
|
45
|
+
return highlighter;
|
|
46
|
+
}
|
package/dist/@types/astro.d.ts
CHANGED
|
@@ -6,13 +6,13 @@ import type * as babel from '@babel/core';
|
|
|
6
6
|
import type { OutgoingHttpHeaders } from 'node:http';
|
|
7
7
|
import type { AddressInfo } from 'node:net';
|
|
8
8
|
import type * as rollup from 'rollup';
|
|
9
|
-
import type { TsConfigJson } from 'tsconfig-resolver';
|
|
10
9
|
import type * as vite from 'vite';
|
|
11
10
|
import type { RemotePattern } from '../assets/utils/remotePattern.js';
|
|
12
11
|
import type { SerializedSSRManifest } from '../core/app/types.js';
|
|
13
12
|
import type { PageBuildData } from '../core/build/types.js';
|
|
14
13
|
import type { AstroConfigType } from '../core/config/index.js';
|
|
15
14
|
import type { AstroTimer } from '../core/config/timer.js';
|
|
15
|
+
import type { TSConfig } from '../core/config/tsconfig.js';
|
|
16
16
|
import type { AstroCookies } from '../core/cookies/index.js';
|
|
17
17
|
import type { ResponseWithEncoding } from '../core/endpoint/index.js';
|
|
18
18
|
import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
|
|
@@ -1381,7 +1381,7 @@ export interface AstroSettings {
|
|
|
1381
1381
|
* Map of directive name (e.g. `load`) to the directive script code
|
|
1382
1382
|
*/
|
|
1383
1383
|
clientDirectives: Map<string, string>;
|
|
1384
|
-
tsConfig:
|
|
1384
|
+
tsConfig: TSConfig | undefined;
|
|
1385
1385
|
tsConfigPath: string | undefined;
|
|
1386
1386
|
watchFiles: string[];
|
|
1387
1387
|
timer: AstroTimer;
|
package/dist/assets/consts.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export declare const VALID_INPUT_FORMATS: readonly ["jpeg", "jpg", "png", "tiff"
|
|
|
6
6
|
* Certain formats can be imported (namely SVGs) but will not be processed.
|
|
7
7
|
*/
|
|
8
8
|
export declare const VALID_SUPPORTED_FORMATS: readonly ["jpeg", "jpg", "png", "tiff", "webp", "gif", "svg", "avif"];
|
|
9
|
+
export declare const DEFAULT_OUTPUT_FORMAT: "webp";
|
|
9
10
|
export declare const VALID_OUTPUT_FORMATS: readonly ["avif", "png", "webp", "jpeg", "jpg", "svg"];
|
package/dist/assets/consts.js
CHANGED
|
@@ -20,8 +20,10 @@ const VALID_SUPPORTED_FORMATS = [
|
|
|
20
20
|
"svg",
|
|
21
21
|
"avif"
|
|
22
22
|
];
|
|
23
|
+
const DEFAULT_OUTPUT_FORMAT = "webp";
|
|
23
24
|
const VALID_OUTPUT_FORMATS = ["avif", "png", "webp", "jpeg", "jpg", "svg"];
|
|
24
25
|
export {
|
|
26
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
25
27
|
VALID_INPUT_FORMATS,
|
|
26
28
|
VALID_OUTPUT_FORMATS,
|
|
27
29
|
VALID_SUPPORTED_FORMATS,
|
package/dist/assets/internal.js
CHANGED
|
@@ -56,16 +56,32 @@ async function getImage(options, imageConfig) {
|
|
|
56
56
|
src: typeof options.src === "object" && "then" in options.src ? (await options.src).default ?? await options.src : options.src
|
|
57
57
|
};
|
|
58
58
|
const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
|
|
59
|
+
const srcSetTransforms = service.getSrcSet ? await service.getSrcSet(validatedOptions, imageConfig) : [];
|
|
59
60
|
let imageURL = await service.getURL(validatedOptions, imageConfig);
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
let srcSets = await Promise.all(
|
|
62
|
+
srcSetTransforms.map(async (srcSet) => ({
|
|
63
|
+
url: await service.getURL(srcSet.transform, imageConfig),
|
|
64
|
+
descriptor: srcSet.descriptor,
|
|
65
|
+
attributes: srcSet.attributes
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
if (isLocalService(service) && globalThis.astroAsset.addStaticImage && !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
|
|
62
69
|
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
|
|
70
|
+
srcSets = srcSetTransforms.map((srcSet) => ({
|
|
71
|
+
url: globalThis.astroAsset.addStaticImage(srcSet.transform),
|
|
72
|
+
descriptor: srcSet.descriptor,
|
|
73
|
+
attributes: srcSet.attributes
|
|
74
|
+
}));
|
|
63
75
|
}
|
|
64
76
|
return {
|
|
65
77
|
rawOptions: resolvedOptions,
|
|
66
78
|
options: validatedOptions,
|
|
67
79
|
src: imageURL,
|
|
68
|
-
|
|
80
|
+
srcSet: {
|
|
81
|
+
values: srcSets,
|
|
82
|
+
attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(", ")
|
|
83
|
+
},
|
|
84
|
+
attributes: service.getHTMLAttributes !== void 0 ? await service.getHTMLAttributes(validatedOptions, imageConfig) : {}
|
|
69
85
|
};
|
|
70
86
|
}
|
|
71
87
|
export {
|
|
@@ -10,6 +10,11 @@ type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & {
|
|
|
10
10
|
config: T;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
|
+
type SrcSetValue = {
|
|
14
|
+
transform: ImageTransform;
|
|
15
|
+
descriptor?: string;
|
|
16
|
+
attributes?: Record<string, any>;
|
|
17
|
+
};
|
|
13
18
|
interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> {
|
|
14
19
|
/**
|
|
15
20
|
* Return the URL to the endpoint or URL your images are generated from.
|
|
@@ -20,6 +25,13 @@ interface SharedServiceProps<T extends Record<string, any> = Record<string, any>
|
|
|
20
25
|
*
|
|
21
26
|
*/
|
|
22
27
|
getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate additional `srcset` values for the image.
|
|
30
|
+
*
|
|
31
|
+
* While in most cases this is exclusively used for `srcset`, it can also be used in a more generic way to generate
|
|
32
|
+
* multiple variants of the same image. For instance, you can use this to generate multiple aspect ratios or multiple formats.
|
|
33
|
+
*/
|
|
34
|
+
getSrcSet?: (options: ImageTransform, imageConfig: ImageConfig<T>) => SrcSetValue[] | Promise<SrcSetValue[]>;
|
|
23
35
|
/**
|
|
24
36
|
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
|
|
25
37
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroError, AstroErrorData } from "../../core/errors/index.js";
|
|
2
2
|
import { isRemotePath, joinPaths } from "../../core/path.js";
|
|
3
|
-
import { VALID_SUPPORTED_FORMATS } from "../consts.js";
|
|
3
|
+
import { DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from "../consts.js";
|
|
4
4
|
import { isESMImportedImage, isRemoteAllowed } from "../internal.js";
|
|
5
5
|
function isLocalService(service) {
|
|
6
6
|
if (!service) {
|
|
@@ -59,30 +59,21 @@ const baseService = {
|
|
|
59
59
|
)
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
if (options.widths && options.densities) {
|
|
63
|
+
throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
|
|
64
|
+
}
|
|
62
65
|
if (options.src.format === "svg") {
|
|
63
66
|
options.format = "svg";
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
if (!options.format) {
|
|
67
|
-
options.format =
|
|
70
|
+
options.format = DEFAULT_OUTPUT_FORMAT;
|
|
68
71
|
}
|
|
69
72
|
return options;
|
|
70
73
|
},
|
|
71
74
|
getHTMLAttributes(options) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (isESMImportedImage(options.src)) {
|
|
75
|
-
const aspectRatio = options.src.width / options.src.height;
|
|
76
|
-
if (targetHeight && !targetWidth) {
|
|
77
|
-
targetWidth = Math.round(targetHeight * aspectRatio);
|
|
78
|
-
} else if (targetWidth && !targetHeight) {
|
|
79
|
-
targetHeight = Math.round(targetWidth / aspectRatio);
|
|
80
|
-
} else if (!targetWidth && !targetHeight) {
|
|
81
|
-
targetWidth = options.src.width;
|
|
82
|
-
targetHeight = options.src.height;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const { src, width, height, format, quality, ...attributes } = options;
|
|
75
|
+
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
|
76
|
+
const { src, width, height, format, quality, densities, widths, formats, ...attributes } = options;
|
|
86
77
|
return {
|
|
87
78
|
...attributes,
|
|
88
79
|
width: targetWidth,
|
|
@@ -91,6 +82,69 @@ const baseService = {
|
|
|
91
82
|
decoding: attributes.decoding ?? "async"
|
|
92
83
|
};
|
|
93
84
|
},
|
|
85
|
+
getSrcSet(options) {
|
|
86
|
+
const srcSet = [];
|
|
87
|
+
const { targetWidth, targetHeight } = getTargetDimensions(options);
|
|
88
|
+
const { widths, densities } = options;
|
|
89
|
+
const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
|
|
90
|
+
const aspectRatio = targetWidth / targetHeight;
|
|
91
|
+
const imageWidth = isESMImportedImage(options.src) ? options.src.width : options.width;
|
|
92
|
+
const maxWidth = imageWidth ?? Infinity;
|
|
93
|
+
if (densities) {
|
|
94
|
+
const densityValues = densities.map((density) => {
|
|
95
|
+
if (typeof density === "number") {
|
|
96
|
+
return density;
|
|
97
|
+
} else {
|
|
98
|
+
return parseFloat(density);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const densityWidths = densityValues.sort().map((density) => Math.round(targetWidth * density));
|
|
102
|
+
densityWidths.forEach((width, index) => {
|
|
103
|
+
const maxTargetWidth = Math.min(width, maxWidth);
|
|
104
|
+
const { width: transformWidth, height: transformHeight, ...rest } = options;
|
|
105
|
+
const srcSetValue = {
|
|
106
|
+
transform: {
|
|
107
|
+
...rest
|
|
108
|
+
},
|
|
109
|
+
descriptor: `${densityValues[index]}x`,
|
|
110
|
+
attributes: {
|
|
111
|
+
type: `image/${targetFormat}`
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
if (maxTargetWidth !== imageWidth) {
|
|
115
|
+
srcSetValue.transform.width = maxTargetWidth;
|
|
116
|
+
srcSetValue.transform.height = Math.round(maxTargetWidth / aspectRatio);
|
|
117
|
+
}
|
|
118
|
+
if (targetFormat !== options.format) {
|
|
119
|
+
srcSetValue.transform.format = targetFormat;
|
|
120
|
+
}
|
|
121
|
+
srcSet.push(srcSetValue);
|
|
122
|
+
});
|
|
123
|
+
} else if (widths) {
|
|
124
|
+
widths.forEach((width) => {
|
|
125
|
+
const maxTargetWidth = Math.min(width, maxWidth);
|
|
126
|
+
const { width: transformWidth, height: transformHeight, ...rest } = options;
|
|
127
|
+
const srcSetValue = {
|
|
128
|
+
transform: {
|
|
129
|
+
...rest
|
|
130
|
+
},
|
|
131
|
+
descriptor: `${width}w`,
|
|
132
|
+
attributes: {
|
|
133
|
+
type: `image/${targetFormat}`
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
if (maxTargetWidth !== imageWidth) {
|
|
137
|
+
srcSetValue.transform.width = maxTargetWidth;
|
|
138
|
+
srcSetValue.transform.height = Math.round(maxTargetWidth / aspectRatio);
|
|
139
|
+
}
|
|
140
|
+
if (targetFormat !== options.format) {
|
|
141
|
+
srcSetValue.transform.format = targetFormat;
|
|
142
|
+
}
|
|
143
|
+
srcSet.push(srcSetValue);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return srcSet;
|
|
147
|
+
},
|
|
94
148
|
getURL(options, imageConfig) {
|
|
95
149
|
const searchParams = new URLSearchParams();
|
|
96
150
|
if (isESMImportedImage(options.src)) {
|
|
@@ -127,6 +181,25 @@ const baseService = {
|
|
|
127
181
|
return transform;
|
|
128
182
|
}
|
|
129
183
|
};
|
|
184
|
+
function getTargetDimensions(options) {
|
|
185
|
+
let targetWidth = options.width;
|
|
186
|
+
let targetHeight = options.height;
|
|
187
|
+
if (isESMImportedImage(options.src)) {
|
|
188
|
+
const aspectRatio = options.src.width / options.src.height;
|
|
189
|
+
if (targetHeight && !targetWidth) {
|
|
190
|
+
targetWidth = Math.round(targetHeight * aspectRatio);
|
|
191
|
+
} else if (targetWidth && !targetHeight) {
|
|
192
|
+
targetHeight = Math.round(targetWidth / aspectRatio);
|
|
193
|
+
} else if (!targetWidth && !targetHeight) {
|
|
194
|
+
targetWidth = options.src.width;
|
|
195
|
+
targetHeight = options.src.height;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
targetWidth,
|
|
200
|
+
targetHeight
|
|
201
|
+
};
|
|
202
|
+
}
|
|
130
203
|
export {
|
|
131
204
|
baseService,
|
|
132
205
|
isLocalService,
|
|
@@ -24,6 +24,7 @@ const sharpService = {
|
|
|
24
24
|
getURL: baseService.getURL,
|
|
25
25
|
parseURL: baseService.parseURL,
|
|
26
26
|
getHTMLAttributes: baseService.getHTMLAttributes,
|
|
27
|
+
getSrcSet: baseService.getSrcSet,
|
|
27
28
|
async transform(inputBuffer, transformOptions) {
|
|
28
29
|
if (!sharp)
|
|
29
30
|
sharp = await loadSharp();
|
|
@@ -33,9 +34,9 @@ const sharpService = {
|
|
|
33
34
|
let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
|
|
34
35
|
result.rotate();
|
|
35
36
|
if (transform.height && !transform.width) {
|
|
36
|
-
result.resize({ height: transform.height });
|
|
37
|
+
result.resize({ height: Math.round(transform.height) });
|
|
37
38
|
} else if (transform.width) {
|
|
38
|
-
result.resize({ width: transform.width });
|
|
39
|
+
result.resize({ width: Math.round(transform.width) });
|
|
39
40
|
}
|
|
40
41
|
if (transform.format) {
|
|
41
42
|
let quality = void 0;
|
|
@@ -39,6 +39,7 @@ const service = {
|
|
|
39
39
|
getURL: baseService.getURL,
|
|
40
40
|
parseURL: baseService.parseURL,
|
|
41
41
|
getHTMLAttributes: baseService.getHTMLAttributes,
|
|
42
|
+
getSrcSet: baseService.getSrcSet,
|
|
42
43
|
async transform(inputBuffer, transformOptions) {
|
|
43
44
|
const transform = transformOptions;
|
|
44
45
|
let format = transform.format;
|
|
@@ -52,12 +53,12 @@ const service = {
|
|
|
52
53
|
if (transform.height && !transform.width) {
|
|
53
54
|
operations.push({
|
|
54
55
|
type: "resize",
|
|
55
|
-
height: transform.height
|
|
56
|
+
height: Math.round(transform.height)
|
|
56
57
|
});
|
|
57
58
|
} else if (transform.width) {
|
|
58
59
|
operations.push({
|
|
59
60
|
type: "resize",
|
|
60
|
-
width: transform.width
|
|
61
|
+
width: Math.round(transform.width)
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
let quality = void 0;
|
package/dist/assets/types.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface ImageMetadata {
|
|
|
25
25
|
format: ImageInputFormat;
|
|
26
26
|
orientation?: number;
|
|
27
27
|
}
|
|
28
|
+
export interface SrcSetValue {
|
|
29
|
+
url: string;
|
|
30
|
+
descriptor?: string;
|
|
31
|
+
attributes?: Record<string, string>;
|
|
32
|
+
}
|
|
28
33
|
/**
|
|
29
34
|
* A yet to be resolved image transform. Used by `getImage`
|
|
30
35
|
*/
|
|
@@ -39,6 +44,8 @@ export type UnresolvedImageTransform = Omit<ImageTransform, 'src'> & {
|
|
|
39
44
|
export type ImageTransform = {
|
|
40
45
|
src: ImageMetadata | string;
|
|
41
46
|
width?: number | undefined;
|
|
47
|
+
widths?: number[] | undefined;
|
|
48
|
+
densities?: (number | `${number}x`)[] | undefined;
|
|
42
49
|
height?: number | undefined;
|
|
43
50
|
quality?: ImageQuality | undefined;
|
|
44
51
|
format?: ImageOutputFormat | undefined;
|
|
@@ -48,13 +55,17 @@ export interface GetImageResult {
|
|
|
48
55
|
rawOptions: ImageTransform;
|
|
49
56
|
options: ImageTransform;
|
|
50
57
|
src: string;
|
|
58
|
+
srcSet: {
|
|
59
|
+
values: SrcSetValue[];
|
|
60
|
+
attribute: string;
|
|
61
|
+
};
|
|
51
62
|
attributes: Record<string, any>;
|
|
52
63
|
}
|
|
53
64
|
type ImageSharedProps<T> = T & {
|
|
54
65
|
/**
|
|
55
66
|
* Width of the image, the value of this property will be used to assign the `width` property on the final `img` element.
|
|
56
67
|
*
|
|
57
|
-
*
|
|
68
|
+
* This value will additionally be used to resize the image to the desired width, taking into account the original aspect ratio of the image.
|
|
58
69
|
*
|
|
59
70
|
* **Example**:
|
|
60
71
|
* ```astro
|
|
@@ -81,7 +92,23 @@ type ImageSharedProps<T> = T & {
|
|
|
81
92
|
* ```
|
|
82
93
|
*/
|
|
83
94
|
height?: number | `${number}`;
|
|
84
|
-
}
|
|
95
|
+
} & ({
|
|
96
|
+
/**
|
|
97
|
+
* A list of widths to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
|
|
98
|
+
*
|
|
99
|
+
* This attribute is incompatible with `densities`.
|
|
100
|
+
*/
|
|
101
|
+
widths?: number[];
|
|
102
|
+
densities?: never;
|
|
103
|
+
} | {
|
|
104
|
+
/**
|
|
105
|
+
* A list of pixel densities to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
|
|
106
|
+
*
|
|
107
|
+
* This attribute is incompatible with `widths`.
|
|
108
|
+
*/
|
|
109
|
+
densities?: (number | `${number}x`)[];
|
|
110
|
+
widths?: never;
|
|
111
|
+
});
|
|
85
112
|
export type LocalImageProps<T> = ImageSharedProps<T> & {
|
|
86
113
|
/**
|
|
87
114
|
* A reference to a local image imported through an ESM import.
|
|
@@ -12,7 +12,7 @@ function propsToFilename(transform, hash) {
|
|
|
12
12
|
return `/${filename}_${hash}${outputExt}`;
|
|
13
13
|
}
|
|
14
14
|
function hashTransform(transform, imageService) {
|
|
15
|
-
const { alt, ...rest } = transform;
|
|
15
|
+
const { alt, class: className, style, widths, densities, ...rest } = transform;
|
|
16
16
|
const hashFields = { ...rest, imageService };
|
|
17
17
|
return shorthash(JSON.stringify(hashFields));
|
|
18
18
|
}
|
|
@@ -47,6 +47,7 @@ function assets({
|
|
|
47
47
|
export { getConfiguredImageService, isLocalService } from "astro/assets";
|
|
48
48
|
import { getImage as getImageInternal } from "astro/assets";
|
|
49
49
|
export { default as Image } from "astro/components/Image.astro";
|
|
50
|
+
export { default as Picture } from "astro/components/Picture.astro";
|
|
50
51
|
|
|
51
52
|
export const imageConfig = ${JSON.stringify(settings.config.image)};
|
|
52
53
|
export const assetsDir = new URL(${JSON.stringify(
|