astro 3.1.4 → 3.2.1

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.
Files changed (35) hide show
  1. package/client.d.ts +7 -0
  2. package/components/ViewTransitions.astro +9 -431
  3. package/dist/assets/build/generate.js +2 -1
  4. package/dist/assets/{image-endpoint.d.ts → endpoint/generic.d.ts} +1 -1
  5. package/dist/assets/{image-endpoint.js → endpoint/generic.js} +2 -2
  6. package/dist/assets/endpoint/node.d.ts +5 -0
  7. package/dist/assets/endpoint/node.js +68 -0
  8. package/dist/assets/internal.d.ts +1 -1
  9. package/dist/assets/internal.js +2 -2
  10. package/dist/assets/vite-plugin-assets.js +6 -0
  11. package/dist/content/types-generator.js +2 -1
  12. package/dist/core/build/index.js +1 -1
  13. package/dist/core/build/static-build.js +15 -4
  14. package/dist/core/constants.js +1 -1
  15. package/dist/core/create-vite.js +0 -1
  16. package/dist/core/dev/container.js +1 -1
  17. package/dist/core/dev/dev.js +1 -1
  18. package/dist/core/errors/errors-data.js +1 -1
  19. package/dist/core/messages.js +2 -2
  20. package/dist/core/render/params-and-props.js +4 -0
  21. package/dist/integrations/index.js +2 -1
  22. package/dist/runtime/client/hmr.js +0 -34
  23. package/dist/runtime/server/astro-island.js +13 -4
  24. package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
  25. package/dist/runtime/server/astro-island.prebuilt.js +1 -1
  26. package/dist/runtime/server/render/component.js +1 -0
  27. package/dist/transitions/router.d.ts +8 -0
  28. package/dist/transitions/router.js +326 -0
  29. package/dist/transitions/vite-plugin-transitions.js +10 -0
  30. package/dist/vite-plugin-astro-server/css.js +1 -1
  31. package/dist/vite-plugin-astro-server/route.js +17 -24
  32. package/dist/vite-plugin-markdown/images.d.ts +6 -0
  33. package/dist/vite-plugin-markdown/images.js +31 -0
  34. package/dist/vite-plugin-markdown/index.js +6 -23
  35. package/package.json +4 -3
package/client.d.ts CHANGED
@@ -120,6 +120,13 @@ declare module 'astro:transitions' {
120
120
  export const ViewTransitions: ViewTransitionsModule['default'];
121
121
  }
122
122
 
123
+ declare module 'astro:transitions/client' {
124
+ type TransitionRouterModule = typeof import('./dist/transitions/router.js');
125
+ export const supportsViewTransitions: TransitionRouterModule['supportsViewTransitions'];
126
+ export const transitionEnabledOnThisPage: TransitionRouterModule['transitionEnabledOnThisPage'];
127
+ export const navigate: TransitionRouterModule['navigate'];
128
+ }
129
+
123
130
  declare module 'astro:middleware' {
124
131
  export * from 'astro/middleware/namespace';
125
132
  }
@@ -11,91 +11,12 @@ const { fallback = 'animate' } = Astro.props as Props;
11
11
  <meta name="astro-view-transitions-enabled" content="true" />
12
12
  <meta name="astro-view-transitions-fallback" content={fallback} />
13
13
  <script>
14
- type Fallback = 'none' | 'animate' | 'swap';
15
- type Direction = 'forward' | 'back';
16
- type State = {
17
- index: number;
18
- scrollX: number;
19
- scrollY: number;
20
- intraPage?: boolean;
21
- };
22
- type Events = 'astro:page-load' | 'astro:after-swap';
23
-
24
- // only update history entries that are managed by us
25
- // leave other entries alone and do not accidently add state.
26
- const persistState = (state: State) => history.state && history.replaceState(state, '');
27
- // @ts-expect-error: startViewTransition might exist
28
- const supportsViewTransitions = !!document.startViewTransition;
29
- const transitionEnabledOnThisPage = () =>
30
- !!document.querySelector('[name="astro-view-transitions-enabled"]');
31
- const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
32
- const onPageLoad = () => triggerEvent('astro:page-load');
33
- const PERSIST_ATTR = 'data-astro-transition-persist';
34
- const parser = new DOMParser();
35
- // explained at its usage
36
- let noopEl: HTMLDivElement;
37
- if (import.meta.env.DEV) {
38
- noopEl = document.createElement('div');
39
- }
40
-
41
- // The History API does not tell you if navigation is forward or back, so
42
- // you can figure it using an index. On pushState the index is incremented so you
43
- // can use that to determine popstate if going forward or back.
44
- let currentHistoryIndex = 0;
45
- if (history.state) {
46
- // we reloaded a page with history state
47
- // (e.g. history navigation from non-transition page or browser reload)
48
- currentHistoryIndex = history.state.index;
49
- scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
50
- } else if (transitionEnabledOnThisPage()) {
51
- history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
52
- }
53
- const throttle = (cb: (...args: any[]) => any, delay: number) => {
54
- let wait = false;
55
- // During the waiting time additional events are lost.
56
- // So repeat the callback at the end if we have swallowed events.
57
- let onceMore = false;
58
- return (...args: any[]) => {
59
- if (wait) {
60
- onceMore = true;
61
- return;
62
- }
63
- cb(...args);
64
- wait = true;
65
- setTimeout(() => {
66
- if (onceMore) {
67
- onceMore = false;
68
- cb(...args);
69
- }
70
- wait = false;
71
- }, delay);
72
- };
73
- };
74
-
75
- // returns the contents of the page or null if the router can't deal with it.
76
- async function fetchHTML(
77
- href: string
78
- ): Promise<null | { html: string; redirected?: string; mediaType: DOMParserSupportedType }> {
79
- try {
80
- const res = await fetch(href);
81
- // drop potential charset (+ other name/value pairs) as parser needs the mediaType
82
- const mediaType = res.headers.get('content-type')?.replace(/;.*$/, '');
83
- // the DOMParser can handle two types of HTML
84
- if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml') {
85
- // everything else (e.g. audio/mp3) will be handled by the browser but not by us
86
- return null;
87
- }
88
- const html = await res.text();
89
- return {
90
- html,
91
- redirected: res.redirected ? res.url : undefined,
92
- mediaType,
93
- };
94
- } catch (err) {
95
- // can't fetch, let someone else deal with it.
96
- return null;
97
- }
98
- }
14
+ import {
15
+ supportsViewTransitions,
16
+ transitionEnabledOnThisPage,
17
+ navigate,
18
+ } from 'astro:transitions/client';
19
+ export type Fallback = 'none' | 'animate' | 'swap';
99
20
 
100
21
  function getFallback(): Fallback {
101
22
  const el = document.querySelector('[name="astro-view-transitions-fallback"]');
@@ -105,263 +26,6 @@ const { fallback = 'animate' } = Astro.props as Props;
105
26
  return 'animate';
106
27
  }
107
28
 
108
- function markScriptsExec() {
109
- for (const script of document.scripts) {
110
- script.dataset.astroExec = '';
111
- }
112
- }
113
-
114
- function runScripts() {
115
- let wait = Promise.resolve();
116
- for (const script of Array.from(document.scripts)) {
117
- if (script.dataset.astroExec === '') continue;
118
- const newScript = document.createElement('script');
119
- newScript.innerHTML = script.innerHTML;
120
- for (const attr of script.attributes) {
121
- if (attr.name === 'src') {
122
- const p = new Promise((r) => {
123
- newScript.onload = r;
124
- });
125
- wait = wait.then(() => p as any);
126
- }
127
- newScript.setAttribute(attr.name, attr.value);
128
- }
129
- newScript.dataset.astroExec = '';
130
- script.replaceWith(newScript);
131
- }
132
- return wait;
133
- }
134
-
135
- function isInfinite(animation: Animation) {
136
- const effect = animation.effect;
137
- if (!effect || !(effect instanceof KeyframeEffect) || !effect.target) return false;
138
- const style = window.getComputedStyle(effect.target, effect.pseudoElement);
139
- return style.animationIterationCount === 'infinite';
140
- }
141
-
142
- const updateHistoryAndScrollPosition = (toLocation) => {
143
- if (toLocation.href !== location.href) {
144
- history.pushState(
145
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
146
- '',
147
- toLocation.href
148
- );
149
- // now we are on the new page for non-history navigations!
150
- // (with history navigation page change happens before popstate is fired)
151
- }
152
- // freshly loaded pages start from the top
153
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
154
-
155
- if (toLocation.hash) {
156
- // because we are already on the target page ...
157
- // ... what comes next is a intra-page navigation
158
- // that won't reload the page but instead scroll to the fragment
159
- location.href = toLocation.href;
160
- }
161
- };
162
-
163
- // replace head and body of the windows document with contents from newDocument
164
- // if !popstate, update the history entry and scroll position according to toLocation
165
- // if popState is given, this holds the scroll position for history navigation
166
- // if fallback === "animate" then simulate view transitions
167
- async function updateDOM(
168
- newDocument: Document,
169
- toLocation: URL,
170
- popState?: State,
171
- fallback?: Fallback
172
- ) {
173
- // Check for a head element that should persist and returns it,
174
- // either because it has the data attribute or is a link el.
175
- const persistedHeadElement = (el: HTMLElement): Element | null => {
176
- const id = el.getAttribute(PERSIST_ATTR);
177
- const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
178
- if (newEl) {
179
- return newEl;
180
- }
181
- if (el.matches('link[rel=stylesheet]')) {
182
- const href = el.getAttribute('href');
183
- return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
184
- }
185
- // What follows is a fix for an issue (#8472) with missing client:only styles after transition.
186
- // That problem exists only in dev mode where styles are injected into the page by Vite.
187
- // Returning a noop element ensures that the styles are not removed from the old document.
188
- // Guarding the code below with the dev mode check
189
- // allows tree shaking to remove this code in production.
190
- if (import.meta.env.DEV) {
191
- if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
192
- const devId = el.dataset.viteDevId;
193
- // If this same style tag exists, remove it from the new page
194
- return (
195
- newDocument.querySelector(`style[data-astro-dev-id="${devId}"]`) ||
196
- // Otherwise, keep it anyways. This is client:only styles.
197
- noopEl
198
- );
199
- }
200
- }
201
- return null;
202
- };
203
-
204
- const swap = () => {
205
- // swap attributes of the html element
206
- // - delete all attributes from the current document
207
- // - insert all attributes from doc
208
- // - reinsert all original attributes that are named 'data-astro-*'
209
- const html = document.documentElement;
210
- const astro = [...html.attributes].filter(
211
- ({ name }) => (html.removeAttribute(name), name.startsWith('data-astro-'))
212
- );
213
- [...newDocument.documentElement.attributes, ...astro].forEach(({ name, value }) =>
214
- html.setAttribute(name, value)
215
- );
216
-
217
- // Replace scripts in both the head and body.
218
- for (const s1 of document.scripts) {
219
- for (const s2 of newDocument.scripts) {
220
- if (
221
- // Inline
222
- (!s1.src && s1.textContent === s2.textContent) ||
223
- // External
224
- (s1.src && s1.type === s2.type && s1.src === s2.src)
225
- ) {
226
- // the old script is in the new document: we mark it as executed to prevent re-execution
227
- s2.dataset.astroExec = '';
228
- break;
229
- }
230
- }
231
- }
232
-
233
- // Swap head
234
- for (const el of Array.from(document.head.children)) {
235
- const newEl = persistedHeadElement(el as HTMLElement);
236
- // If the element exists in the document already, remove it
237
- // from the new document and leave the current node alone
238
- if (newEl) {
239
- newEl.remove();
240
- } else {
241
- // Otherwise remove the element in the head. It doesn't exist in the new page.
242
- el.remove();
243
- }
244
- }
245
-
246
- // Everything left in the new head is new, append it all.
247
- document.head.append(...newDocument.head.children);
248
-
249
- // Persist elements in the existing body
250
- const oldBody = document.body;
251
-
252
- // this will reset scroll Position
253
- document.body.replaceWith(newDocument.body);
254
- for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
255
- const id = el.getAttribute(PERSIST_ATTR);
256
- const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
257
- if (newEl) {
258
- // The element exists in the new page, replace it with the element
259
- // from the old page so that state is preserved.
260
- newEl.replaceWith(el);
261
- }
262
- }
263
-
264
- if (popState) {
265
- scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
266
- } else {
267
- updateHistoryAndScrollPosition(toLocation);
268
- }
269
-
270
- triggerEvent('astro:after-swap');
271
- };
272
-
273
- // Wait on links to finish, to prevent FOUC
274
- const links: Promise<any>[] = [];
275
- for (const el of newDocument.querySelectorAll('head link[rel=stylesheet]')) {
276
- // Do not preload links that are already on the page.
277
- if (
278
- !document.querySelector(
279
- `[${PERSIST_ATTR}="${el.getAttribute(PERSIST_ATTR)}"], link[rel=stylesheet]`
280
- )
281
- ) {
282
- const c = document.createElement('link');
283
- c.setAttribute('rel', 'preload');
284
- c.setAttribute('as', 'style');
285
- c.setAttribute('href', el.getAttribute('href')!);
286
- links.push(
287
- new Promise<any>((resolve) => {
288
- ['load', 'error'].forEach((evName) => c.addEventListener(evName, resolve));
289
- document.head.append(c);
290
- })
291
- );
292
- }
293
- }
294
- links.length && (await Promise.all(links));
295
-
296
- if (fallback === 'animate') {
297
- // Trigger the animations
298
- const currentAnimations = document.getAnimations();
299
- document.documentElement.dataset.astroTransitionFallback = 'old';
300
- const newAnimations = document
301
- .getAnimations()
302
- .filter((a) => !currentAnimations.includes(a) && !isInfinite(a));
303
- const finished = Promise.all(newAnimations.map((a) => a.finished));
304
- const fallbackSwap = () => {
305
- swap();
306
- document.documentElement.dataset.astroTransitionFallback = 'new';
307
- };
308
- await finished;
309
- fallbackSwap();
310
- } else {
311
- swap();
312
- }
313
- }
314
-
315
- async function transition(direction: Direction, toLocation: URL, popState?: State) {
316
- let finished: Promise<void>;
317
- const href = toLocation.href;
318
- const response = await fetchHTML(href);
319
- // If there is a problem fetching the new page, just do an MPA navigation to it.
320
- if (response === null) {
321
- location.href = href;
322
- return;
323
- }
324
- // if there was a redirection, show the final URL in the browser's address bar
325
- if (response.redirected) {
326
- toLocation = new URL(response.redirected);
327
- }
328
-
329
- const newDocument = parser.parseFromString(response.html, response.mediaType);
330
- // The next line might look like a hack,
331
- // but it is actually necessary as noscript elements
332
- // and their contents are returned as markup by the parser,
333
- // see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString
334
- newDocument.querySelectorAll('noscript').forEach((el) => el.remove());
335
-
336
- if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
337
- location.href = href;
338
- return;
339
- }
340
-
341
- if (!popState) {
342
- // save the current scroll position before we change the DOM and transition to the new page
343
- history.replaceState({ ...history.state, scrollX, scrollY }, '');
344
- }
345
- document.documentElement.dataset.astroTransition = direction;
346
- if (supportsViewTransitions) {
347
- // @ts-expect-error: startViewTransition exist
348
- finished = document.startViewTransition(() =>
349
- updateDOM(newDocument, toLocation, popState)
350
- ).finished;
351
- } else {
352
- finished = updateDOM(newDocument, toLocation, popState, getFallback());
353
- }
354
- try {
355
- await finished;
356
- } finally {
357
- // skip this for the moment as it tends to stop fallback animations
358
- // document.documentElement.removeAttribute('data-astro-transition');
359
- await runScripts();
360
- markScriptsExec();
361
- onPageLoad();
362
- }
363
- }
364
-
365
29
  // Prefetching
366
30
  function maybePrefetch(pathname: string) {
367
31
  if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
@@ -406,83 +70,9 @@ const { fallback = 'animate' } = Astro.props as Props;
406
70
  return;
407
71
  }
408
72
  ev.preventDefault();
409
- navigate(link.href);
410
- });
411
-
412
- function navigate(href) {
413
- // not ours
414
- if (!transitionEnabledOnThisPage()) {
415
- location.href = href;
416
- return;
417
- }
418
- const toLocation = new URL(href, location.href);
419
- // We do not have page transitions on navigations to the same page (intra-page navigation)
420
- // but we want to handle prevent reload on navigation to the same page
421
- // Same page means same origin, path and query params (but maybe different hash)
422
- if (
423
- location.origin === toLocation.origin &&
424
- location.pathname === toLocation.pathname &&
425
- location.search === toLocation.search
426
- ) {
427
- // mark current position as non transition intra-page scrolling
428
- if (location.href !== toLocation.href) {
429
- history.replaceState({ ...history.state, intraPage: true }, '');
430
- history.pushState(
431
- { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
432
- '',
433
- toLocation.href
434
- );
435
- }
436
- if (toLocation.hash) {
437
- location.href = toLocation.href;
438
- } else {
439
- scrollTo({ left: 0, top: 0, behavior: 'instant' });
440
- }
441
- } else {
442
- transition('forward', toLocation);
443
- }
444
- }
445
-
446
- addEventListener('popstate', (ev) => {
447
- if (!transitionEnabledOnThisPage() && ev.state) {
448
- // The current page doesn't have View Transitions enabled
449
- // but the page we navigate to does (because it set the state).
450
- // Do a full page refresh to reload the client-side router from the new page.
451
- // Scroll restauration will then happen during the reload when the router's code is re-executed
452
- if (history.scrollRestoration) {
453
- history.scrollRestoration = 'manual';
454
- }
455
- location.reload();
456
- return;
457
- }
458
-
459
- // History entries without state are created by the browser (e.g. for hash links)
460
- // Our view transition entries always have state.
461
- // Just ignore stateless entries.
462
- // The browser will handle navigation fine without our help
463
- if (ev.state === null) {
464
- if (history.scrollRestoration) {
465
- history.scrollRestoration = 'auto';
466
- }
467
- return;
468
- }
469
-
470
- // With the default "auto", the browser will jump to the old scroll position
471
- // before the ViewTransition is complete.
472
- if (history.scrollRestoration) {
473
- history.scrollRestoration = 'manual';
474
- }
475
-
476
- const state: State = history.state;
477
- if (state.intraPage) {
478
- // this is non transition intra-page scrolling
479
- scrollTo(state.scrollX, state.scrollY);
480
- } else {
481
- const nextIndex = state.index;
482
- const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
483
- currentHistoryIndex = nextIndex;
484
- transition(direction, new URL(location.href), state);
485
- }
73
+ navigate(link.href, {
74
+ history: link.dataset.astroHistory === 'replace' ? 'replace' : 'auto',
75
+ });
486
76
  });
487
77
 
488
78
  ['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
@@ -503,17 +93,5 @@ const { fallback = 'animate' } = Astro.props as Props;
503
93
  { passive: true, capture: true }
504
94
  );
505
95
  });
506
-
507
- addEventListener('load', onPageLoad);
508
- // There's not a good way to record scroll position before a back button.
509
- // So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
510
- const updateState = () => {
511
- persistState({ ...history.state, scrollX, scrollY });
512
- };
513
-
514
- if ('onscrollend' in window) addEventListener('scrollend', updateState);
515
- else addEventListener('scroll', throttle(updateState, 300));
516
-
517
- markScriptsExec();
518
96
  }
519
97
  </script>
@@ -1,5 +1,6 @@
1
1
  import fs, { readFileSync } from "node:fs";
2
2
  import { basename, join } from "node:path/posix";
3
+ import { getOutDirWithinCwd } from "../../core/build/common.js";
3
4
  import { prependForwardSlash } from "../../core/path.js";
4
5
  import { isServerLikeOutput } from "../../prerender/utils.js";
5
6
  import { getConfiguredImageService, isESMImportedImage } from "../internal.js";
@@ -23,7 +24,7 @@ async function generateImage(pipeline, options, filepath) {
23
24
  serverRoot = config.build.server;
24
25
  clientRoot = config.build.client;
25
26
  } else {
26
- serverRoot = config.outDir;
27
+ serverRoot = getOutDirWithinCwd(config.outDir);
27
28
  clientRoot = config.outDir;
28
29
  }
29
30
  const isLocalImage = isESMImportedImage(options.src);
@@ -1,4 +1,4 @@
1
- import type { APIRoute } from '../@types/astro.js';
1
+ import type { APIRoute } from '../../@types/astro.js';
2
2
  /**
3
3
  * Endpoint used in dev and SSR to serve optimized images by the base image services
4
4
  */
@@ -1,7 +1,7 @@
1
1
  import { isRemotePath } from "@astrojs/internal-helpers/path";
2
2
  import mime from "mime/lite.js";
3
- import { getConfiguredImageService, isRemoteAllowed } from "./internal.js";
4
- import { etag } from "./utils/etag.js";
3
+ import { getConfiguredImageService, isRemoteAllowed } from "../internal.js";
4
+ import { etag } from "../utils/etag.js";
5
5
  import { imageConfig } from "astro:assets";
6
6
  async function loadRemoteImage(src) {
7
7
  try {
@@ -0,0 +1,5 @@
1
+ import type { APIRoute } from '../../@types/astro.js';
2
+ /**
3
+ * Endpoint used in dev and SSR to serve optimized images by the base image services
4
+ */
5
+ export declare const GET: APIRoute;
@@ -0,0 +1,68 @@
1
+ import { isRemotePath, removeQueryString } from "@astrojs/internal-helpers/path";
2
+ import { readFile } from "fs/promises";
3
+ import mime from "mime/lite.js";
4
+ import { getConfiguredImageService, isRemoteAllowed } from "../internal.js";
5
+ import { etag } from "../utils/etag.js";
6
+ import { assetsDir, imageConfig } from "astro:assets";
7
+ async function loadLocalImage(src, url) {
8
+ const filePath = import.meta.env.DEV ? removeQueryString(src.slice("/@fs".length)) : new URL("." + src, assetsDir);
9
+ let buffer = void 0;
10
+ try {
11
+ buffer = await readFile(filePath);
12
+ } catch (e) {
13
+ const sourceUrl = new URL(src, url.origin);
14
+ buffer = await loadRemoteImage(sourceUrl);
15
+ }
16
+ return buffer;
17
+ }
18
+ async function loadRemoteImage(src) {
19
+ try {
20
+ const res = await fetch(src);
21
+ if (!res.ok) {
22
+ return void 0;
23
+ }
24
+ return Buffer.from(await res.arrayBuffer());
25
+ } catch (err) {
26
+ return void 0;
27
+ }
28
+ }
29
+ const GET = async ({ request }) => {
30
+ try {
31
+ const imageService = await getConfiguredImageService();
32
+ if (!("transform" in imageService)) {
33
+ throw new Error("Configured image service is not a local service");
34
+ }
35
+ const url = new URL(request.url);
36
+ const transform = await imageService.parseURL(url, imageConfig);
37
+ if (!transform?.src) {
38
+ throw new Error("Incorrect transform returned by `parseURL`");
39
+ }
40
+ let inputBuffer = void 0;
41
+ if (isRemotePath(transform.src)) {
42
+ if (isRemoteAllowed(transform.src, imageConfig) === false) {
43
+ return new Response("Forbidden", { status: 403 });
44
+ }
45
+ inputBuffer = await loadRemoteImage(new URL(transform.src));
46
+ } else {
47
+ inputBuffer = await loadLocalImage(transform.src, url);
48
+ }
49
+ if (!inputBuffer) {
50
+ return new Response("Not Found", { status: 404 });
51
+ }
52
+ const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
53
+ return new Response(data, {
54
+ status: 200,
55
+ headers: {
56
+ "Content-Type": mime.getType(format) ?? `image/${format}`,
57
+ "Cache-Control": "public, max-age=31536000",
58
+ ETag: etag(data.toString()),
59
+ Date: (/* @__PURE__ */ new Date()).toUTCString()
60
+ }
61
+ });
62
+ } catch (err) {
63
+ return new Response(`Server Error: ${err}`, { status: 500 });
64
+ }
65
+ };
66
+ export {
67
+ GET
68
+ };
@@ -1,7 +1,7 @@
1
1
  import type { AstroConfig, AstroSettings } from '../@types/astro.js';
2
2
  import { type ImageService } from './services/service.js';
3
3
  import type { GetImageResult, ImageMetadata, UnresolvedImageTransform } from './types.js';
4
- export declare function injectImageEndpoint(settings: AstroSettings): AstroSettings;
4
+ export declare function injectImageEndpoint(settings: AstroSettings, mode: 'dev' | 'build'): AstroSettings;
5
5
  export declare function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata;
6
6
  export declare function isRemoteImage(src: ImageMetadata | string): src is string;
7
7
  export declare function isRemoteAllowed(src: string, { domains, remotePatterns, }: Partial<Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>>): boolean;
@@ -2,8 +2,8 @@ import { isRemotePath } from "@astrojs/internal-helpers/path";
2
2
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
3
3
  import { isLocalService } from "./services/service.js";
4
4
  import { matchHostname, matchPattern } from "./utils/remotePattern.js";
5
- function injectImageEndpoint(settings) {
6
- const endpointEntrypoint = settings.config.image.endpoint ?? "astro/assets/image-endpoint";
5
+ function injectImageEndpoint(settings, mode) {
6
+ const endpointEntrypoint = settings.config.image.endpoint ?? (mode === "dev" ? "astro/assets/endpoint/node" : "astro/assets/endpoint/generic");
7
7
  settings.injectedRoutes.push({
8
8
  pattern: "/_image",
9
9
  entryPoint: endpointEntrypoint,
@@ -8,6 +8,7 @@ import {
8
8
  prependForwardSlash,
9
9
  removeQueryString
10
10
  } from "../core/path.js";
11
+ import { isServerLikeOutput } from "../prerender/utils.js";
11
12
  import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from "./consts.js";
12
13
  import { emitESMImage } from "./utils/emitAsset.js";
13
14
  import { hashTransform, propsToFilename } from "./utils/transformToPath.js";
@@ -48,6 +49,11 @@ function assets({
48
49
  export { default as Image } from "astro/components/Image.astro";
49
50
 
50
51
  export const imageConfig = ${JSON.stringify(settings.config.image)};
52
+ export const assetsDir = new URL(${JSON.stringify(
53
+ new URL(
54
+ isServerLikeOutput(settings.config) ? settings.config.build.client : settings.config.outDir
55
+ )
56
+ )});
51
57
  export const getImage = async (options) => await getImageInternal(options, imageConfig);
52
58
  `;
53
59
  }
@@ -53,7 +53,8 @@ async function createContentTypesGenerator({
53
53
  objectMode: true
54
54
  });
55
55
  for (const entry of globResult) {
56
- const entryURL = new URL(entry.path, contentPaths.contentDir);
56
+ const fullPath = path.join(fileURLToPath(contentPaths.contentDir), entry.path);
57
+ const entryURL = pathToFileURL(fullPath);
57
58
  if (entryURL.href.startsWith(contentPaths.config.url.href))
58
59
  continue;
59
60
  if (entry.dirent.isFile()) {
@@ -68,7 +68,7 @@ class AstroBuilder {
68
68
  logger
69
69
  });
70
70
  if (isServerLikeOutput(this.settings.config)) {
71
- this.settings = injectImageEndpoint(this.settings);
71
+ this.settings = injectImageEndpoint(this.settings, "build");
72
72
  }
73
73
  this.manifest = createRouteManifest({ settings: this.settings }, this.logger);
74
74
  const viteConfig = await createVite(