astro 2.10.9 → 2.10.11
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 +26 -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-astro-server/route.js +4 -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.
|
|
@@ -163,6 +171,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
163
171
|
}
|
|
164
172
|
if (state?.scrollY != null) {
|
|
165
173
|
scrollTo(0, state.scrollY);
|
|
174
|
+
// Overwrite erroneous updates by the scroll handler during transition
|
|
175
|
+
persistState(state);
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
triggerEvent('astro:beforeload');
|
|
@@ -222,15 +232,17 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
222
232
|
location.href = href;
|
|
223
233
|
return;
|
|
224
234
|
}
|
|
235
|
+
document.documentElement.dataset.astroTransition = dir;
|
|
225
236
|
if (supportsViewTransitions) {
|
|
226
|
-
finished = document.startViewTransition(() => updateDOM(
|
|
237
|
+
finished = document.startViewTransition(() => updateDOM(html, state)).finished;
|
|
227
238
|
} else {
|
|
228
|
-
finished = updateDOM(
|
|
239
|
+
finished = updateDOM(html, state, getFallback());
|
|
229
240
|
}
|
|
230
241
|
try {
|
|
231
242
|
await finished;
|
|
232
243
|
} finally {
|
|
233
|
-
|
|
244
|
+
// skip this for the moment as it tends to stop fallback animations
|
|
245
|
+
// document.documentElement.removeAttribute('data-astro-transition');
|
|
234
246
|
await runScripts();
|
|
235
247
|
markScriptsExec();
|
|
236
248
|
onload();
|
|
@@ -280,8 +292,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
280
292
|
transitionEnabledOnThisPage()
|
|
281
293
|
) {
|
|
282
294
|
ev.preventDefault();
|
|
283
|
-
navigate('forward', link.href, { index: currentHistoryIndex, scrollY: 0 });
|
|
284
|
-
currentHistoryIndex++;
|
|
295
|
+
navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
|
|
285
296
|
const newState: State = { index: currentHistoryIndex, scrollY };
|
|
286
297
|
persistState({ index: currentHistoryIndex - 1, scrollY });
|
|
287
298
|
history.pushState(newState, '', link.href);
|
|
@@ -295,10 +306,11 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
295
306
|
return;
|
|
296
307
|
}
|
|
297
308
|
|
|
298
|
-
//
|
|
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
|
|
299
313
|
if (ev.state === null) {
|
|
300
|
-
persistState({ index: currentHistoryIndex, scrollY });
|
|
301
|
-
ev.preventDefault();
|
|
302
314
|
return;
|
|
303
315
|
}
|
|
304
316
|
|
|
@@ -333,6 +345,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
|
|
333
345
|
addEventListener(
|
|
334
346
|
'scroll',
|
|
335
347
|
throttle(() => {
|
|
348
|
+
// only updste history entries that are managed by us
|
|
349
|
+
// leave other entries alone and do not accidently add state.
|
|
336
350
|
if (history.state) {
|
|
337
351
|
persistState({ ...history.state, scrollY });
|
|
338
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;
|