update prettier width (#2968)

This commit is contained in:
Fred K. Schott 2022-04-02 14:15:41 -06:00 committed by GitHub
parent d63213f119
commit 1335797903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
146 changed files with 2134 additions and 483 deletions

View file

@ -1,5 +1,5 @@
{ {
"printWidth": 180, "printWidth": 100,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 2,

View file

@ -1,4 +1,9 @@
{ {
"recommendations": ["astro-build.astro-vscode", "esbenp.prettier-vscode", "editorconfig.editorconfig", "dbaeumer.vscode-eslint"], "recommendations": [
"astro-build.astro-vscode",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
"unwantedRecommendations": [] "unwantedRecommendations": []
} }

View file

@ -1,8 +1,10 @@
:root { :root {
--font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; --font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
Apple Color Emoji, Segoe UI Emoji;
--font-body: 'IBM Plex Sans', var(--font-fallback); --font-body: 'IBM Plex Sans', var(--font-fallback);
--font-mono: 'IBM Plex Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', --font-mono: 'IBM Plex Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console',
'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
--color-white: #fff; --color-white: #fff;
--color-black: #000014; --color-black: #000014;

View file

@ -6,8 +6,19 @@ import { KNOWN_LANGUAGES, langPathRegex } from '../../languages';
const LanguageSelect: FunctionalComponent<{ lang: string }> = ({ lang }) => { const LanguageSelect: FunctionalComponent<{ lang: string }> = ({ lang }) => {
return ( return (
<div class="language-select-wrapper"> <div class="language-select-wrapper">
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88.6 77.3" height="1.2em" width="1.2em"> <svg
<path fill="currentColor" d="M61,24.6h7.9l18.7,51.6h-7.7l-5.4-15.5H54.3l-5.6,15.5h-7.2L61,24.6z M72.6,55l-8-22.8L56.3,55H72.6z" /> aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 88.6 77.3"
height="1.2em"
width="1.2em"
>
<path
fill="currentColor"
d="M61,24.6h7.9l18.7,51.6h-7.7l-5.4-15.5H54.3l-5.6,15.5h-7.2L61,24.6z M72.6,55l-8-22.8L56.3,55H72.6z"
/>
<path <path
fill="currentColor" fill="currentColor"
d="M53.6,60.6c-10-4-16-9-22-14c0,0,1.3,1.3,0,0c-6,5-20,13-20,13l-4-6c8-5,10-6,19-13c-2.1-1.9-12-13-13-19h8 c4,9,10,14,10,14c10-8,10-19,10-19h8c0,0-1,13-12,24l0,0c5,5,10,9,19,13L53.6,60.6z M1.6,16.6h56v-8h-23v-7h-9v7h-24V16.6z" d="M53.6,60.6c-10-4-16-9-22-14c0,0,1.3,1.3,0,0c-6,5-20,13-20,13l-4-6c8-5,10-6,19-13c-2.1-1.9-12-13-13-19h8 c4,9,10,14,10,14c10-8,10-19,10-19h8c0,0-1,13-12,24l0,0c5,5,10,9,19,13L53.6,60.6z M1.6,16.6h56v-8h-23v-7h-9v7h-24V16.6z"

View file

@ -41,7 +41,13 @@ export default function Search() {
<> <>
<button type="button" ref={searchButtonRef} onClick={onOpen} className="search-input"> <button type="button" ref={searchButtonRef} onClick={onOpen} className="search-input">
<svg width="24" height="24" fill="none"> <svg width="24" height="24" fill="none">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
<span>Search</span> <span>Search</span>
<span className="search-hint"> <span className="search-hint">

View file

@ -15,9 +15,26 @@ const MenuToggle: FunctionalComponent = () => {
}, [sidebarShown]); }, [sidebarShown]);
return ( return (
<button type="button" aria-pressed={sidebarShown ? 'true' : 'false'} id="menu-toggle" onClick={() => setSidebarShown(!sidebarShown)}> <button
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="none" viewBox="0 0 24 24" stroke="currentColor"> type="button"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> aria-pressed={sidebarShown ? 'true' : 'false'}
id="menu-toggle"
onClick={() => setSidebarShown(!sidebarShown)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
<span className="sr-only">Toggle sidebar</span> <span className="sr-only">Toggle sidebar</span>
</button> </button>

View file

@ -33,7 +33,11 @@ const TableOfContents: FunctionalComponent<{ headers: any[] }> = ({ headers = []
{headers {headers
.filter(({ depth }) => depth > 1 && depth < 4) .filter(({ depth }) => depth > 1 && depth < 4)
.map((header) => ( .map((header) => (
<li class={`header-link depth-${header.depth} ${activeId === header.slug ? 'active' : ''}`.trim()}> <li
class={`header-link depth-${header.depth} ${
activeId === header.slug ? 'active' : ''
}`.trim()}
>
<a href={`#${header.slug}`}>{header.text}</a> <a href={`#${header.slug}`}>{header.text}</a>
</li> </li>
))} ))}

View file

@ -6,14 +6,26 @@ import './ThemeToggleButton.css';
const themes = ['light', 'dark']; const themes = ['light', 'dark'];
const icons = [ const icons = [
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> <svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
>
<path <path
fillRule="evenodd" fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd" clipRule="evenodd"
/> />
</svg>, </svg>,
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor"> <svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>, </svg>,
]; ];

View file

@ -7,7 +7,9 @@ export const SITE = {
export const OPEN_GRAPH = { export const OPEN_GRAPH = {
image: { image: {
src: 'https://github.com/withastro/astro/blob/main/assets/social/banner.jpg?raw=true', src: 'https://github.com/withastro/astro/blob/main/assets/social/banner.jpg?raw=true',
alt: 'astro logo on a starry expanse of space,' + ' with a purple saturn-like planet floating in the right foreground', alt:
'astro logo on a starry expanse of space,' +
' with a purple saturn-like planet floating in the right foreground',
}, },
twitter: 'astrodotbuild', twitter: 'astrodotbuild',
}; };

View file

@ -1,8 +1,10 @@
:root { :root {
--font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; --font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
Apple Color Emoji, Segoe UI Emoji;
--font-body: system-ui, var(--font-fallback); --font-body: system-ui, var(--font-fallback);
--font-mono: 'IBM Plex Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', --font-mono: 'IBM Plex Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console',
'Liberation Mono', 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
/* /*
* Variables with --color-base prefix define * Variables with --color-base prefix define

View file

@ -24,7 +24,13 @@ function Nav() {
</svg> </svg>
</a> </a>
<a className={Styles.social} href="https://dev.to/me"> <a className={Styles.social} href="https://dev.to/me">
<svg className={Styles.socialicon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 40" style="enable-background:new 0 0 50 40" xmlSpace="preserve"> <svg
className={Styles.socialicon}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 40"
style="enable-background:new 0 0 50 40"
xmlSpace="preserve"
>
<path d="M15.7 15.5c-.4-.3-.7-.4-1.1-.4h-1.7v10.1h1.7c.4 0 .8-.1 1.1-.4.4-.3.6-.7.6-1.3v-6.7c0-.6-.2-1-.6-1.3z" /> <path d="M15.7 15.5c-.4-.3-.7-.4-1.1-.4h-1.7v10.1h1.7c.4 0 .8-.1 1.1-.4.4-.3.6-.7.6-1.3v-6.7c0-.6-.2-1-.6-1.3z" />
<path d="M47 0H3C1.3 0 0 1.3 0 3v34c0 1.7 1.3 3 3 3h44c1.7 0 3-1.3 3-3V3c0-1.7-1.3-3-3-3zM19.1 23.5c0 1.3-.4 2.4-1.3 3.2-.8.9-1.9 1.3-3.3 1.3h-4.4V12.3h4.5c1.3 0 2.4.4 3.2 1.3.8.8 1.3 1.9 1.3 3.2v6.7zm9.1-8.4h-5.1v3.6h3.1v2.8h-3.1v3.7h5.1V28h-5.9c-.6 0-1-.2-1.4-.6-.4-.4-.6-.8-.6-1.4V14.2c0-.6.2-1 .6-1.4.4-.4.8-.6 1.4-.6h5.9v2.9zM37.5 26c-.6 1.3-1.3 2-2.2 2-.9 0-1.7-.7-2.2-2l-3.7-13.8h3.1L35.3 23l2.8-10.8h3.1L37.5 26z" /> <path d="M47 0H3C1.3 0 0 1.3 0 3v34c0 1.7 1.3 3 3 3h44c1.7 0 3-1.3 3-3V3c0-1.7-1.3-3-3-3zM19.1 23.5c0 1.3-.4 2.4-1.3 3.2-.8.9-1.9 1.3-3.3 1.3h-4.4V12.3h4.5c1.3 0 2.4.4 3.2 1.3.8.8 1.3 1.9 1.3 3.2v6.7zm9.1-8.4h-5.1v3.6h3.1v2.8h-3.1v3.7h5.1V28h-5.9c-.6 0-1-.2-1.4-.6-.4-.4-.6-.8-.6-1.4V14.2c0-.6.2-1 .6-1.4.4-.4.8-.6 1.4-.6h5.9v2.9zM37.5 26c-.6 1.3-1.3 2-2.2 2-.9 0-1.7-.7-2.2-2l-3.7-13.8h3.1L35.3 23l2.8-10.8h3.1L37.5 26z" />
</svg> </svg>

View file

@ -21,7 +21,11 @@ function getOrigin(request: Request): string {
return new URL(request.url).origin.replace('localhost', '127.0.0.1'); return new URL(request.url).origin.replace('localhost', '127.0.0.1');
} }
async function get<T>(incomingReq: Request, endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> { async function get<T>(
incomingReq: Request,
endpoint: string,
cb: (response: Response) => Promise<T>
): Promise<T> {
const response = await fetch(`${getOrigin(incomingReq)}${endpoint}`, { const response = await fetch(`${getOrigin(incomingReq)}${endpoint}`, {
credentials: 'same-origin', credentials: 'same-origin',
}); });

View file

@ -4,7 +4,8 @@
} }
:root { :root {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
Apple Color Emoji, Segoe UI Emoji;
font-size: 1rem; font-size: 1rem;
--user-font-scale: 1rem - 16px; --user-font-scale: 1rem - 16px;
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem); font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);

View file

@ -1,6 +1,7 @@
:root { :root {
--font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', --font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter',
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco,
'Courier New', Courier, monospace;
--color-light: #f3f4f6; --color-light: #f3f4f6;
} }

View file

@ -6,6 +6,11 @@ export default defineConfig({
// Enable Custom Markdown options, plugins, etc. // Enable Custom Markdown options, plugins, etc.
markdown: { markdown: {
remarkPlugins: ['remark-code-titles'], remarkPlugins: ['remark-code-titles'],
rehypePlugins: [['rehype-autolink-headings', { behavior: 'prepend' }], ['rehype-toc', { headings: ['h2', 'h3'] }], [addClasses, { 'h1,h2,h3': 'title' }], 'rehype-slug'], rehypePlugins: [
['rehype-autolink-headings', { behavior: 'prepend' }],
['rehype-toc', { headings: ['h2', 'h3'] }],
[addClasses, { 'h1,h2,h3': 'title' }],
'rehype-slug',
],
}, },
}); });

View file

@ -4,7 +4,8 @@
} }
:root { :root {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
Apple Color Emoji, Segoe UI Emoji;
font-size: 1rem; font-size: 1rem;
--user-font-scale: 1rem - 16px; --user-font-scale: 1rem - 16px;
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem); font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);

View file

@ -1,6 +1,7 @@
:root { :root {
--font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', --font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter',
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco,
'Courier New', Courier, monospace;
--color-light: #f3f4f6; --color-light: #f3f4f6;
} }

View file

@ -8,7 +8,9 @@ export function addAstro(Prism) {
scriptLang = 'typescript'; scriptLang = 'typescript';
} else { } else {
scriptLang = 'javascript'; scriptLang = 'javascript';
console.warn('Prism TypeScript language not loaded, Astro scripts will be treated as JavaScript.'); console.warn(
'Prism TypeScript language not loaded, Astro scripts will be treated as JavaScript.'
);
} }
let script = Prism.util.clone(Prism.languages[scriptLang]); let script = Prism.util.clone(Prism.languages[scriptLang]);
@ -38,10 +40,14 @@ export function addAstro(Prism) {
spread = re(spread).source; spread = re(spread).source;
Prism.languages.astro = Prism.languages.extend('markup', script); Prism.languages.astro = Prism.languages.extend('markup', script);
Prism.languages.astro.tag.pattern = re(/<\/?(?:[\w.:-]+(?:<S>+(?:[\w.:$-]+(?:=(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s{'"/>=]+|<BRACES>))?|<SPREAD>))*<S>*\/?)?>/.source); Prism.languages.astro.tag.pattern = re(
/<\/?(?:[\w.:-]+(?:<S>+(?:[\w.:$-]+(?:=(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s{'"/>=]+|<BRACES>))?|<SPREAD>))*<S>*\/?)?>/
.source
);
Prism.languages.astro.tag.inside['tag'].pattern = /^<\/?[^\s>\/]*/i; Prism.languages.astro.tag.inside['tag'].pattern = /^<\/?[^\s>\/]*/i;
Prism.languages.astro.tag.inside['attr-value'].pattern = /=(?!\{)(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s'">]+)/i; Prism.languages.astro.tag.inside['attr-value'].pattern =
/=(?!\{)(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s'">]+)/i;
Prism.languages.astro.tag.inside['tag'].inside['class-name'] = /^[A-Z]\w*(?:\.[A-Z]\w*)*$/; Prism.languages.astro.tag.inside['tag'].inside['class-name'] = /^[A-Z]\w*(?:\.[A-Z]\w*)*$/;
Prism.languages.astro.tag.inside['comment'] = script['comment']; Prism.languages.astro.tag.inside['comment'] = script['comment'];
@ -109,7 +115,11 @@ export function addAstro(Prism) {
if (token.content[0].content[0].content === '</') { if (token.content[0].content[0].content === '</') {
// Closing tag // Closing tag
if (openedTags.length > 0 && openedTags[openedTags.length - 1].tagName === stringifyToken(token.content[0].content[1])) { if (
openedTags.length > 0 &&
openedTags[openedTags.length - 1].tagName ===
stringifyToken(token.content[0].content[1])
) {
// Pop matching opening tag // Pop matching opening tag
openedTags.pop(); openedTags.pop();
} }
@ -127,7 +137,12 @@ export function addAstro(Prism) {
} else if (openedTags.length > 0 && token.type === 'punctuation' && token.content === '{') { } else if (openedTags.length > 0 && token.type === 'punctuation' && token.content === '{') {
// Here we might have entered a Astro context inside a tag // Here we might have entered a Astro context inside a tag
openedTags[openedTags.length - 1].openedBraces++; openedTags[openedTags.length - 1].openedBraces++;
} else if (openedTags.length > 0 && openedTags[openedTags.length - 1].openedBraces > 0 && token.type === 'punctuation' && token.content === '}') { } else if (
openedTags.length > 0 &&
openedTags[openedTags.length - 1].openedBraces > 0 &&
token.type === 'punctuation' &&
token.content === '}'
) {
// Here we might have left a Astro context inside a tag // Here we might have left a Astro context inside a tag
openedTags[openedTags.length - 1].openedBraces--; openedTags[openedTags.length - 1].openedBraces--;
} else { } else {
@ -141,7 +156,10 @@ export function addAstro(Prism) {
let plainText = stringifyToken(token); let plainText = stringifyToken(token);
// And merge text with adjacent text // And merge text with adjacent text
if (i < tokens.length - 1 && (typeof tokens[i + 1] === 'string' || tokens[i + 1].type === 'plain-text')) { if (
i < tokens.length - 1 &&
(typeof tokens[i + 1] === 'string' || tokens[i + 1].type === 'plain-text')
) {
plainText += stringifyToken(tokens[i + 1]); plainText += stringifyToken(tokens[i + 1]);
tokens.splice(i + 1, 1); tokens.splice(i + 1, 1);
} }

View file

@ -9,7 +9,8 @@
const CI_INSTRUCTIONS = { const CI_INSTRUCTIONS = {
NETLIFY: 'https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript', NETLIFY: 'https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript',
GITHUB_ACTIONS: 'https://docs.github.com/en/actions/guides/building-and-testing-nodejs#specifying-the-nodejs-version', GITHUB_ACTIONS:
'https://docs.github.com/en/actions/guides/building-and-testing-nodejs#specifying-the-nodejs-version',
VERCEL: 'https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-version', VERCEL: 'https://vercel.com/docs/runtimes#official-runtimes/node-js/node-js-version',
}; };
@ -17,7 +18,10 @@ const CI_INSTRUCTIONS = {
async function main() { async function main() {
// Check for ESM support. // Check for ESM support.
// Load the "supports-esm" package in an way that works in both ESM & CJS. // Load the "supports-esm" package in an way that works in both ESM & CJS.
let supportsESM = typeof require !== 'undefined' ? require('supports-esm') : (await import('supports-esm')).default; let supportsESM =
typeof require !== 'undefined'
? require('supports-esm')
: (await import('supports-esm')).default;
// Check for CJS->ESM named export support. // Check for CJS->ESM named export support.
// "path-to-regexp" is a real-world package that we depend on, that only // "path-to-regexp" is a real-world package that we depend on, that only
@ -79,7 +83,9 @@ Please upgrade Node.js to a supported version: "${engines}"\n`);
break; break;
} }
} }
console.log(`${ci.name} CI Environment Detected!\nAdditional steps may be needed to set your Node.js version:`); console.log(
`${ci.name} CI Environment Detected!\nAdditional steps may be needed to set your Node.js version:`
);
console.log(`Documentation: https://docs.astro.build/guides/deploy`); console.log(`Documentation: https://docs.astro.build/guides/deploy`);
if (CI_INSTRUCTIONS[platform]) { if (CI_INSTRUCTIONS[platform]) {
console.log(`${ci.name} Documentation: ${CI_INSTRUCTIONS[platform]}`); console.log(`${ci.name} Documentation: ${CI_INSTRUCTIONS[platform]}`);

View file

@ -56,7 +56,10 @@ export interface AstroGlobal extends AstroGlobalPartial {
/** get information about this page */ /** get information about this page */
request: Request; request: Request;
/** see if slots are used */ /** see if slots are used */
slots: Record<string, true | undefined> & { has(slotName: string): boolean; render(slotName: string, args?: any[]): Promise<string> }; slots: Record<string, true | undefined> & {
has(slotName: string): boolean;
render(slotName: string, args?: any[]): Promise<string>;
};
} }
export interface AstroGlobalPartial { export interface AstroGlobalPartial {
@ -560,7 +563,12 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
}; };
} }
export type AsyncRendererComponentFn<U> = (Component: any, props: any, children: string | undefined, metadata?: AstroComponentMetadata) => Promise<U>; export type AsyncRendererComponentFn<U> = (
Component: any,
props: any,
children: string | undefined,
metadata?: AstroComponentMetadata
) => Promise<U>;
/** Generic interface for a component (Astro, Svelte, React, etc.) */ /** Generic interface for a component (Astro, Svelte, React, etc.) */
export interface ComponentInstance { export interface ComponentInstance {
@ -578,7 +586,9 @@ export interface MarkdownInstance<T extends Record<string, any>> {
getHeaders(): Promise<{ depth: number; slug: string; text: string }[]>; getHeaders(): Promise<{ depth: number; slug: string; text: string }[]>;
} }
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>; export type GetHydrateCallback = () => Promise<
(element: Element, innerHTML: string | null) => void
>;
/** /**
* getStaticPaths() options * getStaticPaths() options
@ -606,14 +616,20 @@ export interface JSXTransformConfig {
plugins?: babel.PluginItem[]; plugins?: babel.PluginItem[];
} }
export type JSXTransformFn = (options: { mode: string; ssr: boolean }) => Promise<JSXTransformConfig>; export type JSXTransformFn = (options: {
mode: string;
ssr: boolean;
}) => Promise<JSXTransformConfig>;
export interface ManifestData { export interface ManifestData {
routes: RouteData[]; routes: RouteData[];
} }
export type MarkdownRenderOptions = [string | MarkdownParser, Record<string, any>]; export type MarkdownRenderOptions = [string | MarkdownParser, Record<string, any>];
export type MarkdownParser = (contents: string, options?: Record<string, any>) => MarkdownParserResponse | PromiseLike<MarkdownParserResponse>; export type MarkdownParser = (
contents: string,
options?: Record<string, any>
) => MarkdownParserResponse | PromiseLike<MarkdownParserResponse>;
export interface MarkdownParserResponse { export interface MarkdownParserResponse {
frontmatter: { frontmatter: {
@ -731,13 +747,23 @@ export interface AstroIntegration {
// more generalized. Consider the SSR use-case as well. // more generalized. Consider the SSR use-case as well.
// injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void; // injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void;
}) => void; }) => void;
'astro:config:done'?: (options: { config: AstroConfig; setAdapter: (adapter: AstroAdapter) => void }) => void | Promise<void>; 'astro:config:done'?: (options: {
config: AstroConfig;
setAdapter: (adapter: AstroAdapter) => void;
}) => void | Promise<void>;
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>; 'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>; 'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>; 'astro:server:done'?: () => void | Promise<void>;
'astro:build:start'?: (options: { buildConfig: BuildConfig }) => void | Promise<void>; 'astro:build:start'?: (options: { buildConfig: BuildConfig }) => void | Promise<void>;
'astro:build:setup'?: (options: { vite: ViteConfigWithSSR; target: 'client' | 'server' }) => void; 'astro:build:setup'?: (options: {
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL; routes: RouteData[] }) => void | Promise<void>; vite: ViteConfigWithSSR;
target: 'client' | 'server';
}) => void;
'astro:build:done'?: (options: {
pages: { pathname: string }[];
dir: URL;
routes: RouteData[];
}) => void | Promise<void>;
}; };
} }
@ -821,7 +847,11 @@ export interface SSRResult {
styles: Set<SSRElement>; styles: Set<SSRElement>;
scripts: Set<SSRElement>; scripts: Set<SSRElement>;
links: Set<SSRElement>; links: Set<SSRElement>;
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal; createAstro(
Astro: AstroGlobalPartial,
props: Record<string, any>,
slots: Record<string, any> | null
): AstroGlobal;
resolve: (s: string) => Promise<string>; resolve: (s: string) => Promise<string>;
_metadata: SSRMetadata; _metadata: SSRMetadata;
} }

View file

@ -8,7 +8,11 @@ import * as path from 'path';
import { pathToFileURL } from 'url'; import { pathToFileURL } from 'url';
import * as fs from 'fs'; import * as fs from 'fs';
async function openAllDocuments(workspaceUri: URL, filePathsToIgnore: string[], checker: AstroCheck) { async function openAllDocuments(
workspaceUri: URL,
filePathsToIgnore: string[],
checker: AstroCheck
) {
const files = await glob('**/*.astro', { const files = await glob('**/*.astro', {
cwd: workspaceUri.pathname, cwd: workspaceUri.pathname,
ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)), ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)),
@ -79,7 +83,11 @@ export async function check(astroConfig: AstroConfig) {
diag.diagnostics.forEach((d) => { diag.diagnostics.forEach((d) => {
switch (d.severity) { switch (d.severity) {
case DiagnosticSeverity.Error: { case DiagnosticSeverity.Error: {
console.error(`${bold(cyan(path.relative(root.pathname, diag.filePath)))}:${bold(yellow(d.range.start.line))}:${bold(yellow(d.range.start.character))} - ${d.message}`); console.error(
`${bold(cyan(path.relative(root.pathname, diag.filePath)))}:${bold(
yellow(d.range.start.line)
)}:${bold(yellow(d.range.start.character))} - ${d.message}`
);
let startOffset = offsetAt({ line: d.range.start.line, character: 0 }, diag.text); let startOffset = offsetAt({ line: d.range.start.line, character: 0 }, diag.text);
let endOffset = offsetAt({ line: d.range.start.line + 1, character: 0 }, diag.text); let endOffset = offsetAt({ line: d.range.start.line + 1, character: 0 }, diag.text);
let str = diag.text.substring(startOffset, endOffset - 1); let str = diag.text.substring(startOffset, endOffset - 1);

View file

@ -14,4 +14,5 @@ export async function generate(ast: t.File) {
return code; return code;
} }
export const parse = (code: string) => parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] }); export const parse = (code: string) =>
parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] });

View file

@ -50,7 +50,9 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
configURL = await resolveConfigURL({ cwd, flags }); configURL = await resolveConfigURL({ cwd, flags });
if (configURL?.pathname.endsWith('package.json')) { if (configURL?.pathname.endsWith('package.json')) {
throw new Error(`Unable to use astro add with package.json#astro configuration! Try migrating to \`astro.config.mjs\` and try again.`); throw new Error(
`Unable to use astro add with package.json#astro configuration! Try migrating to \`astro.config.mjs\` and try again.`
);
} }
applyPolyfill(); applyPolyfill();
@ -102,7 +104,13 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
debug('add', 'Parsed astro config'); debug('add', 'Parsed astro config');
const defineConfig = t.identifier('defineConfig'); const defineConfig = t.identifier('defineConfig');
ensureImport(ast, t.importDeclaration([t.importSpecifier(defineConfig, defineConfig)], t.stringLiteral('astro/config'))); ensureImport(
ast,
t.importDeclaration(
[t.importSpecifier(defineConfig, defineConfig)],
t.stringLiteral('astro/config')
)
);
wrapDefaultExport(ast, defineConfig); wrapDefaultExport(ast, defineConfig);
debug('add', 'Astro config ensured `defineConfig`'); debug('add', 'Astro config ensured `defineConfig`');
@ -136,9 +144,13 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
case UpdateResult.none: { case UpdateResult.none: {
const pkgURL = new URL('./package.json', configURL); const pkgURL = new URL('./package.json', configURL);
if (existsSync(fileURLToPath(pkgURL))) { if (existsSync(fileURLToPath(pkgURL))) {
const { dependencies = {}, devDependencies = {} } = await fs.readFile(fileURLToPath(pkgURL)).then((res) => JSON.parse(res.toString())); const { dependencies = {}, devDependencies = {} } = await fs
.readFile(fileURLToPath(pkgURL))
.then((res) => JSON.parse(res.toString()));
const deps = Object.keys(Object.assign(dependencies, devDependencies)); const deps = Object.keys(Object.assign(dependencies, devDependencies));
const missingDeps = integrations.filter((integration) => !deps.includes(integration.packageName)); const missingDeps = integrations.filter(
(integration) => !deps.includes(integration.packageName)
);
if (missingDeps.length === 0) { if (missingDeps.length === 0) {
info(logging, null, msg.success(`Configuration up-to-date.`)); info(logging, null, msg.success(`Configuration up-to-date.`));
return; return;
@ -156,7 +168,11 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
case UpdateResult.updated: { case UpdateResult.updated: {
const len = integrations.length; const len = integrations.length;
if (integrations.find((integration) => integration.id === 'tailwind')) { if (integrations.find((integration) => integration.id === 'tailwind')) {
const possibleConfigFiles = ['./tailwind.config.cjs', './tailwind.config.mjs', './tailwind.config.js'].map((p) => fileURLToPath(new URL(p, configURL))); const possibleConfigFiles = [
'./tailwind.config.cjs',
'./tailwind.config.mjs',
'./tailwind.config.js',
].map((p) => fileURLToPath(new URL(p, configURL)));
let alreadyConfigured = false; let alreadyConfigured = false;
for (const possibleConfigPath of possibleConfigFiles) { for (const possibleConfigPath of possibleConfigFiles) {
if (existsSync(possibleConfigPath)) { if (existsSync(possibleConfigPath)) {
@ -165,9 +181,19 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
} }
} }
if (!alreadyConfigured) { if (!alreadyConfigured) {
info(logging, null, `\n ${magenta(`Astro will generate a minimal ${bold('./tailwind.config.cjs')} file.`)}\n`); info(
logging,
null,
`\n ${magenta(
`Astro will generate a minimal ${bold('./tailwind.config.cjs')} file.`
)}\n`
);
if (await askToContinue({ flags })) { if (await askToContinue({ flags })) {
await fs.writeFile(fileURLToPath(new URL('./tailwind.config.cjs', configURL)), CONSTS.TAILWIND_CONFIG_STUB, { encoding: 'utf-8' }); await fs.writeFile(
fileURLToPath(new URL('./tailwind.config.cjs', configURL)),
CONSTS.TAILWIND_CONFIG_STUB,
{ encoding: 'utf-8' }
);
debug('add', `Generated default ./tailwind.config.cjs file`); debug('add', `Generated default ./tailwind.config.cjs file`);
} }
} else { } else {
@ -175,11 +201,24 @@ export default async function add(names: string[], { cwd, flags, logging }: AddO
} }
} }
const list = integrations.map((integration) => ` - ${integration.packageName}`).join('\n'); const list = integrations.map((integration) => ` - ${integration.packageName}`).join('\n');
info(logging, null, msg.success(`Added the following integration${len === 1 ? '' : 's'} to your project:\n${list}`)); info(
logging,
null,
msg.success(
`Added the following integration${len === 1 ? '' : 's'} to your project:\n${list}`
)
);
return; return;
} }
case UpdateResult.cancelled: { case UpdateResult.cancelled: {
info(logging, null, msg.cancelled(`Dependencies ${bold('NOT')} installed.`, `Be sure to install them manually before continuing!`)); info(
logging,
null,
msg.cancelled(
`Dependencies ${bold('NOT')} installed.`,
`Be sure to install them manually before continuing!`
)
);
return; return;
} }
case UpdateResult.failure: { case UpdateResult.failure: {
@ -193,7 +232,8 @@ async function parseAstroConfig(configURL: URL): Promise<t.File> {
const result = parse(source); const result = parse(source);
if (!result) throw new Error('Unknown error parsing astro config'); if (!result) throw new Error('Unknown error parsing astro config');
if (result.errors.length > 0) throw new Error('Error parsing astro config: ' + JSON.stringify(result.errors)); if (result.errors.length > 0)
throw new Error('Error parsing astro config: ' + JSON.stringify(result.errors));
return result; return result;
} }
@ -217,7 +257,13 @@ Documentation: https://docs.astro.build/en/guides/integrations-guide/`;
async function addIntegration(ast: t.File, integration: IntegrationInfo) { async function addIntegration(ast: t.File, integration: IntegrationInfo) {
const integrationId = t.identifier(toIdent(integration.id)); const integrationId = t.identifier(toIdent(integration.id));
ensureImport(ast, t.importDeclaration([t.importDefaultSpecifier(integrationId)], t.stringLiteral(integration.packageName))); ensureImport(
ast,
t.importDeclaration(
[t.importDefaultSpecifier(integrationId)],
t.stringLiteral(integration.packageName)
)
);
visit(ast, { visit(ast, {
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
@ -241,14 +287,20 @@ async function addIntegration(ast: t.File, integration: IntegrationInfo) {
const integrationCall = t.callExpression(integrationId, []); const integrationCall = t.callExpression(integrationId, []);
if (!integrationsProp) { if (!integrationsProp) {
configObject.properties.push(t.objectProperty(t.identifier('integrations'), t.arrayExpression([integrationCall]))); configObject.properties.push(
t.objectProperty(t.identifier('integrations'), t.arrayExpression([integrationCall]))
);
return; return;
} }
if (integrationsProp.value.type !== 'ArrayExpression') throw new Error('Unable to parse integrations'); if (integrationsProp.value.type !== 'ArrayExpression')
throw new Error('Unable to parse integrations');
const existingIntegrationCall = integrationsProp.value.elements.find( const existingIntegrationCall = integrationsProp.value.elements.find(
(expr) => t.isCallExpression(expr) && t.isIdentifier(expr.callee) && expr.callee.name === integrationId.name (expr) =>
t.isCallExpression(expr) &&
t.isIdentifier(expr.callee) &&
expr.callee.name === integrationId.name
); );
if (existingIntegrationCall) return; if (existingIntegrationCall) return;
@ -265,7 +317,17 @@ const enum UpdateResult {
failure, failure,
} }
async function updateAstroConfig({ configURL, ast, flags, logging }: { configURL: URL; ast: t.File; flags: yargs.Arguments; logging: LogOptions }): Promise<UpdateResult> { async function updateAstroConfig({
configURL,
ast,
flags,
logging,
}: {
configURL: URL;
ast: t.File;
flags: yargs.Arguments;
logging: LogOptions;
}): Promise<UpdateResult> {
const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' }); const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' });
let output = await generate(ast); let output = await generate(ast);
const comment = '// https://astro.build/config'; const comment = '// https://astro.build/config';
@ -299,9 +361,18 @@ async function updateAstroConfig({ configURL, ast, flags, logging }: { configURL
diffed = diffed.replace(newContent, coloredOutput); diffed = diffed.replace(newContent, coloredOutput);
} }
const message = `\n${boxen(diffed, { margin: 0.5, padding: 0.5, borderStyle: 'round', title: configURL.pathname.split('/').pop() })}\n`; const message = `\n${boxen(diffed, {
margin: 0.5,
padding: 0.5,
borderStyle: 'round',
title: configURL.pathname.split('/').pop(),
})}\n`;
info(logging, null, `\n ${magenta('Astro will make the following changes to your config file:')}\n${message}`); info(
logging,
null,
`\n ${magenta('Astro will make the following changes to your config file:')}\n${message}`
);
if (await askToContinue({ flags })) { if (await askToContinue({ flags })) {
await fs.writeFile(fileURLToPath(configURL), output, { encoding: 'utf-8' }); await fs.writeFile(fileURLToPath(configURL), output, { encoding: 'utf-8' });
@ -318,7 +389,13 @@ interface InstallCommand {
flags: string[]; flags: string[];
dependencies: string[]; dependencies: string[];
} }
async function getInstallIntegrationsCommand({ integrations, cwd = process.cwd() }: { integrations: IntegrationInfo[]; cwd?: string }): Promise<InstallCommand | null> { async function getInstallIntegrationsCommand({
integrations,
cwd = process.cwd(),
}: {
integrations: IntegrationInfo[];
cwd?: string;
}): Promise<InstallCommand | null> {
const pm = await preferredPM(cwd); const pm = await preferredPM(cwd);
debug('add', `package manager: ${JSON.stringify(pm)}`); debug('add', `package manager: ${JSON.stringify(pm)}`);
if (!pm) return null; if (!pm) return null;
@ -359,14 +436,30 @@ async function tryToInstallIntegrations({
info(logging, null); info(logging, null);
return UpdateResult.none; return UpdateResult.none;
} else { } else {
const coloredOutput = `${bold(installCommand.pm)} ${installCommand.command} ${installCommand.flags.join(' ')} ${cyan(installCommand.dependencies.join(' '))}`; const coloredOutput = `${bold(installCommand.pm)} ${
const message = `\n${boxen(coloredOutput, { margin: 0.5, padding: 0.5, borderStyle: 'round' })}\n`; installCommand.command
info(logging, null, `\n ${magenta('Astro will run the following command:')}\n ${dim('If you skip this step, you can always run it yourself later')}\n${message}`); } ${installCommand.flags.join(' ')} ${cyan(installCommand.dependencies.join(' '))}`;
const message = `\n${boxen(coloredOutput, {
margin: 0.5,
padding: 0.5,
borderStyle: 'round',
})}\n`;
info(
logging,
null,
`\n ${magenta('Astro will run the following command:')}\n ${dim(
'If you skip this step, you can always run it yourself later'
)}\n${message}`
);
if (await askToContinue({ flags })) { if (await askToContinue({ flags })) {
const spinner = ora('Installing dependencies...').start(); const spinner = ora('Installing dependencies...').start();
try { try {
await execa(installCommand.pm, [installCommand.command, ...installCommand.flags, ...installCommand.dependencies], { cwd }); await execa(
installCommand.pm,
[installCommand.command, ...installCommand.flags, ...installCommand.dependencies],
{ cwd }
);
spinner.succeed(); spinner.succeed();
return UpdateResult.updated; return UpdateResult.updated;
} catch (err) { } catch (err) {
@ -405,7 +498,9 @@ export async function validateIntegrations(integrations: string[]): Promise<Inte
return res.json(); return res.json();
}); });
let dependencies: IntegrationInfo['dependencies'] = [[result['name'], `^${result['version']}`]]; let dependencies: IntegrationInfo['dependencies'] = [
[result['name'], `^${result['version']}`],
];
if (result['peerDependencies']) { if (result['peerDependencies']) {
for (const peer in result['peerDependencies']) { for (const peer in result['peerDependencies']) {

View file

@ -4,7 +4,12 @@ export function wrapDefaultExport(ast: t.File, functionIdentifier: t.Identifier)
visit(ast, { visit(ast, {
ExportDefaultDeclaration(path) { ExportDefaultDeclaration(path) {
if (!t.isExpression(path.node.declaration)) return; if (!t.isExpression(path.node.declaration)) return;
if (t.isCallExpression(path.node.declaration) && t.isIdentifier(path.node.declaration.callee) && path.node.declaration.callee.name === functionIdentifier.name) return; if (
t.isCallExpression(path.node.declaration) &&
t.isIdentifier(path.node.declaration.callee) &&
path.node.declaration.callee.name === functionIdentifier.name
)
return;
path.node.declaration = t.callExpression(functionIdentifier, [path.node.declaration]); path.node.declaration = t.callExpression(functionIdentifier, [path.node.declaration]);
}, },
}); });

View file

@ -1,4 +1,9 @@
import type { ComponentInstance, EndpointHandler, ManifestData, RouteData } from '../../@types/astro'; import type {
ComponentInstance,
EndpointHandler,
ManifestData,
RouteData,
} from '../../@types/astro';
import type { SSRManifest as Manifest, RouteInfo } from './types'; import type { SSRManifest as Manifest, RouteInfo } from './types';
import type { LogOptions } from '../logger/core.js'; import type { LogOptions } from '../logger/core.js';
@ -9,7 +14,10 @@ import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js'; import { render } from '../render/core.js';
import { call as callEndpoint } from '../endpoint/index.js'; import { call as callEndpoint } from '../endpoint/index.js';
import { RouteCache } from '../render/route-cache.js'; import { RouteCache } from '../render/route-cache.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js'; import {
createLinkStylesheetElementSet,
createModuleScriptElementWithSrcSet,
} from '../render/ssr-element.js';
import { prependForwardSlash } from '../path.js'; import { prependForwardSlash } from '../path.js';
import { createRequest } from '../request.js'; import { createRequest } from '../request.js';
@ -58,7 +66,11 @@ export class App {
} }
} }
async #renderPage(request: Request, routeData: RouteData, mod: ComponentInstance): Promise<Response> { async #renderPage(
request: Request,
routeData: RouteData,
mod: ComponentInstance
): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
const manifest = this.#manifest; const manifest = this.#manifest;
const renderers = manifest.renderers; const renderers = manifest.renderers;
@ -105,7 +117,11 @@ export class App {
}); });
} }
async #callEndpoint(request: Request, _routeData: RouteData, mod: ComponentInstance): Promise<Response> { async #callEndpoint(
request: Request,
_routeData: RouteData,
mod: ComponentInstance
): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
const handler = mod as unknown as EndpointHandler; const handler = mod as unknown as EndpointHandler;
const result = await callEndpoint(handler, { const result = await callEndpoint(handler, {

View file

@ -1,4 +1,10 @@
import type { RouteData, SerializedRouteData, MarkdownRenderOptions, ComponentInstance, SSRLoadedRenderer } from '../../@types/astro'; import type {
RouteData,
SerializedRouteData,
MarkdownRenderOptions,
ComponentInstance,
SSRLoadedRenderer,
} from '../../@types/astro';
export type ComponentPath = string; export type ComponentPath = string;
@ -28,4 +34,7 @@ export type SerializedSSRManifest = Omit<SSRManifest, 'routes'> & {
routes: SerializedRouteInfo[]; routes: SerializedRouteInfo[];
}; };
export type AdapterCreateExports<T = any> = (manifest: SSRManifest, args?: T) => Record<string, any>; export type AdapterCreateExports<T = any> = (
manifest: SSRManifest,
args?: T
) => Record<string, any>;

View file

@ -8,7 +8,11 @@ function getOutRoot(astroConfig: AstroConfig): URL {
return new URL('./', astroConfig.outDir); return new URL('./', astroConfig.outDir);
} }
export function getOutFolder(astroConfig: AstroConfig, pathname: string, routeType: RouteType): URL { export function getOutFolder(
astroConfig: AstroConfig,
pathname: string,
routeType: RouteType
): URL {
const outRoot = getOutRoot(astroConfig); const outRoot = getOutRoot(astroConfig);
// This is the root folder to write to. // This is the root folder to write to.
@ -30,7 +34,12 @@ export function getOutFolder(astroConfig: AstroConfig, pathname: string, routeTy
} }
} }
export function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL { export function getOutFile(
astroConfig: AstroConfig,
outFolder: URL,
pathname: string,
routeType: RouteType
): URL {
switch (routeType) { switch (routeType) {
case 'endpoint': case 'endpoint':
return new URL(npath.basename(pathname), outFolder); return new URL(npath.basename(pathname), outFolder);

View file

@ -4,7 +4,12 @@ import { bgGreen, black, cyan, dim, green, magenta } from 'kleur/colors';
import npath from 'path'; import npath from 'path';
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup'; import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import type { AstroConfig, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro'; import type {
AstroConfig,
ComponentInstance,
EndpointHandler,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { BuildInternals } from '../../core/build/internal.js'; import type { BuildInternals } from '../../core/build/internal.js';
import { debug, info } from '../logger/core.js'; import { debug, info } from '../logger/core.js';
import { prependForwardSlash } from '../../core/path.js'; import { prependForwardSlash } from '../../core/path.js';
@ -12,7 +17,10 @@ import type { RenderOptions } from '../../core/render/core';
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { call as callEndpoint } from '../endpoint/index.js'; import { call as callEndpoint } from '../endpoint/index.js';
import { render } from '../render/core.js'; import { render } from '../render/core.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js'; import {
createLinkStylesheetElementSet,
createModuleScriptElementWithSrcSet,
} from '../render/ssr-element.js';
import { getOutputFilename, isBuildingToSSR } from '../util.js'; import { getOutputFilename, isBuildingToSSR } from '../util.js';
import { getOutFile, getOutFolder } from './common.js'; import { getOutFile, getOutFolder } from './common.js';
import { eachPageData, getPageDataByComponent } from './internal.js'; import { eachPageData, getPageDataByComponent } from './internal.js';
@ -56,19 +64,30 @@ export function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig)
} }
// Determines of a Rollup chunk is an entrypoint page. // Determines of a Rollup chunk is an entrypoint page.
export function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk, internals: BuildInternals) { export function chunkIsPage(
astroConfig: AstroConfig,
output: OutputAsset | OutputChunk,
internals: BuildInternals
) {
if (output.type !== 'chunk') { if (output.type !== 'chunk') {
return false; return false;
} }
const chunk = output as OutputChunk; const chunk = output as OutputChunk;
if (chunk.facadeModuleId) { if (chunk.facadeModuleId) {
const facadeToEntryId = prependForwardSlash(rootRelativeFacadeId(chunk.facadeModuleId, astroConfig)); const facadeToEntryId = prependForwardSlash(
rootRelativeFacadeId(chunk.facadeModuleId, astroConfig)
);
return internals.entrySpecifierToBundleMap.has(facadeToEntryId); return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
} }
return false; return false;
} }
export async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { export async function generatePages(
result: RollupOutput,
opts: StaticBuildOptions,
internals: BuildInternals,
facadeIdToPageDataMap: Map<string, PageBuildData>
) {
const timer = performance.now(); const timer = performance.now();
info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`); info(opts.logging, null, `\n${bgGreen(black(' generating static routes '))}`);
@ -101,7 +120,9 @@ async function generatePage(
const pageModule = ssrEntry.pageMap.get(pageData.component); const pageModule = ssrEntry.pageMap.get(pageData.component);
if (!pageModule) { if (!pageModule) {
throw new Error(`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`); throw new Error(
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
);
} }
const generationOptions: Readonly<GeneratePathOptions> = { const generationOptions: Readonly<GeneratePathOptions> = {
@ -141,7 +162,11 @@ function addPageName(pathname: string, opts: StaticBuildOptions): void {
opts.pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, '')); opts.pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
} }
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { async function generatePath(
pathname: string,
opts: StaticBuildOptions,
gopts: GeneratePathOptions
) {
const { astroConfig, logging, origin, routeCache } = opts; const { astroConfig, logging, origin, routeCache } = opts;
const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts; const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts;
@ -197,7 +222,9 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
request: createRequest({ url, headers: new Headers(), logging }), request: createRequest({ url, headers: new Headers(), logging }),
route: pageData.route, route: pageData.route,
routeCache, routeCache,
site: astroConfig.site ? new URL(astroConfig.base, astroConfig.site).toString() : astroConfig.site, site: astroConfig.site
? new URL(astroConfig.base, astroConfig.site).toString()
: astroConfig.site,
ssr: isBuildingToSSR(opts.astroConfig), ssr: isBuildingToSSR(opts.astroConfig),
}; };

View file

@ -7,13 +7,25 @@ import { apply as applyPolyfill } from '../polyfill.js';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import * as vite from 'vite'; import * as vite from 'vite';
import { createVite, ViteConfigWithSSR } from '../create-vite.js'; import { createVite, ViteConfigWithSSR } from '../create-vite.js';
import { debug, info, levels, timerMessage, warn, warnIfUsingExperimentalSSR } from '../logger/core.js'; import {
debug,
info,
levels,
timerMessage,
warn,
warnIfUsingExperimentalSSR,
} from '../logger/core.js';
import { nodeLogOptions } from '../logger/node.js'; import { nodeLogOptions } from '../logger/node.js';
import { createRouteManifest } from '../routing/index.js'; import { createRouteManifest } from '../routing/index.js';
import { collectPagesData } from './page-data.js'; import { collectPagesData } from './page-data.js';
import { staticBuild } from './static-build.js'; import { staticBuild } from './static-build.js';
import { RouteCache } from '../render/route-cache.js'; import { RouteCache } from '../render/route-cache.js';
import { runHookBuildDone, runHookBuildStart, runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js'; import {
runHookBuildDone,
runHookBuildStart,
runHookConfigDone,
runHookConfigSetup,
} from '../../integrations/index.js';
import { getTimeStat } from './util.js'; import { getTimeStat } from './util.js';
import { createSafeError, isBuildingToSSR } from '../util.js'; import { createSafeError, isBuildingToSSR } from '../util.js';
import { fixViteErrorMessage } from '../errors.js'; import { fixViteErrorMessage } from '../errors.js';
@ -24,7 +36,10 @@ export interface BuildOptions {
} }
/** `astro build` */ /** `astro build` */
export default async function build(config: AstroConfig, options: BuildOptions = { logging: nodeLogOptions }): Promise<void> { export default async function build(
config: AstroConfig,
options: BuildOptions = { logging: nodeLogOptions }
): Promise<void> {
applyPolyfill(); applyPolyfill();
const builder = new AstroBuilder(config, options); const builder = new AstroBuilder(config, options);
await builder.run(); await builder.run();
@ -46,7 +61,9 @@ class AstroBuilder {
this.config = config; this.config = config;
this.logging = options.logging; this.logging = options.logging;
this.routeCache = new RouteCache(this.logging); this.routeCache = new RouteCache(this.logging);
this.origin = config.site ? new URL(config.site).origin : `http://localhost:${config.server.port}`; this.origin = config.site
? new URL(config.site).origin
: `http://localhost:${config.server.port}`;
this.manifest = createRouteManifest({ config }, this.logging); this.manifest = createRouteManifest({ config }, this.logging);
this.timer = {}; this.timer = {};
} }
@ -76,7 +93,13 @@ class AstroBuilder {
} }
/** Run the build logic. build() is marked private because usage should go through ".run()" */ /** Run the build logic. build() is marked private because usage should go through ".run()" */
private async build({ viteConfig, viteServer }: { viteConfig: ViteConfigWithSSR; viteServer: vite.ViteDevServer }) { private async build({
viteConfig,
viteServer,
}: {
viteConfig: ViteConfigWithSSR;
viteServer: vite.ViteDevServer;
}) {
const { origin } = this; const { origin } = this;
const buildConfig: BuildConfig = { const buildConfig: BuildConfig = {
client: new URL('./client/', this.config.outDir), client: new URL('./client/', this.config.outDir),
@ -118,7 +141,11 @@ class AstroBuilder {
// Bundle the assets in your final build: This currently takes the HTML output // Bundle the assets in your final build: This currently takes the HTML output
// of every page (stored in memory) and bundles the assets pointed to on those pages. // of every page (stored in memory) and bundles the assets pointed to on those pages.
this.timer.buildStart = performance.now(); this.timer.buildStart = performance.now();
info(this.logging, 'build', colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)); info(
this.logging,
'build',
colors.dim(`Completed in ${getTimeStat(this.timer.init, performance.now())}.`)
);
await staticBuild({ await staticBuild({
allPages, allPages,
@ -145,11 +172,20 @@ class AstroBuilder {
// You're done! Time to clean up. // You're done! Time to clean up.
await viteServer.close(); await viteServer.close();
await runHookBuildDone({ config: this.config, pages: pageNames, routes: Object.values(allPages).map((pd) => pd.route) }); await runHookBuildDone({
config: this.config,
pages: pageNames,
routes: Object.values(allPages).map((pd) => pd.route),
});
if (this.logging.level && levels[this.logging.level] <= levels['info']) { if (this.logging.level && levels[this.logging.level] <= levels['info']) {
const buildMode = isBuildingToSSR(this.config) ? 'ssr' : 'static'; const buildMode = isBuildingToSSR(this.config) ? 'ssr' : 'static';
await this.printStats({ logging: this.logging, timeStart: this.timer.init, pageCount: pageNames.length, buildMode }); await this.printStats({
logging: this.logging,
timeStart: this.timer.init,
pageCount: pageNames.length,
buildMode,
});
} }
} }
@ -165,7 +201,17 @@ class AstroBuilder {
} }
/** Stats */ /** Stats */
private async printStats({ logging, timeStart, pageCount, buildMode }: { logging: LogOptions; timeStart: number; pageCount: number; buildMode: 'static' | 'ssr' }) { private async printStats({
logging,
timeStart,
pageCount,
buildMode,
}: {
logging: LogOptions;
timeStart: number;
pageCount: number;
buildMode: 'static' | 'ssr';
}) {
const buildTime = performance.now() - timeStart; const buildTime = performance.now() - timeStart;
const total = getTimeStat(timeStart, performance.now()); const total = getTimeStat(timeStart, performance.now());

View file

@ -76,13 +76,22 @@ export function createBuildInternals(): BuildInternals {
}; };
} }
export function trackPageData(internals: BuildInternals, component: string, pageData: PageBuildData, componentModuleId: string, componentURL: URL): void { export function trackPageData(
internals: BuildInternals,
component: string,
pageData: PageBuildData,
componentModuleId: string,
componentURL: URL
): void {
pageData.moduleSpecifier = componentModuleId; pageData.moduleSpecifier = componentModuleId;
internals.pagesByComponent.set(component, pageData); internals.pagesByComponent.set(component, pageData);
internals.pagesByViteID.set(viteID(componentURL), pageData); internals.pagesByViteID.set(viteID(componentURL), pageData);
} }
export function* getPageDatasByChunk(internals: BuildInternals, chunk: RenderedChunk): Generator<PageBuildData, void, unknown> { export function* getPageDatasByChunk(
internals: BuildInternals,
chunk: RenderedChunk
): Generator<PageBuildData, void, unknown> {
const pagesByViteID = internals.pagesByViteID; const pagesByViteID = internals.pagesByViteID;
for (const [modulePath] of Object.entries(chunk.modules)) { for (const [modulePath] of Object.entries(chunk.modules)) {
if (pagesByViteID.has(modulePath)) { if (pagesByViteID.has(modulePath)) {
@ -91,14 +100,20 @@ export function* getPageDatasByChunk(internals: BuildInternals, chunk: RenderedC
} }
} }
export function getPageDataByComponent(internals: BuildInternals, component: string): PageBuildData | undefined { export function getPageDataByComponent(
internals: BuildInternals,
component: string
): PageBuildData | undefined {
if (internals.pagesByComponent.has(component)) { if (internals.pagesByComponent.has(component)) {
return internals.pagesByComponent.get(component); return internals.pagesByComponent.get(component);
} }
return undefined; return undefined;
} }
export function getPageDataByViteID(internals: BuildInternals, viteid: ViteID): PageBuildData | undefined { export function getPageDataByViteID(
internals: BuildInternals,
viteid: ViteID
): PageBuildData | undefined {
if (internals.pagesByViteID.has(viteid)) { if (internals.pagesByViteID.has(viteid)) {
return internals.pagesByViteID.get(viteid); return internals.pagesByViteID.get(viteid);
} }

View file

@ -28,7 +28,9 @@ export interface CollectPagesDataResult {
} }
// Examines the routes and returns a collection of information about each page. // Examines the routes and returns a collection of information about each page.
export async function collectPagesData(opts: CollectPagesDataOptions): Promise<CollectPagesDataResult> { export async function collectPagesData(
opts: CollectPagesDataOptions
): Promise<CollectPagesDataResult> {
const { astroConfig, logging, manifest, origin, routeCache, viteServer } = opts; const { astroConfig, logging, manifest, origin, routeCache, viteServer } = opts;
const assets: Record<string, string> = {}; const assets: Record<string, string> = {};
@ -75,7 +77,10 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
clearInterval(routeCollectionLogTimeout); clearInterval(routeCollectionLogTimeout);
if (buildMode === 'static') { if (buildMode === 'static') {
const html = `${route.pathname}`.replace(/\/?$/, '/index.html'); const html = `${route.pathname}`.replace(/\/?$/, '/index.html');
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`); debug(
'build',
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.yellow(html)}`
);
} else { } else {
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`); debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}`);
} }
@ -93,7 +98,12 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
const result = await getStaticPathsForRoute(opts, route) const result = await getStaticPathsForRoute(opts, route)
.then((_result) => { .then((_result) => {
const label = _result.staticPaths.length === 1 ? 'page' : 'pages'; const label = _result.staticPaths.length === 1 ? 'page' : 'pages';
debug('build', `├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.magenta(`[${_result.staticPaths.length} ${label}]`)}`); debug(
'build',
`├── ${colors.bold(colors.green('✔'))} ${route.component}${colors.magenta(
`[${_result.staticPaths.length} ${label}]`
)}`
);
return _result; return _result;
}) })
.catch((err) => { .catch((err) => {
@ -108,7 +118,9 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
if (content) { if (content) {
const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.outDir); const rssFile = new URL(url.replace(/^\/?/, './'), astroConfig.outDir);
if (assets[fileURLToPath(rssFile)]) { if (assets[fileURLToPath(rssFile)]) {
throw new Error(`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`); throw new Error(
`[getStaticPaths] RSS feed ${url} already exists.\nUse \`rss(data, {url: '...'})\` to choose a unique, custom URL. (${route.component})`
);
} }
assets[fileURLToPath(rssFile)] = content; assets[fileURLToPath(rssFile)] = content;
} }
@ -124,7 +136,9 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
assets[fileURLToPath(stylesheetFile)] = content; assets[fileURLToPath(stylesheetFile)] = content;
} }
} }
const finalPaths = result.staticPaths.map((staticPath) => staticPath.params && route.generate(staticPath.params)).filter(Boolean); const finalPaths = result.staticPaths
.map((staticPath) => staticPath.params && route.generate(staticPath.params))
.filter(Boolean);
allPages[route.component] = { allPages[route.component] = {
component: route.component, component: route.component,
route, route,
@ -146,7 +160,10 @@ export async function collectPagesData(opts: CollectPagesDataOptions): Promise<C
return { assets, allPages }; return { assets, allPages };
} }
async function getStaticPathsForRoute(opts: CollectPagesDataOptions, route: RouteData): Promise<RouteCacheEntry> { async function getStaticPathsForRoute(
opts: CollectPagesDataOptions,
route: RouteData
): Promise<RouteCacheEntry> {
const { astroConfig, logging, routeCache, ssr, viteServer } = opts; const { astroConfig, logging, routeCache, ssr, viteServer } = opts;
if (!viteServer) throw new Error(`vite.createServer() not called!`); if (!viteServer) throw new Error(`vite.createServer() not called!`);
const filePath = new URL(`./${route.component}`, astroConfig.root); const filePath = new URL(`./${route.component}`, astroConfig.root);

View file

@ -64,7 +64,9 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Any hydration directive like astro/client/idle.js // Any hydration directive like astro/client/idle.js
...metadata.hydrationDirectiveSpecifiers(), ...metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer // The client path for each renderer
...renderers.filter((renderer) => !!renderer.clientEntrypoint).map((renderer) => renderer.clientEntrypoint!), ...renderers
.filter((renderer) => !!renderer.clientEntrypoint)
.map((renderer) => renderer.clientEntrypoint!),
]); ]);
// Add hoisted scripts // Add hoisted scripts
@ -149,7 +151,8 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
}), }),
...(viteConfig.plugins || []), ...(viteConfig.plugins || []),
// SSR needs to be last // SSR needs to be last
isBuildingToSSR(opts.astroConfig) && vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!), isBuildingToSSR(opts.astroConfig) &&
vitePluginSSR(opts, internals, opts.astroConfig._ctx.adapter!),
], ],
publicDir: ssr ? false : viteConfig.publicDir, publicDir: ssr ? false : viteConfig.publicDir,
root: viteConfig.root, root: viteConfig.root,
@ -166,7 +169,11 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
return await vite.build(viteBuildConfig); return await vite.build(viteBuildConfig);
} }
async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) { async function clientBuild(
opts: StaticBuildOptions,
internals: BuildInternals,
input: Set<string>
) {
const { astroConfig, viteConfig } = opts; const { astroConfig, viteConfig } = opts;
const timer = performance.now(); const timer = performance.now();
const ssr = isBuildingToSSR(astroConfig); const ssr = isBuildingToSSR(astroConfig);
@ -260,7 +267,9 @@ async function copyFiles(fromFolder: URL, toFolder: URL) {
async function ssrMoveAssets(opts: StaticBuildOptions) { async function ssrMoveAssets(opts: StaticBuildOptions) {
info(opts.logging, 'build', 'Rearranging server assets...'); info(opts.logging, 'build', 'Rearranging server assets...');
const serverRoot = opts.buildConfig.staticMode ? opts.buildConfig.client : opts.buildConfig.server; const serverRoot = opts.buildConfig.staticMode
? opts.buildConfig.client
: opts.buildConfig.server;
const clientRoot = opts.buildConfig.client; const clientRoot = opts.buildConfig.client;
const serverAssets = new URL('./assets/', serverRoot); const serverAssets = new URL('./assets/', serverRoot);
const clientAssets = new URL('./assets/', clientRoot); const clientAssets = new URL('./assets/', clientRoot);

View file

@ -1,5 +1,12 @@
import type { ComponentPreload } from '../render/dev/index'; import type { ComponentPreload } from '../render/dev/index';
import type { AstroConfig, BuildConfig, ManifestData, RouteData, ComponentInstance, SSRLoadedRenderer } from '../../@types/astro'; import type {
AstroConfig,
BuildConfig,
ManifestData,
RouteData,
ComponentInstance,
SSRLoadedRenderer,
} from '../../@types/astro';
import type { ViteConfigWithSSR } from '../../create-vite'; import type { ViteConfigWithSSR } from '../../create-vite';
import type { LogOptions } from '../../logger'; import type { LogOptions } from '../../logger';
import type { RouteCache } from '../../render/route-cache.js'; import type { RouteCache } from '../../render/route-cache.js';

View file

@ -8,7 +8,10 @@ function virtualHoistedEntry(id: string) {
return id.endsWith('.astro/hoisted.js') || id.endsWith('.md/hoisted.js'); return id.endsWith('.astro/hoisted.js') || id.endsWith('.md/hoisted.js');
} }
export function vitePluginHoistedScripts(astroConfig: AstroConfig, internals: BuildInternals): VitePlugin { export function vitePluginHoistedScripts(
astroConfig: AstroConfig,
internals: BuildInternals
): VitePlugin {
return { return {
name: '@astro/rollup-plugin-astro-hoisted-scripts', name: '@astro/rollup-plugin-astro-hoisted-scripts',
@ -35,7 +38,11 @@ export function vitePluginHoistedScripts(astroConfig: AstroConfig, internals: Bu
// Find all page entry points and create a map of the entry point to the hashed hoisted script. // Find all page entry points and create a map of the entry point to the hashed hoisted script.
// This is used when we render so that we can add the script to the head. // This is used when we render so that we can add the script to the head.
for (const [id, output] of Object.entries(bundle)) { for (const [id, output] of Object.entries(bundle)) {
if (output.type === 'chunk' && output.facadeModuleId && virtualHoistedEntry(output.facadeModuleId)) { if (
output.type === 'chunk' &&
output.facadeModuleId &&
virtualHoistedEntry(output.facadeModuleId)
) {
const facadeId = output.facadeModuleId!; const facadeId = output.facadeModuleId!;
const pathname = facadeId.slice(0, facadeId.length - '/hoisted.js'.length); const pathname = facadeId.slice(0, facadeId.length - '/hoisted.js'.length);

View file

@ -15,7 +15,11 @@ export const virtualModuleId = '@astrojs-ssr-virtual-entry';
const resolvedVirtualModuleId = '\0' + virtualModuleId; const resolvedVirtualModuleId = '\0' + virtualModuleId;
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
export function vitePluginSSR(buildOpts: StaticBuildOptions, internals: BuildInternals, adapter: AstroAdapter): VitePlugin { export function vitePluginSSR(
buildOpts: StaticBuildOptions,
internals: BuildInternals,
adapter: AstroAdapter
): VitePlugin {
return { return {
name: '@astrojs/vite-plugin-astro-ssr', name: '@astrojs/vite-plugin-astro-ssr',
enforce: 'post', enforce: 'post',
@ -91,7 +95,8 @@ function buildManifest(opts: StaticBuildOptions, internals: BuildInternals): Ser
// HACK! Patch this special one. // HACK! Patch this special one.
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries()); const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = 'data:text/javascript;charset=utf-8,//[no before-hydration script]'; entryModules[BEFORE_HYDRATION_SCRIPT_ID] =
'data:text/javascript;charset=utf-8,//[no before-hydration script]';
const ssrManifest: SerializedSSRManifest = { const ssrManifest: SerializedSSRManifest = {
routes, routes,

View file

@ -124,7 +124,9 @@ export const AstroConfigSchema = z.object({
// preprocess // preprocess
(val) => (Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val), (val) => (Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val),
// validate // validate
z.array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) })).default([]) z
.array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }))
.default([])
), ),
style: z style: z
.object({ .object({
@ -168,7 +170,11 @@ export const AstroConfigSchema = z.object({
}); });
/** Turn raw config values into normalized values */ /** Turn raw config values into normalized values */
export async function validateConfig(userConfig: any, root: string, cmd: string): Promise<AstroConfig> { export async function validateConfig(
userConfig: any,
root: string,
cmd: string
): Promise<AstroConfig> {
const fileProtocolRoot = pathToFileURL(root + path.sep); const fileProtocolRoot = pathToFileURL(root + path.sep);
// Manual deprecation checks // Manual deprecation checks
/* eslint-disable no-console */ /* eslint-disable no-console */
@ -176,8 +182,12 @@ export async function validateConfig(userConfig: any, root: string, cmd: string)
console.error('Astro "renderers" are now "integrations"!'); console.error('Astro "renderers" are now "integrations"!');
console.error('Update your configuration and install new dependencies:'); console.error('Update your configuration and install new dependencies:');
try { try {
const rendererKeywords = userConfig.renderers.map((r: string) => r.replace('@astrojs/renderer-', '')); const rendererKeywords = userConfig.renderers.map((r: string) =>
const rendererImports = rendererKeywords.map((r: string) => ` import ${r} from '@astrojs/${r === 'solid' ? 'solid-js' : r}';`).join('\n'); r.replace('@astrojs/renderer-', '')
);
const rendererImports = rendererKeywords
.map((r: string) => ` import ${r} from '@astrojs/${r === 'solid' ? 'solid-js' : r}';`)
.join('\n');
const rendererIntegrations = rendererKeywords.map((r: string) => ` ${r}(),`).join('\n'); const rendererIntegrations = rendererKeywords.map((r: string) => ` ${r}(),`).join('\n');
console.error(''); console.error('');
console.error(colors.dim(' // astro.config.js')); console.error(colors.dim(' // astro.config.js'));
@ -208,7 +218,9 @@ export async function validateConfig(userConfig: any, root: string, cmd: string)
} }
} }
if (oldConfig) { if (oldConfig) {
throw new Error(`Legacy configuration detected. Please update your configuration to the new format!\nSee https://astro.build/config for more information.`); throw new Error(
`Legacy configuration detected. Please update your configuration to the new format!\nSee https://astro.build/config for more information.`
);
} }
/* eslint-enable no-console */ /* eslint-enable no-console */
@ -233,7 +245,8 @@ export async function validateConfig(userConfig: any, root: string, cmd: string)
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)), .transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
server: z.preprocess( server: z.preprocess(
// preprocess // preprocess
(val) => (typeof val === 'function' ? val({ command: cmd === 'dev' ? 'dev' : 'preview' }) : val), (val) =>
typeof val === 'function' ? val({ command: cmd === 'dev' ? 'dev' : 'preview' }) : val,
// validate // validate
z z
.object({ .object({
@ -265,7 +278,10 @@ export async function validateConfig(userConfig: any, root: string, cmd: string)
_ctx: { scripts: [], renderers: [], adapter: undefined }, _ctx: { scripts: [], renderers: [], adapter: undefined },
}; };
// Final-Pass Validation (perform checks that require the full config object) // Final-Pass Validation (perform checks that require the full config object)
if (!result.experimental?.integrations && !result.integrations.every((int) => int.name.startsWith('@astrojs/'))) { if (
!result.experimental?.integrations &&
!result.integrations.every((int) => int.name.startsWith('@astrojs/'))
) {
throw new Error( throw new Error(
[ [
`Astro integrations are still experimental.`, `Astro integrations are still experimental.`,
@ -288,9 +304,11 @@ function resolveFlags(flags: Partial<Flags>): CLIFlags {
site: typeof flags.site === 'string' ? flags.site : undefined, site: typeof flags.site === 'string' ? flags.site : undefined,
port: typeof flags.port === 'number' ? flags.port : undefined, port: typeof flags.port === 'number' ? flags.port : undefined,
config: typeof flags.config === 'string' ? flags.config : undefined, config: typeof flags.config === 'string' ? flags.config : undefined,
host: typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined, host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false, experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false,
experimentalIntegrations: typeof flags.experimentalIntegrations === 'boolean' ? flags.experimentalIntegrations : false, experimentalIntegrations:
typeof flags.experimentalIntegrations === 'boolean' ? flags.experimentalIntegrations : false,
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false, drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false,
}; };
} }
@ -301,15 +319,21 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: strin
astroConfig.experimental = astroConfig.experimental || {}; astroConfig.experimental = astroConfig.experimental || {};
astroConfig.markdown = astroConfig.markdown || {}; astroConfig.markdown = astroConfig.markdown || {};
if (typeof flags.site === 'string') astroConfig.site = flags.site; if (typeof flags.site === 'string') astroConfig.site = flags.site;
if (typeof flags.experimentalSsr === 'boolean') astroConfig.experimental.ssr = flags.experimentalSsr; if (typeof flags.experimentalSsr === 'boolean')
if (typeof flags.experimentalIntegrations === 'boolean') astroConfig.experimental.integrations = flags.experimentalIntegrations; astroConfig.experimental.ssr = flags.experimentalSsr;
if (typeof flags.experimentalIntegrations === 'boolean')
astroConfig.experimental.integrations = flags.experimentalIntegrations;
if (typeof flags.drafts === 'boolean') astroConfig.markdown.drafts = flags.drafts; if (typeof flags.drafts === 'boolean') astroConfig.markdown.drafts = flags.drafts;
if (typeof flags.port === 'number') {
// @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function. // @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function.
// TODO: Come back here and refactor to remove this expected error. // TODO: Come back here and refactor to remove this expected error.
if (typeof flags.port === 'number') astroConfig.server.port = flags.port; astroConfig.server.port = flags.port;
}
if (typeof flags.host === 'string' || typeof flags.host === 'boolean') {
// @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function. // @ts-expect-error astroConfig.server may be a function, but TS doesn't like attaching properties to a function.
// TODO: Come back here and refactor to remove this expected error. // TODO: Come back here and refactor to remove this expected error.
if (typeof flags.host === 'string' || typeof flags.host === 'boolean') astroConfig.server.host = flags.host; astroConfig.server.host = flags.host;
}
return astroConfig; return astroConfig;
} }
@ -325,7 +349,9 @@ interface LoadConfigOptions {
* Note: currently the same as loadConfig but only returns the `filePath` * Note: currently the same as loadConfig but only returns the `filePath`
* instead of the resolved config * instead of the resolved config
*/ */
export async function resolveConfigURL(configOptions: Pick<LoadConfigOptions, 'cwd' | 'flags'>): Promise<URL | undefined> { export async function resolveConfigURL(
configOptions: Pick<LoadConfigOptions, 'cwd' | 'flags'>
): Promise<URL | undefined> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
const flags = resolveFlags(configOptions.flags || {}); const flags = resolveFlags(configOptions.flags || {});
let userConfigPath: string | undefined; let userConfigPath: string | undefined;
@ -363,14 +389,23 @@ export async function loadConfig(configOptions: LoadConfigOptions): Promise<Astr
} }
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */ /** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
export async function resolveConfig(userConfig: AstroUserConfig, root: string, flags: CLIFlags = {}, cmd: string): Promise<AstroConfig> { export async function resolveConfig(
userConfig: AstroUserConfig,
root: string,
flags: CLIFlags = {},
cmd: string
): Promise<AstroConfig> {
const mergedConfig = mergeCLIFlags(userConfig, flags, cmd); const mergedConfig = mergeCLIFlags(userConfig, flags, cmd);
const validatedConfig = await validateConfig(mergedConfig, root, cmd); const validatedConfig = await validateConfig(mergedConfig, root, cmd);
return validatedConfig; return validatedConfig;
} }
function mergeConfigRecursively(defaults: Record<string, any>, overrides: Record<string, any>, rootPath: string) { function mergeConfigRecursively(
defaults: Record<string, any>,
overrides: Record<string, any>,
rootPath: string
) {
const merged: Record<string, any> = { ...defaults }; const merged: Record<string, any> = { ...defaults };
for (const key in overrides) { for (const key in overrides) {
const value = overrides[key]; const value = overrides[key];
@ -405,6 +440,10 @@ function mergeConfigRecursively(defaults: Record<string, any>, overrides: Record
return merged; return merged;
} }
export function mergeConfig(defaults: Record<string, any>, overrides: Record<string, any>, isRoot = true): Record<string, any> { export function mergeConfig(
defaults: Record<string, any>,
overrides: Record<string, any>,
isRoot = true
): Record<string, any> {
return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.'); return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.');
} }

View file

@ -43,7 +43,10 @@ interface CreateViteOptions {
} }
/** Return a common starting point for all Vite actions */ /** Return a common starting point for all Vite actions */
export async function createVite(commandConfig: ViteConfigWithSSR, { astroConfig, logging, mode }: CreateViteOptions): Promise<ViteConfigWithSSR> { export async function createVite(
commandConfig: ViteConfigWithSSR,
{ astroConfig, logging, mode }: CreateViteOptions
): Promise<ViteConfigWithSSR> {
// Scan for any third-party Astro packages. Vite needs these to be passed to `ssr.noExternal`. // Scan for any third-party Astro packages. Vite needs these to be passed to `ssr.noExternal`.
const astroPackages = await getAstroPackages(astroConfig); const astroPackages = await getAstroPackages(astroConfig);
// Start with the Vite configuration that Astro core needs // Start with the Vite configuration that Astro core needs
@ -72,7 +75,10 @@ export async function createVite(commandConfig: ViteConfigWithSSR, { astroConfig
envPrefix: 'PUBLIC_', envPrefix: 'PUBLIC_',
server: { server: {
force: true, // force dependency rebuild (TODO: enabled only while next is unstable; eventually only call in "production" mode?) force: true, // force dependency rebuild (TODO: enabled only while next is unstable; eventually only call in "production" mode?)
hmr: process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production' ? false : undefined, // disable HMR for test hmr:
process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production'
? false
: undefined, // disable HMR for test
// handle Vite URLs // handle Vite URLs
proxy: { proxy: {
// add proxies here // add proxies here
@ -127,7 +133,11 @@ async function getAstroPackages({ root }: AstroConfig): Promise<string[]> {
const depPkgPath = fileURLToPath(depPkgUrl); const depPkgPath = fileURLToPath(depPkgUrl);
if (!fs.existsSync(depPkgPath)) return false; if (!fs.existsSync(depPkgPath)) return false;
const { dependencies = {}, peerDependencies = {}, keywords = [] } = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8')); const {
dependencies = {},
peerDependencies = {},
keywords = [],
} = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
// Attempt: package relies on `astro`. ✅ Definitely an Astro package // Attempt: package relies on `astro`. ✅ Definitely an Astro package
if (peerDependencies.astro || dependencies.astro) return true; if (peerDependencies.astro || dependencies.astro) return true;
// Attempt: package is tagged with `astro` or `astro-component`. ✅ Likely a community package // Attempt: package is tagged with `astro` or `astro-component`. ✅ Likely a community package
@ -181,7 +191,10 @@ function isCommonNotAstro(dep: string): boolean {
return ( return (
COMMON_DEPENDENCIES_NOT_ASTRO.includes(dep) || COMMON_DEPENDENCIES_NOT_ASTRO.includes(dep) ||
COMMON_PREFIXES_NOT_ASTRO.some( COMMON_PREFIXES_NOT_ASTRO.some(
(prefix) => (prefix.startsWith('@') ? dep.startsWith(prefix) : dep.substring(dep.lastIndexOf('/') + 1).startsWith(prefix)) // check prefix omitting @scope/ (prefix) =>
prefix.startsWith('@')
? dep.startsWith(prefix)
: dep.substring(dep.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/
) )
); );
} }

View file

@ -2,7 +2,13 @@ import type { AddressInfo } from 'net';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import * as vite from 'vite'; import * as vite from 'vite';
import type { AstroConfig } from '../../@types/astro'; import type { AstroConfig } from '../../@types/astro';
import { runHookConfigDone, runHookConfigSetup, runHookServerDone, runHookServerSetup, runHookServerStart } from '../../integrations/index.js'; import {
runHookConfigDone,
runHookConfigSetup,
runHookServerDone,
runHookServerSetup,
runHookServerStart,
} from '../../integrations/index.js';
import { createVite } from '../create-vite.js'; import { createVite } from '../create-vite.js';
import { info, LogOptions, warn, warnIfUsingExperimentalSSR } from '../logger/core.js'; import { info, LogOptions, warn, warnIfUsingExperimentalSSR } from '../logger/core.js';
import { nodeLogOptions } from '../logger/node.js'; import { nodeLogOptions } from '../logger/node.js';
@ -19,7 +25,10 @@ export interface DevServer {
} }
/** `astro dev` */ /** `astro dev` */
export default async function dev(config: AstroConfig, options: DevOptions = { logging: nodeLogOptions }): Promise<DevServer> { export default async function dev(
config: AstroConfig,
options: DevOptions = { logging: nodeLogOptions }
): Promise<DevServer> {
const devStart = performance.now(); const devStart = performance.now();
applyPolyfill(); applyPolyfill();
config = await runHookConfigSetup({ config, command: 'dev' }); config = await runHookConfigSetup({ config, command: 'dev' });
@ -39,7 +48,17 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo; const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
const site = config.site ? new URL(config.base, config.site) : undefined; const site = config.site ? new URL(config.base, config.site) : undefined;
info(options.logging, null, msg.devStart({ startupTime: performance.now() - devStart, config, devServerAddressInfo, site, https: !!viteConfig.server?.https })); info(
options.logging,
null,
msg.devStart({
startupTime: performance.now() - devStart,
config,
devServerAddressInfo,
site,
https: !!viteConfig.server?.https,
})
);
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0'; const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
if (currentVersion.includes('-')) { if (currentVersion.includes('-')) {

View file

@ -3,7 +3,10 @@ import type { RenderOptions } from '../render/core';
import { renderEndpoint } from '../../runtime/server/index.js'; import { renderEndpoint } from '../../runtime/server/index.js';
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
export type EndpointOptions = Pick<RenderOptions, 'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr'>; export type EndpointOptions = Pick<
RenderOptions,
'logging' | 'origin' | 'request' | 'route' | 'routeCache' | 'pathname' | 'route' | 'site' | 'ssr'
>;
type EndpointCallResult = type EndpointCallResult =
| { | {
@ -15,11 +18,16 @@ type EndpointCallResult =
response: Response; response: Response;
}; };
export async function call(mod: EndpointHandler, opts: EndpointOptions): Promise<EndpointCallResult> { export async function call(
mod: EndpointHandler,
opts: EndpointOptions
): Promise<EndpointCallResult> {
const paramsAndPropsResp = await getParamsAndProps({ ...opts, mod: mod as any }); const paramsAndPropsResp = await getParamsAndProps({ ...opts, mod: mod as any });
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
throw new Error(`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`); throw new Error(
`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`
);
} }
const [params] = paramsAndPropsResp; const [params] = paramsAndPropsResp;

View file

@ -47,7 +47,12 @@ export const levels: Record<LoggerLevel, number> = {
}; };
/** Full logging API */ /** Full logging API */
export function log(opts: LogOptions, level: LoggerLevel, type: string | null, ...args: Array<any>) { export function log(
opts: LogOptions,
level: LoggerLevel,
type: string | null,
...args: Array<any>
) {
const logLevel = opts.level; const logLevel = opts.level;
const dest = opts.dest; const dest = opts.dest;
const event: LogMessage = { const event: LogMessage = {
@ -120,7 +125,8 @@ if (typeof process !== 'undefined') {
/** Print out a timer message for debug() */ /** Print out a timer message for debug() */
export function timerMessage(message: string, startTime: number = Date.now()) { export function timerMessage(message: string, startTime: number = Date.now()) {
let timeDiff = Date.now() - startTime; let timeDiff = Date.now() - startTime;
let timeDisplay = timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`; let timeDisplay =
timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`;
return `${message} ${dim(timeDisplay)}`; return `${message} ${dim(timeDisplay)}`;
} }

View file

@ -127,5 +127,8 @@ export const logger = {
export function enableVerboseLogging() { export function enableVerboseLogging() {
//debugPackage.enable('*,-babel'); //debugPackage.enable('*,-babel');
debug('cli', '--verbose flag enabled! Enabling: DEBUG="*,-babel"'); debug('cli', '--verbose flag enabled! Enabling: DEBUG="*,-babel"');
debug('cli', 'Tip: Set the DEBUG env variable directly for more control. Example: "DEBUG=astro:*,vite:* astro build".'); debug(
'cli',
'Tip: Set the DEBUG env variable directly for more control. Example: "DEBUG=astro:*,vite:* astro build".'
);
} }

View file

@ -2,7 +2,20 @@
* Dev server messages (organized here to prevent clutter) * Dev server messages (organized here to prevent clutter)
*/ */
import { bold, dim, red, green, underline, yellow, bgYellow, cyan, bgGreen, black, bgRed, bgWhite } from 'kleur/colors'; import {
bold,
dim,
red,
green,
underline,
yellow,
bgYellow,
cyan,
bgGreen,
black,
bgRed,
bgWhite,
} from 'kleur/colors';
import os from 'os'; import os from 'os';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
@ -13,13 +26,23 @@ import { emoji, getLocalAddress, padMultilineString } from './util.js';
const PREFIX_PADDING = 6; const PREFIX_PADDING = 6;
/** Display */ /** Display */
export function req({ url, statusCode, reqTime }: { url: string; statusCode: number; reqTime?: number }): string { export function req({
url,
statusCode,
reqTime,
}: {
url: string;
statusCode: number;
reqTime?: number;
}): string {
let color = dim; let color = dim;
if (statusCode >= 500) color = red; if (statusCode >= 500) color = red;
else if (statusCode >= 400) color = yellow; else if (statusCode >= 400) color = yellow;
else if (statusCode >= 300) color = dim; else if (statusCode >= 300) color = dim;
else if (statusCode >= 200) color = green; else if (statusCode >= 200) color = green;
return `${bold(color(`${statusCode}`.padStart(PREFIX_PADDING)))} ${url.padStart(40)} ${reqTime ? dim(Math.round(reqTime) + 'ms') : ''}`.trim(); return `${bold(color(`${statusCode}`.padStart(PREFIX_PADDING)))} ${url.padStart(40)} ${
reqTime ? dim(Math.round(reqTime) + 'ms') : ''
}`.trim();
} }
export function reload({ file }: { file: string }): string { export function reload({ file }: { file: string }): string {
@ -53,17 +76,23 @@ export function devStart({
const { address: networkAddress, port } = devServerAddressInfo; const { address: networkAddress, port } = devServerAddressInfo;
const localAddress = getLocalAddress(networkAddress, config.server.host); const localAddress = getLocalAddress(networkAddress, config.server.host);
const networkLogging = getNetworkLogging(config.server.host); const networkLogging = getNetworkLogging(config.server.host);
const toDisplayUrl = (hostname: string) => `${https ? 'https' : 'http'}://${hostname}:${port}${rootPath}`; const toDisplayUrl = (hostname: string) =>
`${https ? 'https' : 'http'}://${hostname}:${port}${rootPath}`;
let addresses = []; let addresses = [];
if (networkLogging === 'none') { if (networkLogging === 'none') {
addresses = [`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`]; addresses = [`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`];
} else if (networkLogging === 'host-to-expose') { } else if (networkLogging === 'host-to-expose') {
addresses = [`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`, `${networkPrefix}${dim('use --host to expose')}`]; addresses = [
`${localPrefix}${bold(cyan(toDisplayUrl(localAddress)))}`,
`${networkPrefix}${dim('use --host to expose')}`,
];
} else { } else {
addresses = Object.values(os.networkInterfaces()) addresses = Object.values(os.networkInterfaces())
.flatMap((networkInterface) => networkInterface ?? []) .flatMap((networkInterface) => networkInterface ?? [])
.filter((networkInterface) => networkInterface?.address && networkInterface?.family === 'IPv4') .filter(
(networkInterface) => networkInterface?.address && networkInterface?.family === 'IPv4'
)
.map(({ address }) => { .map(({ address }) => {
if (address.includes('127.0.0.1')) { if (address.includes('127.0.0.1')) {
const displayAddress = address.replace('127.0.0.1', localAddress); const displayAddress = address.replace('127.0.0.1', localAddress);
@ -76,7 +105,14 @@ export function devStart({
.sort((msg) => (msg.startsWith(localPrefix) ? -1 : 1)); .sort((msg) => (msg.startsWith(localPrefix) ? -1 : 1));
} }
const messages = [`${emoji('🚀 ', '')}${bgGreen(black(` astro `))} ${green(`v${version}`)} ${dim(`started in ${Math.round(startupTime)}ms`)}`, '', ...addresses, '']; const messages = [
`${emoji('🚀 ', '')}${bgGreen(black(` astro `))} ${green(`v${version}`)} ${dim(
`started in ${Math.round(startupTime)}ms`
)}`,
'',
...addresses,
'',
];
return messages.map((msg) => ` ${msg}`).join('\n'); return messages.map((msg) => ` ${msg}`).join('\n');
} }
@ -136,8 +172,12 @@ export function getNetworkLogging(host: string | boolean): 'none' | 'host-to-exp
} }
export function formatConfigErrorMessage(err: ZodError) { export function formatConfigErrorMessage(err: ZodError) {
const errorList = err.issues.map((issue) => ` ! ${bold(issue.path.join('.'))} ${red(issue.message + '.')}`); const errorList = err.issues.map(
return `${red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join('\n')}`; (issue) => ` ! ${bold(issue.path.join('.'))} ${red(issue.message + '.')}`
);
return `${red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join(
'\n'
)}`;
} }
export function formatErrorMessage(_err: Error, args: string[] = []): string { export function formatErrorMessage(_err: Error, args: string[] = []): string {
@ -196,7 +236,12 @@ export function printHelp({
let message = []; let message = [];
if (headline) { if (headline) {
message.push(linebreak(), ` ${bgGreen(black(` ${commandName} `))} ${green(`v${process.env.PACKAGE_VERSION ?? ''}`)} ${headline}`); message.push(
linebreak(),
` ${bgGreen(black(` ${commandName} `))} ${green(
`v${process.env.PACKAGE_VERSION ?? ''}`
)} ${headline}`
);
} }
if (usage) { if (usage) {
@ -204,7 +249,11 @@ export function printHelp({
} }
if (commands) { if (commands) {
message.push(linebreak(), title('Commands'), table(commands, { padding: 28, prefix: ' astro ' })); message.push(
linebreak(),
title('Commands'),
table(commands, { padding: 28, prefix: ' astro ' })
);
} }
if (flags) { if (flags) {

View file

@ -24,7 +24,10 @@ export interface PreviewServer {
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/; const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
/** The primary dev action */ /** The primary dev action */
export default async function preview(config: AstroConfig, { logging }: PreviewOptions): Promise<PreviewServer> { export default async function preview(
config: AstroConfig,
{ logging }: PreviewOptions
): Promise<PreviewServer> {
const startServerTime = performance.now(); const startServerTime = performance.now();
const defaultOrigin = 'http://localhost'; const defaultOrigin = 'http://localhost';
const trailingSlash = config.trailingSlash; const trailingSlash = config.trailingSlash;
@ -61,7 +64,10 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
case hasTrailingSlash && trailingSlash == 'never' && !isRoot: case hasTrailingSlash && trailingSlash == 'never' && !isRoot:
sendError('Not Found (trailingSlash is set to "never")'); sendError('Not Found (trailingSlash is set to "never")');
return; return;
case !hasTrailingSlash && trailingSlash == 'always' && !isRoot && !HAS_FILE_EXTENSION_REGEXP.test(pathname): case !hasTrailingSlash &&
trailingSlash == 'always' &&
!isRoot &&
!HAS_FILE_EXTENSION_REGEXP.test(pathname):
sendError('Not Found (trailingSlash is set to "always")'); sendError('Not Found (trailingSlash is set to "always")');
return; return;
default: { default: {
@ -87,7 +93,17 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO
httpServer = server.listen(port, host, async () => { httpServer = server.listen(port, host, async () => {
if (!showedListenMsg) { if (!showedListenMsg) {
const devServerAddressInfo = server.address() as AddressInfo; const devServerAddressInfo = server.address() as AddressInfo;
info(logging, null, msg.devStart({ startupTime: performance.now() - timerStart, config, devServerAddressInfo, https: false, site: baseURL })); info(
logging,
null,
msg.devStart({
startupTime: performance.now() - timerStart,
config,
devServerAddressInfo,
https: false,
site: baseURL,
})
);
} }
showedListenMsg = true; showedListenMsg = true;
resolve(); resolve();

View file

@ -1,4 +1,13 @@
import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, SSRLoadedRenderer, RouteData, SSRElement } from '../../@types/astro'; import type {
ComponentInstance,
EndpointHandler,
MarkdownRenderOptions,
Params,
Props,
SSRLoadedRenderer,
RouteData,
SSRElement,
} from '../../@types/astro';
import type { LogOptions } from '../logger/core.js'; import type { LogOptions } from '../logger/core.js';
import { renderHead, renderPage } from '../../runtime/server/index.js'; import { renderHead, renderPage } from '../../runtime/server/index.js';
@ -19,7 +28,9 @@ export const enum GetParamsAndPropsError {
NoMatchingStaticPath, NoMatchingStaticPath,
} }
export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props] | GetParamsAndPropsError> { export async function getParamsAndProps(
opts: GetParamsAndPropsOptions
): Promise<[Params, Props] | GetParamsAndPropsError> {
const { logging, mod, route, routeCache, pathname, ssr } = opts; const { logging, mod, route, routeCache, pathname, ssr } = opts;
// Handle dynamic routes // Handle dynamic routes
let params: Params = {}; let params: Params = {};
@ -73,8 +84,26 @@ export interface RenderOptions {
request: Request; request: Request;
} }
export async function render(opts: RenderOptions): Promise<{ type: 'html'; html: string } | { type: 'response'; response: Response }> { export async function render(
const { legacyBuild, links, logging, origin, markdownRender, mod, pathname, scripts, renderers, request, resolve, route, routeCache, site, ssr } = opts; opts: RenderOptions
): Promise<{ type: 'html'; html: string } | { type: 'response'; response: Response }> {
const {
legacyBuild,
links,
logging,
origin,
markdownRender,
mod,
pathname,
scripts,
renderers,
request,
resolve,
route,
routeCache,
site,
ssr,
} = opts;
const paramsAndPropsRes = await getParamsAndProps({ const paramsAndPropsRes = await getParamsAndProps({
logging, logging,
@ -86,14 +115,18 @@ export async function render(opts: RenderOptions): Promise<{ type: 'html'; html:
}); });
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) { if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
throw new Error(`[getStaticPath] route pattern matched, but no matching static path found. (${pathname})`); throw new Error(
`[getStaticPath] route pattern matched, but no matching static path found. (${pathname})`
);
} }
const [params, pageProps] = paramsAndPropsRes; const [params, pageProps] = paramsAndPropsRes;
// Validate the page component before rendering the page // Validate the page component before rendering the page
const Component = await mod.default; const Component = await mod.default;
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); if (!Component)
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`); throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory)
throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
const result = createResult({ const result = createResult({
legacyBuild, legacyBuild,

View file

@ -98,7 +98,10 @@ function serializeTag({ tag, attrs, children }: vite.HtmlTagDescriptor, indent =
if (unaryTags.has(tag)) { if (unaryTags.has(tag)) {
return `<${tag}${serializeAttrs(attrs)}>`; return `<${tag}${serializeAttrs(attrs)}>`;
} else { } else {
return `<${tag}${serializeAttrs(attrs)}>${serializeTags(children, incrementIndent(indent))}</${tag}>`; return `<${tag}${serializeAttrs(attrs)}>${serializeTags(
children,
incrementIndent(indent)
)}</${tag}>`;
} }
} }

View file

@ -1,7 +1,15 @@
import astroRemark from '@astrojs/markdown-remark'; import astroRemark from '@astrojs/markdown-remark';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import type * as vite from 'vite'; import type * as vite from 'vite';
import type { AstroConfig, AstroRenderer, ComponentInstance, RouteData, RuntimeMode, SSRElement, SSRLoadedRenderer } from '../../../@types/astro'; import type {
AstroConfig,
AstroRenderer,
ComponentInstance,
RouteData,
RuntimeMode,
SSRElement,
SSRLoadedRenderer,
} from '../../../@types/astro';
import { LogOptions } from '../../logger/core.js'; import { LogOptions } from '../../logger/core.js';
import { render as coreRender } from '../core.js'; import { render as coreRender } from '../core.js';
import { prependForwardSlash } from '../../../core/path.js'; import { prependForwardSlash } from '../../../core/path.js';
@ -37,24 +45,38 @@ export interface SSROptions {
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance]; export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
export type RenderResponse = { type: 'html'; html: string } | { type: 'response'; response: Response }; export type RenderResponse =
| { type: 'html'; html: string }
| { type: 'response'; response: Response };
const svelteStylesRE = /svelte\?svelte&type=style/; const svelteStylesRE = /svelte\?svelte&type=style/;
async function loadRenderer(viteServer: vite.ViteDevServer, renderer: AstroRenderer): Promise<SSRLoadedRenderer> { async function loadRenderer(
viteServer: vite.ViteDevServer,
renderer: AstroRenderer
): Promise<SSRLoadedRenderer> {
// Vite modules can be out-of-date when using an un-resolved url // Vite modules can be out-of-date when using an un-resolved url
// We also encountered inconsistencies when using the resolveUrl and resolveId helpers // We also encountered inconsistencies when using the resolveUrl and resolveId helpers
// We've found that pulling the ID directly from the urlToModuleMap is the most stable! // We've found that pulling the ID directly from the urlToModuleMap is the most stable!
const id = viteServer.moduleGraph.urlToModuleMap.get(renderer.serverEntrypoint)?.id ?? renderer.serverEntrypoint; const id =
viteServer.moduleGraph.urlToModuleMap.get(renderer.serverEntrypoint)?.id ??
renderer.serverEntrypoint;
const mod = (await viteServer.ssrLoadModule(id)) as { default: SSRLoadedRenderer['ssr'] }; const mod = (await viteServer.ssrLoadModule(id)) as { default: SSRLoadedRenderer['ssr'] };
return { ...renderer, ssr: mod.default }; return { ...renderer, ssr: mod.default };
} }
export async function loadRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<SSRLoadedRenderer[]> { export async function loadRenderers(
viteServer: vite.ViteDevServer,
astroConfig: AstroConfig
): Promise<SSRLoadedRenderer[]> {
return Promise.all(astroConfig._ctx.renderers.map((r) => loadRenderer(viteServer, r))); return Promise.all(astroConfig._ctx.renderers.map((r) => loadRenderer(viteServer, r)));
} }
export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROptions, 'astroConfig' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> { export async function preload({
astroConfig,
filePath,
viteServer,
}: Pick<SSROptions, 'astroConfig' | 'filePath' | 'viteServer'>): Promise<ComponentPreload> {
// Important: This needs to happen first, in case a renderer provides polyfills. // Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await loadRenderers(viteServer, astroConfig); const renderers = await loadRenderers(viteServer, astroConfig);
// Load the module from the Vite SSR Runtime. // Load the module from the Vite SSR Runtime.
@ -64,13 +86,32 @@ export async function preload({ astroConfig, filePath, viteServer }: Pick<SSROpt
} }
/** use Vite to SSR */ /** use Vite to SSR */
export async function render(renderers: SSRLoadedRenderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<RenderResponse> { export async function render(
const { astroConfig, filePath, logging, mode, origin, pathname, request, route, routeCache, viteServer } = ssrOpts; renderers: SSRLoadedRenderer[],
mod: ComponentInstance,
ssrOpts: SSROptions
): Promise<RenderResponse> {
const {
astroConfig,
filePath,
logging,
mode,
origin,
pathname,
request,
route,
routeCache,
viteServer,
} = ssrOpts;
// TODO: clean up "legacy" flag passed through helper functions // TODO: clean up "legacy" flag passed through helper functions
const isLegacyBuild = false; const isLegacyBuild = false;
// Add hoisted script tags // Add hoisted script tags
const scripts = createModuleScriptElementWithSrcSet(!isLegacyBuild && mod.hasOwnProperty('$$metadata') ? Array.from(mod.$$metadata.hoistedScriptPaths()) : []); const scripts = createModuleScriptElementWithSrcSet(
!isLegacyBuild && mod.hasOwnProperty('$$metadata')
? Array.from(mod.$$metadata.hoistedScriptPaths())
: []
);
// Inject HMR scripts // Inject HMR scripts
if (mod.hasOwnProperty('$$metadata') && mode === 'development' && !isLegacyBuild) { if (mod.hasOwnProperty('$$metadata') && mode === 'development' && !isLegacyBuild) {
@ -79,7 +120,10 @@ export async function render(renderers: SSRLoadedRenderer[], mod: ComponentInsta
children: '', children: '',
}); });
scripts.add({ scripts.add({
props: { type: 'module', src: new URL('../../../runtime/client/hmr.js', import.meta.url).pathname }, props: {
type: 'module',
src: new URL('../../../runtime/client/hmr.js', import.meta.url).pathname,
},
children: '', children: '',
}); });
} }
@ -203,7 +247,10 @@ export async function render(renderers: SSRLoadedRenderer[], mod: ComponentInsta
}; };
} }
export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise<RenderResponse> { export async function ssr(
preloadedComponent: ComponentPreload,
ssrOpts: SSROptions
): Promise<RenderResponse> {
const [renderers, mod] = preloadedComponent; const [renderers, mod] = preloadedComponent;
return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors wont get caught below return await render(renderers, mod, ssrOpts); // NOTE: without "await", errors wont get caught below
} }

View file

@ -1,7 +1,17 @@
import { GetStaticPathsResult, Page, PaginateFunction, Params, Props, RouteData } from '../../@types/astro'; import {
GetStaticPathsResult,
Page,
PaginateFunction,
Params,
Props,
RouteData,
} from '../../@types/astro';
export function generatePaginateFunction(routeMatch: RouteData): PaginateFunction { export function generatePaginateFunction(routeMatch: RouteData): PaginateFunction {
return function paginateUtility(data: any[], args: { pageSize?: number; params?: Params; props?: Props } = {}) { return function paginateUtility(
data: any[],
args: { pageSize?: number; params?: Params; props?: Props } = {}
) {
let { pageSize: _pageSize, params: _params, props: _props } = args; let { pageSize: _pageSize, params: _params, props: _props } = args;
const pageSize = _pageSize || 10; const pageSize = _pageSize || 10;
const paramName = 'page'; const paramName = 'page';
@ -41,8 +51,20 @@ export function generatePaginateFunction(routeMatch: RouteData): PaginateFunctio
lastPage: lastPage, lastPage: lastPage,
url: { url: {
current: routeMatch.generate({ ...params }), current: routeMatch.generate({ ...params }),
next: pageNum === lastPage ? undefined : routeMatch.generate({ ...params, page: String(pageNum + 1) }), next:
prev: pageNum === 1 ? undefined : routeMatch.generate({ ...params, page: !includesFirstPageNumber && pageNum - 1 === 1 ? undefined : String(pageNum - 1) }), pageNum === lastPage
? undefined
: routeMatch.generate({ ...params, page: String(pageNum + 1) }),
prev:
pageNum === 1
? undefined
: routeMatch.generate({
...params,
page:
!includesFirstPageNumber && pageNum - 1 === 1
? undefined
: String(pageNum - 1),
}),
}, },
} as Page, } as Page,
}, },

View file

@ -1,5 +1,14 @@
import { bold } from 'kleur/colors'; import { bold } from 'kleur/colors';
import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro'; import type {
AstroGlobal,
AstroGlobalPartial,
MarkdownParser,
MarkdownRenderOptions,
Params,
SSRElement,
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
import { renderSlot } from '../../runtime/server/index.js'; import { renderSlot } from '../../runtime/server/index.js';
import { LogOptions, warn } from '../logger/core.js'; import { LogOptions, warn } from '../logger/core.js';
import { createCanonicalURL, isCSSRequest } from './util.js'; import { createCanonicalURL, isCSSRequest } from './util.js';
@ -45,7 +54,9 @@ class Slots {
if (slots) { if (slots) {
for (const key of Object.keys(slots)) { for (const key of Object.keys(slots)) {
if ((this as any)[key] !== undefined) { if ((this as any)[key] !== undefined) {
throw new Error(`Unable to create a slot named "${key}". "${key}" is a reserved slot name!\nPlease update the name of this slot.`); throw new Error(
`Unable to create a slot named "${key}". "${key}" is a reserved slot name!\nPlease update the name of this slot.`
);
} }
Object.defineProperty(this, key, { Object.defineProperty(this, key, {
get() { get() {
@ -75,10 +86,14 @@ class Slots {
const expression = getFunctionExpression(component); const expression = getFunctionExpression(component);
if (expression) { if (expression) {
const slot = expression(...args); const slot = expression(...args);
return await renderSlot(this.#result, slot).then((res) => (res != null ? String(res) : res)); return await renderSlot(this.#result, slot).then((res) =>
res != null ? String(res) : res
);
} }
} }
const content = await renderSlot(this.#result, this.#slots[name]).then((res) => (res != null ? String(res) : res)); const content = await renderSlot(this.#result, this.#slots[name]).then((res) =>
res != null ? String(res) : res
);
if (cacheable) this.#cache.set(name, content); if (cacheable) this.#cache.set(name, content);
return content; return content;
} }
@ -98,7 +113,11 @@ export function createResult(args: CreateResultArgs): SSRResult {
scripts: args.scripts ?? new Set<SSRElement>(), scripts: args.scripts ?? new Set<SSRElement>(),
links: args.links ?? new Set<SSRElement>(), links: args.links ?? new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */ /** This function returns the `Astro` faux-global */
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) { createAstro(
astroGlobal: AstroGlobalPartial,
props: Record<string, any>,
slots: Record<string, any> | null
) {
const astroSlots = new Slots(result, slots); const astroSlots = new Slots(result, slots);
const Astro = { const Astro = {
@ -142,7 +161,9 @@ or consider make it a module like so:
warn( warn(
args.logging, args.logging,
`deprecation`, `deprecation`,
`${bold('Astro.resolve()')} is deprecated. We see that you are trying to resolve ${path}. `${bold(
'Astro.resolve()'
)} is deprecated. We see that you are trying to resolve ${path}.
${extra}` ${extra}`
); );
// Intentionally return an empty string so that it is not relied upon. // Intentionally return an empty string so that it is not relied upon.

View file

@ -1,8 +1,19 @@
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, Params, RouteData, RSS } from '../../@types/astro'; import type {
ComponentInstance,
GetStaticPathsItem,
GetStaticPathsResult,
GetStaticPathsResultKeyed,
Params,
RouteData,
RSS,
} from '../../@types/astro';
import { LogOptions, warn, debug } from '../logger/core.js'; import { LogOptions, warn, debug } from '../logger/core.js';
import { generatePaginateFunction } from './paginate.js'; import { generatePaginateFunction } from './paginate.js';
import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../routing/validation.js'; import {
validateGetStaticPathsModule,
validateGetStaticPathsResult,
} from '../routing/validation.js';
type RSSFn = (...args: any[]) => any; type RSSFn = (...args: any[]) => any;
@ -19,7 +30,13 @@ interface CallGetStaticPathsOptions {
ssr: boolean; ssr: boolean;
} }
export async function callGetStaticPaths({ isValidate, logging, mod, route, ssr }: CallGetStaticPathsOptions): Promise<RouteCacheEntry> { export async function callGetStaticPaths({
isValidate,
logging,
mod,
route,
ssr,
}: CallGetStaticPathsOptions): Promise<RouteCacheEntry> {
validateGetStaticPathsModule(mod, { ssr }); validateGetStaticPathsModule(mod, { ssr });
const resultInProgress = { const resultInProgress = {
rss: [] as RSS[], rss: [] as RSS[],
@ -80,7 +97,11 @@ export class RouteCache {
// Warn here so that an unexpected double-call of getStaticPaths() // Warn here so that an unexpected double-call of getStaticPaths()
// isn't invisible and developer can track down the issue. // isn't invisible and developer can track down the issue.
if (this.cache[route.component]) { if (this.cache[route.component]) {
warn(this.logging, 'routeCache', `Internal Warning: route cache overwritten. (${route.component})`); warn(
this.logging,
'routeCache',
`Internal Warning: route cache overwritten. (${route.component})`
);
} }
this.cache[route.component] = entry; this.cache[route.component] = entry;
} }
@ -98,5 +119,7 @@ export function findPathItemByKey(staticPaths: GetStaticPathsResultKeyed, params
} }
debug('findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`); debug('findPathItemByKey', `Unexpected cache miss looking for ${paramsKey}`);
matchedStaticPath = staticPaths.find(({ params: _params }) => JSON.stringify(_params) === paramsKey); matchedStaticPath = staticPaths.find(
({ params: _params }) => JSON.stringify(_params) === paramsKey
);
} }

View file

@ -9,8 +9,10 @@ export function validateRSS(args: GenerateRSSArgs): void {
const { rssData, srcFile } = args; const { rssData, srcFile } = args;
if (!rssData.title) throw new Error(`[${srcFile}] rss.title required`); if (!rssData.title) throw new Error(`[${srcFile}] rss.title required`);
if (!rssData.description) throw new Error(`[${srcFile}] rss.description required`); if (!rssData.description) throw new Error(`[${srcFile}] rss.description required`);
if ((rssData as any).item) throw new Error(`[${srcFile}] \`item: Function\` should be \`items: Item[]\``); if ((rssData as any).item)
if (!Array.isArray(rssData.items)) throw new Error(`[${srcFile}] rss.items should be an array of items`); throw new Error(`[${srcFile}] \`item: Function\` should be \`items: Item[]\``);
if (!Array.isArray(rssData.items))
throw new Error(`[${srcFile}] rss.items should be an array of items`);
} }
type GenerateRSSArgs = { site: string; rssData: RSS; srcFile: string }; type GenerateRSSArgs = { site: string; rssData: RSS; srcFile: string };
@ -19,7 +21,10 @@ type GenerateRSSArgs = { site: string; rssData: RSS; srcFile: string };
export function generateRSS(args: GenerateRSSArgs): string { export function generateRSS(args: GenerateRSSArgs): string {
validateRSS(args); validateRSS(args);
const { srcFile, rssData, site } = args; const { srcFile, rssData, site } = args;
if ((rssData as any).item) throw new Error(`[${srcFile}] rss() \`item()\` function was deprecated, and is now \`items: object[]\`.`); if ((rssData as any).item)
throw new Error(
`[${srcFile}] rss() \`item()\` function was deprecated, and is now \`items: object[]\`.`
);
let xml = `<?xml version="1.0" encoding="UTF-8"?>`; let xml = `<?xml version="1.0" encoding="UTF-8"?>`;
if (typeof rssData.stylesheet === 'string') { if (typeof rssData.stylesheet === 'string') {
@ -45,12 +50,27 @@ export function generateRSS(args: GenerateRSSArgs): string {
for (const result of rssData.items) { for (const result of rssData.items) {
xml += `<item>`; xml += `<item>`;
// validate // validate
if (typeof result !== 'object') throw new Error(`[${srcFile}] rss.items expected an object. got: "${JSON.stringify(result)}"`); if (typeof result !== 'object')
if (!result.title) throw new Error(`[${srcFile}] rss.items required "title" property is missing. got: "${JSON.stringify(result)}"`); throw new Error(
if (!result.link) throw new Error(`[${srcFile}] rss.items required "link" property is missing. got: "${JSON.stringify(result)}"`); `[${srcFile}] rss.items expected an object. got: "${JSON.stringify(result)}"`
);
if (!result.title)
throw new Error(
`[${srcFile}] rss.items required "title" property is missing. got: "${JSON.stringify(
result
)}"`
);
if (!result.link)
throw new Error(
`[${srcFile}] rss.items required "link" property is missing. got: "${JSON.stringify(
result
)}"`
);
xml += `<title><![CDATA[${result.title}]]></title>`; xml += `<title><![CDATA[${result.title}]]></title>`;
// If the item's link is already a valid URL, don't mess with it. // If the item's link is already a valid URL, don't mess with it.
const itemLink = isValidURL(result.link) ? result.link : createCanonicalURL(result.link, site).href; const itemLink = isValidURL(result.link)
? result.link
: createCanonicalURL(result.link, site).href;
xml += `<link>${itemLink}</link>`; xml += `<link>${itemLink}</link>`;
xml += `<guid>${itemLink}</guid>`; xml += `<guid>${itemLink}</guid>`;
if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`; if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`;
@ -87,7 +107,9 @@ export function generateRSSStylesheet() {
export function generateRssFunction(site: string | undefined, route: RouteData): RSSFunction { export function generateRssFunction(site: string | undefined, route: RouteData): RSSFunction {
return function rssUtility(args: RSS): RSSResult { return function rssUtility(args: RSS): RSSResult {
if (!site) { if (!site) {
throw new Error(`[${route.component}] rss() tried to generate RSS but "site" missing in astro.config.mjs`); throw new Error(
`[${route.component}] rss() tried to generate RSS but "site" missing in astro.config.mjs`
);
} }
let result: RSSResult = {} as any; let result: RSSResult = {} as any;
const { dest, ...rssData } = args; const { dest, ...rssData } = args;

View file

@ -35,6 +35,9 @@ export function createModuleScriptElementWithSrc(src: string, site?: string): SS
}; };
} }
export function createModuleScriptElementWithSrcSet(srces: string[], site?: string): Set<SSRElement> { export function createModuleScriptElementWithSrcSet(
srces: string[],
site?: string
): Set<SSRElement> {
return new Set<SSRElement>(srces.map((src) => createModuleScriptElementWithSrc(src, site))); return new Set<SSRElement>(srces.map((src) => createModuleScriptElementWithSrc(src, site)));
} }

View file

@ -19,7 +19,16 @@ export function isValidURL(url: string): boolean {
} }
// https://vitejs.dev/guide/features.html#css-pre-processors // https://vitejs.dev/guide/features.html#css-pre-processors
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']); export const STYLE_EXTENSIONS = new Set([
'.css',
'.pcss',
'.postcss',
'.scss',
'.sass',
'.styl',
'.stylus',
'.less',
]);
const cssRe = new RegExp( const cssRe = new RegExp(
`\\.(${Array.from(STYLE_EXTENSIONS) `\\.(${Array.from(STYLE_EXTENSIONS)

View file

@ -13,8 +13,17 @@ export interface CreateRequestOptions {
logging: LogOptions; logging: LogOptions;
} }
export function createRequest({ url, headers, method = 'GET', body = undefined, logging }: CreateRequestOptions): Request { export function createRequest({
let headersObj = headers instanceof Headers ? headers : new Headers(Object.entries(headers as Record<string, any>)); url,
headers,
method = 'GET',
body = undefined,
logging,
}: CreateRequestOptions): Request {
let headersObj =
headers instanceof Headers
? headers
: new Headers(Object.entries(headers as Record<string, any>));
const request = new Request(url.toString(), { const request = new Request(url.toString(), {
method: method, method: method,
@ -25,7 +34,11 @@ export function createRequest({ url, headers, method = 'GET', body = undefined,
Object.defineProperties(request, { Object.defineProperties(request, {
canonicalURL: { canonicalURL: {
get() { get() {
warn(logging, 'deprecation', `Astro.request.canonicalURL has been moved to Astro.canonicalURL`); warn(
logging,
'deprecation',
`Astro.request.canonicalURL has been moved to Astro.canonicalURL`
);
return undefined; return undefined;
}, },
}, },

View file

@ -79,7 +79,8 @@ function getPattern(segments: Part[][], addTrailingSlash: AstroConfig['trailingS
}) })
.join(''); .join('');
const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$'; const trailing =
addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
return new RegExp(`^${pathname || '\\/'}${trailing}`); return new RegExp(`^${pathname || '\\/'}${trailing}`);
} }
@ -155,7 +156,10 @@ function comparator(a: Item, b: Item) {
} }
if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) { if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1); return (
bSubPart.content.length - aSubPart.content.length ||
(aSubPart.content < bSubPart.content ? -1 : 1)
);
} }
} }
@ -168,7 +172,10 @@ function comparator(a: Item, b: Item) {
} }
/** Create manifest of all static routes */ /** Create manifest of all static routes */
export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?: string }, logging: LogOptions): ManifestData { export function createRouteManifest(
{ config, cwd }: { config: AstroConfig; cwd?: string },
logging: LogOptions
): ManifestData {
const components: string[] = []; const components: string[] = [];
const routes: RouteData[] = []; const routes: RouteData[] = [];
const validPageExtensions: Set<string> = new Set(['.astro', '.md']); const validPageExtensions: Set<string> = new Set(['.astro', '.md']);
@ -196,7 +203,9 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?
} }
const segment = isDir ? basename : name; const segment = isDir ? basename : name;
if (/^\$/.test(segment)) { if (/^\$/.test(segment)) {
throw new Error(`Invalid route ${file} — Astro's Collections API has been replaced by dynamic route params.`); throw new Error(
`Invalid route ${file} — Astro's Collections API has been replaced by dynamic route params.`
);
} }
if (/\]\[/.test(segment)) { if (/\]\[/.test(segment)) {
throw new Error(`Invalid route ${file} — parameters must be separated`); throw new Error(`Invalid route ${file} — parameters must be separated`);
@ -269,7 +278,9 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?
const trailingSlash = item.isPage ? config.trailingSlash : 'never'; const trailingSlash = item.isPage ? config.trailingSlash : 'never';
const pattern = getPattern(segments, trailingSlash); const pattern = getPattern(segments, trailingSlash);
const generate = getGenerator(segments, trailingSlash); const generate = getGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
: null;
routes.push({ routes.push({
type: item.isPage ? 'page' : 'endpoint', type: item.isPage ? 'page' : 'endpoint',

View file

@ -1,6 +1,12 @@
import type { RouteData, SerializedRouteData } from '../../../@types/astro'; import type { RouteData, SerializedRouteData } from '../../../@types/astro';
function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined, type: 'page' | 'endpoint'): RouteData { function createRouteData(
pattern: RegExp,
params: string[],
component: string,
pathname: string | undefined,
type: 'page' | 'endpoint'
): RouteData {
return { return {
type, type,
pattern, pattern,

View file

@ -12,26 +12,48 @@ export function validateGetStaticPathsModule(mod: ComponentInstance, { ssr }: Va
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`); throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
} }
if (!mod.getStaticPaths && !ssr) { if (!mod.getStaticPaths && !ssr) {
throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`); throw new Error(
`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`
);
} }
} }
/** Throw error for malformed getStaticPaths() response */ /** Throw error for malformed getStaticPaths() response */
export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) { export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`); throw new Error(
`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(
result
)}\`.`
);
} }
result.forEach((pathObject) => { result.forEach((pathObject) => {
if (!pathObject.params) { if (!pathObject.params) {
warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`); warn(
logging,
'getStaticPaths',
`invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(
pathObject
)}\`. Skipped.`
);
return; return;
} }
for (const [key, val] of Object.entries(pathObject.params)) { for (const [key, val] of Object.entries(pathObject.params)) {
if (!(typeof val === 'undefined' || typeof val === 'string')) { if (!(typeof val === 'undefined' || typeof val === 'string')) {
warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`); warn(
logging,
'getStaticPaths',
`invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(
val
)}\`.`
);
} }
if (val === '') { if (val === '') {
warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`); warn(
logging,
'getStaticPaths',
`invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`
);
} }
} }
}); });

View file

@ -41,7 +41,9 @@ export function getOutputFilename(astroConfig: AstroConfig, name: string) {
} }
/** is a specifier an npm package? */ /** is a specifier an npm package? */
export function parseNpmName(spec: string): { scope?: string; name: string; subpath?: string } | undefined { export function parseNpmName(
spec: string
): { scope?: string; name: string; subpath?: string } | undefined {
// not an npm package // not an npm package
if (!spec || spec[0] === '.' || spec[0] === '/') return undefined; if (!spec || spec[0] === '.' || spec[0] === '/') return undefined;
@ -66,7 +68,9 @@ export function parseNpmName(spec: string): { scope?: string; name: string; subp
/** Coalesce any throw variable to an Error instance. */ /** Coalesce any throw variable to an Error instance. */
export function createSafeError(err: any): Error { export function createSafeError(err: any): Error {
return err instanceof Error || (err && err.name && err.message) ? err : new Error(JSON.stringify(err)); return err instanceof Error || (err && err.name && err.message)
? err
: new Error(JSON.stringify(err));
} }
/** generate code frame from esbuild error */ /** generate code frame from esbuild error */
@ -90,7 +94,10 @@ export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string
const isFocusedLine = lineNo === loc.line - 1; const isFocusedLine = lineNo === loc.line - 1;
output += isFocusedLine ? '> ' : ' '; output += isFocusedLine ? '> ' : ' ';
output += `${lineNo + 1} | ${lines[lineNo]}\n`; output += `${lineNo + 1} | ${lines[lineNo]}\n`;
if (isFocusedLine) output += `${[...new Array(gutterWidth)].join(' ')} | ${[...new Array(loc.column)].join(' ')}^\n`; if (isFocusedLine)
output += `${[...new Array(gutterWidth)].join(' ')} | ${[...new Array(loc.column)].join(
' '
)}^\n`;
} }
return output; return output;
} }

View file

@ -5,7 +5,13 @@ import { mergeConfig } from '../core/config.js';
import ssgAdapter from '../adapter-ssg/index.js'; import ssgAdapter from '../adapter-ssg/index.js';
import type { ViteConfigWithSSR } from '../core/create-vite.js'; import type { ViteConfigWithSSR } from '../core/create-vite.js';
export async function runHookConfigSetup({ config: _config, command }: { config: AstroConfig; command: 'dev' | 'build' }): Promise<AstroConfig> { export async function runHookConfigSetup({
config: _config,
command,
}: {
config: AstroConfig;
command: 'dev' | 'build';
}): Promise<AstroConfig> {
if (_config.adapter) { if (_config.adapter) {
_config.integrations.push(_config.adapter); _config.integrations.push(_config.adapter);
} }
@ -38,7 +44,9 @@ export async function runHookConfigDone({ config }: { config: AstroConfig }) {
config, config,
setAdapter(adapter) { setAdapter(adapter) {
if (config._ctx.adapter && config._ctx.adapter.name !== adapter.name) { if (config._ctx.adapter && config._ctx.adapter.name !== adapter.name) {
throw new Error(`Adapter already set to ${config._ctx.adapter.name}. You can only have one adapter.`); throw new Error(
`Adapter already set to ${config._ctx.adapter.name}. You can only have one adapter.`
);
} }
config._ctx.adapter = adapter; config._ctx.adapter = adapter;
}, },
@ -60,7 +68,13 @@ export async function runHookConfigDone({ config }: { config: AstroConfig }) {
} }
} }
export async function runHookServerSetup({ config, server }: { config: AstroConfig; server: ViteDevServer }) { export async function runHookServerSetup({
config,
server,
}: {
config: AstroConfig;
server: ViteDevServer;
}) {
for (const integration of config.integrations) { for (const integration of config.integrations) {
if (integration.hooks['astro:server:setup']) { if (integration.hooks['astro:server:setup']) {
await integration.hooks['astro:server:setup']({ server }); await integration.hooks['astro:server:setup']({ server });
@ -68,7 +82,13 @@ export async function runHookServerSetup({ config, server }: { config: AstroConf
} }
} }
export async function runHookServerStart({ config, address }: { config: AstroConfig; address: AddressInfo }) { export async function runHookServerStart({
config,
address,
}: {
config: AstroConfig;
address: AddressInfo;
}) {
for (const integration of config.integrations) { for (const integration of config.integrations) {
if (integration.hooks['astro:server:start']) { if (integration.hooks['astro:server:start']) {
await integration.hooks['astro:server:start']({ address }); await integration.hooks['astro:server:start']({ address });
@ -84,7 +104,13 @@ export async function runHookServerDone({ config }: { config: AstroConfig }) {
} }
} }
export async function runHookBuildStart({ config, buildConfig }: { config: AstroConfig; buildConfig: BuildConfig }) { export async function runHookBuildStart({
config,
buildConfig,
}: {
config: AstroConfig;
buildConfig: BuildConfig;
}) {
for (const integration of config.integrations) { for (const integration of config.integrations) {
if (integration.hooks['astro:build:start']) { if (integration.hooks['astro:build:start']) {
await integration.hooks['astro:build:start']({ buildConfig }); await integration.hooks['astro:build:start']({ buildConfig });
@ -92,7 +118,15 @@ export async function runHookBuildStart({ config, buildConfig }: { config: Astro
} }
} }
export async function runHookBuildSetup({ config, vite, target }: { config: AstroConfig; vite: ViteConfigWithSSR; target: 'server' | 'client' }) { export async function runHookBuildSetup({
config,
vite,
target,
}: {
config: AstroConfig;
vite: ViteConfigWithSSR;
target: 'server' | 'client';
}) {
for (const integration of config.integrations) { for (const integration of config.integrations) {
if (integration.hooks['astro:build:setup']) { if (integration.hooks['astro:build:setup']) {
await integration.hooks['astro:build:setup']({ vite, target }); await integration.hooks['astro:build:setup']({ vite, target });
@ -100,10 +134,22 @@ export async function runHookBuildSetup({ config, vite, target }: { config: Astr
} }
} }
export async function runHookBuildDone({ config, pages, routes }: { config: AstroConfig; pages: string[]; routes: RouteData[] }) { export async function runHookBuildDone({
config,
pages,
routes,
}: {
config: AstroConfig;
pages: string[];
routes: RouteData[];
}) {
for (const integration of config.integrations) { for (const integration of config.integrations) {
if (integration.hooks['astro:build:done']) { if (integration.hooks['astro:build:done']) {
await integration.hooks['astro:build:done']({ pages: pages.map((p) => ({ pathname: p })), dir: config.outDir, routes }); await integration.hooks['astro:build:done']({
pages: pages.map((p) => ({ pathname: p })),
dir: config.outDir,
routes,
});
} }
} }
} }

View file

@ -4,7 +4,11 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component as soon as the main thread is free * Hydrate this component as soon as the main thread is free
* (or after a short delay, if `requestIdleCallback`) isn't supported * (or after a short delay, if `requestIdleCallback`) isn't supported
*/ */
export default async function onIdle(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { export default async function onIdle(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
const cb = async () => { const cb = async () => {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
if (roots.length === 0) { if (roots.length === 0) {

View file

@ -3,7 +3,11 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
/** /**
* Hydrate this component immediately * Hydrate this component immediately
*/ */
export default async function onLoad(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { export default async function onLoad(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
if (roots.length === 0) { if (roots.length === 0) {
throw new Error(`Unable to find the root for the component ${options.name}`); throw new Error(`Unable to find the root for the component ${options.name}`);

View file

@ -3,7 +3,11 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
/** /**
* Hydrate this component when a matching media query is found * Hydrate this component when a matching media query is found
*/ */
export default async function onMedia(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { export default async function onMedia(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
if (roots.length === 0) { if (roots.length === 0) {
throw new Error(`Unable to find the root for the component ${options.name}`); throw new Error(`Unable to find the root for the component ${options.name}`);

View file

@ -3,7 +3,11 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
/** /**
* Hydrate this component immediately * Hydrate this component immediately
*/ */
export default async function onLoad(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { export default async function onLoad(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
if (roots.length === 0) { if (roots.length === 0) {
throw new Error(`Unable to find the root for the component ${options.name}`); throw new Error(`Unable to find the root for the component ${options.name}`);

View file

@ -5,7 +5,11 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* We target the children because `astro-root` is set to `display: contents` * We target the children because `astro-root` is set to `display: contents`
* which doesn't work with IntersectionObserver * which doesn't work with IntersectionObserver
*/ */
export default async function onVisible(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { export default async function onVisible(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
if (roots.length === 0) { if (roots.length === 0) {
throw new Error(`Unable to find the root for the component ${options.name}`); throw new Error(`Unable to find the root for the component ${options.name}`);

View file

@ -58,12 +58,21 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
// throw an error if an invalid hydration directive was provided // throw an error if an invalid hydration directive was provided
if (HydrationDirectives.indexOf(extracted.hydration.directive) < 0) { if (HydrationDirectives.indexOf(extracted.hydration.directive) < 0) {
throw new Error(`Error: invalid hydration directive "${key}". Supported hydration methods: ${HydrationDirectives.map((d) => `"client:${d}"`).join(', ')}`); throw new Error(
`Error: invalid hydration directive "${key}". Supported hydration methods: ${HydrationDirectives.map(
(d) => `"client:${d}"`
).join(', ')}`
);
} }
// throw an error if the query wasn't provided for client:media // throw an error if the query wasn't provided for client:media
if (extracted.hydration.directive === 'media' && typeof extracted.hydration.value !== 'string') { if (
throw new Error('Error: Media query must be provided for "client:media", similar to client:media="(max-width: 600px)"'); extracted.hydration.directive === 'media' &&
typeof extracted.hydration.value !== 'string'
) {
throw new Error(
'Error: Media query must be provided for "client:media", similar to client:media="(max-width: 600px)"'
);
} }
break; break;
@ -88,20 +97,27 @@ interface HydrateScriptOptions {
} }
/** For hydrated components, generate a <script type="module"> to load the component */ /** For hydrated components, generate a <script type="module"> to load the component */
export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>): Promise<SSRElement> { export async function generateHydrateScript(
scriptOptions: HydrateScriptOptions,
metadata: Required<AstroComponentMetadata>
): Promise<SSRElement> {
const { renderer, result, astroId, props } = scriptOptions; const { renderer, result, astroId, props } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata; const { hydrate, componentUrl, componentExport } = metadata;
if (!componentExport) { if (!componentExport) {
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`); throw new Error(
`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`
);
} }
let hydrationSource = ``; let hydrationSource = ``;
hydrationSource += renderer.clientEntrypoint hydrationSource += renderer.clientEntrypoint
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(componentUrl)}"), import("${await result.resolve( ? `const [{ ${
renderer.clientEntrypoint componentExport.value
)}")]); }: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(
componentUrl
)}"), import("${await result.resolve(renderer.clientEntrypoint)}")]);
return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children); return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
` `
: `await import("${await result.resolve(componentUrl)}"); : `await import("${await result.resolve(componentUrl)}");
@ -113,7 +129,9 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
props: { type: 'module', 'data-astro-component-hydration': true }, props: { type: 'module', 'data-astro-component-hydration': true },
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}'; children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
${`import '${await result.resolve('astro:scripts/before-hydration.js')}';`} ${`import '${await result.resolve('astro:scripts/before-hydration.js')}';`}
setup("${astroId}", {name:"${metadata.displayName}",${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => { setup("${astroId}", {name:"${metadata.displayName}",${
metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''
}}, async () => {
${hydrationSource} ${hydrationSource}
}); });
`, `,

View file

@ -1,5 +1,13 @@
import shorthash from 'shorthash'; import shorthash from 'shorthash';
import type { AstroComponentMetadata, AstroGlobalPartial, EndpointHandler, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro'; import type {
AstroComponentMetadata,
AstroGlobalPartial,
EndpointHandler,
Params,
SSRElement,
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
import { escapeHTML, HTMLString, markHTMLString } from './escape.js'; import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js'; import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
import { serializeListValue } from './util.js'; import { serializeListValue } from './util.js';
@ -8,7 +16,8 @@ export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
export type { Metadata } from './metadata'; export type { Metadata } from './metadata';
export { createMetadata } from './metadata.js'; export { createMetadata } from './metadata.js';
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const voidElementNames =
/^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes = const htmlBooleanAttributes =
/^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i; /^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i;
const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i; const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
@ -40,7 +49,10 @@ async function _render(child: any): Promise<any> {
} }
// Add a comment explaining why each of these are needed. // Add a comment explaining why each of these are needed.
// Maybe create clearly named function for what this is doing. // Maybe create clearly named function for what this is doing.
else if (child instanceof AstroComponent || Object.prototype.toString.call(child) === '[object AstroComponent]') { else if (
child instanceof AstroComponent ||
Object.prototype.toString.call(child) === '[object AstroComponent]'
) {
return markHTMLString(await renderAstroComponent(child)); return markHTMLString(await renderAstroComponent(child));
} else { } else {
return child; return child;
@ -76,7 +88,9 @@ export class AstroComponent {
} }
function isAstroComponent(obj: any): obj is AstroComponent { function isAstroComponent(obj: any): obj is AstroComponent {
return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'; return (
typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'
);
} }
export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) { export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) {
@ -128,7 +142,13 @@ function formatList(values: string[]): string {
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`; return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
} }
export async function renderComponent(result: SSRResult, displayName: string, Component: unknown, _props: Record<string | number, any>, slots: any = {}) { export async function renderComponent(
result: SSRResult,
displayName: string,
Component: unknown,
_props: Record<string | number, any>,
slots: any = {}
) {
Component = await Component; Component = await Component;
const children = await renderSlot(result, slots?.default); const children = await renderSlot(result, slots?.default);
@ -145,7 +165,9 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
} }
if (Component === null && !_props['client:only']) { if (Component === null && !_props['client:only']) {
throw new Error(`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`); throw new Error(
`Unable to render ${displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`
);
} }
const { renderers } = result._metadata; const { renderers } = result._metadata;
@ -162,7 +184,12 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
} }
const probableRendererNames = guessRenderers(metadata.componentUrl); const probableRendererNames = guessRenderers(metadata.componentUrl);
if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !componentIsHTMLElement(Component)) { if (
Array.isArray(renderers) &&
renderers.length === 0 &&
typeof Component !== 'string' &&
!componentIsHTMLElement(Component)
) {
const message = `Unable to render ${metadata.displayName}! const message = `Unable to render ${metadata.displayName}!
There are no \`integrations\` set in your \`astro.config.mjs\` file. There are no \`integrations\` set in your \`astro.config.mjs\` file.
@ -189,7 +216,9 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
// Attempt: use explicitly passed renderer name // Attempt: use explicitly passed renderer name
if (metadata.hydrateArgs) { if (metadata.hydrateArgs) {
const rendererName = metadata.hydrateArgs; const rendererName = metadata.hydrateArgs;
renderer = renderers.filter(({ name }) => name === `@astrojs/${rendererName}` || name === rendererName)[0]; renderer = renderers.filter(
({ name }) => name === `@astrojs/${rendererName}` || name === rendererName
)[0];
} }
// Attempt: user only has a single renderer, default to that // Attempt: user only has a single renderer, default to that
if (!renderer && renderers.length === 1) { if (!renderer && renderers.length === 1) {
@ -198,7 +227,9 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
// Attempt: can we guess the renderer from the export extension? // Attempt: can we guess the renderer from the export extension?
if (!renderer) { if (!renderer) {
const extname = metadata.componentUrl?.split('.').pop(); const extname = metadata.componentUrl?.split('.').pop();
renderer = renderers.filter(({ name }) => name === `@astrojs/${extname}` || name === extname)[0]; renderer = renderers.filter(
({ name }) => name === `@astrojs/${extname}` || name === extname
)[0];
} }
} }
@ -209,7 +240,9 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
throw new Error(`Unable to render ${metadata.displayName}! throw new Error(`Unable to render ${metadata.displayName}!
Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer. Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.
Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames.map((r) => r.replace('@astrojs/', '')).join('|')}" /> Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames
.map((r) => r.replace('@astrojs/', ''))
.join('|')}" />
`); `);
} else if (typeof Component !== 'string') { } else if (typeof Component !== 'string') {
const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name)); const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name));
@ -217,7 +250,9 @@ Did you mean to pass <${metadata.displayName} client:only="${probableRendererNam
if (matchingRenderers.length === 0) { if (matchingRenderers.length === 0) {
throw new Error(`Unable to render ${metadata.displayName}! throw new Error(`Unable to render ${metadata.displayName}!
There ${plural ? 'are' : 'is'} ${renderers.length} renderer${plural ? 's' : ''} configured in your \`astro.config.mjs\` file, There ${plural ? 'are' : 'is'} ${renderers.length} renderer${
plural ? 's' : ''
} configured in your \`astro.config.mjs\` file,
but ${plural ? 'none were' : 'it was not'} able to server-side render ${metadata.displayName}. but ${plural ? 'none were' : 'it was not'} able to server-side render ${metadata.displayName}.
Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`); Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`);
@ -253,7 +288,9 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
if (!html && typeof Component === 'string') { if (!html && typeof Component === 'string') {
html = await renderAstroComponent( html = await renderAstroComponent(
await render`<${Component}${spreadAttributes(props)}${markHTMLString( await render`<${Component}${spreadAttributes(props)}${markHTMLString(
(children == null || children == '') && voidElementNames.test(Component) ? `/>` : `>${children}</${Component}>` (children == null || children == '') && voidElementNames.test(Component)
? `/>`
: `>${children}</${Component}>`
)}` )}`
); );
} }
@ -263,16 +300,29 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
} }
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands // Include componentExport name, componentUrl, and props in hash to dedupe identical islands
const astroId = shorthash.unique(`<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(props)}`); const astroId = shorthash.unique(
`<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(
props
)}`
);
// Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head. // Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point. // INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
result.scripts.add(await generateHydrateScript({ renderer: renderer!, result, astroId, props }, metadata as Required<AstroComponentMetadata>)); result.scripts.add(
await generateHydrateScript(
{ renderer: renderer!, result, astroId, props },
metadata as Required<AstroComponentMetadata>
)
);
// Render a template if no fragment is provided. // Render a template if no fragment is provided.
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html); const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : ''; const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
return markHTMLString(`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${html ?? ''}${template}</astro-root>`); return markHTMLString(
`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${
html ?? ''
}${template}</astro-root>`
);
} }
/** Create the Astro.fetchContent() runtime function. */ /** Create the Astro.fetchContent() runtime function. */
@ -299,7 +349,11 @@ function createAstroGlobFn() {
// This is used to create the top-level Astro global; the one that you can use // This is used to create the top-level Astro global; the one that you can use
// Inside of getStaticPaths. // Inside of getStaticPaths.
export function createAstro(filePathname: string, _site: string, projectRootStr: string): AstroGlobalPartial { export function createAstro(
filePathname: string,
_site: string,
projectRootStr: string
): AstroGlobalPartial {
const site = new URL(_site); const site = new URL(_site);
const url = new URL(filePathname, site); const url = new URL(filePathname, site);
const projectRoot = new URL(projectRootStr); const projectRoot = new URL(projectRootStr);
@ -320,7 +374,8 @@ export function createAstro(filePathname: string, _site: string, projectRootStr:
}; };
} }
const toAttributeString = (value: any, shouldEscape = true) => (shouldEscape ? String(value).replace(/&/g, '&#38;').replace(/"/g, '&#34;') : value); const toAttributeString = (value: any, shouldEscape = true) =>
shouldEscape ? String(value).replace(/&/g, '&#38;').replace(/"/g, '&#34;') : value;
const STATIC_DIRECTIVES = new Set(['set:html', 'set:text']); const STATIC_DIRECTIVES = new Set(['set:html', 'set:text']);
@ -392,7 +447,9 @@ export async function renderEndpoint(mod: EndpointHandler, request: Request, par
const handler = mod[chosenMethod]; const handler = mod[chosenMethod];
if (!handler || typeof handler !== 'function') { if (!handler || typeof handler !== 'function') {
throw new Error(`Endpoint handler not found! Expected an exported function for "${chosenMethod}"`); throw new Error(
`Endpoint handler not found! Expected an exported function for "${chosenMethod}"`
);
} }
return await handler.call(mod, params, request); return await handler.call(mod, params, request);
@ -409,7 +466,12 @@ async function replaceHeadInjection(result: SSRResult, html: string): Promise<st
} }
// Calls a component and renders it into a string of HTML // Calls a component and renders it into a string of HTML
export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any): Promise<string> { export async function renderToString(
result: SSRResult,
componentFactory: AstroComponentFactory,
props: any,
children: any
): Promise<string> {
const Component = await componentFactory(result, props, children); const Component = await componentFactory(result, props, children);
if (!isAstroComponent(Component)) { if (!isAstroComponent(Component)) {
const response: Response = Component; const response: Response = Component;
@ -458,7 +520,9 @@ export async function renderPage(
const uniqueElements = (item: any, index: number, all: any[]) => { const uniqueElements = (item: any, index: number, all: any[]) => {
const props = JSON.stringify(item.props); const props = JSON.stringify(item.props);
const children = item.children; const children = item.children;
return index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children); return (
index === all.findIndex((i) => JSON.stringify(i.props) === props && i.children == children)
);
}; };
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending // Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
@ -487,12 +551,19 @@ export async function renderHead(result: SSRResult): Promise<string> {
}); });
}); });
if (needsHydrationStyles) { if (needsHydrationStyles) {
styles.push(renderElement('style', { props: { 'astro-style': true }, children: 'astro-root, astro-fragment { display: contents; }' })); styles.push(
renderElement('style', {
props: { 'astro-style': true },
children: 'astro-root, astro-fragment { display: contents; }',
})
);
} }
const links = Array.from(result.links) const links = Array.from(result.links)
.filter(uniqueElements) .filter(uniqueElements)
.map((link) => renderElement('link', link, false)); .map((link) => renderElement('link', link, false));
return markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->'); return markHTMLString(
links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->'
);
} }
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) { export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
@ -511,7 +582,12 @@ function componentIsHTMLElement(Component: unknown) {
return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object); return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object);
} }
export async function renderHTMLElement(result: SSRResult, constructor: typeof HTMLElement, props: any, slots: any) { export async function renderHTMLElement(
result: SSRResult,
constructor: typeof HTMLElement,
props: any,
slots: any
) {
const name = getHTMLElementName(constructor); const name = getHTMLElementName(constructor);
let attrHTML = ''; let attrHTML = '';
@ -520,11 +596,15 @@ export async function renderHTMLElement(result: SSRResult, constructor: typeof H
attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`; attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
} }
return markHTMLString(`<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`); return markHTMLString(
`<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`
);
} }
function getHTMLElementName(constructor: typeof HTMLElement) { function getHTMLElementName(constructor: typeof HTMLElement) {
const definedName = (customElements as CustomElementRegistry & { getName(_constructor: typeof HTMLElement): string }).getName(constructor); const definedName = (
customElements as CustomElementRegistry & { getName(_constructor: typeof HTMLElement): string }
).getName(constructor);
if (definedName) return definedName; if (definedName) return definedName;
const assignedName = constructor.name const assignedName = constructor.name
@ -535,7 +615,11 @@ function getHTMLElementName(constructor: typeof HTMLElement) {
return assignedName; return assignedName;
} }
function renderElement(name: string, { props: _props, children = '' }: SSRElement, shouldEscape = true) { function renderElement(
name: string,
{ props: _props, children = '' }: SSRElement,
shouldEscape = true
) {
// Do not print `hoist`, `lang`, `is:global` // Do not print `hoist`, `lang`, `is:global`
const { lang: _, 'data-astro-id': astroId, 'define:vars': defineVars, ...props } = _props; const { lang: _, 'data-astro-id': astroId, 'define:vars': defineVars, ...props } = _props;
if (defineVars) { if (defineVars) {

View file

@ -15,7 +15,13 @@ interface ErrorTemplateOptions {
} }
/** Display all errors */ /** Display all errors */
export default function template({ title, pathname, statusCode = 404, tabTitle, body }: ErrorTemplateOptions): string { export default function template({
title,
pathname,
statusCode = 404,
tabTitle,
body,
}: ErrorTemplateOptions): string {
return `<!doctype html> return `<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@ -46,7 +52,9 @@ export default function template({ title, pathname, statusCode = 404, tabTitle,
<body> <body>
<main class="center"> <main class="center">
<svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg> <svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg>
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''}<span class="statusMessage">${title}</span></h1> <h1>${
statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''
}<span class="statusMessage">${title}</span></h1>
${ ${
body || body ||
` `

View file

@ -17,7 +17,14 @@ interface ErrorTemplateOptions {
} }
/** Display all errors */ /** Display all errors */
export default function template({ title, url, message, stack, statusCode, tabTitle }: ErrorTemplateOptions): string { export default function template({
title,
url,
message,
stack,
statusCode,
tabTitle,
}: ErrorTemplateOptions): string {
let error = url ? message.replace(url, '') : message; let error = url ? message.replace(url, '') : message;
return `<!doctype html> return `<!doctype html>
<html lang="en"> <html lang="en">
@ -60,7 +67,9 @@ export default function template({ title, url, message, stack, statusCode, tabTi
<main class="wrapper"> <main class="wrapper">
<header> <header>
<svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg> <svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg>
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''}<span class="statusMessage">${title}</span></h1> <h1>${
statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''
}<span class="statusMessage">${title}</span></h1>
</header> </header>
<pre>${encode(error)}</pre> <pre>${encode(error)}</pre>
${url ? `<a target="_blank" href="${url}">${url}</a>` : ''} ${url ? `<a target="_blank" href="${url}">${url}</a>` : ''}

View file

@ -1,5 +1,9 @@
import { parse as babelParser } from '@babel/parser'; import { parse as babelParser } from '@babel/parser';
import type { ArrowFunctionExpressionKind, CallExpressionKind, StringLiteralKind } from 'ast-types/gen/kinds'; import type {
ArrowFunctionExpressionKind,
CallExpressionKind,
StringLiteralKind,
} from 'ast-types/gen/kinds';
import type { NodePath } from 'ast-types/lib/node-path'; import type { NodePath } from 'ast-types/lib/node-path';
import { parse, print, types, visit } from 'recast'; import { parse, print, types, visit } from 'recast';
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
@ -58,7 +62,11 @@ export default function astro({ config }: AstroPluginOptions): Plugin {
type: 'CallExpression', type: 'CallExpression',
callee: { callee: {
type: 'MemberExpression', type: 'MemberExpression',
object: { type: 'MetaProperty', meta: { type: 'Identifier', name: 'import' }, property: { type: 'Identifier', name: 'meta' } }, object: {
type: 'MetaProperty',
meta: { type: 'Identifier', name: 'import' },
property: { type: 'Identifier', name: 'meta' },
},
property: { type: 'Identifier', name: 'glob' }, property: { type: 'Identifier', name: 'glob' },
computed: false, computed: false,
}, },

View file

@ -22,7 +22,11 @@ interface AstroPluginOptions {
logging: LogOptions; logging: LogOptions;
} }
const BAD_VITE_MIDDLEWARE = ['viteIndexHtmlMiddleware', 'vite404Middleware', 'viteSpaFallbackMiddleware']; const BAD_VITE_MIDDLEWARE = [
'viteIndexHtmlMiddleware',
'vite404Middleware',
'viteSpaFallbackMiddleware',
];
function removeViteHttpMiddleware(server: vite.Connect.Server) { function removeViteHttpMiddleware(server: vite.Connect.Server) {
for (let i = server.stack.length - 1; i > 0; i--) { for (let i = server.stack.length - 1; i > 0; i--) {
// @ts-expect-error using internals until https://github.com/vitejs/vite/pull/4640 is merged // @ts-expect-error using internals until https://github.com/vitejs/vite/pull/4640 is merged
@ -62,7 +66,11 @@ async function writeWebResponse(res: http.ServerResponse, webResponse: Response)
res.end(); res.end();
} }
async function writeSSRResult(result: RenderResponse, res: http.ServerResponse, statusCode: 200 | 404) { async function writeSSRResult(
result: RenderResponse,
res: http.ServerResponse,
statusCode: 200 | 404
) {
if (result.type === 'response') { if (result.type === 'response') {
const { response } = result; const { response } = result;
await writeWebResponse(res, response); await writeWebResponse(res, response);
@ -73,7 +81,12 @@ async function writeSSRResult(result: RenderResponse, res: http.ServerResponse,
writeHtmlResponse(res, statusCode, html); writeHtmlResponse(res, statusCode, html);
} }
async function handle404Response(origin: string, config: AstroConfig, req: http.IncomingMessage, res: http.ServerResponse) { async function handle404Response(
origin: string,
config: AstroConfig,
req: http.IncomingMessage,
res: http.ServerResponse
) {
const site = config.site ? new URL(config.base, config.site) : undefined; const site = config.site ? new URL(config.base, config.site) : undefined;
const devRoot = site ? site.pathname : '/'; const devRoot = site ? site.pathname : '/';
const pathname = decodeURI(new URL(origin + req.url).pathname); const pathname = decodeURI(new URL(origin + req.url).pathname);
@ -81,12 +94,23 @@ async function handle404Response(origin: string, config: AstroConfig, req: http.
if (pathname === '/' && !pathname.startsWith(devRoot)) { if (pathname === '/' && !pathname.startsWith(devRoot)) {
html = subpathNotUsedTemplate(devRoot, pathname); html = subpathNotUsedTemplate(devRoot, pathname);
} else { } else {
html = notFoundTemplate({ statusCode: 404, title: 'Not found', tabTitle: '404: Not Found', pathname }); html = notFoundTemplate({
statusCode: 404,
title: 'Not found',
tabTitle: '404: Not Found',
pathname,
});
} }
writeHtmlResponse(res, 404, html); writeHtmlResponse(res, 404, html);
} }
async function handle500Response(viteServer: vite.ViteDevServer, origin: string, req: http.IncomingMessage, res: http.ServerResponse, err: any) { async function handle500Response(
viteServer: vite.ViteDevServer,
origin: string,
req: http.IncomingMessage,
res: http.ServerResponse,
err: any
) {
const pathname = decodeURI(new URL(origin + req.url).pathname); const pathname = decodeURI(new URL(origin + req.url).pathname);
const html = serverErrorTemplate({ const html = serverErrorTemplate({
statusCode: 500, statusCode: 500,
@ -188,12 +212,20 @@ async function handleRequest(
ssr: isBuildingToSSR(config), ssr: isBuildingToSSR(config),
}); });
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) { if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
warn(logging, 'getStaticPaths', `Route pattern matched, but no matching static path found. (${pathname})`); warn(
logging,
'getStaticPaths',
`Route pattern matched, but no matching static path found. (${pathname})`
);
log404(logging, pathname); log404(logging, pathname);
const routeCustom404 = getCustom404Route(config, manifest); const routeCustom404 = getCustom404Route(config, manifest);
if (routeCustom404) { if (routeCustom404) {
const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.root); const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.root);
const preloadedCompCustom404 = await preload({ astroConfig: config, filePath: filePathCustom404, viteServer }); const preloadedCompCustom404 = await preload({
astroConfig: config,
filePath: filePathCustom404,
viteServer,
});
const result = await ssr(preloadedCompCustom404, { const result = await ssr(preloadedCompCustom404, {
astroConfig: config, astroConfig: config,
filePath: filePathCustom404, filePath: filePathCustom404,

View file

@ -34,7 +34,13 @@ function safelyReplaceImportPlaceholder(code: string) {
const configCache = new WeakMap<AstroConfig, CompilationCache>(); const configCache = new WeakMap<AstroConfig, CompilationCache>();
async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> { async function compile(
config: AstroConfig,
filename: string,
source: string,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
const filenameURL = new URL(`file://${filename}`); const filenameURL = new URL(`file://${filename}`);
const normalizedID = fileURLToPath(filenameURL); const normalizedID = fileURLToPath(filenameURL);
const pathname = filenameURL.pathname.substr(config.root.pathname.length - 1); const pathname = filenameURL.pathname.substr(config.root.pathname.length - 1);
@ -51,7 +57,9 @@ async function compile(config: AstroConfig, filename: string, source: string, vi
site: config.site ? new URL(config.base, config.site).toString() : undefined, site: config.site ? new URL(config.base, config.site).toString() : undefined,
sourcefile: filename, sourcefile: filename,
sourcemap: 'both', sourcemap: 'both',
internalURL: `/@fs${prependForwardSlash(viteID(new URL('../runtime/server/index.js', import.meta.url)))}`, internalURL: `/@fs${prependForwardSlash(
viteID(new URL('../runtime/server/index.js', import.meta.url))
)}`,
// TODO: baseline flag // TODO: baseline flag
experimentalStaticExtraction: true, experimentalStaticExtraction: true,
preprocessStyle: async (value: string, attrs: Record<string, string>) => { preprocessStyle: async (value: string, attrs: Record<string, string>) => {
@ -59,7 +67,9 @@ async function compile(config: AstroConfig, filename: string, source: string, vi
try { try {
// In the static build, grab any @import as CSS dependencies for HMR. // In the static build, grab any @import as CSS dependencies for HMR.
value.replace(/(?:@import)\s(?:url\()?\s?["\'](.*?)["\']\s?\)?(?:[^;]*);?/gi, (match, spec) => { value.replace(
/(?:@import)\s(?:url\()?\s?["\'](.*?)["\']\s?\)?(?:[^;]*);?/gi,
(match, spec) => {
rawCSSDeps.add(spec); rawCSSDeps.add(spec);
// If the language is CSS: prevent `@import` inlining to prevent scoping of imports. // If the language is CSS: prevent `@import` inlining to prevent scoping of imports.
// Otherwise: Sass, etc. need to see imports for variables, so leave in for their compiler to handle. // Otherwise: Sass, etc. need to see imports for variables, so leave in for their compiler to handle.
@ -68,7 +78,8 @@ async function compile(config: AstroConfig, filename: string, source: string, vi
} else { } else {
return match; return match;
} }
}); }
);
const result = await transformWithVite({ const result = await transformWithVite({
value, value,
@ -120,7 +131,13 @@ export function invalidateCompilation(config: AstroConfig, filename: string) {
} }
} }
export async function cachedCompilation(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: { ssr: boolean }): Promise<CompileResult> { export async function cachedCompilation(
config: AstroConfig,
filename: string,
source: string,
viteTransform: TransformHook,
opts: { ssr: boolean }
): Promise<CompileResult> {
let cache: CompilationCache; let cache: CompilationCache;
if (!configCache.has(config)) { if (!configCache.has(config)) {
cache = new Map(); cache = new Map();

View file

@ -13,7 +13,10 @@ interface TrackCSSDependenciesOptions {
deps: Set<string>; deps: Set<string>;
} }
export async function trackCSSDependencies(this: RollupPluginContext, opts: TrackCSSDependenciesOptions): Promise<void> { export async function trackCSSDependencies(
this: RollupPluginContext,
opts: TrackCSSDependenciesOptions
): Promise<void> {
const { viteDevServer, filename, deps, id } = opts; const { viteDevServer, filename, deps, id } = opts;
// Dev, register CSS dependencies for HMR. // Dev, register CSS dependencies for HMR.
if (viteDevServer) { if (viteDevServer) {

View file

@ -105,10 +105,17 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
throw new Error(`Requests for Astro CSS must include an index.`); throw new Error(`Requests for Astro CSS must include an index.`);
} }
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) }); const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
// Track any CSS dependencies so that HMR is triggered when they change. // Track any CSS dependencies so that HMR is triggered when they change.
await trackCSSDependencies.call(this, { viteDevServer, id, filename, deps: transformResult.rawCSSDeps }); await trackCSSDependencies.call(this, {
viteDevServer,
id,
filename,
deps: transformResult.rawCSSDeps,
});
const csses = transformResult.css; const csses = transformResult.css;
const code = csses[query.index]; const code = csses[query.index];
@ -120,7 +127,9 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
throw new Error(`Requests for hoisted scripts must include an index`); throw new Error(`Requests for hoisted scripts must include an index`);
} }
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) }); const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
const scripts = transformResult.scripts; const scripts = transformResult.scripts;
const hoistedScript = scripts[query.index]; const hoistedScript = scripts[query.index];
@ -139,13 +148,18 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
} }
return { return {
code: hoistedScript.type === 'inline' ? hoistedScript.code! : `import "${hoistedScript.src!}";`, code:
hoistedScript.type === 'inline'
? hoistedScript.code!
: `import "${hoistedScript.src!}";`,
}; };
} }
} }
try { try {
const transformResult = await cachedCompilation(config, filename, source, viteTransform, { ssr: Boolean(opts?.ssr) }); const transformResult = await cachedCompilation(config, filename, source, viteTransform, {
ssr: Boolean(opts?.ssr),
});
// Compile all TypeScript to JavaScript. // Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning. // Also, catches invalid JS/TS in the compiled output before returning.
@ -182,12 +196,19 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
const scannedFrontmatter = FRONTMATTER_PARSE_REGEXP.exec(source); const scannedFrontmatter = FRONTMATTER_PARSE_REGEXP.exec(source);
if (scannedFrontmatter) { if (scannedFrontmatter) {
try { try {
await esbuild.transform(scannedFrontmatter[1], { loader: 'ts', sourcemap: false, sourcefile: id }); await esbuild.transform(scannedFrontmatter[1], {
loader: 'ts',
sourcemap: false,
sourcefile: id,
});
} catch (frontmatterErr: any) { } catch (frontmatterErr: any) {
// Improve the error by replacing the phrase "unexpected end of file" // Improve the error by replacing the phrase "unexpected end of file"
// with "unexpected end of frontmatter" in the esbuild error message. // with "unexpected end of frontmatter" in the esbuild error message.
if (frontmatterErr && frontmatterErr.message) { if (frontmatterErr && frontmatterErr.message) {
frontmatterErr.message = frontmatterErr.message.replace('end of file', 'end of frontmatter'); frontmatterErr.message = frontmatterErr.message.replace(
'end of file',
'end of frontmatter'
);
} }
throw frontmatterErr; throw frontmatterErr;
} }

View file

@ -2,7 +2,11 @@ import type * as vite from 'vite';
import { STYLE_EXTENSIONS } from '../core/render/util.js'; import { STYLE_EXTENSIONS } from '../core/render/util.js';
export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>; export type TransformHook = (
code: string,
id: string,
ssr?: boolean
) => Promise<vite.TransformResult>;
/** Load vite:css transform() hook */ /** Load vite:css transform() hook */
export function getViteTransform(viteConfig: vite.ResolvedConfig): TransformHook { export function getViteTransform(viteConfig: vite.ResolvedConfig): TransformHook {
@ -21,7 +25,13 @@ interface TransformWithViteOptions {
} }
/** Transform style using Vite hook */ /** Transform style using Vite hook */
export async function transformWithVite({ value, lang, transformHook, id, ssr }: TransformWithViteOptions): Promise<vite.TransformResult | null> { export async function transformWithVite({
value,
lang,
transformHook,
id,
ssr,
}: TransformWithViteOptions): Promise<vite.TransformResult | null> {
if (!STYLE_EXTENSIONS.has(lang)) { if (!STYLE_EXTENSIONS.has(lang)) {
return null; // only preprocess langs supported by Vite return null; // only preprocess langs supported by Vite
} }

View file

@ -5,7 +5,11 @@ import * as path from 'path';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import { Plugin as VitePlugin } from 'vite'; import { Plugin as VitePlugin } from 'vite';
import { isCSSRequest } from '../core/render/util.js'; import { isCSSRequest } from '../core/render/util.js';
import { getPageDatasByChunk, getPageDataByViteID, hasPageDataByViteID } from '../core/build/internal.js'; import {
getPageDatasByChunk,
getPageDataByViteID,
hasPageDataByViteID,
} from '../core/build/internal.js';
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css'; const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
@ -52,7 +56,11 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
const { internals, legacy } = options; const { internals, legacy } = options;
const styleSourceMap = new Map<string, string>(); const styleSourceMap = new Map<string, string>();
function* walkStyles(ctx: PluginContext, id: string, seen = new Set<string>()): Generator<[string, string], void, unknown> { function* walkStyles(
ctx: PluginContext,
id: string,
seen = new Set<string>()
): Generator<[string, string], void, unknown> {
seen.add(id); seen.add(id);
if (styleSourceMap.has(id)) { if (styleSourceMap.has(id)) {
yield [id, styleSourceMap.get(id)!]; yield [id, styleSourceMap.get(id)!];
@ -203,12 +211,19 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
// Delete CSS chunks so JS is not produced for them. // Delete CSS chunks so JS is not produced for them.
async generateBundle(opts, bundle) { async generateBundle(opts, bundle) {
const hasPureCSSChunks = internals.pureCSSChunks.size; const hasPureCSSChunks = internals.pureCSSChunks.size;
const pureChunkFilenames = new Set([...internals.pureCSSChunks].map((chunk) => chunk.fileName)); const pureChunkFilenames = new Set(
[...internals.pureCSSChunks].map((chunk) => chunk.fileName)
);
const emptyChunkFiles = [...pureChunkFilenames] const emptyChunkFiles = [...pureChunkFilenames]
.map((file) => path.basename(file)) .map((file) => path.basename(file))
.join('|') .join('|')
.replace(/\./g, '\\.'); .replace(/\./g, '\\.');
const emptyChunkRE = new RegExp(opts.format === 'es' ? `\\bimport\\s*"[^"]*(?:${emptyChunkFiles})";\n?` : `\\brequire\\(\\s*"[^"]*(?:${emptyChunkFiles})"\\);\n?`, 'g'); const emptyChunkRE = new RegExp(
opts.format === 'es'
? `\\bimport\\s*"[^"]*(?:${emptyChunkFiles})";\n?`
: `\\brequire\\(\\s*"[^"]*(?:${emptyChunkFiles})"\\);\n?`,
'g'
);
// Crawl the module graph to find CSS chunks to create // Crawl the module graph to find CSS chunks to create
if (!legacy) { if (!legacy) {

View file

@ -12,7 +12,9 @@ function getSrcSetUrls(srcset: string) {
return []; return [];
} }
const srcsetParts = srcset.includes(',') ? srcset.split(',') : [srcset]; const srcsetParts = srcset.includes(',') ? srcset.split(',') : [srcset];
const urls = srcsetParts.map((url) => url.trim()).map((url) => (url.includes(' ') ? url.split(' ')[0] : url)); const urls = srcsetParts
.map((url) => url.trim())
.map((url) => (url.includes(' ') ? url.split(' ')[0] : url));
return urls; return urls;
} }
@ -116,9 +118,20 @@ export function isHashedAsset(node: Element) {
} }
} }
export function resolveAssetFilePath(browserPath: string, htmlDir: string, projectRootDir: string, absolutePathPrefix?: string) { export function resolveAssetFilePath(
const _browserPath = absolutePathPrefix && browserPath[0] === '/' ? '/' + npath.posix.relative(absolutePathPrefix, browserPath) : browserPath; browserPath: string,
return npath.join(_browserPath.startsWith('/') ? projectRootDir : htmlDir, _browserPath.split('/').join(npath.sep)); htmlDir: string,
projectRootDir: string,
absolutePathPrefix?: string
) {
const _browserPath =
absolutePathPrefix && browserPath[0] === '/'
? '/' + npath.posix.relative(absolutePathPrefix, browserPath)
: browserPath;
return npath.join(
_browserPath.startsWith('/') ? projectRootDir : htmlDir,
_browserPath.split('/').join(npath.sep)
);
} }
export function getSourceAttribute(node: Element) { export function getSourceAttribute(node: Element) {

View file

@ -1,4 +1,12 @@
import { createElement, createScript, getAttribute, hasAttribute, insertBefore, remove, setAttribute } from '@web/parse5-utils'; import {
createElement,
createScript,
getAttribute,
hasAttribute,
insertBefore,
remove,
setAttribute,
} from '@web/parse5-utils';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import parse5 from 'parse5'; import parse5 from 'parse5';
import * as npath from 'path'; import * as npath from 'path';
@ -15,8 +23,21 @@ import { RouteCache } from '../core/render/route-cache.js';
import { getOutputFilename } from '../core/util.js'; import { getOutputFilename } from '../core/util.js';
import { getAstroPageStyleId, getAstroStyleId } from '../vite-plugin-build-css/index.js'; import { getAstroPageStyleId, getAstroStyleId } from '../vite-plugin-build-css/index.js';
import { addRollupInput } from './add-rollup-input.js'; import { addRollupInput } from './add-rollup-input.js';
import { findAssets, findExternalScripts, findInlineScripts, findInlineStyles, getAttributes, getTextContent } from './extract-assets.js'; import {
import { hasSrcSet, isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory } from './util.js'; findAssets,
findExternalScripts,
findInlineScripts,
findInlineStyles,
getAttributes,
getTextContent,
} from './extract-assets.js';
import {
hasSrcSet,
isBuildableImage,
isBuildableLink,
isHoistedScript,
isInSrcDirectory,
} from './util.js';
import { createRequest } from '../core/request.js'; import { createRequest } from '../core/request.js';
// This package isn't real ESM, so have to coerce it // This package isn't real ESM, so have to coerce it
@ -45,7 +66,8 @@ function relativePath(from: string, to: string): string {
} }
export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin { export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin {
const { astroConfig, internals, logging, origin, allPages, routeCache, viteServer, pageNames } = options; const { astroConfig, internals, logging, origin, allPages, routeCache, viteServer, pageNames } =
options;
// The filepath root of the src folder // The filepath root of the src folder
const srcRoot = astroConfig.srcDir.pathname; const srcRoot = astroConfig.srcDir.pathname;
@ -322,7 +344,12 @@ export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin {
// Keep track of links added so we don't do so twice. // Keep track of links added so we don't do so twice.
const linkChunksAdded = new Set<string>(); const linkChunksAdded = new Set<string>();
const appendStyleChunksBefore = (ref: parse5.Element, pathname: string, referenceIds: string[] | undefined, attrs: Record<string, any> = {}) => { const appendStyleChunksBefore = (
ref: parse5.Element,
pathname: string,
referenceIds: string[] | undefined,
attrs: Record<string, any> = {}
) => {
let added = false; let added = false;
if (referenceIds) { if (referenceIds) {
const lastNode = ref; const lastNode = ref;
@ -433,7 +460,12 @@ export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin {
if (!pageCSSAdded) { if (!pageCSSAdded) {
const attrs = getAttributes(node); const attrs = getAttributes(node);
delete attrs['data-astro-injected']; delete attrs['data-astro-injected'];
pageCSSAdded = appendStyleChunksBefore(node, pathname, cssChunkMap.get(styleId), attrs); pageCSSAdded = appendStyleChunksBefore(
node,
pathname,
cssChunkMap.get(styleId),
attrs
);
} }
remove(node); remove(node);
break; break;

View file

@ -12,7 +12,12 @@ function startsWithSrcRoot(pathname: string, srcRoot: string, srcRootWeb: string
); // Windows fix: some paths are missing leading "/" ); // Windows fix: some paths are missing leading "/"
} }
export function isInSrcDirectory(node: parse5.Element, attr: string, srcRoot: string, srcRootWeb: string): boolean { export function isInSrcDirectory(
node: parse5.Element,
attr: string,
srcRoot: string,
srcRootWeb: string
): boolean {
const value = getAttribute(node, attr); const value = getAttribute(node, attr);
return value ? startsWithSrcRoot(value, srcRoot, srcRootWeb) : false; return value ? startsWithSrcRoot(value, srcRoot, srcRootWeb) : false;
} }
@ -21,7 +26,11 @@ export function isAstroInjectedLink(node: parse5.Element): boolean {
return isStylesheetLink(node) && getAttribute(node, 'data-astro-injected') === ''; return isStylesheetLink(node) && getAttribute(node, 'data-astro-injected') === '';
} }
export function isBuildableLink(node: parse5.Element, srcRoot: string, srcRootWeb: string): boolean { export function isBuildableLink(
node: parse5.Element,
srcRoot: string,
srcRootWeb: string
): boolean {
if (isAstroInjectedLink(node)) { if (isAstroInjectedLink(node)) {
return true; return true;
} }
@ -34,7 +43,11 @@ export function isBuildableLink(node: parse5.Element, srcRoot: string, srcRootWe
return startsWithSrcRoot(href, srcRoot, srcRootWeb); return startsWithSrcRoot(href, srcRoot, srcRootWeb);
} }
export function isBuildableImage(node: parse5.Element, srcRoot: string, srcRootWeb: string): boolean { export function isBuildableImage(
node: parse5.Element,
srcRoot: string,
srcRootWeb: string
): boolean {
if (getTagName(node) === 'img') { if (getTagName(node) === 'img') {
const src = getAttribute(node, 'src'); const src = getAttribute(node, 'src');
return src ? startsWithSrcRoot(src, srcRoot, srcRootWeb) : false; return src ? startsWithSrcRoot(src, srcRoot, srcRootWeb) : false;

View file

@ -14,7 +14,10 @@ export declare interface Alias {
const normalize = (pathname: string) => String(pathname).split(path.sep).join(path.posix.sep); const normalize = (pathname: string) => String(pathname).split(path.sep).join(path.posix.sep);
/** Returns the results of a config file if it exists, otherwise null. */ /** Returns the results of a config file if it exists, otherwise null. */
const getExistingConfig = (searchName: string, cwd: string | undefined): tsr.TsConfigResultSuccess | null => { const getExistingConfig = (
searchName: string,
cwd: string | undefined
): tsr.TsConfigResultSuccess | null => {
const config = tsr.tsconfigResolverSync({ cwd, searchName }); const config = tsr.tsconfigResolverSync({ cwd, searchName });
return config.exists ? config : null; return config.exists ? config : null;
@ -35,24 +38,37 @@ const getConfigAlias = (cwd: string | undefined): Alias[] | null => {
if (!compilerOptions.baseUrl) return null; if (!compilerOptions.baseUrl) return null;
// resolve the base url from the configuration file directory // resolve the base url from the configuration file directory
const baseUrl = path.posix.resolve(path.posix.dirname(normalize(config.path).replace(/^\/?/, '/')), normalize(compilerOptions.baseUrl)); const baseUrl = path.posix.resolve(
path.posix.dirname(normalize(config.path).replace(/^\/?/, '/')),
normalize(compilerOptions.baseUrl)
);
/** List of compiled alias expressions. */ /** List of compiled alias expressions. */
const aliases: Alias[] = []; const aliases: Alias[] = [];
// compile any alias expressions and push them to the list // compile any alias expressions and push them to the list
for (let [alias, values] of Object.entries(Object(compilerOptions.paths) as { [key: string]: string[] })) { for (let [alias, values] of Object.entries(
Object(compilerOptions.paths) as { [key: string]: string[] }
)) {
values = [].concat(values as never); values = [].concat(values as never);
/** Regular Expression used to match a given path. */ /** Regular Expression used to match a given path. */
const find = new RegExp(`^${[...alias].map((segment) => (segment === '*' ? '(.+)' : segment.replace(/[\\^$*+?.()|[\]{}]/, '\\$&'))).join('')}$`); const find = new RegExp(
`^${[...alias]
.map((segment) =>
segment === '*' ? '(.+)' : segment.replace(/[\\^$*+?.()|[\]{}]/, '\\$&')
)
.join('')}$`
);
/** Internal index used to calculate the matching id in a replacement. */ /** Internal index used to calculate the matching id in a replacement. */
let matchId = 0; let matchId = 0;
for (let value of values) { for (let value of values) {
/** String used to replace a matched path. */ /** String used to replace a matched path. */
const replacement = [...path.posix.resolve(baseUrl, value)].map((segment) => (segment === '*' ? `$${++matchId}` : segment === '$' ? '$$' : segment)).join(''); const replacement = [...path.posix.resolve(baseUrl, value)]
.map((segment) => (segment === '*' ? `$${++matchId}` : segment === '$' ? '$$' : segment))
.join('');
aliases.push({ find, replacement }); aliases.push({ find, replacement });
} }
@ -70,7 +86,10 @@ const getConfigAlias = (cwd: string | undefined): Alias[] | null => {
}; };
/** Returns a Vite plugin used to alias pathes from tsconfig.json and jsconfig.json. */ /** Returns a Vite plugin used to alias pathes from tsconfig.json and jsconfig.json. */
export default function configAliasVitePlugin(astroConfig: { root?: URL; [key: string]: unknown }): vite.PluginOption { export default function configAliasVitePlugin(astroConfig: {
root?: URL;
[key: string]: unknown;
}): vite.PluginOption {
/** Aliases from the tsconfig.json or jsconfig.json configuration. */ /** Aliases from the tsconfig.json or jsconfig.json configuration. */
const configAlias = getConfigAlias(astroConfig.root && url.fileURLToPath(astroConfig.root)); const configAlias = getConfigAlias(astroConfig.root && url.fileURLToPath(astroConfig.root));
@ -94,7 +113,10 @@ export default function configAliasVitePlugin(astroConfig: { root?: URL; [key: s
const aliasedSourceId = sourceId.replace(alias.find, alias.replacement); const aliasedSourceId = sourceId.replace(alias.find, alias.replacement);
/** Resolved ID conditionally handled by any other resolver. (this also gives priority to all other resolvers) */ /** Resolved ID conditionally handled by any other resolver. (this also gives priority to all other resolvers) */
const resolvedAliasedId = await this.resolve(aliasedSourceId, importer, { skipSelf: true, ...options }); const resolvedAliasedId = await this.resolve(aliasedSourceId, importer, {
skipSelf: true,
...options,
});
// if the existing resolvers find the file, return that resolution // if the existing resolvers find the file, return that resolution
if (resolvedAliasedId) return resolvedAliasedId; if (resolvedAliasedId) return resolvedAliasedId;

View file

@ -12,9 +12,15 @@ interface EnvPluginOptions {
function getPrivateEnv(viteConfig: vite.ResolvedConfig, astroConfig: AstroConfig) { function getPrivateEnv(viteConfig: vite.ResolvedConfig, astroConfig: AstroConfig) {
let envPrefixes: string[] = ['PUBLIC_']; let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) { if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix) ? viteConfig.envPrefix : [viteConfig.envPrefix]; envPrefixes = Array.isArray(viteConfig.envPrefix)
? viteConfig.envPrefix
: [viteConfig.envPrefix];
} }
const fullEnv = loadEnv(viteConfig.mode, viteConfig.envDir ?? fileURLToPath(astroConfig.root), ''); const fullEnv = loadEnv(
viteConfig.mode,
viteConfig.envDir ?? fileURLToPath(astroConfig.root),
''
);
const privateKeys = Object.keys(fullEnv).filter((key) => { const privateKeys = Object.keys(fullEnv).filter((key) => {
// don't expose any variables also on `process.env` // don't expose any variables also on `process.env`
// note: this filters out `CLI_ARGS=1` passed to node! // note: this filters out `CLI_ARGS=1` passed to node!
@ -44,7 +50,9 @@ function getReferencedPrivateKeys(source: string, privateEnv: Record<string, any
return references; return references;
} }
export default function envVitePlugin({ config: astroConfig }: EnvPluginOptions): vite.PluginOption { export default function envVitePlugin({
config: astroConfig,
}: EnvPluginOptions): vite.PluginOption {
let privateEnv: Record<string, any> | null; let privateEnv: Record<string, any> | null;
let config: vite.ResolvedConfig; let config: vite.ResolvedConfig;
let replacements: Record<string, string>; let replacements: Record<string, string>;
@ -69,7 +77,10 @@ export default function envVitePlugin({ config: astroConfig }: EnvPluginOptions)
if (typeof privateEnv === 'undefined') { if (typeof privateEnv === 'undefined') {
privateEnv = getPrivateEnv(config, astroConfig); privateEnv = getPrivateEnv(config, astroConfig);
if (privateEnv) { if (privateEnv) {
const entries = Object.entries(privateEnv).map(([key, value]) => [`import.meta.env.${key}`, value]); const entries = Object.entries(privateEnv).map(([key, value]) => [
`import.meta.env.${key}`,
value,
]);
replacements = Object.fromEntries(entries); replacements = Object.fromEntries(entries);
// These additional replacements are needed to match Vite // These additional replacements are needed to match Vite
replacements = Object.assign(replacements, { replacements = Object.assign(replacements, {

View file

@ -3,7 +3,11 @@ import { AstroConfig } from '../@types/astro.js';
import { runHookServerSetup } from '../integrations/index.js'; import { runHookServerSetup } from '../integrations/index.js';
/** Connect Astro integrations into Vite, as needed. */ /** Connect Astro integrations into Vite, as needed. */
export default function astroIntegrationsContainerPlugin({ config }: { config: AstroConfig }): VitePlugin { export default function astroIntegrationsContainerPlugin({
config,
}: {
config: AstroConfig;
}): VitePlugin {
return { return {
name: 'astro:integration-container', name: 'astro:integration-container',
configureServer(server) { configureServer(server) {

View file

@ -30,7 +30,9 @@ function getEsbuildLoader(fileExt: string): string {
function collectJSXRenderers(renderers: AstroRenderer[]): Map<string, AstroRenderer> { function collectJSXRenderers(renderers: AstroRenderer[]): Map<string, AstroRenderer> {
const renderersWithJSXSupport = renderers.filter((r) => r.jsxImportSource); const renderersWithJSXSupport = renderers.filter((r) => r.jsxImportSource);
return new Map(renderersWithJSXSupport.map((r) => [r.jsxImportSource, r] as [string, AstroRenderer])); return new Map(
renderersWithJSXSupport.map((r) => [r.jsxImportSource, r] as [string, AstroRenderer])
);
} }
interface TransformJSXOptions { interface TransformJSXOptions {
@ -41,7 +43,13 @@ interface TransformJSXOptions {
ssr: boolean; ssr: boolean;
} }
async function transformJSX({ code, mode, id, ssr, renderer }: TransformJSXOptions): Promise<TransformResult> { async function transformJSX({
code,
mode,
id,
ssr,
renderer,
}: TransformJSXOptions): Promise<TransformResult> {
const { jsxTransformOptions } = renderer; const { jsxTransformOptions } = renderer;
const options = await jsxTransformOptions!({ mode, ssr }); const options = await jsxTransformOptions!({ mode, ssr });
const plugins = [...(options.plugins || [])]; const plugins = [...(options.plugins || [])];
@ -117,7 +125,13 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
sourcefile: id, sourcefile: id,
sourcemap: 'inline', sourcemap: 'inline',
}); });
return transformJSX({ code: jsxCode, id, renderer: [...jsxRenderers.values()][0], mode, ssr }); return transformJSX({
code: jsxCode,
id,
renderer: [...jsxRenderers.values()][0],
mode,
ssr,
});
} }
// Attempt: Multiple JSX renderers // Attempt: Multiple JSX renderers
@ -165,7 +179,13 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
const jsxRenderer = jsxRenderers.get(importSource); const jsxRenderer = jsxRenderers.get(importSource);
// if renderer not installed for this JSX source, throw error // if renderer not installed for this JSX source, throw error
if (!jsxRenderer) { if (!jsxRenderer) {
error(logging, 'renderer', `${colors.yellow(id)} No renderer installed for ${importSource}. Try adding \`@astrojs/${importSource}\` to your project.`); error(
logging,
'renderer',
`${colors.yellow(
id
)} No renderer installed for ${importSource}. Try adding \`@astrojs/${importSource}\` to your project.`
);
return null; return null;
} }
// downlevel any non-standard syntax, but preserve JSX // downlevel any non-standard syntax, but preserve JSX
@ -175,7 +195,13 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
sourcefile: id, sourcefile: id,
sourcemap: 'inline', sourcemap: 'inline',
}); });
return await transformJSX({ code: jsxCode, id, renderer: jsxRenderers.get(importSource) as AstroRenderer, mode, ssr }); return await transformJSX({
code: jsxCode,
id,
renderer: jsxRenderers.get(importSource) as AstroRenderer,
mode,
ssr,
});
} }
// if we still cant tell, throw error // if we still cant tell, throw error
@ -185,7 +211,9 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
'renderer', 'renderer',
`${colors.yellow(id)} `${colors.yellow(id)}
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment. Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
Add ${colors.cyan(IMPORT_STATEMENTS[defaultRenderer] || `import '${defaultRenderer}';`)} or ${colors.cyan(`/* jsxImportSource: ${defaultRenderer} */`)} to this file. Add ${colors.cyan(
IMPORT_STATEMENTS[defaultRenderer] || `import '${defaultRenderer}';`
)} or ${colors.cyan(`/* jsxImportSource: ${defaultRenderer} */`)} to this file.
` `
); );
return null; return null;

View file

@ -82,9 +82,13 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// Return the file's JS representation, including all Markdown // Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content. // frontmatter and a deferred `import() of the compiled markdown content.
if (id.startsWith(VIRTUAL_MODULE_ID)) { if (id.startsWith(VIRTUAL_MODULE_ID)) {
const sitePathname = config.site ? appendForwardSlash(new URL(config.base, config.site).pathname) : '/'; const sitePathname = config.site
? appendForwardSlash(new URL(config.base, config.site).pathname)
: '/';
const fileId = id.substring(VIRTUAL_MODULE_ID.length); const fileId = id.substring(VIRTUAL_MODULE_ID.length);
const fileUrl = fileId.includes('/pages/') ? fileId.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '') : undefined; const fileUrl = fileId.includes('/pages/')
? fileId.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '')
: undefined;
const source = await fs.promises.readFile(fileId, 'utf8'); const source = await fs.promises.readFile(fileId, 'utf8');
const { data: frontmatter } = matter(source); const { data: frontmatter } = matter(source);
return { return {
@ -160,7 +164,11 @@ export const frontmatter = ${JSON.stringify(content)};
${tsResult}`; ${tsResult}`;
// Compile from `.ts` to `.js` // Compile from `.ts` to `.js`
const { code } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: false, sourcefile: id }); const { code } = await esbuild.transform(tsResult, {
loader: 'ts',
sourcemap: false,
sourcefile: id,
});
return { return {
code, code,
map: null, map: null,

View file

@ -5,7 +5,9 @@ import { AstroConfig, InjectedScriptStage } from '../@types/astro.js';
// inject these as ESM imports into actual code, where they would not // inject these as ESM imports into actual code, where they would not
// resolve correctly. // resolve correctly.
const SCRIPT_ID_PREFIX = `astro:scripts/`; const SCRIPT_ID_PREFIX = `astro:scripts/`;
export const BEFORE_HYDRATION_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'before-hydration' as InjectedScriptStage}.js`; export const BEFORE_HYDRATION_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${
'before-hydration' as InjectedScriptStage
}.js`;
export const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page' as InjectedScriptStage}.js`; export const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page' as InjectedScriptStage}.js`;
export const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page-ssr' as InjectedScriptStage}.js`; export const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page-ssr' as InjectedScriptStage}.js`;
@ -45,7 +47,9 @@ export default function astroScriptsPlugin({ config }: { config: AstroConfig }):
// for the frontend AND some hydrated components exist in // for the frontend AND some hydrated components exist in
// the final build. We can detect this by looking for a // the final build. We can detect this by looking for a
// `astro/client/*` input, which signifies both conditions are met. // `astro/client/*` input, which signifies both conditions are met.
const hasHydratedComponents = Array.isArray(options.input) && options.input.some((input) => input.startsWith('astro/client')); const hasHydratedComponents =
Array.isArray(options.input) &&
options.input.some((input) => input.startsWith('astro/client'));
const hasHydrationScripts = config._ctx.scripts.some((s) => s.stage === 'before-hydration'); const hasHydrationScripts = config._ctx.scripts.some((s) => s.stage === 'before-hydration');
if (hasHydratedComponents && hasHydrationScripts) { if (hasHydratedComponents && hasHydrationScripts) {
this.emitFile({ this.emitFile({

View file

@ -214,37 +214,49 @@ describe('CSS', function () {
it('<style>', async () => { it('<style>', async () => {
const el = $('#svelte-css'); const el = $('#svelte-css');
const classes = el.attr('class').split(' '); const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => name !== 'svelte-css' && /^svelte-[A-Za-z0-9-]+/.test(name)); const scopedClass = classes.find(
(name) => name !== 'svelte-css' && /^svelte-[A-Za-z0-9-]+/.test(name)
);
// 1. check HTML // 1. check HTML
expect(el.attr('class')).to.include('svelte-css'); expect(el.attr('class')).to.include('svelte-css');
// 2. check CSS // 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-css.${scopedClass}[^{]*{font-family:Comic Sans MS`)); expect(bundledCSS).to.match(
new RegExp(`.svelte-css.${scopedClass}[^{]*{font-family:Comic Sans MS`)
);
}); });
it('<style lang="sass">', async () => { it('<style lang="sass">', async () => {
const el = $('#svelte-sass'); const el = $('#svelte-sass');
const classes = el.attr('class').split(' '); const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => name !== 'svelte-sass' && /^svelte-[A-Za-z0-9-]+/.test(name)); const scopedClass = classes.find(
(name) => name !== 'svelte-sass' && /^svelte-[A-Za-z0-9-]+/.test(name)
);
// 1. check HTML // 1. check HTML
expect(el.attr('class')).to.include('svelte-sass'); expect(el.attr('class')).to.include('svelte-sass');
// 2. check CSS // 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-sass.${scopedClass}[^{]*{font-family:Comic Sans MS`)); expect(bundledCSS).to.match(
new RegExp(`.svelte-sass.${scopedClass}[^{]*{font-family:Comic Sans MS`)
);
}); });
it('<style lang="scss">', async () => { it('<style lang="scss">', async () => {
const el = $('#svelte-scss'); const el = $('#svelte-scss');
const classes = el.attr('class').split(' '); const classes = el.attr('class').split(' ');
const scopedClass = classes.find((name) => name !== 'svelte-scss' && /^svelte-[A-Za-z0-9-]+/.test(name)); const scopedClass = classes.find(
(name) => name !== 'svelte-scss' && /^svelte-[A-Za-z0-9-]+/.test(name)
);
// 1. check HTML // 1. check HTML
expect(el.attr('class')).to.include('svelte-scss'); expect(el.attr('class')).to.include('svelte-scss');
// 2. check CSS // 2. check CSS
expect(bundledCSS).to.match(new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:Comic Sans MS`)); expect(bundledCSS).to.match(
new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:Comic Sans MS`)
);
}); });
}); });
}); });
@ -285,7 +297,14 @@ describe('CSS', function () {
}); });
it('resolves Styles from React', async () => { it('resolves Styles from React', async () => {
const styles = ['ReactCSS.css', 'ReactModules.module.css', 'ReactModules.module.scss', 'ReactModules.module.sass', 'ReactSass.sass', 'ReactScss.scss']; const styles = [
'ReactCSS.css',
'ReactModules.module.css',
'ReactModules.module.scss',
'ReactModules.module.sass',
'ReactSass.sass',
'ReactScss.scss',
];
for (const style of styles) { for (const style of styles) {
const href = $(`link[href$="${style}"]`).attr('href'); const href = $(`link[href$="${style}"]`).attr('href');
expect((await fixture.fetch(href)).status, style).to.equal(200); expect((await fixture.fetch(href)).status, style).to.equal(200);
@ -293,7 +312,11 @@ describe('CSS', function () {
}); });
it('resolves CSS from Svelte', async () => { it('resolves CSS from Svelte', async () => {
const scripts = ['SvelteCSS.svelte?svelte&type=style&lang.css', 'SvelteSass.svelte?svelte&type=style&lang.css', 'SvelteScss.svelte?svelte&type=style&lang.css']; const scripts = [
'SvelteCSS.svelte?svelte&type=style&lang.css',
'SvelteSass.svelte?svelte&type=style&lang.css',
'SvelteScss.svelte?svelte&type=style&lang.css',
];
for (const script of scripts) { for (const script of scripts) {
const src = $(`script[src$="${script}"]`).attr('src'); const src = $(`script[src$="${script}"]`).attr('src');
expect((await fixture.fetch(src)).status, script).to.equal(200); expect((await fixture.fetch(src)).status, script).to.equal(200);

View file

@ -104,7 +104,9 @@ describe('Astro basics', () => {
expect($('body > :nth-child(3)').prop('outerHTML')).to.equal('<input type="text">'); expect($('body > :nth-child(3)').prop('outerHTML')).to.equal('<input type="text">');
// <Input type="select"><option>option</option></Input> // <Input type="select"><option>option</option></Input>
expect($('body > :nth-child(4)').prop('outerHTML')).to.equal('<select><option>option</option></select>'); expect($('body > :nth-child(4)').prop('outerHTML')).to.equal(
'<select><option>option</option></select>'
);
// <Input type="textarea">textarea</Input> // <Input type="textarea">textarea</Input>
expect($('body > :nth-child(5)').prop('outerHTML')).to.equal('<textarea>textarea</textarea>'); expect($('body > :nth-child(5)').prop('outerHTML')).to.equal('<textarea>textarea</textarea>');

View file

@ -75,8 +75,14 @@ describe('Component children', () => {
expect($('#ssr-only').children()).to.have.lengthOf(0); expect($('#ssr-only').children()).to.have.lengthOf(0);
// test 2: If client, and no children are rendered, a template is. // test 2: If client, and no children are rendered, a template is.
expect($('#client').parent().children()).to.have.lengthOf(2, 'rendered the client component and a template'); expect($('#client').parent().children()).to.have.lengthOf(
expect($('#client').parent().find('template[data-astro-template]')).to.have.lengthOf(1, 'Found 1 template'); 2,
'rendered the client component and a template'
);
expect($('#client').parent().find('template[data-astro-template]')).to.have.lengthOf(
1,
'Found 1 template'
);
// test 3: If client, and children are rendered, no template is. // test 3: If client, and children are rendered, no template is.
expect($('#client-render').parent().children()).to.have.lengthOf(1); expect($('#client-render').parent().children()).to.have.lengthOf(1);

View file

@ -14,7 +14,10 @@ describe('<Code>', () => {
let html = await fixture.readFile('/no-lang/index.html'); let html = await fixture.readFile('/no-lang/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('pre')).to.have.lengthOf(1); expect($('pre')).to.have.lengthOf(1);
expect($('pre').attr('style')).to.equal('background-color: #0d1117; overflow-x: auto;', 'applies default and overflow'); expect($('pre').attr('style')).to.equal(
'background-color: #0d1117; overflow-x: auto;',
'applies default and overflow'
);
expect($('pre > code')).to.have.lengthOf(1); expect($('pre > code')).to.have.lengthOf(1);
// test: contains some generated spans // test: contains some generated spans
@ -36,7 +39,10 @@ describe('<Code>', () => {
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('pre')).to.have.lengthOf(1); expect($('pre')).to.have.lengthOf(1);
expect($('pre').attr('class')).to.equal('astro-code'); expect($('pre').attr('class')).to.equal('astro-code');
expect($('pre').attr('style')).to.equal('background-color: #2e3440ff; overflow-x: auto;', 'applies custom theme'); expect($('pre').attr('style')).to.equal(
'background-color: #2e3440ff; overflow-x: auto;',
'applies custom theme'
);
}); });
it('<Code wrap>', async () => { it('<Code wrap>', async () => {
@ -45,7 +51,9 @@ describe('<Code>', () => {
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('pre')).to.have.lengthOf(1); expect($('pre')).to.have.lengthOf(1);
// test: applies wrap overflow // test: applies wrap overflow
expect($('pre').attr('style')).to.equal('background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'); expect($('pre').attr('style')).to.equal(
'background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'
);
} }
{ {
let html = await fixture.readFile('/wrap-false/index.html'); let html = await fixture.readFile('/wrap-false/index.html');

View file

@ -9,7 +9,14 @@ const EXPECTED_CSS = {
'/one/index.html': ['/assets/'], '/one/index.html': ['/assets/'],
'/two/index.html': ['/assets/'], '/two/index.html': ['/assets/'],
}; };
const UNEXPECTED_CSS = ['/src/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css']; const UNEXPECTED_CSS = [
'/src/components/nav.css',
'../css/typography.css',
'../css/colors.css',
'../css/page-index.css',
'../css/page-one.css',
'../css/page-two.css',
];
describe('CSS Bundling', function () { describe('CSS Bundling', function () {
let fixture; let fixture;

View file

@ -10,8 +10,15 @@ describe('Astro Markdown plugins', () => {
fixture = await loadFixture({ fixture = await loadFixture({
root: './fixtures/astro-markdown-plugins/', root: './fixtures/astro-markdown-plugins/',
markdown: { markdown: {
remarkPlugins: ['remark-code-titles', ['rehype-autolink-headings', { behavior: 'prepend' }]], remarkPlugins: [
rehypePlugins: [['rehype-toc', { headings: ['h2', 'h3'] }], [addClasses, { 'h1,h2,h3': 'title' }], 'rehype-slug'], 'remark-code-titles',
['rehype-autolink-headings', { behavior: 'prepend' }],
],
rehypePlugins: [
['rehype-toc', { headings: ['h2', 'h3'] }],
[addClasses, { 'h1,h2,h3': 'title' }],
'rehype-slug',
],
}, },
}); });
await fixture.build(); await fixture.build();

View file

@ -126,7 +126,8 @@ describe('Astro Markdown Shiki', () => {
describe('Wrap', () => { describe('Wrap', () => {
describe('wrap = true', () => { describe('wrap = true', () => {
const style = 'background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;'; const style =
'background-color: #0d1117; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
let fixture; let fixture;
before(async () => { before(async () => {

View file

@ -15,13 +15,21 @@ describe('Pagination', () => {
}); });
it('optional root page', async () => { it('optional root page', async () => {
for (const file of ['/posts/optional-root-page/index.html', '/posts/optional-root-page/2/index.html', '/posts/optional-root-page/3/index.html']) { for (const file of [
'/posts/optional-root-page/index.html',
'/posts/optional-root-page/2/index.html',
'/posts/optional-root-page/3/index.html',
]) {
expect(await fixture.readFile(file)).to.be.ok; expect(await fixture.readFile(file)).to.be.ok;
} }
}); });
it('named root page', async () => { it('named root page', async () => {
for (const file of ['/posts/named-root-page/1/index.html', '/posts/named-root-page/2/index.html', '/posts/named-root-page/3/index.html']) { for (const file of [
'/posts/named-root-page/1/index.html',
'/posts/named-root-page/2/index.html',
'/posts/named-root-page/3/index.html',
]) {
expect(await fixture.readFile(file)).to.be.ok; expect(await fixture.readFile(file)).to.be.ok;
} }
}); });

View file

@ -48,7 +48,10 @@ describe('astro cli', () => {
const networkLogFlags = [['--host'], ['--host', '0.0.0.0']]; const networkLogFlags = [['--host'], ['--host', '0.0.0.0']];
networkLogFlags.forEach(([flag, flagValue]) => { networkLogFlags.forEach(([flag, flagValue]) => {
it(`astro ${cmd} ${flag} ${flagValue ?? ''} - network log`, async () => { it(`astro ${cmd} ${flag} ${flagValue ?? ''} - network log`, async () => {
const { local, network } = await cliServerLogSetupWithFixture(flagValue ? [flag, flagValue] : [flag], cmd); const { local, network } = await cliServerLogSetupWithFixture(
flagValue ? [flag, flagValue] : [flag],
cmd
);
expect(local).to.not.be.undefined; expect(local).to.not.be.undefined;
expect(network).to.not.be.undefined; expect(network).to.not.be.undefined;
@ -56,11 +59,23 @@ describe('astro cli', () => {
const localURL = new URL(local); const localURL = new URL(local);
const networkURL = new URL(network); const networkURL = new URL(network);
expect(localURL.hostname).to.be.equal(flagValue ?? 'localhost', `Expected local URL to be on localhost`); expect(localURL.hostname).to.be.equal(
flagValue ?? 'localhost',
`Expected local URL to be on localhost`
);
// Note: our tests run in parallel so this could be 3000+! // Note: our tests run in parallel so this could be 3000+!
expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(3000, `Expected Port to be >= 3000`); expect(Number.parseInt(localURL.port)).to.be.greaterThanOrEqual(
expect(networkURL.port).to.be.equal(localURL.port, `Expected local and network ports to be equal`); 3000,
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --host flag`); `Expected Port to be >= 3000`
);
expect(networkURL.port).to.be.equal(
localURL.port,
`Expected local and network ports to be equal`
);
expect(isIPv4(networkURL.hostname)).to.be.equal(
true,
`Expected network URL to respect --host flag`
);
}); });
}); });

View file

@ -40,7 +40,10 @@ describe('Config Validation', () => {
}); });
it('ignores falsey "integration" values', async () => { it('ignores falsey "integration" values', async () => {
const result = await validateConfig({ integrations: [0, false, null, undefined] }, process.cwd()); const result = await validateConfig(
{ integrations: [0, false, null, undefined] },
process.cwd()
);
expect(result.integrations).to.deep.equal([]); expect(result.integrations).to.deep.equal([]);
}); });
it('normalizes "integration" values', async () => { it('normalizes "integration" values', async () => {
@ -48,7 +51,10 @@ describe('Config Validation', () => {
expect(result.integrations).to.deep.equal([{ name: '@astrojs/a', hooks: {} }]); expect(result.integrations).to.deep.equal([{ name: '@astrojs/a', hooks: {} }]);
}); });
it('flattens array "integration" values', async () => { it('flattens array "integration" values', async () => {
const result = await validateConfig({ integrations: [{ name: '@astrojs/a' }, [{ name: '@astrojs/b' }, { name: '@astrojs/c' }]] }, process.cwd()); const result = await validateConfig(
{ integrations: [{ name: '@astrojs/a' }, [{ name: '@astrojs/b' }, { name: '@astrojs/c' }]] },
process.cwd()
);
expect(result.integrations).to.deep.equal([ expect(result.integrations).to.deep.equal([
{ name: '@astrojs/a', hooks: {} }, { name: '@astrojs/a', hooks: {} },
{ name: '@astrojs/b', hooks: {} }, { name: '@astrojs/b', hooks: {} },
@ -56,11 +62,17 @@ describe('Config Validation', () => {
]); ]);
}); });
it('blocks third-party "integration" values', async () => { it('blocks third-party "integration" values', async () => {
const configError = await validateConfig({ integrations: [{ name: '@my-plugin/a' }] }, process.cwd()).catch((err) => err); const configError = await validateConfig(
{ integrations: [{ name: '@my-plugin/a' }] },
process.cwd()
).catch((err) => err);
expect(configError).to.be.instanceOf(Error); expect(configError).to.be.instanceOf(Error);
expect(configError.message).to.include('Astro integrations are still experimental.'); expect(configError.message).to.include('Astro integrations are still experimental.');
}); });
it('allows third-party "integration" values with the --experimental-integrations flag', async () => { it('allows third-party "integration" values with the --experimental-integrations flag', async () => {
await validateConfig({ integrations: [{ name: '@my-plugin/a' }], experimental: { integrations: true } }, process.cwd()).catch((err) => err); await validateConfig(
{ integrations: [{ name: '@my-plugin/a' }], experimental: { integrations: true } },
process.cwd()
).catch((err) => err);
}); });
}); });

View file

@ -31,10 +31,17 @@ describe('config', () => {
it('can be specified via --host flag', async () => { it('can be specified via --host flag', async () => {
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url); const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const { network } = await cliServerLogSetup(['--root', fileURLToPath(projectRootURL), '--host']); const { network } = await cliServerLogSetup([
'--root',
fileURLToPath(projectRootURL),
'--host',
]);
const networkURL = new URL(network); const networkURL = new URL(network);
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --host flag`); expect(isIPv4(networkURL.hostname)).to.be.equal(
true,
`Expected network URL to respect --host flag`
);
}); });
}); });
@ -42,10 +49,18 @@ describe('config', () => {
it('can be passed via --config', async () => { it('can be passed via --config', async () => {
const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url); const projectRootURL = new URL('./fixtures/astro-basic/', import.meta.url);
const configFileURL = new URL('./fixtures/config-path/config/my-config.mjs', import.meta.url); const configFileURL = new URL('./fixtures/config-path/config/my-config.mjs', import.meta.url);
const { network } = await cliServerLogSetup(['--root', fileURLToPath(projectRootURL), '--config', configFileURL.pathname]); const { network } = await cliServerLogSetup([
'--root',
fileURLToPath(projectRootURL),
'--config',
configFileURL.pathname,
]);
const networkURL = new URL(network); const networkURL = new URL(network);
expect(isIPv4(networkURL.hostname)).to.be.equal(true, `Expected network URL to respect --host flag`); expect(isIPv4(networkURL.hostname)).to.be.equal(
true,
`Expected network URL to respect --host flag`
);
}); });
}); });

View file

@ -18,7 +18,10 @@ describe('Global Fetch', () => {
it('Is available in Astro components', async () => { it('Is available in Astro components', async () => {
const html = await fixture.readFile('/index.html'); const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('#astro-component').text()).to.equal('function', 'Fetch supported in .astro components'); expect($('#astro-component').text()).to.equal(
'function',
'Fetch supported in .astro components'
);
}); });
it('Is available in non-Astro components', async () => { it('Is available in non-Astro components', async () => {
const html = await fixture.readFile('/index.html'); const html = await fixture.readFile('/index.html');
@ -31,6 +34,9 @@ describe('Global Fetch', () => {
const html = await fixture.readFile('/index.html'); const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html); const $ = cheerio.load(html);
expect($('#already-imported').text()).to.equal('function', 'Existing fetch imports respected'); expect($('#already-imported').text()).to.equal('function', 'Existing fetch imports respected');
expect($('#custom-declaration').text()).to.equal('number', 'Custom fetch declarations respected'); expect($('#custom-declaration').text()).to.equal(
'number',
'Custom fetch declarations respected'
);
}); });
}); });

View file

@ -36,7 +36,9 @@ describe('LitElement test', function () {
expect($('my-element').html()).to.include(`<div>Testing...</div>`); expect($('my-element').html()).to.include(`<div>Testing...</div>`);
// test 3: string reactive property set // test 3: string reactive property set
expect(stripExpressionMarkers($('my-element').html())).to.include(`<div id="str">initialized</div>`); expect(stripExpressionMarkers($('my-element').html())).to.include(
`<div id="str">initialized</div>`
);
// test 4: boolean reactive property correctly set // test 4: boolean reactive property correctly set
// <my-element bool="false"> Lit will equate to true because it uses // <my-element bool="false"> Lit will equate to true because it uses
@ -45,7 +47,9 @@ describe('LitElement test', function () {
// test 5: object reactive property set // test 5: object reactive property set
// by default objects will be stringifed to [object Object] // by default objects will be stringifed to [object Object]
expect(stripExpressionMarkers($('my-element').html())).to.include(`<div id="data">data: 1</div>`); expect(stripExpressionMarkers($('my-element').html())).to.include(
`<div id="data">data: 1</div>`
);
// test 6: reactive properties are not rendered as attributes // test 6: reactive properties are not rendered as attributes
expect($('my-element').attr('obj')).to.equal(undefined); expect($('my-element').attr('obj')).to.equal(undefined);

View file

@ -38,7 +38,9 @@ describe('Tailwind', () => {
expect(bundledCSS, 'includes responsive classes').to.match(/\.lg\\:py-3{/); expect(bundledCSS, 'includes responsive classes').to.match(/\.lg\\:py-3{/);
// tailwind escapes brackets, `font-[900]` compiles to `font-\[900\]` // tailwind escapes brackets, `font-[900]` compiles to `font-\[900\]`
expect(bundledCSS, 'supports arbitrary value classes').to.match(/\.font-\\\[900\\\]{font-weight:900}/); expect(bundledCSS, 'supports arbitrary value classes').to.match(
/\.font-\\\[900\\\]{font-weight:900}/
);
}); });
it('maintains classes in HTML', async () => { it('maintains classes in HTML', async () => {

View file

@ -55,7 +55,8 @@ polyfill(globalThis, {
* .clean() - Async. Removes the projects dist folder. * .clean() - Async. Removes the projects dist folder.
*/ */
export async function loadFixture(inlineConfig) { export async function loadFixture(inlineConfig) {
if (!inlineConfig || !inlineConfig.root) throw new Error("Must provide { root: './fixtures/...' }"); if (!inlineConfig || !inlineConfig.root)
throw new Error("Must provide { root: './fixtures/...' }");
// load config // load config
let cwd = inlineConfig.root; let cwd = inlineConfig.root;
@ -93,12 +94,14 @@ export async function loadFixture(inlineConfig) {
return devResult; return devResult;
}, },
config, config,
fetch: (url, init) => fetch(`http://${'127.0.0.1'}:${config.server.port}${url.replace(/^\/?/, '/')}`, init), fetch: (url, init) =>
fetch(`http://${'127.0.0.1'}:${config.server.port}${url.replace(/^\/?/, '/')}`, init),
preview: async (opts = {}) => { preview: async (opts = {}) => {
const previewServer = await preview(config, { logging, ...opts }); const previewServer = await preview(config, { logging, ...opts });
return previewServer; return previewServer;
}, },
readFile: (filePath) => fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'), readFile: (filePath) =>
fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir), 'utf8'),
readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)), readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)),
clean: () => fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true }), clean: () => fs.promises.rm(config.outDir, { maxRetries: 10, recursive: true, force: true }),
loadTestAdapterApp: async () => { loadTestAdapterApp: async () => {
@ -120,7 +123,11 @@ function merge(a, b) {
const c = {}; const c = {};
for (const k of allKeys) { for (const k of allKeys) {
const needsObjectMerge = const needsObjectMerge =
typeof a[k] === 'object' && typeof b[k] === 'object' && (Object.keys(a[k]).length || Object.keys(b[k]).length) && !Array.isArray(a[k]) && !Array.isArray(b[k]); typeof a[k] === 'object' &&
typeof b[k] === 'object' &&
(Object.keys(a[k]).length || Object.keys(b[k]).length) &&
!Array.isArray(a[k]) &&
!Array.isArray(b[k]);
if (needsObjectMerge) { if (needsObjectMerge) {
c[k] = merge(a[k] || {}, b[k] || {}); c[k] = merge(a[k] || {}, b[k] || {});
continue; continue;

View file

@ -28,14 +28,20 @@ export function mkdirp(dir: string) {
} }
} }
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')); const { version } = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')
);
const POSTPROCESS_FILES = ['package.json', 'astro.config.mjs', 'CHANGELOG.md']; // some files need processing after copying. const POSTPROCESS_FILES = ['package.json', 'astro.config.mjs', 'CHANGELOG.md']; // some files need processing after copying.
export async function main() { export async function main() {
logger.debug('Verbose logging turned on'); logger.debug('Verbose logging turned on');
console.log(`\n${bold('Welcome to Astro!')} ${gray(`(create-astro v${version})`)}`); console.log(`\n${bold('Welcome to Astro!')} ${gray(`(create-astro v${version})`)}`);
console.log(`If you encounter a problem, visit ${cyan('https://github.com/withastro/astro/issues')} to search or file a new issue.\n`); console.log(
`If you encounter a problem, visit ${cyan(
'https://github.com/withastro/astro/issues'
)} to search or file a new issue.\n`
);
let spinner = ora({ color: 'green', text: 'Prepare for liftoff.' }); let spinner = ora({ color: 'green', text: 'Prepare for liftoff.' });
@ -74,7 +80,9 @@ export async function main() {
const hash = args.commit ? `#${args.commit}` : ''; const hash = args.commit ? `#${args.commit}` : '';
const templateTarget = options.template.includes('/') ? options.template : `withastro/astro/examples/${options.template}#latest`; const templateTarget = options.template.includes('/')
? options.template
: `withastro/astro/examples/${options.template}#latest`;
const emitter = degit(`${templateTarget}${hash}`, { const emitter = degit(`${templateTarget}${hash}`, {
cache: false, cache: false,
@ -118,13 +126,25 @@ export async function main() {
// Warning for issue #655 // Warning for issue #655
if (err.message === 'zlib: unexpected end of file') { if (err.message === 'zlib: unexpected end of file') {
console.log(yellow("This seems to be a cache related problem. Remove the folder '~/.degit/github/withastro' to fix this error.")); console.log(
console.log(yellow('For more information check out this issue: https://github.com/withastro/astro/issues/655')); yellow(
"This seems to be a cache related problem. Remove the folder '~/.degit/github/withastro' to fix this error."
)
);
console.log(
yellow(
'For more information check out this issue: https://github.com/withastro/astro/issues/655'
)
);
} }
// Helpful message when encountering the "could not find commit hash for ..." error // Helpful message when encountering the "could not find commit hash for ..." error
if (err.code === 'MISSING_REF') { if (err.code === 'MISSING_REF') {
console.log(yellow("This seems to be an issue with degit. Please check if you have 'git' installed on your system, and install it if you don't have (https://git-scm.com).")); console.log(
yellow(
"This seems to be an issue with degit. Please check if you have 'git' installed on your system, and install it if you don't have (https://git-scm.com)."
)
);
console.log( console.log(
yellow( yellow(
"If you do have 'git' installed, please run this command with the --verbose flag and file a new issue with the command output here: https://github.com/withastro/astro/issues" "If you do have 'git' installed, please run this command with the --verbose flag and file a new issue with the command output here: https://github.com/withastro/astro/issues"
@ -178,8 +198,13 @@ export async function main() {
) )
).flat(1); ).flat(1);
// merge and sort dependencies // merge and sort dependencies
packageJSON.devDependencies = { ...(packageJSON.devDependencies ?? {}), ...Object.fromEntries(integrationEntries) }; packageJSON.devDependencies = {
packageJSON.devDependencies = Object.fromEntries(Object.entries(packageJSON.devDependencies).sort((a, b) => a[0].localeCompare(b[0]))); ...(packageJSON.devDependencies ?? {}),
...Object.fromEntries(integrationEntries),
};
packageJSON.devDependencies = Object.fromEntries(
Object.entries(packageJSON.devDependencies).sort((a, b) => a[0].localeCompare(b[0]))
);
await fs.promises.writeFile(fileLoc, JSON.stringify(packageJSON, undefined, 2)); await fs.promises.writeFile(fileLoc, JSON.stringify(packageJSON, undefined, 2));
break; break;
} }
@ -196,7 +221,9 @@ export async function main() {
const component = COUNTER_COMPONENTS[integration.id as keyof typeof COUNTER_COMPONENTS]; const component = COUNTER_COMPONENTS[integration.id as keyof typeof COUNTER_COMPONENTS];
const componentName = path.basename(component.filename, path.extname(component.filename)); const componentName = path.basename(component.filename, path.extname(component.filename));
const absFileLoc = path.resolve(cwd, component.filename); const absFileLoc = path.resolve(cwd, component.filename);
importStatements.push(`import ${componentName} from '${component.filename.replace(/^src/, '..')}';`); importStatements.push(
`import ${componentName} from '${component.filename.replace(/^src/, '..')}';`
);
components.push(`<${componentName} client:visible />`); components.push(`<${componentName} client:visible />`);
await fs.promises.writeFile(absFileLoc, component.content); await fs.promises.writeFile(absFileLoc, component.content);
}) })
@ -225,7 +252,11 @@ export async function main() {
} }
console.log(` ${i++}: ${bold(cyan('npm install'))} (or pnpm install, yarn, etc)`); console.log(` ${i++}: ${bold(cyan('npm install'))} (or pnpm install, yarn, etc)`);
console.log(` ${i++}: ${bold(cyan('git init && git add -A && git commit -m "Initial commit"'))} (optional step)`); console.log(
` ${i++}: ${bold(
cyan('git init && git add -A && git commit -m "Initial commit"')
)} (optional step)`
);
console.log(` ${i++}: ${bold(cyan('npm run dev'))} (or pnpm, yarn, etc)`); console.log(` ${i++}: ${bold(cyan('npm run dev'))} (or pnpm, yarn, etc)`);
console.log(`\nTo close the dev server, hit ${bold(cyan('Ctrl-C'))}`); console.log(`\nTo close the dev server, hit ${bold(cyan('Ctrl-C'))}`);

View file

@ -96,7 +96,12 @@ export const levels: Record<LoggerLevel, number> = {
}; };
/** Full logging API */ /** Full logging API */
export function log(opts: LogOptions = {}, level: LoggerLevel, type: string | null, ...args: Array<any>) { export function log(
opts: LogOptions = {},
level: LoggerLevel,
type: string | null,
...args: Array<any>
) {
const logLevel = opts.level ?? defaultLogOptions.level; const logLevel = opts.level ?? defaultLogOptions.level;
const dest = opts.dest ?? defaultLogOptions.dest; const dest = opts.dest ?? defaultLogOptions.dest;
const event: LogMessage = { const event: LogMessage = {

Some files were not shown because too many files have changed in this diff Show more