Merge branch 'main' into bare-standalone-paths
This commit is contained in:
commit
7be8230e43
21 changed files with 303 additions and 183 deletions
5
.changeset/honest-snakes-peel.md
Normal file
5
.changeset/honest-snakes-peel.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix relative images in Markdown breaking the build process in certain circumstances
|
5
.changeset/wise-donuts-tickle.md
Normal file
5
.changeset/wise-donuts-tickle.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Astro HMR from a CSS dependency
|
|
@ -17,18 +17,26 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
index: number;
|
index: number;
|
||||||
scrollX: number;
|
scrollX: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
|
intraPage?: boolean;
|
||||||
};
|
};
|
||||||
type Events = 'astro:page-load' | 'astro:after-swap';
|
type Events = 'astro:page-load' | 'astro:after-swap';
|
||||||
|
|
||||||
// only update history entries that are managed by us
|
// only update history entries that are managed by us
|
||||||
// leave other entries alone and do not accidently add state.
|
// leave other entries alone and do not accidently add state.
|
||||||
const persistState = (state: State) => history.state && history.replaceState(state, '');
|
const persistState = (state: State) => history.state && history.replaceState(state, '');
|
||||||
|
// @ts-expect-error: startViewTransition might exist
|
||||||
const supportsViewTransitions = !!document.startViewTransition;
|
const supportsViewTransitions = !!document.startViewTransition;
|
||||||
const transitionEnabledOnThisPage = () =>
|
const transitionEnabledOnThisPage = () =>
|
||||||
!!document.querySelector('[name="astro-view-transitions-enabled"]');
|
!!document.querySelector('[name="astro-view-transitions-enabled"]');
|
||||||
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
||||||
const onPageLoad = () => triggerEvent('astro:page-load');
|
const onPageLoad = () => triggerEvent('astro:page-load');
|
||||||
const PERSIST_ATTR = 'data-astro-transition-persist';
|
const PERSIST_ATTR = 'data-astro-transition-persist';
|
||||||
|
const parser = new DOMParser();
|
||||||
|
// explained at its usage
|
||||||
|
let noopEl: HTMLDivElement;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
noopEl = document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
// The History API does not tell you if navigation is forward or back, so
|
// The History API does not tell you if navigation is forward or back, so
|
||||||
// you can figure it using an index. On pushState the index is incremented so you
|
// you can figure it using an index. On pushState the index is incremented so you
|
||||||
|
@ -40,7 +48,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
currentHistoryIndex = history.state.index;
|
currentHistoryIndex = history.state.index;
|
||||||
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
|
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
|
||||||
} else if (transitionEnabledOnThisPage()) {
|
} else if (transitionEnabledOnThisPage()) {
|
||||||
history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
|
history.replaceState({ index: currentHistoryIndex, scrollX, scrollY, intraPage: false }, '');
|
||||||
}
|
}
|
||||||
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
||||||
let wait = false;
|
let wait = false;
|
||||||
|
@ -64,19 +72,28 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getHTML(href: string) {
|
// returns the contents of the page or null if the router can't deal with it.
|
||||||
|
async function fetchHTML(
|
||||||
|
href: string
|
||||||
|
): Promise<null | { html: string; redirected?: string; mediaType: DOMParserSupportedType }> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(href);
|
const res = await fetch(href);
|
||||||
|
// drop potential charset (+ other name/value pairs) as parser needs the mediaType
|
||||||
|
const mediaType = res.headers.get('content-type')?.replace(/;.*$/, '');
|
||||||
|
// the DOMParser can handle two types of HTML
|
||||||
|
if (mediaType !== 'text/html' && mediaType !== 'application/xhtml+xml') {
|
||||||
|
// everything else (e.g. audio/mp3) will be handled by the browser but not by us
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
return {
|
return {
|
||||||
ok: res.ok,
|
|
||||||
html,
|
html,
|
||||||
redirected: res.redirected ? res.url : undefined,
|
redirected: res.redirected ? res.url : undefined,
|
||||||
// drop potential charset (+ other name/value pairs) as parser needs the mediaType
|
mediaType,
|
||||||
mediaType: res.headers.get('content-type')?.replace(/;.*$/, ''),
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false };
|
// can't fetch, let someone else deal with it.
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,19 +115,19 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
let wait = Promise.resolve();
|
let wait = Promise.resolve();
|
||||||
for (const script of Array.from(document.scripts)) {
|
for (const script of Array.from(document.scripts)) {
|
||||||
if (script.dataset.astroExec === '') continue;
|
if (script.dataset.astroExec === '') continue;
|
||||||
const s = document.createElement('script');
|
const newScript = document.createElement('script');
|
||||||
s.innerHTML = script.innerHTML;
|
newScript.innerHTML = script.innerHTML;
|
||||||
for (const attr of script.attributes) {
|
for (const attr of script.attributes) {
|
||||||
if (attr.name === 'src') {
|
if (attr.name === 'src') {
|
||||||
const p = new Promise((r) => {
|
const p = new Promise((r) => {
|
||||||
s.onload = r;
|
newScript.onload = r;
|
||||||
});
|
});
|
||||||
wait = wait.then(() => p as any);
|
wait = wait.then(() => p as any);
|
||||||
}
|
}
|
||||||
s.setAttribute(attr.name, attr.value);
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
}
|
}
|
||||||
s.dataset.astroExec = '';
|
newScript.dataset.astroExec = '';
|
||||||
script.replaceWith(s);
|
script.replaceWith(newScript);
|
||||||
}
|
}
|
||||||
return wait;
|
return wait;
|
||||||
}
|
}
|
||||||
|
@ -122,16 +139,39 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
return style.animationIterationCount === 'infinite';
|
return style.animationIterationCount === 'infinite';
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const updateHistoryAndScrollPosition = (toLocation) => {
|
||||||
|
if (toLocation.href !== location.href) {
|
||||||
// A noop element used to prevent styles from being removed
|
history.pushState(
|
||||||
if (import.meta.env.DEV) {
|
{ index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
|
||||||
var noopEl = document.createElement('div');
|
'',
|
||||||
|
toLocation.href
|
||||||
|
);
|
||||||
|
// now we are on the new page for non-history navigations!
|
||||||
|
// (with history navigation page change happens before popstate is fired)
|
||||||
}
|
}
|
||||||
|
// freshly loaded pages start from the top
|
||||||
|
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||||
|
|
||||||
async function updateDOM(newDocument: Document, loc: URL, state?: State, fallback?: Fallback) {
|
if (toLocation.hash) {
|
||||||
// Check for a head element that should persist, either because it has the data
|
// because we are already on the target page ...
|
||||||
// attribute or is a link el.
|
// ... what comes next is a intra-page navigation
|
||||||
|
// that won't reload the page but instead scroll to the fragment
|
||||||
|
location.href = toLocation.href;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// replace head and body of the windows document with contents from newDocument
|
||||||
|
// if !popstate, update the history entry and scroll position according to toLocation
|
||||||
|
// if popState is given, this holds the scroll position for history navigation
|
||||||
|
// if fallback === "animate" then simulate view transitions
|
||||||
|
async function updateDOM(
|
||||||
|
newDocument: Document,
|
||||||
|
toLocation: URL,
|
||||||
|
popState?: State,
|
||||||
|
fallback?: Fallback
|
||||||
|
) {
|
||||||
|
// Check for a head element that should persist and returns it,
|
||||||
|
// either because it has the data attribute or is a link el.
|
||||||
const persistedHeadElement = (el: HTMLElement): Element | null => {
|
const persistedHeadElement = (el: HTMLElement): Element | null => {
|
||||||
const id = el.getAttribute(PERSIST_ATTR);
|
const id = el.getAttribute(PERSIST_ATTR);
|
||||||
const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
|
const newEl = id && newDocument.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
|
||||||
|
@ -142,7 +182,11 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
const href = el.getAttribute('href');
|
const href = el.getAttribute('href');
|
||||||
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
|
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
|
||||||
}
|
}
|
||||||
// Only run this in dev. This will get stripped from production builds and is not needed.
|
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
|
||||||
|
// That problem exists only in dev mode where styles are injected into the page by Vite.
|
||||||
|
// Returning a noop element ensures that the styles are not removed from the old document.
|
||||||
|
// Guarding the code below with the dev mode check
|
||||||
|
// allows tree shaking to remove this code in production.
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
|
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
|
||||||
const devId = el.dataset.viteDevId;
|
const devId = el.dataset.viteDevId;
|
||||||
|
@ -158,10 +202,6 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
};
|
};
|
||||||
|
|
||||||
const swap = () => {
|
const swap = () => {
|
||||||
// noscript tags inside head element are not honored on swap (#7969).
|
|
||||||
// Remove them before swapping.
|
|
||||||
newDocument.querySelectorAll('head noscript').forEach((el) => el.remove());
|
|
||||||
|
|
||||||
// swap attributes of the html element
|
// swap attributes of the html element
|
||||||
// - delete all attributes from the current document
|
// - delete all attributes from the current document
|
||||||
// - insert all attributes from doc
|
// - insert all attributes from doc
|
||||||
|
@ -208,6 +248,8 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
|
|
||||||
// Persist elements in the existing body
|
// Persist elements in the existing body
|
||||||
const oldBody = document.body;
|
const oldBody = document.body;
|
||||||
|
|
||||||
|
// this will reset scroll Position
|
||||||
document.body.replaceWith(newDocument.body);
|
document.body.replaceWith(newDocument.body);
|
||||||
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
|
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
|
||||||
const id = el.getAttribute(PERSIST_ATTR);
|
const id = el.getAttribute(PERSIST_ATTR);
|
||||||
|
@ -219,33 +261,12 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate scroll behavior of Safari and
|
if (popState) {
|
||||||
// Chromium based browsers (Chrome, Edge, Opera, ...)
|
scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
|
||||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
} else {
|
||||||
|
updateHistoryAndScrollPosition(toLocation);
|
||||||
|
}
|
||||||
|
|
||||||
let initialScrollX = 0;
|
|
||||||
let initialScrollY = 0;
|
|
||||||
if (!state && loc.hash) {
|
|
||||||
const id = decodeURIComponent(loc.hash.slice(1));
|
|
||||||
const elem = document.getElementById(id);
|
|
||||||
// prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
|
|
||||||
if (elem) {
|
|
||||||
elem.scrollIntoView();
|
|
||||||
initialScrollX = Math.max(
|
|
||||||
0,
|
|
||||||
elem.offsetLeft + elem.offsetWidth - document.documentElement.clientWidth
|
|
||||||
);
|
|
||||||
initialScrollY = elem.offsetTop;
|
|
||||||
}
|
|
||||||
} else if (state) {
|
|
||||||
scrollTo(state.scrollX, state.scrollY); // usings default scrollBehavior
|
|
||||||
}
|
|
||||||
!state &&
|
|
||||||
history.pushState(
|
|
||||||
{ index: ++currentHistoryIndex, scrollX: initialScrollX, scrollY: initialScrollY },
|
|
||||||
'',
|
|
||||||
loc.href
|
|
||||||
);
|
|
||||||
triggerEvent('astro:after-swap');
|
triggerEvent('astro:after-swap');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -291,32 +312,44 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigate(dir: Direction, loc: URL, state?: State) {
|
async function transition(direction: Direction, toLocation: URL, popState?: State) {
|
||||||
let finished: Promise<void>;
|
let finished: Promise<void>;
|
||||||
const href = loc.href;
|
const href = toLocation.href;
|
||||||
const { html, ok, mediaType, redirected } = await getHTML(href);
|
const response = await fetchHTML(href);
|
||||||
// if there was a redirection, show the final URL in the browser's address bar
|
|
||||||
redirected && (loc = new URL(redirected));
|
|
||||||
// If there is a problem fetching the new page, just do an MPA navigation to it.
|
// If there is a problem fetching the new page, just do an MPA navigation to it.
|
||||||
if (!ok || !(mediaType === 'text/html' || mediaType === 'application/xhtml+xml')) {
|
if (response === null) {
|
||||||
location.href = href;
|
location.href = href;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// if there was a redirection, show the final URL in the browser's address bar
|
||||||
|
if (response.redirected) {
|
||||||
|
toLocation = new URL(response.redirected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDocument = parser.parseFromString(response.html, response.mediaType);
|
||||||
|
// The next line might look like a hack,
|
||||||
|
// but it is actually necessary as noscript elements
|
||||||
|
// and their contents are returned as markup by the parser,
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString
|
||||||
|
newDocument.querySelectorAll('noscript').forEach((el) => el.remove());
|
||||||
|
|
||||||
const newDocument = parser.parseFromString(html, mediaType);
|
|
||||||
if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
|
if (!newDocument.querySelector('[name="astro-view-transitions-enabled"]')) {
|
||||||
location.href = href;
|
location.href = href;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we are sure that we will push state, and it is time to create a state if it is still missing.
|
if (!popState) {
|
||||||
!state && history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
|
// save the current scroll position before we change the DOM and transition to the new page
|
||||||
|
history.replaceState({ ...history.state, scrollX, scrollY }, '');
|
||||||
document.documentElement.dataset.astroTransition = dir;
|
}
|
||||||
|
document.documentElement.dataset.astroTransition = direction;
|
||||||
if (supportsViewTransitions) {
|
if (supportsViewTransitions) {
|
||||||
finished = document.startViewTransition(() => updateDOM(newDocument, loc, state)).finished;
|
// @ts-expect-error: startViewTransition exist
|
||||||
|
finished = document.startViewTransition(() =>
|
||||||
|
updateDOM(newDocument, toLocation, popState)
|
||||||
|
).finished;
|
||||||
} else {
|
} else {
|
||||||
finished = updateDOM(newDocument, loc, state, getFallback());
|
finished = updateDOM(newDocument, toLocation, popState, getFallback());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await finished;
|
await finished;
|
||||||
|
@ -332,7 +365,9 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
// Prefetching
|
// Prefetching
|
||||||
function maybePrefetch(pathname: string) {
|
function maybePrefetch(pathname: string) {
|
||||||
if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
|
if (document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
|
||||||
|
// @ts-expect-error: connection might exist
|
||||||
if (navigator.connection) {
|
if (navigator.connection) {
|
||||||
|
// @ts-expect-error: connection does exist
|
||||||
let conn = navigator.connection;
|
let conn = navigator.connection;
|
||||||
if (conn.saveData || /(2|3)g/.test(conn.effectiveType || '')) return;
|
if (conn.saveData || /(2|3)g/.test(conn.effectiveType || '')) return;
|
||||||
}
|
}
|
||||||
|
@ -343,8 +378,6 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportsViewTransitions || getFallback() !== 'none') {
|
if (supportsViewTransitions || getFallback() !== 'none') {
|
||||||
markScriptsExec();
|
|
||||||
|
|
||||||
document.addEventListener('click', (ev) => {
|
document.addEventListener('click', (ev) => {
|
||||||
let link = ev.target;
|
let link = ev.target;
|
||||||
if (link instanceof Element && link.tagName !== 'A') {
|
if (link instanceof Element && link.tagName !== 'A') {
|
||||||
|
@ -366,51 +399,59 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
ev.ctrlKey || // new tab (windows)
|
ev.ctrlKey || // new tab (windows)
|
||||||
ev.altKey || // download
|
ev.altKey || // download
|
||||||
ev.shiftKey || // new window
|
ev.shiftKey || // new window
|
||||||
ev.defaultPrevented ||
|
ev.defaultPrevented
|
||||||
!transitionEnabledOnThisPage()
|
|
||||||
) {
|
) {
|
||||||
// No page transitions in these cases,
|
// No page transitions in these cases,
|
||||||
// Let the browser standard action handle this
|
// Let the browser standard action handle this
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We do not need to handle same page links because there are no page transitions
|
|
||||||
// Same page means same path and same query params (but different hash)
|
|
||||||
if (location.pathname === link.pathname && location.search === link.search) {
|
|
||||||
if (link.hash) {
|
|
||||||
// The browser default action will handle navigations with hash fragments
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Special case: self link without hash
|
|
||||||
// If handed to the browser it will reload the page
|
|
||||||
// But we want to handle it like any other same page navigation
|
|
||||||
// So we scroll to the top of the page but do not start page transitions
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
// push state on the first navigation but not if we were here already
|
navigate(link.href);
|
||||||
if (location.hash) {
|
|
||||||
history.replaceState(
|
|
||||||
{ index: currentHistoryIndex, scrollX, scrollY: -(scrollY + 1) },
|
|
||||||
''
|
|
||||||
);
|
|
||||||
const newState: State = { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 };
|
|
||||||
history.pushState(newState, '', link.href);
|
|
||||||
}
|
|
||||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// these are the cases we will handle: same origin, different page
|
|
||||||
ev.preventDefault();
|
|
||||||
navigate('forward', new URL(link.href));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function navigate(href) {
|
||||||
|
// not ours
|
||||||
|
if (!transitionEnabledOnThisPage()) {
|
||||||
|
location.href = href;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toLocation = new URL(href, location.href);
|
||||||
|
// We do not have page transitions on navigations to the same page (intra-page navigation)
|
||||||
|
// but we want to handle prevent reload on navigation to the same page
|
||||||
|
// Same page means same origin, path and query params (but maybe different hash)
|
||||||
|
if (
|
||||||
|
location.origin === toLocation.origin &&
|
||||||
|
location.pathname === toLocation.pathname &&
|
||||||
|
location.search === toLocation.search
|
||||||
|
) {
|
||||||
|
// mark current position as non transition intra-page scrolling
|
||||||
|
if (location.href !== toLocation.href) {
|
||||||
|
history.replaceState({ ...history.state, intraPage: true }, '');
|
||||||
|
history.pushState(
|
||||||
|
{ index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 },
|
||||||
|
'',
|
||||||
|
toLocation.href
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (toLocation.hash) {
|
||||||
|
location.href = toLocation.href;
|
||||||
|
} else {
|
||||||
|
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transition('forward', toLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addEventListener('popstate', (ev) => {
|
addEventListener('popstate', (ev) => {
|
||||||
if (!transitionEnabledOnThisPage() && ev.state) {
|
if (!transitionEnabledOnThisPage() && ev.state) {
|
||||||
// The current page doesn't have View Transitions enabled
|
// The current page doesn't have View Transitions enabled
|
||||||
// but the page we navigate to does (because it set the state).
|
// but the page we navigate to does (because it set the state).
|
||||||
// Do a full page refresh to reload the client-side router from the new page.
|
// Do a full page refresh to reload the client-side router from the new page.
|
||||||
// Scroll restauration will then happen during the reload when the router's code is re-executed
|
// Scroll restauration will then happen during the reload when the router's code is re-executed
|
||||||
history.scrollRestoration && (history.scrollRestoration = 'manual');
|
if (history.scrollRestoration) {
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
location.reload();
|
location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -433,13 +474,14 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = history.state;
|
const state: State = history.state;
|
||||||
|
if (state.intraPage) {
|
||||||
|
// this is non transition intra-page scrolling
|
||||||
|
scrollTo(state.scrollX, state.scrollY);
|
||||||
|
} else {
|
||||||
const nextIndex = state.index;
|
const nextIndex = state.index;
|
||||||
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
||||||
currentHistoryIndex = nextIndex;
|
currentHistoryIndex = nextIndex;
|
||||||
if (state.scrollY < 0) {
|
transition(direction, new URL(location.href), state);
|
||||||
scrollTo(state.scrollX, -(state.scrollY + 1));
|
|
||||||
} else {
|
|
||||||
navigate(direction, new URL(location.href), state);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -461,6 +503,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
{ passive: true, capture: true }
|
{ passive: true, capture: true }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener('load', onPageLoad);
|
addEventListener('load', onPageLoad);
|
||||||
// There's not a good way to record scroll position before a back button.
|
// There's not a good way to record scroll position before a back button.
|
||||||
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
||||||
|
@ -470,5 +513,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
|
|
||||||
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
||||||
else addEventListener('scroll', throttle(updateState, 300));
|
else addEventListener('scroll', throttle(updateState, 300));
|
||||||
|
|
||||||
|
markScriptsExec();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { getColor, testFactory } from './test-utils.js';
|
import { testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({ root: './fixtures/astro-component/' });
|
const test = testFactory({ root: './fixtures/astro-component/' });
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ test.describe('Astro component HMR', () => {
|
||||||
test('update linked dep Astro style', async ({ page, astro }) => {
|
test('update linked dep Astro style', async ({ page, astro }) => {
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
let h1 = page.locator('#astro-linked-lib');
|
let h1 = page.locator('#astro-linked-lib');
|
||||||
expect(await getColor(h1)).toBe('rgb(255, 0, 0)');
|
await expect(h1).toHaveCSS('color', 'rgb(255, 0, 0)');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForLoadState('networkidle'),
|
page.waitForLoadState('networkidle'),
|
||||||
await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) =>
|
await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) =>
|
||||||
|
@ -107,6 +107,6 @@ test.describe('Astro component HMR', () => {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
h1 = page.locator('#astro-linked-lib');
|
h1 = page.locator('#astro-linked-lib');
|
||||||
expect(await getColor(h1)).toBe('rgb(0, 128, 0)');
|
await expect(h1).toHaveCSS('color', 'rgb(0, 128, 0)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { getColor, testFactory } from './test-utils.js';
|
import { testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
root: './fixtures/css/',
|
root: './fixtures/css/',
|
||||||
|
@ -20,13 +20,13 @@ test.describe('CSS HMR', () => {
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
const h = page.locator('h1');
|
const h = page.locator('h1');
|
||||||
expect(await getColor(h)).toBe('rgb(255, 0, 0)');
|
await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
|
||||||
|
|
||||||
await astro.editFile('./src/styles/main.css', (original) =>
|
await astro.editFile('./src/styles/main.css', (original) =>
|
||||||
original.replace('--h1-color: red;', '--h1-color: green;')
|
original.replace('--h1-color: red;', '--h1-color: green;')
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await getColor(h)).toBe('rgb(0, 128, 0)');
|
await expect(h).toHaveCSS('color', 'rgb(0, 128, 0)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
|
test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@e2e/invalidate-script-deps",
|
"name": "@e2e/hmr",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "workspace:*"
|
"astro": "workspace:*",
|
||||||
|
"sass": "^1.66.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
14
packages/astro/e2e/fixtures/hmr/src/pages/css-dep.astro
Normal file
14
packages/astro/e2e/fixtures/hmr/src/pages/css-dep.astro
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 class="css-dep">This is blue</h1>
|
||||||
|
<style lang="scss">
|
||||||
|
@use "../styles/vars.scss" as *;
|
||||||
|
.css-dep {
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
packages/astro/e2e/fixtures/hmr/src/styles/vars.scss
Normal file
1
packages/astro/e2e/fixtures/hmr/src/styles/vars.scss
Normal file
|
@ -0,0 +1 @@
|
||||||
|
$color: blue;
|
|
@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
|
||||||
import { testFactory } from './test-utils.js';
|
import { testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({
|
const test = testFactory({
|
||||||
root: './fixtures/invalidate-script-deps/',
|
root: './fixtures/hmr/',
|
||||||
});
|
});
|
||||||
|
|
||||||
let devServer;
|
let devServer;
|
||||||
|
@ -17,7 +17,7 @@ test.afterAll(async () => {
|
||||||
|
|
||||||
test.describe('Scripts with dependencies', () => {
|
test.describe('Scripts with dependencies', () => {
|
||||||
test('refresh with HMR', async ({ page, astro }) => {
|
test('refresh with HMR', async ({ page, astro }) => {
|
||||||
await page.goto(astro.resolveUrl('/'));
|
await page.goto(astro.resolveUrl('/script-dep'));
|
||||||
|
|
||||||
const h = page.locator('h1');
|
const h = page.locator('h1');
|
||||||
await expect(h, 'original text set').toHaveText('before');
|
await expect(h, 'original text set').toHaveText('before');
|
||||||
|
@ -29,3 +29,16 @@ test.describe('Scripts with dependencies', () => {
|
||||||
await expect(h, 'text changed').toHaveText('after');
|
await expect(h, 'text changed').toHaveText('after');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Styles with dependencies', () => {
|
||||||
|
test('refresh with HMR', async ({ page, astro }) => {
|
||||||
|
await page.goto(astro.resolveUrl('/css-dep'));
|
||||||
|
|
||||||
|
const h = page.locator('h1');
|
||||||
|
await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)');
|
||||||
|
|
||||||
|
await astro.editFile('./src/styles/vars.scss', (original) => original.replace('blue', 'red'));
|
||||||
|
|
||||||
|
await expect(h).toHaveCSS('color', 'rgb(255, 0, 0)');
|
||||||
|
});
|
||||||
|
});
|
|
@ -71,13 +71,6 @@ export async function getErrorOverlayContent(page) {
|
||||||
return { message, hint, absoluteFileLocation, fileLocation };
|
return { message, hint, absoluteFileLocation, fileLocation };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
export async function getColor(el) {
|
|
||||||
return await el.evaluate((e) => getComputedStyle(e).color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for `astro-island` that contains the `el` to hydrate
|
* Wait for `astro-island` that contains the `el` to hydrate
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
|
|
|
@ -293,12 +293,12 @@ test.describe('View Transitions', () => {
|
||||||
locator = page.locator('#click-one-again');
|
locator = page.locator('#click-one-again');
|
||||||
await expect(locator).toBeInViewport();
|
await expect(locator).toBeInViewport();
|
||||||
|
|
||||||
// Scroll up to top fragment
|
// goto page 1
|
||||||
await page.click('#click-one-again');
|
await page.click('#click-one-again');
|
||||||
locator = page.locator('#one');
|
locator = page.locator('#one');
|
||||||
await expect(locator).toHaveText('Page 1');
|
await expect(locator).toHaveText('Page 1');
|
||||||
|
|
||||||
// Back to middle of the page
|
// Back to middle of the previous page
|
||||||
await page.goBack();
|
await page.goBack();
|
||||||
locator = page.locator('#click-one-again');
|
locator = page.locator('#click-one-again');
|
||||||
await expect(locator).toBeInViewport();
|
await expect(locator).toBeInViewport();
|
||||||
|
|
|
@ -29,8 +29,11 @@ const qualityTable: Record<
|
||||||
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
|
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRotationForEXIF(inputBuffer: Buffer): Promise<Operation | undefined> {
|
async function getRotationForEXIF(
|
||||||
const meta = await imageMetadata(inputBuffer);
|
inputBuffer: Buffer,
|
||||||
|
src?: string
|
||||||
|
): Promise<Operation | undefined> {
|
||||||
|
const meta = await imageMetadata(inputBuffer, src);
|
||||||
if (!meta) return undefined;
|
if (!meta) return undefined;
|
||||||
|
|
||||||
// EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list.
|
// EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list.
|
||||||
|
@ -63,7 +66,7 @@ const service: LocalImageService = {
|
||||||
|
|
||||||
const operations: Operation[] = [];
|
const operations: Operation[] = [];
|
||||||
|
|
||||||
const rotation = await getRotationForEXIF(inputBuffer);
|
const rotation = await getRotationForEXIF(inputBuffer, transform.src);
|
||||||
|
|
||||||
if (rotation) {
|
if (rotation) {
|
||||||
operations.push(rotation);
|
operations.push(rotation);
|
||||||
|
|
|
@ -22,11 +22,7 @@ export async function emitESMImage(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileMetadata = await imageMetadata(fileData);
|
const fileMetadata = await imageMetadata(fileData, id);
|
||||||
|
|
||||||
if (!fileMetadata) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emittedImage: ImageMetadata = {
|
const emittedImage: ImageMetadata = {
|
||||||
src: '',
|
src: '',
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
import probe from 'probe-image-size';
|
import probe from 'probe-image-size';
|
||||||
|
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||||
import type { ImageInputFormat, ImageMetadata } from '../types.js';
|
import type { ImageInputFormat, ImageMetadata } from '../types.js';
|
||||||
|
|
||||||
export async function imageMetadata(data: Buffer): Promise<Omit<ImageMetadata, 'src'> | undefined> {
|
export async function imageMetadata(
|
||||||
|
data: Buffer,
|
||||||
|
src?: string
|
||||||
|
): Promise<Omit<ImageMetadata, 'src'>> {
|
||||||
const result = probe.sync(data);
|
const result = probe.sync(data);
|
||||||
|
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
throw new Error('Failed to probe image size.');
|
throw new AstroError({
|
||||||
|
...AstroErrorData.NoImageMetadata,
|
||||||
|
message: AstroErrorData.NoImageMetadata.message(src),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width, height, type, orientation } = result;
|
const { width, height, type, orientation } = result;
|
||||||
const isPortrait = (orientation || 0) >= 5;
|
const isPortrait = (orientation || 0) >= 5;
|
||||||
|
|
||||||
if (!width || !height || !type) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: isPortrait ? height : width,
|
width: isPortrait ? height : width,
|
||||||
height: isPortrait ? width : height,
|
height: isPortrait ? width : height,
|
||||||
|
|
|
@ -2,6 +2,8 @@ import MagicString from 'magic-string';
|
||||||
import type * as vite from 'vite';
|
import type * as vite from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
|
import type { AstroPluginOptions, ImageTransform } from '../@types/astro.js';
|
||||||
|
import { extendManualChunks } from '../core/build/plugins/util.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import {
|
import {
|
||||||
appendForwardSlash,
|
appendForwardSlash,
|
||||||
joinPaths,
|
joinPaths,
|
||||||
|
@ -28,6 +30,18 @@ export default function assets({
|
||||||
// Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
|
// Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev
|
||||||
{
|
{
|
||||||
name: 'astro:assets',
|
name: 'astro:assets',
|
||||||
|
outputOptions(outputOptions) {
|
||||||
|
// Specifically split out chunk for asset files to prevent TLA deadlock
|
||||||
|
// caused by `getImage()` for markdown components.
|
||||||
|
// https://github.com/rollup/rollup/issues/4708
|
||||||
|
extendManualChunks(outputOptions, {
|
||||||
|
after(id) {
|
||||||
|
if (id.includes('astro/dist/assets/services/')) {
|
||||||
|
return `astro-assets-services`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
async resolveId(id) {
|
async resolveId(id) {
|
||||||
if (id === VIRTUAL_SERVICE_ID) {
|
if (id === VIRTUAL_SERVICE_ID) {
|
||||||
return await this.resolve(settings.config.image.service.entrypoint);
|
return await this.resolve(settings.config.image.service.entrypoint);
|
||||||
|
@ -125,6 +139,14 @@ export default function assets({
|
||||||
}
|
}
|
||||||
if (assetRegex.test(id)) {
|
if (assetRegex.test(id)) {
|
||||||
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
|
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
throw new AstroError({
|
||||||
|
...AstroErrorData.ImageNotFound,
|
||||||
|
message: AstroErrorData.ImageNotFound.message(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return `export default ${JSON.stringify(meta)}`;
|
return `export default ${JSON.stringify(meta)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -620,8 +620,42 @@ export const ExpectedImageOptions = {
|
||||||
message: (options: string) =>
|
message: (options: string) =>
|
||||||
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
|
`Expected getImage() parameter to be an object. Received \`${options}\`.`,
|
||||||
} satisfies ErrorData;
|
} satisfies ErrorData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
|
* @see
|
||||||
|
* - [Images](https://docs.astro.build/en/guides/images/)
|
||||||
|
* @description
|
||||||
|
* Astro could not find an image you imported. Often, this is simply caused by a typo in the path.
|
||||||
|
*
|
||||||
|
* Images in Markdown are relative to the current file. To refer to an image that is located in the same folder as the `.md` file, the path should start with `./`
|
||||||
|
*/
|
||||||
|
export const ImageNotFound = {
|
||||||
|
name: 'ImageNotFound',
|
||||||
|
title: 'Image not found.',
|
||||||
|
message: (imagePath: string) => `Could not find requested image \`${imagePath}\`. Does it exist?`,
|
||||||
|
hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
|
||||||
|
} satisfies ErrorData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @message Could not process image metadata for `IMAGE_PATH`.
|
||||||
|
* @see
|
||||||
|
* - [Images](https://docs.astro.build/en/guides/images/)
|
||||||
|
* @description
|
||||||
|
* Astro could not process the metadata of an image you imported. This is often caused by a corrupted or malformed image and re-exporting the image from your image editor may fix this issue.
|
||||||
|
*/
|
||||||
|
export const NoImageMetadata = {
|
||||||
|
name: 'NoImageMetadata',
|
||||||
|
title: 'Could not process image metadata.',
|
||||||
|
message: (imagePath: string | undefined) =>
|
||||||
|
`Could not process image metadata${imagePath ? ' for `${imagePath}`' : ''}.`,
|
||||||
|
hint: 'This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue.',
|
||||||
|
} satisfies ErrorData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @deprecated This error is no longer Markdown specific and as such, as been replaced by `ImageNotFound`
|
||||||
* @message
|
* @message
|
||||||
* Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`.
|
* Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`.
|
||||||
* @see
|
* @see
|
||||||
|
@ -640,6 +674,7 @@ export const MarkdownImageNotFound = {
|
||||||
}`,
|
}`,
|
||||||
hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
|
hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
|
||||||
} satisfies ErrorData;
|
} satisfies ErrorData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @description
|
* @description
|
||||||
|
|
|
@ -90,7 +90,7 @@ export async function handleHotUpdate(
|
||||||
|
|
||||||
// Bugfix: sometimes style URLs get normalized and end with `lang.css=`
|
// Bugfix: sometimes style URLs get normalized and end with `lang.css=`
|
||||||
// These will cause full reloads, so filter them out here
|
// These will cause full reloads, so filter them out here
|
||||||
const mods = ctx.modules.filter((m) => !m.url.endsWith('='));
|
const mods = [...filtered].filter((m) => !m.url.endsWith('='));
|
||||||
const file = ctx.file.replace(config.root.pathname, '/');
|
const file = ctx.file.replace(config.root.pathname, '/');
|
||||||
|
|
||||||
// If only styles are changed, remove the component file from the update list
|
// If only styles are changed, remove the component file from the update list
|
||||||
|
@ -109,17 +109,6 @@ export async function handleHotUpdate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a module that is imported from a <script>, invalidate the Astro
|
|
||||||
// component so that it is cached by the time the script gets transformed.
|
|
||||||
for (const mod of filtered) {
|
|
||||||
if (mod.id && isAstroScript(mod.id) && mod.file) {
|
|
||||||
const astroMod = ctx.server.moduleGraph.getModuleById(mod.file);
|
|
||||||
if (astroMod) {
|
|
||||||
mods.unshift(astroMod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Svelte files should be marked as `isSelfAccepting` but they don't appear to be
|
// TODO: Svelte files should be marked as `isSelfAccepting` but they don't appear to be
|
||||||
const isSelfAccepting = mods.every((m) => m.isSelfAccepting || m.url.endsWith('.svelte'));
|
const isSelfAccepting = mods.every((m) => m.isSelfAccepting || m.url.endsWith('.svelte'));
|
||||||
if (isSelfAccepting) {
|
if (isSelfAccepting) {
|
||||||
|
|
|
@ -12,7 +12,8 @@ import { normalizePath } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro.js';
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
|
||||||
import type { Logger } from '../core/logger/core.js';
|
import type { Logger } from '../core/logger/core.js';
|
||||||
import { isMarkdownFile, rootRelativePath } from '../core/util.js';
|
import { isMarkdownFile } from '../core/util.js';
|
||||||
|
import { shorthash } from '../runtime/server/shorthash.js';
|
||||||
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
|
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
|
||||||
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
|
|
||||||
|
@ -92,12 +93,13 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
|
||||||
const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata;
|
const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata;
|
||||||
|
|
||||||
// Resolve all the extracted images from the content
|
// Resolve all the extracted images from the content
|
||||||
const imagePaths: { raw: string; resolved: string }[] = [];
|
const imagePaths: { raw: string; resolved: string; safeName: string }[] = [];
|
||||||
for (const imagePath of rawImagePaths.values()) {
|
for (const imagePath of rawImagePaths.values()) {
|
||||||
imagePaths.push({
|
imagePaths.push({
|
||||||
raw: imagePath,
|
raw: imagePath,
|
||||||
resolved:
|
resolved:
|
||||||
(await this.resolve(imagePath, id))?.id ?? path.join(path.dirname(id), imagePath),
|
(await this.resolve(imagePath, id))?.id ?? path.join(path.dirname(id), imagePath),
|
||||||
|
safeName: shorthash(imagePath),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,39 +120,28 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
|
||||||
|
|
||||||
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
|
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
|
||||||
import { getImage } from "astro:assets";
|
import { getImage } from "astro:assets";
|
||||||
|
${imagePaths.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)}
|
||||||
|
|
||||||
export const images = {
|
const images = async function() {
|
||||||
${imagePaths.map(
|
return {
|
||||||
(entry) =>
|
${imagePaths
|
||||||
`'${entry.raw}': await getImageSafely((await import("${entry.raw}")).default, "${
|
.map((entry) => `"${entry.raw}": await getImage({src: Astro__${entry.safeName}})`)
|
||||||
entry.raw
|
.join('\n')}
|
||||||
}", "${rootRelativePath(settings.config.root, entry.resolved)}")`
|
}
|
||||||
)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageSafely(imageSrc, imagePath, resolvedImagePath) {
|
async function updateImageReferences(html) {
|
||||||
if (!imageSrc) {
|
return images().then((images) => {
|
||||||
throw new AstroError({
|
return html.replaceAll(/__ASTRO_IMAGE_="([^"]+)"/gm, (full, imagePath) =>
|
||||||
...AstroErrorData.MarkdownImageNotFound,
|
spreadAttributes({
|
||||||
message: AstroErrorData.MarkdownImageNotFound.message(
|
src: images[imagePath].src,
|
||||||
imagePath,
|
...images[imagePath].attributes,
|
||||||
resolvedImagePath
|
})
|
||||||
),
|
);
|
||||||
location: { file: "${id}" },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getImage({src: imageSrc})
|
const html = await updateImageReferences(${JSON.stringify(html)});
|
||||||
}
|
|
||||||
|
|
||||||
function updateImageReferences(html) {
|
|
||||||
return html.replaceAll(
|
|
||||||
/__ASTRO_IMAGE_="([^"]+)"/gm,
|
|
||||||
(full, imagePath) => spreadAttributes({src: images[imagePath].src, ...images[imagePath].attributes})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = updateImageReferences(${JSON.stringify(html)});
|
|
||||||
|
|
||||||
export const frontmatter = ${JSON.stringify(frontmatter)};
|
export const frontmatter = ${JSON.stringify(frontmatter)};
|
||||||
export const file = ${JSON.stringify(fileId)};
|
export const file = ${JSON.stringify(fileId)};
|
||||||
|
|
|
@ -992,6 +992,15 @@ importers:
|
||||||
specifier: ^3.3.4
|
specifier: ^3.3.4
|
||||||
version: 3.3.4
|
version: 3.3.4
|
||||||
|
|
||||||
|
packages/astro/e2e/fixtures/hmr:
|
||||||
|
devDependencies:
|
||||||
|
astro:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../..
|
||||||
|
sass:
|
||||||
|
specifier: ^1.66.1
|
||||||
|
version: 1.66.1
|
||||||
|
|
||||||
packages/astro/e2e/fixtures/hydration-race:
|
packages/astro/e2e/fixtures/hydration-race:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/preact':
|
'@astrojs/preact':
|
||||||
|
@ -1004,12 +1013,6 @@ importers:
|
||||||
specifier: ^10.17.1
|
specifier: ^10.17.1
|
||||||
version: 10.17.1
|
version: 10.17.1
|
||||||
|
|
||||||
packages/astro/e2e/fixtures/invalidate-script-deps:
|
|
||||||
devDependencies:
|
|
||||||
astro:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../..
|
|
||||||
|
|
||||||
packages/astro/e2e/fixtures/lit-component:
|
packages/astro/e2e/fixtures/lit-component:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/lit':
|
'@astrojs/lit':
|
||||||
|
|
Loading…
Reference in a new issue