From 35ba69650e05533718ebcb0d8facf88922420ae9 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 20 Dec 2021 16:03:57 -0500 Subject: [PATCH 01/12] Work on removing vite-postprocess --- .../fast-build/src/components/Counter.vue | 24 ++ examples/fast-build/src/pages/index.astro | 18 +- packages/astro/src/@types/astro.ts | 11 +- packages/astro/src/core/build/internal.ts | 21 +- packages/astro/src/core/build/static-build.ts | 287 +++++++++------ packages/astro/src/core/ssr/index.ts | 342 +++++++----------- packages/astro/src/core/ssr/result.ts | 83 +++++ .../astro/src/runtime/server/hydration.ts | 125 ++++--- packages/astro/src/runtime/server/index.ts | 80 ++-- packages/astro/src/runtime/server/metadata.ts | 101 ++++-- packages/astro/src/runtime/server/util.ts | 16 + 11 files changed, 623 insertions(+), 485 deletions(-) create mode 100644 examples/fast-build/src/components/Counter.vue create mode 100644 packages/astro/src/core/ssr/result.ts diff --git a/examples/fast-build/src/components/Counter.vue b/examples/fast-build/src/components/Counter.vue new file mode 100644 index 000000000000..d36776319208 --- /dev/null +++ b/examples/fast-build/src/components/Counter.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/examples/fast-build/src/pages/index.astro b/examples/fast-build/src/pages/index.astro index 2bdadbf5b273..ee228aa193b2 100644 --- a/examples/fast-build/src/pages/index.astro +++ b/examples/fast-build/src/pages/index.astro @@ -2,6 +2,7 @@ import imgUrl from '../images/penguin.jpg'; import grayscaleUrl from '../images/random.jpg?grayscale=true'; import Greeting from '../components/Greeting.vue'; +import Counter from '../components/Counter.vue'; --- @@ -26,9 +27,14 @@ import Greeting from '../components/Greeting.vue'; -
-

ImageTools

- -
- - +
+

ImageTools

+ +
+ +
+

Hydrated component

+ +
+ + \ No newline at end of file diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f6f47cd98b56..d24115dbe2e2 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -367,9 +367,10 @@ export interface SSRMetadata { } export interface SSRResult { - styles: Set; - scripts: Set; - links: Set; - createAstro(Astro: AstroGlobalPartial, props: Record, slots: Record | null): AstroGlobal; - _metadata: SSRMetadata; + styles: Set; + scripts: Set; + links: Set; + createAstro(Astro: AstroGlobalPartial, props: Record, slots: Record | null): AstroGlobal; + resolve: (s: string) => Promise; + _metadata: SSRMetadata; } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index b30ea7ddf884..adbddecea2cf 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -12,8 +12,10 @@ export interface BuildInternals { // This is a virtual JS module that imports all dependent styles for a page. astroPageStyleMap: Map; - // A mapping to entrypoints (facadeId) to assets (styles) that are added. - facadeIdToAssetsMap: Map; + // A mapping to entrypoints (facadeId) to assets (styles) that are added. + facadeIdToAssetsMap: Map; + + entrySpecifierToBundleMap: Map; } /** @@ -35,11 +37,12 @@ export function createBuildInternals(): BuildInternals { // A mapping to entrypoints (facadeId) to assets (styles) that are added. const facadeIdToAssetsMap = new Map(); - return { - pureCSSChunks, - chunkToReferenceIdMap, - astroStyleMap, - astroPageStyleMap, - facadeIdToAssetsMap, - }; + return { + pureCSSChunks, + chunkToReferenceIdMap, + astroStyleMap, + astroPageStyleMap, + facadeIdToAssetsMap, + entrySpecifierToBundleMap: new Map(), + }; } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 191cdb9870cd..4838f8983e0e 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -1,6 +1,6 @@ import type { OutputChunk, PreRenderedChunk, RollupOutput } from 'rollup'; import type { Plugin as VitePlugin } from '../vite'; -import type { AstroConfig, RouteCache } from '../../@types/astro'; +import type { AstroConfig, RouteCache, SSRElement } from '../../@types/astro'; import type { AllPagesData } from './types'; import type { LogOptions } from '../logger'; import type { ViteConfigWithSSR } from '../create-vite'; @@ -14,7 +14,9 @@ import vite from '../vite.js'; import { debug, info, error } from '../../core/logger.js'; import { createBuildInternals } from '../../core/build/internal.js'; import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; -import { renderComponent, getParamsAndProps } from '../ssr/index.js'; +import { getParamsAndProps } from '../ssr/index.js'; +import { createResult } from '../ssr/result.js'; +import { renderPage } from '../../runtime/server/index.js'; export interface StaticBuildOptions { allPages: AllPagesData; @@ -38,11 +40,18 @@ export async function staticBuild(opts: StaticBuildOptions) { for (const [component, pageData] of Object.entries(allPages)) { const [renderers, mod] = pageData.preload; - // Hydrated components are statically identified. - for (const path of mod.$$metadata.getAllHydratedComponentPaths()) { - // Note that this part is not yet implemented in the static build. - //jsInput.add(path); - } + const topLevelImports = new Set([ + // Any component that gets hydrated + ...mod.$$metadata.hydratedComponentPaths(), + // Any hydration directive like astro/client/idle.js + ...mod.$$metadata.hydrationDirectiveSpecifiers(), + // The client path for each renderer + ...renderers.filter(renderer => !!renderer.source).map(renderer => renderer.source!), + ]); + + for(const specifier of topLevelImports) { + jsInput.add(specifier); + } let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname; jsInput.add(astroModuleId); @@ -60,37 +69,37 @@ export async function staticBuild(opts: StaticBuildOptions) { } async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { - const { astroConfig, viteConfig } = opts; - - return await vite.build({ - logLevel: 'error', - mode: 'production', - build: { - emptyOutDir: true, - minify: false, // 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles - outDir: fileURLToPath(astroConfig.dist), - ssr: true, - rollupOptions: { - input: Array.from(input), - output: { - format: 'esm', - }, - }, - target: 'es2020', // must match an esbuild target - }, - plugins: [ - vitePluginNewBuild(), - rollupPluginAstroBuildCSS({ - internals, - }), - ...(viteConfig.plugins || []), - ], - publicDir: viteConfig.publicDir, - root: viteConfig.root, - envPrefix: 'PUBLIC_', - server: viteConfig.server, - base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', - }); + const { astroConfig, viteConfig } = opts; + + return await vite.build({ + logLevel: 'error', + mode: 'production', + build: { + emptyOutDir: true, + minify: false, // 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles + outDir: fileURLToPath(astroConfig.dist), + ssr: true, + rollupOptions: { + input: Array.from(input), + output: { + format: 'esm', + }, + }, + target: 'es2020', // must match an esbuild target + }, + plugins: [ + vitePluginNewBuild(input, internals), + rollupPluginAstroBuildCSS({ + internals, + }), + ...(viteConfig.plugins || []), + ], + publicDir: viteConfig.publicDir, + root: viteConfig.root, + envPrefix: 'PUBLIC_', + server: viteConfig.server, + base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', + }); } async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { @@ -105,91 +114,133 @@ async function generatePages(result: RollupOutput, opts: StaticBuildOptions, int } async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { - const { astroConfig } = opts; - - let url = new URL('./' + output.fileName, astroConfig.dist); - const facadeId: string = output.facadeModuleId as string; - let pageData = - facadeIdToPageDataMap.get(facadeId) || - // Check with a leading `/` because on Windows it doesn't have one. - facadeIdToPageDataMap.get('/' + facadeId); - - if (!pageData) { - throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`); - } - - let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || []; - let compiledModule = await import(url.toString()); - let Component = compiledModule.default; - - const generationOptions: Readonly = { - pageData, - linkIds, - Component, - }; - - const renderPromises = pageData.paths.map((path) => { - return generatePath(path, opts, generationOptions); - }); - return await Promise.all(renderPromises); + const { astroConfig } = opts; + + let url = new URL('./' + output.fileName, astroConfig.dist); + const facadeId: string = output.facadeModuleId as string; + let pageData = + facadeIdToPageDataMap.get(facadeId) || + // Check with a leading `/` because on Windows it doesn't have one. + facadeIdToPageDataMap.get('/' + facadeId); + + if (!pageData) { + throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`); + } + + let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || []; + let compiledModule = await import(url.toString()); + let Component = compiledModule.default; + + const generationOptions: Readonly = { + pageData, + internals, + linkIds, + Component, + }; + + const renderPromises = pageData.paths.map((path) => { + return generatePath(path, opts, generationOptions); + }); + return await Promise.all(renderPromises); } interface GeneratePathOptions { - pageData: PageBuildData; - linkIds: string[]; - Component: AstroComponentFactory; + pageData: PageBuildData; + internals: BuildInternals; + linkIds: string[]; + Component: AstroComponentFactory; } -async function generatePath(path: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { - const { astroConfig, logging, origin, routeCache } = opts; - const { Component, linkIds, pageData } = gopts; - - const [renderers, mod] = pageData.preload; - - try { - const [params, pageProps] = await getParamsAndProps({ - route: pageData.route, - routeCache, - logging, - pathname: path, - mod, - }); - - info(logging, 'generate', `Generating: ${path}`); - - const html = await renderComponent(renderers, Component, astroConfig, path, origin, params, pageProps, linkIds); - const outFolder = new URL('.' + path + '/', astroConfig.dist); - const outFile = new URL('./index.html', outFolder); - await fs.promises.mkdir(outFolder, { recursive: true }); - await fs.promises.writeFile(outFile, html, 'utf-8'); - } catch (err) { - error(opts.logging, 'build', `Error rendering:`, err); - } +async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { + const { astroConfig, logging, origin, routeCache } = opts; + const { Component, internals, linkIds, pageData } = gopts; + + const [renderers, mod] = pageData.preload; + + try { + const [params, pageProps] = await getParamsAndProps({ + route: pageData.route, + routeCache, + logging, + pathname, + mod, + }); + + info(logging, 'generate', `Generating: ${pathname}`); + + const result = createResult({ astroConfig, origin, params, pathname, renderers }); + result.links = new Set( + linkIds.map((href) => ({ + props: { + rel: 'stylesheet', + href, + }, + children: '', + })) + ); + result.resolve = async (specifier: string) => { + const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); + if(typeof hashedFilePath !== 'string') { + throw new Error(`Cannot find the built path for ${specifier}`); + } + console.log("WE GOT", hashedFilePath) + return hashedFilePath; + }; + + let html = await renderPage(result, Component, pageProps, null); + const outFolder = new URL('.' + pathname + '/', astroConfig.dist); + const outFile = new URL('./index.html', outFolder); + await fs.promises.mkdir(outFolder, { recursive: true }); + await fs.promises.writeFile(outFile, html, 'utf-8'); + } catch (err) { + error(opts.logging, 'build', `Error rendering:`, err); + } } -export function vitePluginNewBuild(): VitePlugin { - return { - name: '@astro/rollup-plugin-new-build', - - configResolved(resolvedConfig) { - // Delete this hook because it causes assets not to be built - const plugins = resolvedConfig.plugins as VitePlugin[]; - const viteAsset = plugins.find((p) => p.name === 'vite:asset'); - if (viteAsset) { - delete viteAsset.generateBundle; - } - }, - - outputOptions(outputOptions) { - Object.assign(outputOptions, { - entryFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].mjs'; - }, - chunkFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].mjs'; - }, - }); - return outputOptions; - }, - }; +export function vitePluginNewBuild(input: Set, internals: BuildInternals): VitePlugin { + return { + name: '@astro/rollup-plugin-new-build', + + configResolved(resolvedConfig) { + // Delete this hook because it causes assets not to be built + const plugins = resolvedConfig.plugins as VitePlugin[]; + const viteAsset = plugins.find((p) => p.name === 'vite:asset'); + if (viteAsset) { + delete viteAsset.generateBundle; + } + }, + + outputOptions(outputOptions) { + Object.assign(outputOptions, { + entryFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].mjs'; + }, + chunkFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].mjs'; + }, + }); + return outputOptions; + }, + + async generateBundle(_options, bundle) { + const promises = []; + const mapping = new Map(); + for(const specifier of input) { + promises.push(this.resolve(specifier).then(result => { + if(result) { + mapping.set(result.id, specifier); + } + })); + } + await Promise.all(promises); + for(const [, chunk] of Object.entries(bundle)) { + if(chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) { + const specifier = mapping.get(chunk.facadeModuleId)!; + internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName); + } + } + + console.log(internals.entrySpecifierToBundleMap); + } + }; } diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts index b53411b912f5..5553154d8978 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -1,20 +1,17 @@ import type { BuildResult } from 'esbuild'; import type vite from '../vite'; import type { - AstroConfig, - AstroGlobal, - AstroGlobalPartial, - ComponentInstance, - GetStaticPathsResult, - Params, - Props, - Renderer, - RouteCache, - RouteData, - RuntimeMode, - SSRElement, - SSRError, - SSRResult, + AstroConfig, + ComponentInstance, + GetStaticPathsResult, + Params, + Props, + Renderer, + RouteCache, + RouteData, + RuntimeMode, + SSRElement, + SSRError, } from '../../@types/astro'; import type { LogOptions } from '../logger'; import type { AstroComponentFactory } from '../../runtime/server/index'; @@ -23,12 +20,13 @@ import eol from 'eol'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { renderPage, renderSlot } from '../../runtime/server/index.js'; -import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency } from '../util.js'; +import { renderPage } from '../../runtime/server/index.js'; +import { codeFrame, resolveDependency } from '../util.js'; import { getStylesForURL } from './css.js'; import { injectTags } from './html.js'; import { generatePaginateFunction } from './paginate.js'; import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js'; +import { createResult } from './result.js'; const svelteStylesRE = /svelte\?svelte&type=style/; @@ -139,6 +137,7 @@ export async function preload({ astroConfig, filePath, viteServer }: SSROptions) return [renderers, mod]; } +// TODO REMOVE export async function renderComponent( renderers: Renderer[], Component: AstroComponentFactory, @@ -149,63 +148,20 @@ export async function renderComponent( pageProps: Props, links: string[] = [] ): Promise { - const _links = new Set( - links.map((href) => ({ - props: { - rel: 'stylesheet', - href, - }, - children: '', - })) - ); - const result: SSRResult = { - styles: new Set(), - scripts: new Set(), - links: _links, - /** This function returns the `Astro` faux-global */ - createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { - const site = new URL(origin); - const url = new URL('.' + pathname, site); - const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); - return { - __proto__: astroGlobal, - props, - request: { - canonicalURL, - params, - url, - }, - slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), - // This is used for but shouldn't be used publicly - privateRenderSlotDoNotUse(slotName: string) { - return renderSlot(result, slots ? slots[slotName] : null); - }, - // also needs the same `astroConfig.markdownOptions.render` as `.md` pages - async privateRenderMarkdownDoNotUse(content: string, opts: any) { - let mdRender = astroConfig.markdownOptions.render; - let renderOpts = {}; - if (Array.isArray(mdRender)) { - renderOpts = mdRender[1]; - mdRender = mdRender[0]; - } - if (typeof mdRender === 'string') { - ({ default: mdRender } = await import(mdRender)); - } - const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); - return code; - }, - } as unknown as AstroGlobal; - }, - _metadata: { - renderers, - pathname, - experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, - }, - }; - - let html = await renderPage(result, Component, pageProps, null); - - return html; + const result = createResult({ astroConfig, origin, params, pathname, renderers }); + result.links = new Set( + links.map((href) => ({ + props: { + rel: 'stylesheet', + href, + }, + children: '', + })) + ); + + let html = await renderPage(result, Component, pageProps, null); + + return html; } export async function getParamsAndProps({ @@ -255,149 +211,103 @@ export async function getParamsAndProps({ /** use Vite to SSR */ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise { - const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; - - // Handle dynamic routes - let params: Params = {}; - let pageProps: Props = {}; - if (route && !route.pathname) { - if (route.params.length) { - const paramsMatch = route.pattern.exec(pathname); - if (paramsMatch) { - params = getParams(route.params)(paramsMatch); - } - } - validateGetStaticPathsModule(mod); - if (!routeCache[route.component]) { - routeCache[route.component] = await ( - await mod.getStaticPaths!({ - paginate: generatePaginateFunction(route), - rss: () => { - /* noop */ - }, - }) - ).flat(); - } - validateGetStaticPathsResult(routeCache[route.component], logging); - const routePathParams: GetStaticPathsResult = routeCache[route.component]; - const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params)); - if (!matchedStaticPath) { - throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`); - } - pageProps = { ...matchedStaticPath.props } || {}; - } - - // Validate the page component before rendering the page - const Component = await mod.default; - if (!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})`); - - // Create the result object that will be passed into the render function. - // This object starts here as an empty shell (not yet the result) but then - // calling the render() function will populate the object with scripts, styles, etc. - const result: SSRResult = { - styles: new Set(), - scripts: new Set(), - links: new Set(), - /** This function returns the `Astro` faux-global */ - createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { - const site = new URL(origin); - const url = new URL('.' + pathname, site); - const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); - return { - __proto__: astroGlobal, - props, - request: { - canonicalURL, - params, - url, - }, - slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), - // This is used for but shouldn't be used publicly - privateRenderSlotDoNotUse(slotName: string) { - return renderSlot(result, slots ? slots[slotName] : null); - }, - // also needs the same `astroConfig.markdownOptions.render` as `.md` pages - async privateRenderMarkdownDoNotUse(content: string, opts: any) { - let mdRender = astroConfig.markdownOptions.render; - let renderOpts = {}; - if (Array.isArray(mdRender)) { - renderOpts = mdRender[1]; - mdRender = mdRender[0]; - } - // ['rehype-toc', opts] - if (typeof mdRender === 'string') { - ({ default: mdRender } = await import(mdRender)); - } - // [import('rehype-toc'), opts] - else if (mdRender instanceof Promise) { - ({ default: mdRender } = await mdRender); - } - const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); - return code; - }, - } as unknown as AstroGlobal; - }, - _metadata: { - renderers, - pathname, - experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, - }, - }; - - let html = await renderPage(result, Component, pageProps, null); - - // inject tags - const tags: vite.HtmlTagDescriptor[] = []; - - // dev only: inject Astro HMR client - if (mode === 'development') { - tags.push({ - tag: 'script', - attrs: { type: 'module' }, - // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure - // `import.meta.hot` is properly handled by Vite - children: await getHmrScript(), - injectTo: 'head', - }); - } - - // inject CSS - [...getStylesForURL(filePath, viteServer)].forEach((href) => { - if (mode === 'development' && svelteStylesRE.test(href)) { - tags.push({ - tag: 'script', - attrs: { type: 'module', src: href }, - injectTo: 'head', - }); - } else { - tags.push({ - tag: 'link', - attrs: { - rel: 'stylesheet', - href, - 'data-astro-injected': true, - }, - injectTo: 'head', - }); - } - }); - - // add injected tags - html = injectTags(html, tags); - - // run transformIndexHtml() in dev to run Vite dev transformations - if (mode === 'development') { - const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); - html = await viteServer.transformIndexHtml(relativeURL, html, pathname); - } - - // inject if missing (TODO: is a more robust check needed for comments, etc.?) - if (!/\n' + html; - } - - return html; + const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; + + // Handle dynamic routes + let params: Params = {}; + let pageProps: Props = {}; + if (route && !route.pathname) { + if (route.params.length) { + const paramsMatch = route.pattern.exec(pathname); + if (paramsMatch) { + params = getParams(route.params)(paramsMatch); + } + } + validateGetStaticPathsModule(mod); + if (!routeCache[route.component]) { + routeCache[route.component] = await ( + await mod.getStaticPaths!({ + paginate: generatePaginateFunction(route), + rss: () => { + /* noop */ + }, + }) + ).flat(); + } + validateGetStaticPathsResult(routeCache[route.component], logging); + const routePathParams: GetStaticPathsResult = routeCache[route.component]; + const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params)); + if (!matchedStaticPath) { + throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`); + } + pageProps = { ...matchedStaticPath.props } || {}; + } + + // Validate the page component before rendering the page + const Component = await mod.default; + if (!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({ astroConfig, origin, params, pathname, renderers }); + result.resolve = async (s: string) => { + const [, path] = await viteServer.moduleGraph.resolveUrl(s); + return path; + }; + + let html = await renderPage(result, Component, pageProps, null); + + // inject tags + const tags: vite.HtmlTagDescriptor[] = []; + + // dev only: inject Astro HMR client + if (mode === 'development') { + tags.push({ + tag: 'script', + attrs: { type: 'module' }, + // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure + // `import.meta.hot` is properly handled by Vite + children: await getHmrScript(), + injectTo: 'head', + }); + } + + // inject CSS + [...getStylesForURL(filePath, viteServer)].forEach((href) => { + if (mode === 'development' && svelteStylesRE.test(href)) { + tags.push({ + tag: 'script', + attrs: { type: 'module', src: href }, + injectTo: 'head', + }); + } else { + tags.push({ + tag: 'link', + attrs: { + rel: 'stylesheet', + href, + 'data-astro-injected': true, + }, + injectTo: 'head', + }); + } + }); + + // add injected tags + html = injectTags(html, tags); + + // run transformIndexHtml() in dev to run Vite dev transformations + if (mode === 'development') { + const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); + console.log("TRANFORM", relativeURL, html); + //html = await viteServer.transformIndexHtml(relativeURL, html, pathname); + } + + // inject if missing (TODO: is a more robust check needed for comments, etc.?) + if (!/\n' + html; + } + + return html; } let hmrScript: string; diff --git a/packages/astro/src/core/ssr/result.ts b/packages/astro/src/core/ssr/result.ts new file mode 100644 index 000000000000..c8dc731b7aba --- /dev/null +++ b/packages/astro/src/core/ssr/result.ts @@ -0,0 +1,83 @@ +import type { + AstroConfig, + AstroGlobal, + AstroGlobalPartial, + Params, + Renderer, + SSRElement, + SSRResult, +} from '../../@types/astro'; + +import { canonicalURL as getCanonicalURL } from '../util.js'; +import { renderSlot } from '../../runtime/server/index.js'; + +export interface CreateResultArgs { + astroConfig: AstroConfig; + origin: string; + params: Params; + pathname: string; + renderers: Renderer[]; +} + +export function createResult(args: CreateResultArgs): SSRResult { + const { astroConfig, origin, params, pathname, renderers } = args; + + // Create the result object that will be passed into the render function. + // This object starts here as an empty shell (not yet the result) but then + // calling the render() function will populate the object with scripts, styles, etc. + const result: SSRResult = { + styles: new Set(), + scripts: new Set(), + links: new Set(), + /** This function returns the `Astro` faux-global */ + createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { + const site = new URL(origin); + const url = new URL('.' + pathname, site); + const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); + return { + __proto__: astroGlobal, + props, + request: { + canonicalURL, + params, + url, + }, + slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), + // This is used for but shouldn't be used publicly + privateRenderSlotDoNotUse(slotName: string) { + return renderSlot(result, slots ? slots[slotName] : null); + }, + // also needs the same `astroConfig.markdownOptions.render` as `.md` pages + async privateRenderMarkdownDoNotUse(content: string, opts: any) { + let mdRender = astroConfig.markdownOptions.render; + let renderOpts = {}; + if (Array.isArray(mdRender)) { + renderOpts = mdRender[1]; + mdRender = mdRender[0]; + } + // ['rehype-toc', opts] + if (typeof mdRender === 'string') { + ({ default: mdRender } = await import(mdRender)); + } + // [import('rehype-toc'), opts] + else if (mdRender instanceof Promise) { + ({ default: mdRender } = await mdRender); + } + const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); + return code; + }, + } as unknown as AstroGlobal; + }, + // This is a stub and will be implemented by dev and build. + async resolve(s: string): Promise { + return ''; + }, + _metadata: { + renderers, + pathname, + experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, + }, + }; + + return result; +} \ No newline at end of file diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index fd71287ddb75..3cf985f5e7d5 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,8 +1,8 @@ import type { AstroComponentMetadata } from '../../@types/astro'; -import type { SSRElement } from '../../@types/astro'; +import type { SSRElement, SSRResult } from '../../@types/astro'; import { valueToEstree } from 'estree-util-value-to-estree'; import * as astring from 'astring'; -import { serializeListValue } from './util.js'; +import { hydrationSpecifier, serializeListValue } from './util.js'; const { generate, GENERATOR } = astring; @@ -46,66 +46,70 @@ interface ExtractedProps { // Used to extract the directives, aka `client:load` information about a component. // Finds these special props and removes them from what gets passed into the component. export function extractDirectives(inputProps: Record): ExtractedProps { - let extracted: ExtractedProps = { - hydration: null, - props: {}, - }; - for (const [key, value] of Object.entries(inputProps)) { - if (key.startsWith('client:')) { - if (!extracted.hydration) { - extracted.hydration = { - directive: '', - value: '', - componentUrl: '', - componentExport: { value: '' }, - }; - } - switch (key) { - case 'client:component-path': { - extracted.hydration.componentUrl = value; - break; - } - case 'client:component-export': { - extracted.hydration.componentExport.value = value; - break; - } - default: { - extracted.hydration.directive = key.split(':')[1]; - extracted.hydration.value = value; - - // throw an error if an invalid hydration directive was provided - 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 an error if the query wasn't provided for client:media - if (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; - } - } - } else if (key === 'class:list') { - // support "class" from an expression passed into a component (#782) - extracted.props[key.slice(0, -5)] = serializeListValue(value); - } else { - extracted.props[key] = value; - } - } - return extracted; + let extracted: ExtractedProps = { + hydration: null, + props: {}, + }; + for (const [key, value] of Object.entries(inputProps)) { + if (key.startsWith('client:')) { + if (!extracted.hydration) { + extracted.hydration = { + directive: '', + value: '', + componentUrl: '', + componentExport: { value: '' }, + }; + } + switch (key) { + case 'client:component-path': { + extracted.hydration.componentUrl = value; + break; + } + case 'client:component-export': { + extracted.hydration.componentExport.value = value; + break; + } + case 'client:component-hydration': { + break; + } + default: { + extracted.hydration.directive = key.split(':')[1]; + extracted.hydration.value = value; + + // throw an error if an invalid hydration directive was provided + 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 an error if the query wasn't provided for client:media + if (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; + } + } + } else if (key === 'class:list') { + // support "class" from an expression passed into a component (#782) + extracted.props[key.slice(0, -5)] = serializeListValue(value); + } else { + extracted.props[key] = value; + } + } + return extracted; } interface HydrateScriptOptions { - renderer: any; - astroId: string; - props: Record; + renderer: any; + result: SSRResult; + astroId: string; + props: Record; } /** For hydrated components, generate a \ No newline at end of file + return { + count, + add, + subtract, + }; + }, +}; + diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index d24115dbe2e2..2e60da3b7703 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -367,10 +367,10 @@ export interface SSRMetadata { } export interface SSRResult { - styles: Set; - scripts: Set; - links: Set; - createAstro(Astro: AstroGlobalPartial, props: Record, slots: Record | null): AstroGlobal; - resolve: (s: string) => Promise; - _metadata: SSRMetadata; + styles: Set; + scripts: Set; + links: Set; + createAstro(Astro: AstroGlobalPartial, props: Record, slots: Record | null): AstroGlobal; + resolve: (s: string) => Promise; + _metadata: SSRMetadata; } diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index adbddecea2cf..e60f0ed1c09f 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -12,10 +12,10 @@ export interface BuildInternals { // This is a virtual JS module that imports all dependent styles for a page. astroPageStyleMap: Map; - // A mapping to entrypoints (facadeId) to assets (styles) that are added. - facadeIdToAssetsMap: Map; + // A mapping to entrypoints (facadeId) to assets (styles) that are added. + facadeIdToAssetsMap: Map; - entrySpecifierToBundleMap: Map; + entrySpecifierToBundleMap: Map; } /** @@ -37,12 +37,12 @@ export function createBuildInternals(): BuildInternals { // A mapping to entrypoints (facadeId) to assets (styles) that are added. const facadeIdToAssetsMap = new Map(); - return { - pureCSSChunks, - chunkToReferenceIdMap, - astroStyleMap, - astroPageStyleMap, - facadeIdToAssetsMap, - entrySpecifierToBundleMap: new Map(), - }; + return { + pureCSSChunks, + chunkToReferenceIdMap, + astroStyleMap, + astroPageStyleMap, + facadeIdToAssetsMap, + entrySpecifierToBundleMap: new Map(), + }; } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 6a7f06eb820c..2e893838086a 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -32,119 +32,115 @@ export interface StaticBuildOptions { export async function staticBuild(opts: StaticBuildOptions) { const { allPages, astroConfig } = opts; - // The pages - const pageInput = new Set(); + // The pages + const pageInput = new Set(); - // The JavaScript entrypoints. - const jsInput = new Set(); + // The JavaScript entrypoints. + const jsInput = new Set(); // A map of each page .astro file, to the PageBuildData which contains information // about that page, such as its paths. const facadeIdToPageDataMap = new Map(); - for (const [component, pageData] of Object.entries(allPages)) { - const [renderers, mod] = pageData.preload; - const metadata = mod.$$metadata; - - const topLevelImports = new Set([ - // Any component that gets hydrated - ...metadata.hydratedComponentPaths(), - // Any hydration directive like astro/client/idle.js - ...metadata.hydrationDirectiveSpecifiers(), - // The client path for each renderer - ...renderers.filter(renderer => !!renderer.source).map(renderer => renderer.source!), - ]); - - for(const specifier of topLevelImports) { - jsInput.add(specifier); - } + for (const [component, pageData] of Object.entries(allPages)) { + const [renderers, mod] = pageData.preload; + const metadata = mod.$$metadata; + + const topLevelImports = new Set([ + // Any component that gets hydrated + ...metadata.hydratedComponentPaths(), + // Any hydration directive like astro/client/idle.js + ...metadata.hydrationDirectiveSpecifiers(), + // The client path for each renderer + ...renderers.filter((renderer) => !!renderer.source).map((renderer) => renderer.source!), + ]); + + for (const specifier of topLevelImports) { + jsInput.add(specifier); + } - let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname; - pageInput.add(astroModuleId); - facadeIdToPageDataMap.set(astroModuleId, pageData); - } + let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname; + pageInput.add(astroModuleId); + facadeIdToPageDataMap.set(astroModuleId, pageData); + } // Build internals needed by the CSS plugin const internals = createBuildInternals(); - // Run the SSR build and client build in parallel - const [ssrResult] = await Promise.all([ - ssrBuild(opts, internals, pageInput), - clientBuild(opts, internals, jsInput) - ]) as RollupOutput[]; + // Run the SSR build and client build in parallel + const [ssrResult] = (await Promise.all([ssrBuild(opts, internals, pageInput), clientBuild(opts, internals, jsInput)])) as RollupOutput[]; - // Generate each of the pages. - await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap); - await cleanSsrOutput(opts); + // Generate each of the pages. + await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap); + await cleanSsrOutput(opts); } async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { - const { astroConfig, viteConfig } = opts; - - return await vite.build({ - logLevel: 'error', - mode: 'production', - build: { - emptyOutDir: true, - minify: false, - outDir: fileURLToPath(astroConfig.dist), - ssr: true, - rollupOptions: { - input: Array.from(input), - output: { - format: 'esm', - }, - }, - target: 'es2020', // must match an esbuild target - }, - plugins: [ - vitePluginNewBuild(input, internals, 'mjs'), - rollupPluginAstroBuildCSS({ - internals, - }), - ...(viteConfig.plugins || []), - ], - publicDir: viteConfig.publicDir, - root: viteConfig.root, - envPrefix: 'PUBLIC_', - server: viteConfig.server, - base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', - }); + const { astroConfig, viteConfig } = opts; + + return await vite.build({ + logLevel: 'error', + mode: 'production', + build: { + emptyOutDir: true, + minify: false, + outDir: fileURLToPath(astroConfig.dist), + ssr: true, + rollupOptions: { + input: Array.from(input), + output: { + format: 'esm', + }, + }, + target: 'es2020', // must match an esbuild target + }, + plugins: [ + vitePluginNewBuild(input, internals, 'mjs'), + rollupPluginAstroBuildCSS({ + internals, + }), + ...(viteConfig.plugins || []), + ], + publicDir: viteConfig.publicDir, + root: viteConfig.root, + envPrefix: 'PUBLIC_', + server: viteConfig.server, + base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', + }); } async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { - const { astroConfig, viteConfig } = opts; - - return await vite.build({ - logLevel: 'error', - mode: 'production', - build: { - emptyOutDir: false, - minify: 'esbuild', - outDir: fileURLToPath(astroConfig.dist), - rollupOptions: { - input: Array.from(input), - output: { - format: 'esm', - }, - preserveEntrySignatures: 'exports-only', - }, - target: 'es2020', // must match an esbuild target - - }, - plugins: [ - vitePluginNewBuild(input, internals, 'js'), - rollupPluginAstroBuildCSS({ - internals, - }), - ...(viteConfig.plugins || []), - ], - publicDir: viteConfig.publicDir, - root: viteConfig.root, - envPrefix: 'PUBLIC_', - server: viteConfig.server, - base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', - }); + const { astroConfig, viteConfig } = opts; + + return await vite.build({ + logLevel: 'error', + mode: 'production', + build: { + emptyOutDir: false, + minify: 'esbuild', + outDir: fileURLToPath(astroConfig.dist), + rollupOptions: { + input: Array.from(input), + output: { + format: 'esm', + }, + preserveEntrySignatures: 'exports-only', + }, + target: 'es2020', // must match an esbuild target + }, + plugins: [ + vitePluginNewBuild(input, internals, 'js'), + rollupPluginAstroBuildCSS({ + internals, + }), + ...(viteConfig.plugins || []), + ], + publicDir: viteConfig.publicDir, + root: viteConfig.root, + envPrefix: 'PUBLIC_', + server: viteConfig.server, + base: astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/', + }); } async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { @@ -159,144 +155,148 @@ async function generatePages(result: RollupOutput, opts: StaticBuildOptions, int } async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map) { - const { astroConfig } = opts; - - let url = new URL('./' + output.fileName, astroConfig.dist); - const facadeId: string = output.facadeModuleId as string; - let pageData = - facadeIdToPageDataMap.get(facadeId) || - // Check with a leading `/` because on Windows it doesn't have one. - facadeIdToPageDataMap.get('/' + facadeId); - - if (!pageData) { - throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`); - } - - let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || []; - let compiledModule = await import(url.toString()); - let Component = compiledModule.default; - - const generationOptions: Readonly = { - pageData, - internals, - linkIds, - Component, - }; - - const renderPromises = pageData.paths.map((path) => { - return generatePath(path, opts, generationOptions); - }); - return await Promise.all(renderPromises); + const { astroConfig } = opts; + + let url = new URL('./' + output.fileName, astroConfig.dist); + const facadeId: string = output.facadeModuleId as string; + let pageData = + facadeIdToPageDataMap.get(facadeId) || + // Check with a leading `/` because on Windows it doesn't have one. + facadeIdToPageDataMap.get('/' + facadeId); + + if (!pageData) { + throw new Error(`Unable to find a PageBuildData for the Astro page: ${facadeId}. There are the PageBuilDatas we have ${Array.from(facadeIdToPageDataMap.keys()).join(', ')}`); + } + + let linkIds = internals.facadeIdToAssetsMap.get(facadeId) || []; + let compiledModule = await import(url.toString()); + let Component = compiledModule.default; + + const generationOptions: Readonly = { + pageData, + internals, + linkIds, + Component, + }; + + const renderPromises = pageData.paths.map((path) => { + return generatePath(path, opts, generationOptions); + }); + return await Promise.all(renderPromises); } interface GeneratePathOptions { - pageData: PageBuildData; - internals: BuildInternals; - linkIds: string[]; - Component: AstroComponentFactory; + pageData: PageBuildData; + internals: BuildInternals; + linkIds: string[]; + Component: AstroComponentFactory; } async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { - const { astroConfig, logging, origin, routeCache } = opts; - const { Component, internals, linkIds, pageData } = gopts; - - const [renderers, mod] = pageData.preload; - - try { - const [params, pageProps] = await getParamsAndProps({ - route: pageData.route, - routeCache, - logging, - pathname, - mod, - }); - - debug(logging, 'generate', `Generating: ${pathname}`); - - const result = createResult({ astroConfig, origin, params, pathname, renderers }); - result.links = new Set( - linkIds.map((href) => ({ - props: { - rel: 'stylesheet', - href, - }, - children: '', - })) - ); - result.resolve = async (specifier: string) => { - const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); - if(typeof hashedFilePath !== 'string') { - throw new Error(`Cannot find the built path for ${specifier}`); - } - const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); - const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; - return fullyRelativePath; - }; - - let html = await renderPage(result, Component, pageProps, null); - const outFolder = new URL('.' + pathname + '/', astroConfig.dist); - const outFile = new URL('./index.html', outFolder); - await fs.promises.mkdir(outFolder, { recursive: true }); - await fs.promises.writeFile(outFile, html, 'utf-8'); - } catch (err) { - error(opts.logging, 'build', `Error rendering:`, err); - } + const { astroConfig, logging, origin, routeCache } = opts; + const { Component, internals, linkIds, pageData } = gopts; + + const [renderers, mod] = pageData.preload; + + try { + const [params, pageProps] = await getParamsAndProps({ + route: pageData.route, + routeCache, + logging, + pathname, + mod, + }); + + debug(logging, 'generate', `Generating: ${pathname}`); + + const result = createResult({ astroConfig, origin, params, pathname, renderers }); + result.links = new Set( + linkIds.map((href) => ({ + props: { + rel: 'stylesheet', + href, + }, + children: '', + })) + ); + result.resolve = async (specifier: string) => { + const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier); + if (typeof hashedFilePath !== 'string') { + throw new Error(`Cannot find the built path for ${specifier}`); + } + const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); + const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; + return fullyRelativePath; + }; + + let html = await renderPage(result, Component, pageProps, null); + const outFolder = new URL('.' + pathname + '/', astroConfig.dist); + const outFile = new URL('./index.html', outFolder); + await fs.promises.mkdir(outFolder, { recursive: true }); + await fs.promises.writeFile(outFile, html, 'utf-8'); + } catch (err) { + error(opts.logging, 'build', `Error rendering:`, err); + } } async function cleanSsrOutput(opts: StaticBuildOptions) { - // The SSR output is all .mjs files, the client output is not. - const files = await glob('**/*.mjs', { - cwd: opts.astroConfig.dist.pathname, - //ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)), - }); - await Promise.all(files.map(async filename => { - const url = new URL(filename, opts.astroConfig.dist); - await fs.promises.rm(url); - })); + // The SSR output is all .mjs files, the client output is not. + const files = await glob('**/*.mjs', { + cwd: opts.astroConfig.dist.pathname, + //ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)), + }); + await Promise.all( + files.map(async (filename) => { + const url = new URL(filename, opts.astroConfig.dist); + await fs.promises.rm(url); + }) + ); } export function vitePluginNewBuild(input: Set, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin { - return { - name: '@astro/rollup-plugin-new-build', - - configResolved(resolvedConfig) { - // Delete this hook because it causes assets not to be built - const plugins = resolvedConfig.plugins as VitePlugin[]; - const viteAsset = plugins.find((p) => p.name === 'vite:asset'); - if (viteAsset) { - delete viteAsset.generateBundle; - } - }, - - outputOptions(outputOptions) { - Object.assign(outputOptions, { - entryFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].' + ext; - }, - chunkFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].' + ext; - }, - }); - return outputOptions; - }, - - async generateBundle(_options, bundle) { - const promises = []; - const mapping = new Map(); - for(const specifier of input) { - promises.push(this.resolve(specifier).then(result => { - if(result) { - mapping.set(result.id, specifier); - } - })); - } - await Promise.all(promises); - for(const [, chunk] of Object.entries(bundle)) { - if(chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) { - const specifier = mapping.get(chunk.facadeModuleId)!; - internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName); - } - } - } - }; + return { + name: '@astro/rollup-plugin-new-build', + + configResolved(resolvedConfig) { + // Delete this hook because it causes assets not to be built + const plugins = resolvedConfig.plugins as VitePlugin[]; + const viteAsset = plugins.find((p) => p.name === 'vite:asset'); + if (viteAsset) { + delete viteAsset.generateBundle; + } + }, + + outputOptions(outputOptions) { + Object.assign(outputOptions, { + entryFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].' + ext; + }, + chunkFileNames(_chunk: PreRenderedChunk) { + return 'assets/[name].[hash].' + ext; + }, + }); + return outputOptions; + }, + + async generateBundle(_options, bundle) { + const promises = []; + const mapping = new Map(); + for (const specifier of input) { + promises.push( + this.resolve(specifier).then((result) => { + if (result) { + mapping.set(result.id, specifier); + } + }) + ); + } + await Promise.all(promises); + for (const [, chunk] of Object.entries(bundle)) { + if (chunk.type === 'chunk' && chunk.facadeModuleId && mapping.has(chunk.facadeModuleId)) { + const specifier = mapping.get(chunk.facadeModuleId)!; + internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName); + } + } + }, + }; } diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts index 5553154d8978..35ab11ca6f76 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -1,18 +1,6 @@ import type { BuildResult } from 'esbuild'; import type vite from '../vite'; -import type { - AstroConfig, - ComponentInstance, - GetStaticPathsResult, - Params, - Props, - Renderer, - RouteCache, - RouteData, - RuntimeMode, - SSRElement, - SSRError, -} from '../../@types/astro'; +import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, Renderer, RouteCache, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro'; import type { LogOptions } from '../logger'; import type { AstroComponentFactory } from '../../runtime/server/index'; @@ -148,20 +136,20 @@ export async function renderComponent( pageProps: Props, links: string[] = [] ): Promise { - const result = createResult({ astroConfig, origin, params, pathname, renderers }); - result.links = new Set( - links.map((href) => ({ - props: { - rel: 'stylesheet', - href, - }, - children: '', - })) - ); - - let html = await renderPage(result, Component, pageProps, null); - - return html; + const result = createResult({ astroConfig, origin, params, pathname, renderers }); + result.links = new Set( + links.map((href) => ({ + props: { + rel: 'stylesheet', + href, + }, + children: '', + })) + ); + + let html = await renderPage(result, Component, pageProps, null); + + return html; } export async function getParamsAndProps({ @@ -211,103 +199,103 @@ export async function getParamsAndProps({ /** use Vite to SSR */ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise { - const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; - - // Handle dynamic routes - let params: Params = {}; - let pageProps: Props = {}; - if (route && !route.pathname) { - if (route.params.length) { - const paramsMatch = route.pattern.exec(pathname); - if (paramsMatch) { - params = getParams(route.params)(paramsMatch); - } - } - validateGetStaticPathsModule(mod); - if (!routeCache[route.component]) { - routeCache[route.component] = await ( - await mod.getStaticPaths!({ - paginate: generatePaginateFunction(route), - rss: () => { - /* noop */ - }, - }) - ).flat(); - } - validateGetStaticPathsResult(routeCache[route.component], logging); - const routePathParams: GetStaticPathsResult = routeCache[route.component]; - const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params)); - if (!matchedStaticPath) { - throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`); - } - pageProps = { ...matchedStaticPath.props } || {}; - } - - // Validate the page component before rendering the page - const Component = await mod.default; - if (!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({ astroConfig, origin, params, pathname, renderers }); - result.resolve = async (s: string) => { - const [, path] = await viteServer.moduleGraph.resolveUrl(s); - return path; - }; - - let html = await renderPage(result, Component, pageProps, null); - - // inject tags - const tags: vite.HtmlTagDescriptor[] = []; - - // dev only: inject Astro HMR client - if (mode === 'development') { - tags.push({ - tag: 'script', - attrs: { type: 'module' }, - // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure - // `import.meta.hot` is properly handled by Vite - children: await getHmrScript(), - injectTo: 'head', - }); - } - - // inject CSS - [...getStylesForURL(filePath, viteServer)].forEach((href) => { - if (mode === 'development' && svelteStylesRE.test(href)) { - tags.push({ - tag: 'script', - attrs: { type: 'module', src: href }, - injectTo: 'head', - }); - } else { - tags.push({ - tag: 'link', - attrs: { - rel: 'stylesheet', - href, - 'data-astro-injected': true, - }, - injectTo: 'head', - }); - } - }); - - // add injected tags - html = injectTags(html, tags); - - // run transformIndexHtml() in dev to run Vite dev transformations - if (mode === 'development') { - const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); - console.log("TRANFORM", relativeURL, html); - //html = await viteServer.transformIndexHtml(relativeURL, html, pathname); - } - - // inject if missing (TODO: is a more robust check needed for comments, etc.?) - if (!/\n' + html; - } - - return html; + const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; + + // Handle dynamic routes + let params: Params = {}; + let pageProps: Props = {}; + if (route && !route.pathname) { + if (route.params.length) { + const paramsMatch = route.pattern.exec(pathname); + if (paramsMatch) { + params = getParams(route.params)(paramsMatch); + } + } + validateGetStaticPathsModule(mod); + if (!routeCache[route.component]) { + routeCache[route.component] = await ( + await mod.getStaticPaths!({ + paginate: generatePaginateFunction(route), + rss: () => { + /* noop */ + }, + }) + ).flat(); + } + validateGetStaticPathsResult(routeCache[route.component], logging); + const routePathParams: GetStaticPathsResult = routeCache[route.component]; + const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params)); + if (!matchedStaticPath) { + throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`); + } + pageProps = { ...matchedStaticPath.props } || {}; + } + + // Validate the page component before rendering the page + const Component = await mod.default; + if (!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({ astroConfig, origin, params, pathname, renderers }); + result.resolve = async (s: string) => { + const [, path] = await viteServer.moduleGraph.resolveUrl(s); + return path; + }; + + let html = await renderPage(result, Component, pageProps, null); + + // inject tags + const tags: vite.HtmlTagDescriptor[] = []; + + // dev only: inject Astro HMR client + if (mode === 'development') { + tags.push({ + tag: 'script', + attrs: { type: 'module' }, + // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure + // `import.meta.hot` is properly handled by Vite + children: await getHmrScript(), + injectTo: 'head', + }); + } + + // inject CSS + [...getStylesForURL(filePath, viteServer)].forEach((href) => { + if (mode === 'development' && svelteStylesRE.test(href)) { + tags.push({ + tag: 'script', + attrs: { type: 'module', src: href }, + injectTo: 'head', + }); + } else { + tags.push({ + tag: 'link', + attrs: { + rel: 'stylesheet', + href, + 'data-astro-injected': true, + }, + injectTo: 'head', + }); + } + }); + + // add injected tags + html = injectTags(html, tags); + + // run transformIndexHtml() in dev to run Vite dev transformations + if (mode === 'development') { + const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); + console.log('TRANFORM', relativeURL, html); + //html = await viteServer.transformIndexHtml(relativeURL, html, pathname); + } + + // inject if missing (TODO: is a more robust check needed for comments, etc.?) + if (!/\n' + html; + } + + return html; } let hmrScript: string; diff --git a/packages/astro/src/core/ssr/result.ts b/packages/astro/src/core/ssr/result.ts index c8dc731b7aba..32694dd05330 100644 --- a/packages/astro/src/core/ssr/result.ts +++ b/packages/astro/src/core/ssr/result.ts @@ -1,83 +1,75 @@ -import type { - AstroConfig, - AstroGlobal, - AstroGlobalPartial, - Params, - Renderer, - SSRElement, - SSRResult, -} from '../../@types/astro'; +import type { AstroConfig, AstroGlobal, AstroGlobalPartial, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro'; import { canonicalURL as getCanonicalURL } from '../util.js'; import { renderSlot } from '../../runtime/server/index.js'; export interface CreateResultArgs { - astroConfig: AstroConfig; - origin: string; - params: Params; - pathname: string; - renderers: Renderer[]; + astroConfig: AstroConfig; + origin: string; + params: Params; + pathname: string; + renderers: Renderer[]; } export function createResult(args: CreateResultArgs): SSRResult { - const { astroConfig, origin, params, pathname, renderers } = args; + const { astroConfig, origin, params, pathname, renderers } = args; - // Create the result object that will be passed into the render function. - // This object starts here as an empty shell (not yet the result) but then - // calling the render() function will populate the object with scripts, styles, etc. - const result: SSRResult = { - styles: new Set(), - scripts: new Set(), - links: new Set(), - /** This function returns the `Astro` faux-global */ - createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { - const site = new URL(origin); - const url = new URL('.' + pathname, site); - const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); - return { - __proto__: astroGlobal, - props, - request: { - canonicalURL, - params, - url, - }, - slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), - // This is used for but shouldn't be used publicly - privateRenderSlotDoNotUse(slotName: string) { - return renderSlot(result, slots ? slots[slotName] : null); - }, - // also needs the same `astroConfig.markdownOptions.render` as `.md` pages - async privateRenderMarkdownDoNotUse(content: string, opts: any) { - let mdRender = astroConfig.markdownOptions.render; - let renderOpts = {}; - if (Array.isArray(mdRender)) { - renderOpts = mdRender[1]; - mdRender = mdRender[0]; - } - // ['rehype-toc', opts] - if (typeof mdRender === 'string') { - ({ default: mdRender } = await import(mdRender)); - } - // [import('rehype-toc'), opts] - else if (mdRender instanceof Promise) { - ({ default: mdRender } = await mdRender); - } - const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); - return code; - }, - } as unknown as AstroGlobal; - }, - // This is a stub and will be implemented by dev and build. - async resolve(s: string): Promise { - return ''; - }, - _metadata: { - renderers, - pathname, - experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, - }, - }; + // Create the result object that will be passed into the render function. + // This object starts here as an empty shell (not yet the result) but then + // calling the render() function will populate the object with scripts, styles, etc. + const result: SSRResult = { + styles: new Set(), + scripts: new Set(), + links: new Set(), + /** This function returns the `Astro` faux-global */ + createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { + const site = new URL(origin); + const url = new URL('.' + pathname, site); + const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin); + return { + __proto__: astroGlobal, + props, + request: { + canonicalURL, + params, + url, + }, + slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])), + // This is used for but shouldn't be used publicly + privateRenderSlotDoNotUse(slotName: string) { + return renderSlot(result, slots ? slots[slotName] : null); + }, + // also needs the same `astroConfig.markdownOptions.render` as `.md` pages + async privateRenderMarkdownDoNotUse(content: string, opts: any) { + let mdRender = astroConfig.markdownOptions.render; + let renderOpts = {}; + if (Array.isArray(mdRender)) { + renderOpts = mdRender[1]; + mdRender = mdRender[0]; + } + // ['rehype-toc', opts] + if (typeof mdRender === 'string') { + ({ default: mdRender } = await import(mdRender)); + } + // [import('rehype-toc'), opts] + else if (mdRender instanceof Promise) { + ({ default: mdRender } = await mdRender); + } + const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) }); + return code; + }, + } as unknown as AstroGlobal; + }, + // This is a stub and will be implemented by dev and build. + async resolve(s: string): Promise { + return ''; + }, + _metadata: { + renderers, + pathname, + experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild, + }, + }; - return result; -} \ No newline at end of file + return result; +} diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index cab73e12346c..fa68817f9cb7 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -46,70 +46,70 @@ interface ExtractedProps { // Used to extract the directives, aka `client:load` information about a component. // Finds these special props and removes them from what gets passed into the component. export function extractDirectives(inputProps: Record): ExtractedProps { - let extracted: ExtractedProps = { - hydration: null, - props: {}, - }; - for (const [key, value] of Object.entries(inputProps)) { - if (key.startsWith('client:')) { - if (!extracted.hydration) { - extracted.hydration = { - directive: '', - value: '', - componentUrl: '', - componentExport: { value: '' }, - }; - } - switch (key) { - case 'client:component-path': { - extracted.hydration.componentUrl = value; - break; - } - case 'client:component-export': { - extracted.hydration.componentExport.value = value; - break; - } - case 'client:component-hydration': { - break; - } - default: { - extracted.hydration.directive = key.split(':')[1]; - extracted.hydration.value = value; - - // throw an error if an invalid hydration directive was provided - 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 an error if the query wasn't provided for client:media - if (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; - } - } - } else if (key === 'class:list') { - // support "class" from an expression passed into a component (#782) - extracted.props[key.slice(0, -5)] = serializeListValue(value); - } else { - extracted.props[key] = value; - } - } - return extracted; + let extracted: ExtractedProps = { + hydration: null, + props: {}, + }; + for (const [key, value] of Object.entries(inputProps)) { + if (key.startsWith('client:')) { + if (!extracted.hydration) { + extracted.hydration = { + directive: '', + value: '', + componentUrl: '', + componentExport: { value: '' }, + }; + } + switch (key) { + case 'client:component-path': { + extracted.hydration.componentUrl = value; + break; + } + case 'client:component-export': { + extracted.hydration.componentExport.value = value; + break; + } + case 'client:component-hydration': { + break; + } + default: { + extracted.hydration.directive = key.split(':')[1]; + extracted.hydration.value = value; + + // throw an error if an invalid hydration directive was provided + 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 an error if the query wasn't provided for client:media + if (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; + } + } + } else if (key === 'class:list') { + // support "class" from an expression passed into a component (#782) + extracted.props[key.slice(0, -5)] = serializeListValue(value); + } else { + extracted.props[key] = value; + } + } + return extracted; } interface HydrateScriptOptions { - renderer: any; - result: SSRResult; - astroId: string; - props: Record; + renderer: any; + result: SSRResult; + astroId: string; + props: Record; } /** For hydrated components, generate a