diff --git a/examples/fast-build/src/components/Counter.vue b/examples/fast-build/src/components/Counter.vue new file mode 100644 index 000000000..d36776319 --- /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 b5b9785da..556f6389f 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'; --- @@ -28,5 +29,10 @@ import Greeting from '../components/Greeting.vue';

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 cdb2771d5..73501ab1d 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -371,5 +371,6 @@ export interface SSRResult { 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 1028f3e4e..e8db79129 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -14,6 +14,8 @@ export interface BuildInternals { // A mapping to entrypoints (facadeId) to assets (styles) that are added. facadeIdToAssetsMap: Map; + + entrySpecifierToBundleMap: Map; } /** @@ -41,5 +43,6 @@ export function createBuildInternals(): BuildInternals { 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 a6ed89e3c..af929dda7 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,10 +40,17 @@ 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; @@ -79,7 +88,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp target: 'es2020', // must match an esbuild target }, plugins: [ - vitePluginNewBuild(), + vitePluginNewBuild(input, internals), rollupPluginAstroBuildCSS({ internals, }), @@ -124,6 +133,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter const generationOptions: Readonly = { pageData, + internals, linkIds, Component, }; @@ -136,13 +146,14 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter interface GeneratePathOptions { pageData: PageBuildData; + internals: BuildInternals; linkIds: string[]; Component: AstroComponentFactory; } -async function generatePath(path: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { +async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { const { astroConfig, logging, origin, routeCache } = opts; - const { Component, linkIds, pageData } = gopts; + const { Component, internals, linkIds, pageData } = gopts; const [renderers, mod] = pageData.preload; @@ -151,14 +162,33 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener route: pageData.route, routeCache, logging, - pathname: path, + pathname, mod, }); - info(logging, 'generate', `Generating: ${path}`); + info(logging, 'generate', `Generating: ${pathname}`); - const html = await renderComponent(renderers, Component, astroConfig, path, origin, params, pageProps, linkIds); - const outFolder = new URL('.' + path + '/', astroConfig.dist); + 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'); @@ -167,7 +197,7 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener } } -export function vitePluginNewBuild(): VitePlugin { +export function vitePluginNewBuild(input: Set, internals: BuildInternals): VitePlugin { return { name: '@astro/rollup-plugin-new-build', @@ -191,5 +221,26 @@ export function vitePluginNewBuild(): VitePlugin { }); 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 18d2e8c67..d75be2a81 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -2,8 +2,6 @@ import type { BuildResult } from 'esbuild'; import type vite from '../vite'; import type { AstroConfig, - AstroGlobal, - AstroGlobalPartial, ComponentInstance, GetStaticPathsResult, Params, @@ -14,7 +12,6 @@ import type { RuntimeMode, SSRElement, SSRError, - SSRResult, } 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,7 +148,8 @@ export async function renderComponent( pageProps: Props, links: string[] = [] ): Promise { - const _links = new Set( + const result = createResult({ astroConfig, origin, params, pathname, renderers }); + result.links = new Set( links.map((href) => ({ props: { rel: 'stylesheet', @@ -158,50 +158,6 @@ export async function renderComponent( 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); @@ -292,57 +248,10 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO 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, - }, + 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); @@ -389,7 +298,8 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO // 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); + 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.?) diff --git a/packages/astro/src/core/ssr/result.ts b/packages/astro/src/core/ssr/result.ts new file mode 100644 index 000000000..c8dc731b7 --- /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 935e543a2..2e4130540 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; @@ -69,6 +69,9 @@ export function extractDirectives(inputProps: Record): Ext extracted.hydration.componentExport.value = value; break; } + case 'client:component-hydration': { + break; + } default: { extracted.hydration.directive = key.split(':')[1]; extracted.hydration.value = value; @@ -98,13 +101,14 @@ export function extractDirectives(inputProps: Record): Ext interface HydrateScriptOptions { renderer: any; + result: SSRResult; astroId: string; props: Record; } /** For hydrated components, generate a