From 82d02aa968e779e9444976d6bf541753c6f6c7de Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 22 Dec 2021 12:13:36 -0500 Subject: [PATCH] Gets hydration totally working --- examples/fast-build/package.json | 1 + examples/fast-build/src/pages/[pokemon].astro | 20 +++++ packages/astro/src/core/build/static-build.ts | 90 +++++++++++++++---- .../astro/src/runtime/server/hydration.ts | 2 +- packages/astro/src/runtime/server/index.ts | 1 + packages/astro/src/runtime/server/metadata.ts | 8 +- .../astro/src/vite-plugin-astro/compile.ts | 5 +- .../astro/src/vite-plugin-build-html/index.ts | 2 +- packages/renderers/renderer-vue/server.js | 2 +- 9 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 examples/fast-build/src/pages/[pokemon].astro diff --git a/examples/fast-build/package.json b/examples/fast-build/package.json index 4abd5fc13..0d95a121b 100644 --- a/examples/fast-build/package.json +++ b/examples/fast-build/package.json @@ -6,6 +6,7 @@ "dev": "astro dev --experimental-static-build", "start": "astro dev", "build": "astro build --experimental-static-build", + "scan-build": "astro build", "preview": "astro preview" }, "devDependencies": { diff --git a/examples/fast-build/src/pages/[pokemon].astro b/examples/fast-build/src/pages/[pokemon].astro new file mode 100644 index 000000000..ea01cc4f7 --- /dev/null +++ b/examples/fast-build/src/pages/[pokemon].astro @@ -0,0 +1,20 @@ +--- +import Greeting from '../components/Greeting.vue'; + +export async function getStaticPaths() { + const response = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=2000`); + const result = await response.json(); + const allPokemon = result.results; + return allPokemon.map(pokemon => ({params: {pokemon: pokemon.name}, props: {pokemon}})); +} +--- + + + Hello + + + +

{Astro.props.pokemon.name}

+ + + \ No newline at end of file diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index af929dda7..ee583cd65 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -9,7 +9,9 @@ import type { BuildInternals } from '../../core/build/internal.js'; import type { AstroComponentFactory } from '../../runtime/server'; import fs from 'fs'; +import npath from 'path'; import { fileURLToPath } from 'url'; +import glob from 'fast-glob'; import vite from '../vite.js'; import { debug, info, error } from '../../core/logger.js'; import { createBuildInternals } from '../../core/build/internal.js'; @@ -30,8 +32,11 @@ export interface StaticBuildOptions { export async function staticBuild(opts: StaticBuildOptions) { const { allPages, astroConfig } = opts; + // The pages + const pageInput = new Set(); + // The JavaScript entrypoints. - const jsInput: Set = new Set(); + const jsInput = new Set(); // A map of each page .astro file, to the PageBuildData which contains information // about that page, such as its paths. @@ -39,12 +44,13 @@ export async function staticBuild(opts: StaticBuildOptions) { 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 - ...mod.$$metadata.hydratedComponentPaths(), + ...metadata.hydratedComponentPaths(), // Any hydration directive like astro/client/idle.js - ...mod.$$metadata.hydrationDirectiveSpecifiers(), + ...metadata.hydrationDirectiveSpecifiers(), // The client path for each renderer ...renderers.filter(renderer => !!renderer.source).map(renderer => renderer.source!), ]); @@ -54,18 +60,22 @@ export async function staticBuild(opts: StaticBuildOptions) { } let astroModuleId = new URL('./' + component, astroConfig.projectRoot).pathname; - jsInput.add(astroModuleId); + pageInput.add(astroModuleId); facadeIdToPageDataMap.set(astroModuleId, pageData); } // Build internals needed by the CSS plugin const internals = createBuildInternals(); - // Perform the SSR build - const result = (await ssrBuild(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(result, opts, internals, facadeIdToPageDataMap); + await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap); + await cleanSsrOutput(opts); } async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set) { @@ -76,7 +86,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp mode: 'production', build: { emptyOutDir: true, - minify: false, // 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles + minify: false, outDir: fileURLToPath(astroConfig.dist), ssr: true, rollupOptions: { @@ -88,7 +98,42 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp target: 'es2020', // must match an esbuild target }, plugins: [ - vitePluginNewBuild(input, internals), + 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, }), @@ -166,7 +211,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G mod, }); - info(logging, 'generate', `Generating: ${pathname}`); + debug(logging, 'generate', `Generating: ${pathname}`); const result = createResult({ astroConfig, origin, params, pathname, renderers }); result.links = new Set( @@ -183,8 +228,9 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G if(typeof hashedFilePath !== 'string') { throw new Error(`Cannot find the built path for ${specifier}`); } - console.log("WE GOT", hashedFilePath) - return hashedFilePath; + const relPath = npath.posix.relative(pathname, '/' + hashedFilePath); + const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; + return fullyRelativePath; }; let html = await renderPage(result, Component, pageProps, null); @@ -197,7 +243,19 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G } } -export function vitePluginNewBuild(input: Set, internals: BuildInternals): VitePlugin { +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); + })); +} + +export function vitePluginNewBuild(input: Set, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin { return { name: '@astro/rollup-plugin-new-build', @@ -213,10 +271,10 @@ export function vitePluginNewBuild(input: Set, internals: BuildInternals outputOptions(outputOptions) { Object.assign(outputOptions, { entryFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].mjs'; + return 'assets/[name].[hash].' + ext; }, chunkFileNames(_chunk: PreRenderedChunk) { - return 'assets/[name].[hash].mjs'; + return 'assets/[name].[hash].' + ext; }, }); return outputOptions; @@ -239,8 +297,6 @@ export function vitePluginNewBuild(input: Set, internals: BuildInternals internals.entrySpecifierToBundleMap.set(specifier, chunk.fileName); } } - - console.log(internals.entrySpecifierToBundleMap); } }; } diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index 2e4130540..eee8d845f 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -121,7 +121,7 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, } hydrationSource += renderer.source - ? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${await result.resolve(renderer.source)}")]); + ? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(componentUrl)}"), import("${await result.resolve(renderer.source)}")]); return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children); ` : `await import("${componentUrl}"); diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 443c2e8ea..c36b49845 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -157,6 +157,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + ' // Call the renderers `check` hook to see if any claim this component. let renderer: Renderer | undefined; + debugger; if (metadata.hydrate !== 'only') { for (const r of renderers) { if (await r.ssr.check(Component, props, children)) { diff --git a/packages/astro/src/runtime/server/metadata.ts b/packages/astro/src/runtime/server/metadata.ts index 842bda6e6..1a7b366e0 100644 --- a/packages/astro/src/runtime/server/metadata.ts +++ b/packages/astro/src/runtime/server/metadata.ts @@ -26,12 +26,12 @@ export class Metadata { private metadataCache: Map; - constructor(fileURL: string, opts: CreateMetadataOptions) { + constructor(filePathname: string, opts: CreateMetadataOptions) { this.modules = opts.modules; this.hoisted = opts.hoisted; this.hydratedComponents = opts.hydratedComponents; this.hydrationDirectives = opts.hydrationDirectives; - this.fileURL = new URL(fileURL); + this.fileURL = new URL(filePathname, 'http://example.com'); this.metadataCache = new Map(); } @@ -128,6 +128,6 @@ export class Metadata { } } -export function createMetadata(fileURL: string, options: CreateMetadataOptions) { - return new Metadata(fileURL, options); +export function createMetadata(filePathname: string, options: CreateMetadataOptions) { + return new Metadata(filePathname, options); } diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index 71a08a94a..a9cf1d59c 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -29,8 +29,10 @@ function isSSR(options: undefined | boolean | { ssr: boolean }): boolean { async function compile(config: AstroConfig, filename: string, source: string, viteTransform: TransformHook, opts: boolean | undefined) { // pages and layouts should be transformed as full documents (implicit etc) // everything else is treated as a fragment - const normalizedID = fileURLToPath(new URL(`file://${filename}`)); + const filenameURL = new URL(`file://${filename}`); + const normalizedID = fileURLToPath(filenameURL); const isPage = normalizedID.startsWith(fileURLToPath(config.pages)) || normalizedID.startsWith(fileURLToPath(config.layouts)); + const pathname = filenameURL.pathname.substr(config.projectRoot.pathname.length - 1) let cssTransformError: Error | undefined; @@ -39,6 +41,7 @@ async function compile(config: AstroConfig, filename: string, source: string, vi // result passed to esbuild, but also available in the catch handler. const transformResult = await transform(source, { as: isPage ? 'document' : 'fragment', + pathname, projectRoot: config.projectRoot.toString(), site: config.buildOptions.site, sourcefile: filename, diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index 90765c35d..b5666ec84 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -69,7 +69,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { const [renderers, mod] = pageData.preload; // Hydrated components are statically identified. - for (const path of mod.$$metadata.getAllHydratedComponentPaths()) { + for (const path of mod.$$metadata.hydratedComponentPaths()) { jsInput.add(path); } diff --git a/packages/renderers/renderer-vue/server.js b/packages/renderers/renderer-vue/server.js index 83e389b5d..4f2e485e6 100644 --- a/packages/renderers/renderer-vue/server.js +++ b/packages/renderers/renderer-vue/server.js @@ -3,7 +3,7 @@ import { renderToString } from 'vue/server-renderer'; import StaticHtml from './static-html.js'; function check(Component) { - return !!Component['ssrRender']; + return !!Component['ssrRender'] || !!Component.render; } async function renderToStaticMarkup(Component, props, children) {