astro 2.10.8 → 2.10.10
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/components/ViewTransitions.astro +30 -12
- package/dist/@types/astro.d.ts +68 -6
- package/dist/assets/{generate.d.ts → build/generate.d.ts} +2 -2
- package/dist/assets/build/generate.js +123 -0
- package/dist/assets/build/remote.d.ts +9 -0
- package/dist/assets/build/remote.js +42 -0
- package/dist/assets/image-endpoint.js +7 -8
- package/dist/assets/internal.d.ts +4 -2
- package/dist/assets/internal.js +23 -6
- package/dist/assets/services/service.d.ts +15 -8
- package/dist/assets/services/service.js +19 -10
- package/dist/assets/utils/remotePattern.d.ts +11 -0
- package/dist/assets/utils/remotePattern.js +46 -0
- package/dist/assets/utils/transformToPath.js +4 -5
- package/dist/assets/vite-plugin-assets.js +2 -6
- package/dist/core/app/index.js +21 -9
- package/dist/core/build/generate.js +4 -2
- package/dist/core/build/static-build.js +14 -4
- package/dist/core/config/schema.d.ts +104 -0
- package/dist/core/config/schema.js +20 -2
- package/dist/core/constants.js +1 -1
- package/dist/core/dev/container.js +5 -1
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/errors/errors-data.js +1 -1
- package/dist/core/messages.js +2 -2
- package/dist/runtime/server/render/common.d.ts +1 -1
- package/dist/runtime/server/render/common.js +13 -15
- package/dist/runtime/server/render/component.d.ts +1 -1
- package/dist/runtime/server/render/component.js +2 -1
- package/dist/runtime/server/render/head.d.ts +1 -1
- package/dist/runtime/server/render/head.js +3 -2
- package/dist/runtime/server/render/index.d.ts +1 -1
- package/dist/runtime/server/render/instruction.d.ts +16 -0
- package/dist/runtime/server/render/instruction.js +13 -0
- package/dist/runtime/server/render/slot.d.ts +1 -1
- package/dist/vite-plugin-integrations-container/index.js +2 -2
- package/package.json +3 -1
- package/dist/assets/generate.js +0 -90
- package/dist/runtime/server/render/types.d.ts +0 -12
- package/dist/runtime/server/render/types.js +0 -0
|
@@ -38,12 +38,21 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
38
38
|
|
|
39
39
|
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
|
40
40
|
let wait = false;
|
|
41
|
+
// During the waiting time additional events are lost.
|
|
42
|
+
// So repeat the callback at the end if we have swallowed events.
|
|
43
|
+
let onceMore = false;
|
|
41
44
|
return (...args: any[]) => {
|
|
42
|
-
if (wait)
|
|
43
|
-
|
|
45
|
+
if (wait) {
|
|
46
|
+
onceMore = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
44
49
|
cb(...args);
|
|
45
50
|
wait = true;
|
|
46
51
|
setTimeout(() => {
|
|
52
|
+
if (onceMore) {
|
|
53
|
+
onceMore = false;
|
|
54
|
+
cb(...args);
|
|
55
|
+
}
|
|
47
56
|
wait = false;
|
|
48
57
|
}, delay);
|
|
49
58
|
};
|
|
@@ -92,9 +101,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
92
101
|
|
|
93
102
|
const parser = new DOMParser();
|
|
94
103
|
|
|
95
|
-
async function updateDOM(
|
|
104
|
+
async function updateDOM(html: string, state?: State, fallback?: Fallback) {
|
|
96
105
|
const doc = parser.parseFromString(html, 'text/html');
|
|
97
|
-
doc.documentElement.dataset.astroTransition = dir;
|
|
98
106
|
|
|
99
107
|
// Check for a head element that should persist, either because it has the data
|
|
100
108
|
// attribute or is a link el.
|
|
@@ -125,6 +133,10 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
125
133
|
};
|
|
126
134
|
|
|
127
135
|
const swap = () => {
|
|
136
|
+
// noscript tags inside head element are not honored on swap (#7969).
|
|
137
|
+
// Remove them before swapping.
|
|
138
|
+
doc.querySelectorAll('head noscript').forEach((el) => el.remove());
|
|
139
|
+
|
|
128
140
|
// Swap head
|
|
129
141
|
for (const el of Array.from(document.head.children)) {
|
|
130
142
|
const newEl = persistedHeadElement(el);
|
|
@@ -159,6 +171,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
159
171
|
}
|
|
160
172
|
if (state?.scrollY != null) {
|
|
161
173
|
scrollTo(0, state.scrollY);
|
|
174
|
+
// Overwrite erroneous updates by the scroll handler during transition
|
|
175
|
+
persistState(state);
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
triggerEvent('astro:beforeload');
|
|
@@ -218,15 +232,17 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
218
232
|
location.href = href;
|
|
219
233
|
return;
|
|
220
234
|
}
|
|
235
|
+
document.documentElement.dataset.astroTransition = dir;
|
|
221
236
|
if (supportsViewTransitions) {
|
|
222
|
-
finished = document.startViewTransition(() => updateDOM(
|
|
237
|
+
finished = document.startViewTransition(() => updateDOM(html, state)).finished;
|
|
223
238
|
} else {
|
|
224
|
-
finished = updateDOM(
|
|
239
|
+
finished = updateDOM(html, state, getFallback());
|
|
225
240
|
}
|
|
226
241
|
try {
|
|
227
242
|
await finished;
|
|
228
243
|
} finally {
|
|
229
|
-
|
|
244
|
+
// skip this for the moment as it tends to stop fallback animations
|
|
245
|
+
// document.documentElement.removeAttribute('data-astro-transition');
|
|
230
246
|
await runScripts();
|
|
231
247
|
markScriptsExec();
|
|
232
248
|
onload();
|
|
@@ -276,8 +292,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
276
292
|
transitionEnabledOnThisPage()
|
|
277
293
|
) {
|
|
278
294
|
ev.preventDefault();
|
|
279
|
-
navigate('forward', link.href, { index: currentHistoryIndex, scrollY: 0 });
|
|
280
|
-
currentHistoryIndex++;
|
|
295
|
+
navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
|
|
281
296
|
const newState: State = { index: currentHistoryIndex, scrollY };
|
|
282
297
|
persistState({ index: currentHistoryIndex - 1, scrollY });
|
|
283
298
|
history.pushState(newState, '', link.href);
|
|
@@ -291,10 +306,11 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
291
306
|
return;
|
|
292
307
|
}
|
|
293
308
|
|
|
294
|
-
//
|
|
309
|
+
// History entries without state are created by the browser (e.g. for hash links)
|
|
310
|
+
// Our view transition entries always have state.
|
|
311
|
+
// Just ignore stateless entries.
|
|
312
|
+
// The browser will handle navigation fine without our help
|
|
295
313
|
if (ev.state === null) {
|
|
296
|
-
persistState({ index: currentHistoryIndex, scrollY });
|
|
297
|
-
ev.preventDefault();
|
|
298
314
|
return;
|
|
299
315
|
}
|
|
300
316
|
|
|
@@ -329,6 +345,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
329
345
|
addEventListener(
|
|
330
346
|
'scroll',
|
|
331
347
|
throttle(() => {
|
|
348
|
+
// only updste history entries that are managed by us
|
|
349
|
+
// leave other entries alone and do not accidently add state.
|
|
332
350
|
if (history.state) {
|
|
333
351
|
persistState({ ...history.state, scrollY });
|
|
334
352
|
}
|
package/dist/@types/astro.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AddressInfo } from 'node:net';
|
|
|
8
8
|
import type * as rollup from 'rollup';
|
|
9
9
|
import type { TsConfigJson } from 'tsconfig-resolver';
|
|
10
10
|
import type * as vite from 'vite';
|
|
11
|
+
import type { RemotePattern } from '../assets/utils/remotePattern';
|
|
11
12
|
import type { SerializedSSRManifest } from '../core/app/types';
|
|
12
13
|
import type { PageBuildData } from '../core/build/types';
|
|
13
14
|
import type { AstroConfigType } from '../core/config';
|
|
@@ -19,6 +20,7 @@ import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'
|
|
|
19
20
|
export type { MarkdownHeading, MarkdownMetadata, MarkdownRenderingResult, RehypePlugins, RemarkPlugins, ShikiConfig, } from '@astrojs/markdown-remark';
|
|
20
21
|
export type { ExternalImageService, ImageService, LocalImageService, } from '../assets/services/service';
|
|
21
22
|
export type { GetImageResult, ImageInputFormat, ImageMetadata, ImageOutputFormat, ImageQuality, ImageQualityPreset, ImageTransform, } from '../assets/types';
|
|
23
|
+
export type { RemotePattern } from '../assets/utils/remotePattern';
|
|
22
24
|
export type { SSRManifest } from '../core/app/types';
|
|
23
25
|
export type { AstroCookies } from '../core/cookies';
|
|
24
26
|
export interface AstroBuiltinProps {
|
|
@@ -312,9 +314,9 @@ type ServerConfig = {
|
|
|
312
314
|
export interface ViteUserConfig extends vite.UserConfig {
|
|
313
315
|
ssr?: vite.SSROptions;
|
|
314
316
|
}
|
|
315
|
-
export interface ImageServiceConfig {
|
|
317
|
+
export interface ImageServiceConfig<T extends Record<string, any> = Record<string, any>> {
|
|
316
318
|
entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
|
|
317
|
-
config?:
|
|
319
|
+
config?: T;
|
|
318
320
|
}
|
|
319
321
|
/**
|
|
320
322
|
* Astro User Config
|
|
@@ -769,10 +771,10 @@ export interface AstroUserConfig {
|
|
|
769
771
|
* @default `never`
|
|
770
772
|
* @version 2.6.0
|
|
771
773
|
* @description
|
|
772
|
-
* Control whether styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options:
|
|
773
|
-
* - `'always'` -
|
|
774
|
-
* - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets.
|
|
775
|
-
* - `'never'` -
|
|
774
|
+
* Control whether project styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options:
|
|
775
|
+
* - `'always'` - project styles are inlined into `<style>` tags
|
|
776
|
+
* - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, project styles are sent in external stylesheets.
|
|
777
|
+
* - `'never'` - project styles are sent in external stylesheets
|
|
776
778
|
*
|
|
777
779
|
* ```js
|
|
778
780
|
* {
|
|
@@ -933,6 +935,66 @@ export interface AstroUserConfig {
|
|
|
933
935
|
* ```
|
|
934
936
|
*/
|
|
935
937
|
service: ImageServiceConfig;
|
|
938
|
+
/**
|
|
939
|
+
* @docs
|
|
940
|
+
* @name image.domains (Experimental)
|
|
941
|
+
* @type {string[]}
|
|
942
|
+
* @default `{domains: []}`
|
|
943
|
+
* @version 2.10.10
|
|
944
|
+
* @description
|
|
945
|
+
* Defines a list of permitted image source domains for local image optimization. No other remote images will be optimized by Astro.
|
|
946
|
+
*
|
|
947
|
+
* This option requires an array of individual domain names as strings. Wildcards are not permitted. Instead, use [`image.remotePatterns`](#imageremotepatterns-experimental) to define a list of allowed source URL patterns.
|
|
948
|
+
*
|
|
949
|
+
* ```js
|
|
950
|
+
* // astro.config.mjs
|
|
951
|
+
* {
|
|
952
|
+
* image: {
|
|
953
|
+
* // Example: Allow remote image optimization from a single domain
|
|
954
|
+
* domains: ['astro.build'],
|
|
955
|
+
* },
|
|
956
|
+
* }
|
|
957
|
+
* ```
|
|
958
|
+
*/
|
|
959
|
+
domains?: string[];
|
|
960
|
+
/**
|
|
961
|
+
* @docs
|
|
962
|
+
* @name image.remotePatterns (Experimental)
|
|
963
|
+
* @type {RemotePattern[]}
|
|
964
|
+
* @default `{remotePatterns: []}`
|
|
965
|
+
* @version 2.10.10
|
|
966
|
+
* @description
|
|
967
|
+
* Defines a list of permitted image source URL patterns for local image optimization.
|
|
968
|
+
*
|
|
969
|
+
* `remotePatterns` can be configured with four properties:
|
|
970
|
+
* 1. protocol
|
|
971
|
+
* 2. hostname
|
|
972
|
+
* 3. port
|
|
973
|
+
* 4. pathname
|
|
974
|
+
*
|
|
975
|
+
* ```js
|
|
976
|
+
* {
|
|
977
|
+
* image: {
|
|
978
|
+
* // Example: allow processing all images from your aws s3 bucket
|
|
979
|
+
* remotePatterns: [{
|
|
980
|
+
* protocol: 'https',
|
|
981
|
+
* hostname: '**.amazonaws.com',
|
|
982
|
+
* }],
|
|
983
|
+
* },
|
|
984
|
+
* }
|
|
985
|
+
* ```
|
|
986
|
+
*
|
|
987
|
+
* You can use wildcards to define the permitted `hostname` and `pathname` values as described below. Otherwise, only the exact values provided will be configured:
|
|
988
|
+
* `hostname`:
|
|
989
|
+
* - Start with '**.' to allow all subdomains ('endsWith').
|
|
990
|
+
* - Start with '*.' to allow only one level of subdomain.
|
|
991
|
+
*
|
|
992
|
+
* `pathname`:
|
|
993
|
+
* - End with '/**' to allow all sub-routes ('startsWith').
|
|
994
|
+
* - End with '/*' to allow only one level of sub-route.
|
|
995
|
+
|
|
996
|
+
*/
|
|
997
|
+
remotePatterns?: Partial<RemotePattern>[];
|
|
936
998
|
};
|
|
937
999
|
/**
|
|
938
1000
|
* @docs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { StaticBuildOptions } from '
|
|
2
|
-
import type { ImageTransform } from '
|
|
1
|
+
import type { StaticBuildOptions } from '../../core/build/types.js';
|
|
2
|
+
import type { ImageTransform } from '../types.js';
|
|
3
3
|
interface GenerationDataUncached {
|
|
4
4
|
cached: false;
|
|
5
5
|
weight: {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs, { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path/posix";
|
|
3
|
+
import { warn } from "../../core/logger/core.js";
|
|
4
|
+
import { prependForwardSlash } from "../../core/path.js";
|
|
5
|
+
import { isServerLikeOutput } from "../../prerender/utils.js";
|
|
6
|
+
import { getConfiguredImageService, isESMImportedImage } from "../internal.js";
|
|
7
|
+
import { loadRemoteImage } from "./remote.js";
|
|
8
|
+
async function generateImage(buildOpts, options, filepath) {
|
|
9
|
+
let useCache = true;
|
|
10
|
+
const assetsCacheDir = new URL("assets/", buildOpts.settings.config.cacheDir);
|
|
11
|
+
try {
|
|
12
|
+
await fs.promises.mkdir(assetsCacheDir, { recursive: true });
|
|
13
|
+
} catch (err) {
|
|
14
|
+
warn(
|
|
15
|
+
buildOpts.logging,
|
|
16
|
+
"astro:assets",
|
|
17
|
+
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
|
|
18
|
+
);
|
|
19
|
+
useCache = false;
|
|
20
|
+
}
|
|
21
|
+
let serverRoot, clientRoot;
|
|
22
|
+
if (isServerLikeOutput(buildOpts.settings.config)) {
|
|
23
|
+
serverRoot = buildOpts.settings.config.build.server;
|
|
24
|
+
clientRoot = buildOpts.settings.config.build.client;
|
|
25
|
+
} else {
|
|
26
|
+
serverRoot = buildOpts.settings.config.outDir;
|
|
27
|
+
clientRoot = buildOpts.settings.config.outDir;
|
|
28
|
+
}
|
|
29
|
+
const isLocalImage = isESMImportedImage(options.src);
|
|
30
|
+
const finalFileURL = new URL("." + filepath, clientRoot);
|
|
31
|
+
const finalFolderURL = new URL("./", finalFileURL);
|
|
32
|
+
const cacheFile = basename(filepath) + (isLocalImage ? "" : ".json");
|
|
33
|
+
const cachedFileURL = new URL(cacheFile, assetsCacheDir);
|
|
34
|
+
await fs.promises.mkdir(finalFolderURL, { recursive: true });
|
|
35
|
+
try {
|
|
36
|
+
if (isLocalImage) {
|
|
37
|
+
await fs.promises.copyFile(cachedFileURL, finalFileURL);
|
|
38
|
+
return {
|
|
39
|
+
cached: true
|
|
40
|
+
};
|
|
41
|
+
} else {
|
|
42
|
+
const JSONData = JSON.parse(readFileSync(cachedFileURL, "utf-8"));
|
|
43
|
+
if (JSONData.expires < Date.now()) {
|
|
44
|
+
await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, "base64"));
|
|
45
|
+
return {
|
|
46
|
+
cached: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (e.code !== "ENOENT") {
|
|
52
|
+
throw new Error(`An error was encountered while reading the cache file. Error: ${e}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const originalImagePath = isLocalImage ? options.src.src : options.src;
|
|
56
|
+
let imageData;
|
|
57
|
+
let resultData = {
|
|
58
|
+
data: void 0,
|
|
59
|
+
expires: void 0
|
|
60
|
+
};
|
|
61
|
+
if (isLocalImage) {
|
|
62
|
+
imageData = await fs.promises.readFile(
|
|
63
|
+
new URL(
|
|
64
|
+
"." + prependForwardSlash(
|
|
65
|
+
join(buildOpts.settings.config.build.assets, basename(originalImagePath))
|
|
66
|
+
),
|
|
67
|
+
serverRoot
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
const remoteImage = await loadRemoteImage(originalImagePath);
|
|
72
|
+
resultData.expires = remoteImage.expires;
|
|
73
|
+
imageData = remoteImage.data;
|
|
74
|
+
}
|
|
75
|
+
const imageService = await getConfiguredImageService();
|
|
76
|
+
resultData.data = (await imageService.transform(
|
|
77
|
+
imageData,
|
|
78
|
+
{ ...options, src: originalImagePath },
|
|
79
|
+
buildOpts.settings.config.image
|
|
80
|
+
)).data;
|
|
81
|
+
try {
|
|
82
|
+
if (useCache) {
|
|
83
|
+
if (isLocalImage) {
|
|
84
|
+
await fs.promises.writeFile(cachedFileURL, resultData.data);
|
|
85
|
+
} else {
|
|
86
|
+
await fs.promises.writeFile(
|
|
87
|
+
cachedFileURL,
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
data: Buffer.from(resultData.data).toString("base64"),
|
|
90
|
+
expires: resultData.expires
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
warn(
|
|
97
|
+
buildOpts.logging,
|
|
98
|
+
"astro:assets",
|
|
99
|
+
`An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
|
|
100
|
+
);
|
|
101
|
+
} finally {
|
|
102
|
+
await fs.promises.writeFile(finalFileURL, resultData.data);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cached: false,
|
|
106
|
+
weight: {
|
|
107
|
+
// Divide by 1024 to get size in kilobytes
|
|
108
|
+
before: Math.trunc(imageData.byteLength / 1024),
|
|
109
|
+
after: Math.trunc(Buffer.from(resultData.data).byteLength / 1024)
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function getStaticImageList() {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
if (!((_a = globalThis == null ? void 0 : globalThis.astroAsset) == null ? void 0 : _a.staticImages)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return (_b = globalThis.astroAsset.staticImages) == null ? void 0 : _b.entries();
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
generateImage,
|
|
122
|
+
getStaticImageList
|
|
123
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import CachePolicy from "http-cache-semantics";
|
|
2
|
+
async function loadRemoteImage(src) {
|
|
3
|
+
const req = new Request(src);
|
|
4
|
+
const res = await fetch(req);
|
|
5
|
+
if (!res.ok) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
`Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
|
|
11
|
+
const expires = policy.storable() ? policy.timeToLive() : 0;
|
|
12
|
+
return {
|
|
13
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
14
|
+
expires: Date.now() + expires
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function webToCachePolicyRequest({ url, method, headers: _headers }) {
|
|
18
|
+
let headers = {};
|
|
19
|
+
try {
|
|
20
|
+
headers = Object.fromEntries(_headers.entries());
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
method,
|
|
25
|
+
url,
|
|
26
|
+
headers
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function webToCachePolicyResponse({ status, headers: _headers }) {
|
|
30
|
+
let headers = {};
|
|
31
|
+
try {
|
|
32
|
+
headers = Object.fromEntries(_headers.entries());
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
status,
|
|
37
|
+
headers
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
loadRemoteImage
|
|
42
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import mime from "mime/lite.js";
|
|
2
2
|
import { isRemotePath } from "../core/path.js";
|
|
3
|
-
import { getConfiguredImageService } from "./internal.js";
|
|
3
|
+
import { getConfiguredImageService, isRemoteAllowed } from "./internal.js";
|
|
4
4
|
import { isLocalService } from "./services/service.js";
|
|
5
5
|
import { etag } from "./utils/etag.js";
|
|
6
|
-
import {
|
|
6
|
+
import { imageConfig } from "astro:assets";
|
|
7
7
|
async function loadRemoteImage(src) {
|
|
8
8
|
try {
|
|
9
9
|
const res = await fetch(src);
|
|
@@ -22,21 +22,20 @@ const get = async ({ request }) => {
|
|
|
22
22
|
throw new Error("Configured image service is not a local service");
|
|
23
23
|
}
|
|
24
24
|
const url = new URL(request.url);
|
|
25
|
-
const transform = await imageService.parseURL(url,
|
|
25
|
+
const transform = await imageService.parseURL(url, imageConfig);
|
|
26
26
|
if (!(transform == null ? void 0 : transform.src)) {
|
|
27
27
|
throw new Error("Incorrect transform returned by `parseURL`");
|
|
28
28
|
}
|
|
29
29
|
let inputBuffer = void 0;
|
|
30
30
|
const sourceUrl = isRemotePath(transform.src) ? new URL(transform.src) : new URL(transform.src, url.origin);
|
|
31
|
+
if (isRemotePath(transform.src) && isRemoteAllowed(transform.src, imageConfig) === false) {
|
|
32
|
+
return new Response("Forbidden", { status: 403 });
|
|
33
|
+
}
|
|
31
34
|
inputBuffer = await loadRemoteImage(sourceUrl);
|
|
32
35
|
if (!inputBuffer) {
|
|
33
36
|
return new Response("Not Found", { status: 404 });
|
|
34
37
|
}
|
|
35
|
-
const { data, format } = await imageService.transform(
|
|
36
|
-
inputBuffer,
|
|
37
|
-
transform,
|
|
38
|
-
imageServiceConfig
|
|
39
|
-
);
|
|
38
|
+
const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
|
|
40
39
|
return new Response(data, {
|
|
41
40
|
status: 200,
|
|
42
41
|
headers: {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { AstroSettings } from '../@types/astro.js';
|
|
1
|
+
import type { AstroConfig, AstroSettings } from '../@types/astro.js';
|
|
2
2
|
import { type ImageService } from './services/service.js';
|
|
3
3
|
import type { GetImageResult, ImageMetadata, ImageTransform, UnresolvedImageTransform } from './types.js';
|
|
4
4
|
export declare function injectImageEndpoint(settings: AstroSettings): AstroSettings;
|
|
5
5
|
export declare function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata;
|
|
6
|
+
export declare function isRemoteImage(src: ImageMetadata | string): src is string;
|
|
7
|
+
export declare function isRemoteAllowed(src: string, { domains, remotePatterns, }: Partial<Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>>): boolean;
|
|
6
8
|
export declare function getConfiguredImageService(): Promise<ImageService>;
|
|
7
|
-
export declare function getImage(options: ImageTransform | UnresolvedImageTransform,
|
|
9
|
+
export declare function getImage(options: ImageTransform | UnresolvedImageTransform, imageConfig: AstroConfig['image']): Promise<GetImageResult>;
|
package/dist/assets/internal.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { isRemotePath } from "@astrojs/internal-helpers/path";
|
|
1
2
|
import { AstroError, AstroErrorData } from "../core/errors/index.js";
|
|
2
3
|
import { isLocalService } from "./services/service.js";
|
|
4
|
+
import { matchHostname, matchPattern } from "./utils/remotePattern.js";
|
|
3
5
|
function injectImageEndpoint(settings) {
|
|
4
6
|
settings.injectedRoutes.push({
|
|
5
7
|
pattern: "/_image",
|
|
@@ -11,6 +13,18 @@ function injectImageEndpoint(settings) {
|
|
|
11
13
|
function isESMImportedImage(src) {
|
|
12
14
|
return typeof src === "object";
|
|
13
15
|
}
|
|
16
|
+
function isRemoteImage(src) {
|
|
17
|
+
return typeof src === "string";
|
|
18
|
+
}
|
|
19
|
+
function isRemoteAllowed(src, {
|
|
20
|
+
domains = [],
|
|
21
|
+
remotePatterns = []
|
|
22
|
+
}) {
|
|
23
|
+
if (!isRemotePath(src))
|
|
24
|
+
return false;
|
|
25
|
+
const url = new URL(src);
|
|
26
|
+
return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
|
|
27
|
+
}
|
|
14
28
|
async function getConfiguredImageService() {
|
|
15
29
|
var _a;
|
|
16
30
|
if (!((_a = globalThis == null ? void 0 : globalThis.astroAsset) == null ? void 0 : _a.imageService)) {
|
|
@@ -29,7 +43,7 @@ async function getConfiguredImageService() {
|
|
|
29
43
|
}
|
|
30
44
|
return globalThis.astroAsset.imageService;
|
|
31
45
|
}
|
|
32
|
-
async function getImage(options,
|
|
46
|
+
async function getImage(options, imageConfig) {
|
|
33
47
|
if (!options || typeof options !== "object") {
|
|
34
48
|
throw new AstroError({
|
|
35
49
|
...AstroErrorData.ExpectedImageOptions,
|
|
@@ -41,21 +55,24 @@ async function getImage(options, serviceConfig) {
|
|
|
41
55
|
...options,
|
|
42
56
|
src: typeof options.src === "object" && "then" in options.src ? (await options.src).default : options.src
|
|
43
57
|
};
|
|
44
|
-
const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions,
|
|
45
|
-
let imageURL = await service.getURL(validatedOptions,
|
|
46
|
-
if (isLocalService(service) && globalThis.astroAsset.addStaticImage
|
|
58
|
+
const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
|
|
59
|
+
let imageURL = await service.getURL(validatedOptions, imageConfig);
|
|
60
|
+
if (isLocalService(service) && globalThis.astroAsset.addStaticImage && // If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything
|
|
61
|
+
!(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
|
|
47
62
|
imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
|
|
48
63
|
}
|
|
49
64
|
return {
|
|
50
65
|
rawOptions: resolvedOptions,
|
|
51
66
|
options: validatedOptions,
|
|
52
67
|
src: imageURL,
|
|
53
|
-
attributes: service.getHTMLAttributes !== void 0 ? service.getHTMLAttributes(validatedOptions,
|
|
68
|
+
attributes: service.getHTMLAttributes !== void 0 ? service.getHTMLAttributes(validatedOptions, imageConfig) : {}
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
71
|
export {
|
|
57
72
|
getConfiguredImageService,
|
|
58
73
|
getImage,
|
|
59
74
|
injectImageEndpoint,
|
|
60
|
-
isESMImportedImage
|
|
75
|
+
isESMImportedImage,
|
|
76
|
+
isRemoteAllowed,
|
|
77
|
+
isRemoteImage
|
|
61
78
|
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
import type { AstroConfig } from '../../@types/astro.js';
|
|
2
3
|
import type { ImageOutputFormat, ImageTransform } from '../types.js';
|
|
3
4
|
export type ImageService = LocalImageService | ExternalImageService;
|
|
4
5
|
export declare function isLocalService(service: ImageService | undefined): service is LocalImageService;
|
|
5
6
|
export declare function parseQuality(quality: string): string | number;
|
|
6
|
-
|
|
7
|
+
type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & {
|
|
8
|
+
service: {
|
|
9
|
+
entrypoint: string;
|
|
10
|
+
config: T;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> {
|
|
7
14
|
/**
|
|
8
15
|
* Return the URL to the endpoint or URL your images are generated from.
|
|
9
16
|
*
|
|
@@ -12,14 +19,14 @@ interface SharedServiceProps {
|
|
|
12
19
|
* For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
|
|
13
20
|
*
|
|
14
21
|
*/
|
|
15
|
-
getURL: (options: ImageTransform,
|
|
22
|
+
getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>;
|
|
16
23
|
/**
|
|
17
24
|
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
|
|
18
25
|
*
|
|
19
26
|
* For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`.
|
|
20
27
|
* In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image.
|
|
21
28
|
*/
|
|
22
|
-
getHTMLAttributes?: (options: ImageTransform,
|
|
29
|
+
getHTMLAttributes?: (options: ImageTransform, imageConfig: ImageConfig<T>) => Record<string, any> | Promise<Record<string, any>>;
|
|
23
30
|
/**
|
|
24
31
|
* Validate and return the options passed by the user.
|
|
25
32
|
*
|
|
@@ -28,25 +35,25 @@ interface SharedServiceProps {
|
|
|
28
35
|
*
|
|
29
36
|
* This method should returns options, and can be used to set defaults (ex: a default output format to be used if the user didn't specify one.)
|
|
30
37
|
*/
|
|
31
|
-
validateOptions?: (options: ImageTransform,
|
|
38
|
+
validateOptions?: (options: ImageTransform, imageConfig: ImageConfig<T>) => ImageTransform | Promise<ImageTransform>;
|
|
32
39
|
}
|
|
33
|
-
export type ExternalImageService = SharedServiceProps
|
|
40
|
+
export type ExternalImageService<T extends Record<string, any> = Record<string, any>> = SharedServiceProps<T>;
|
|
34
41
|
export type LocalImageTransform = {
|
|
35
42
|
src: string;
|
|
36
43
|
[key: string]: any;
|
|
37
44
|
};
|
|
38
|
-
export interface LocalImageService extends SharedServiceProps {
|
|
45
|
+
export interface LocalImageService<T extends Record<string, any> = Record<string, any>> extends SharedServiceProps<T> {
|
|
39
46
|
/**
|
|
40
47
|
* Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`.
|
|
41
48
|
*
|
|
42
49
|
* In most cases, this will get query parameters using, for example, `params.get('width')` and return those.
|
|
43
50
|
*/
|
|
44
|
-
parseURL: (url: URL,
|
|
51
|
+
parseURL: (url: URL, imageConfig: ImageConfig<T>) => LocalImageTransform | undefined | Promise<LocalImageTransform> | Promise<undefined>;
|
|
45
52
|
/**
|
|
46
53
|
* Performs the image transformations on the input image and returns both the binary data and
|
|
47
54
|
* final image format of the optimized image.
|
|
48
55
|
*/
|
|
49
|
-
transform: (inputBuffer: Buffer, transform: LocalImageTransform,
|
|
56
|
+
transform: (inputBuffer: Buffer, transform: LocalImageTransform, imageConfig: ImageConfig<T>) => Promise<{
|
|
50
57
|
data: Buffer;
|
|
51
58
|
format: ImageOutputFormat;
|
|
52
59
|
}>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AstroError, AstroErrorData } from "../../core/errors/index.js";
|
|
2
2
|
import { joinPaths } from "../../core/path.js";
|
|
3
3
|
import { VALID_SUPPORTED_FORMATS } from "../consts.js";
|
|
4
|
-
import { isESMImportedImage } from "../internal.js";
|
|
4
|
+
import { isESMImportedImage, isRemoteAllowed } from "../internal.js";
|
|
5
5
|
function isLocalService(service) {
|
|
6
6
|
if (!service) {
|
|
7
7
|
return false;
|
|
@@ -87,17 +87,26 @@ const baseService = {
|
|
|
87
87
|
decoding: attributes.decoding ?? "async"
|
|
88
88
|
};
|
|
89
89
|
},
|
|
90
|
-
getURL(options) {
|
|
91
|
-
|
|
90
|
+
getURL(options, imageConfig) {
|
|
91
|
+
const searchParams = new URLSearchParams();
|
|
92
|
+
if (isESMImportedImage(options.src)) {
|
|
93
|
+
searchParams.append("href", options.src.src);
|
|
94
|
+
} else if (isRemoteAllowed(options.src, imageConfig)) {
|
|
95
|
+
searchParams.append("href", options.src);
|
|
96
|
+
} else {
|
|
92
97
|
return options.src;
|
|
93
98
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const params = {
|
|
100
|
+
w: "width",
|
|
101
|
+
h: "height",
|
|
102
|
+
q: "quality",
|
|
103
|
+
f: "format"
|
|
104
|
+
};
|
|
105
|
+
Object.entries(params).forEach(([param, key]) => {
|
|
106
|
+
options[key] && searchParams.append(param, options[key].toString());
|
|
107
|
+
});
|
|
108
|
+
const imageEndpoint = joinPaths(import.meta.env.BASE_URL, "/_image");
|
|
109
|
+
return `${imageEndpoint}?${searchParams}`;
|
|
101
110
|
},
|
|
102
111
|
parseURL(url) {
|
|
103
112
|
const params = url.searchParams;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RemotePattern = {
|
|
2
|
+
hostname?: string;
|
|
3
|
+
pathname?: string;
|
|
4
|
+
protocol?: string;
|
|
5
|
+
port?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function matchPattern(url: URL, remotePattern: RemotePattern): boolean;
|
|
8
|
+
export declare function matchPort(url: URL, port?: string): boolean;
|
|
9
|
+
export declare function matchProtocol(url: URL, protocol?: string): boolean;
|
|
10
|
+
export declare function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean): boolean;
|
|
11
|
+
export declare function matchPathname(url: URL, pathname?: string, allowWildcard?: boolean): boolean;
|