Skip to content

Commit

Permalink
Implement hydrated components in the static build (withastro#2260)
Browse files Browse the repository at this point in the history
* Work on removing vite-postprocess

* Gets hydration totally working

* Formatting

* Update based on PR comments

* Bring back vite transform for non-static builds

* Upgrade compiler version

* Update the client-only test

* Add debugging for windows

* More debugging

* Pass pathname into the markdown plugin as well

* Include vite changes

* Revert "Include vite changes"

This reverts commit dee6348.
  • Loading branch information
matthewp authored Dec 30, 2021
1 parent 8b34bd9 commit ae5255d
Show file tree
Hide file tree
Showing 19 changed files with 382 additions and 202 deletions.
1 change: 1 addition & 0 deletions examples/fast-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
24 changes: 24 additions & 0 deletions examples/fast-build/src/components/Counter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<template>
<div id="vue" class="counter">
<button @click="subtract()">-</button>
<pre>{{ count }}</pre>
<button @click="add()">+</button>
</div>
</template>

<script>
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
const add = () => (count.value = count.value + 1);
const subtract = () => (count.value = count.value - 1);
return {
count,
add,
subtract,
};
},
};
</script>
20 changes: 20 additions & 0 deletions examples/fast-build/src/pages/[pokemon].astro
Original file line number Diff line number Diff line change
@@ -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}}));
}
---
<html lang="en">
<head>
<title>Hello</title>
</head>

<body>
<h1>{Astro.props.pokemon.name}</h1>
<Greeting client:load />
</body>
</html>
18 changes: 12 additions & 6 deletions examples/fast-build/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';
---

<html>
Expand All @@ -26,9 +27,14 @@ import Greeting from '../components/Greeting.vue';
<Greeting />
</section>

<section>
<h1>ImageTools</h1>
<img src={grayscaleUrl} />
</section>
</body>
</html>
<section>
<h1>ImageTools</h1>
<img src={grayscaleUrl} />
</section>

<section>
<h1>Hydrated component</h1>
<Counter client:idle />
</section>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"test": "mocha --parallel --timeout 15000"
},
"dependencies": {
"@astrojs/compiler": "^0.6.0",
"@astrojs/compiler": "^0.7.0",
"@astrojs/language-server": "^0.8.2",
"@astrojs/markdown-remark": "^0.6.0",
"@astrojs/prism": "0.4.0",
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,5 +371,6 @@ export interface SSRResult {
scripts: Set<SSRElement>;
links: Set<SSRElement>;
createAstro(Astro: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null): AstroGlobal;
resolve: (s: string) => Promise<string>;
_metadata: SSRMetadata;
}
5 changes: 5 additions & 0 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface BuildInternals {

// A mapping to entrypoints (facadeId) to assets (styles) that are added.
facadeIdToAssetsMap: Map<string, string[]>;

// A mapping of specifiers like astro/client/idle.js to the hashed bundled name.
// Used to render pages with the correct specifiers.
entrySpecifierToBundleMap: Map<string, string>;
}

/**
Expand Down Expand Up @@ -41,5 +45,6 @@ export function createBuildInternals(): BuildInternals {
astroStyleMap,
astroPageStyleMap,
facadeIdToAssetsMap,
entrySpecifierToBundleMap: new Map<string, string>(),
};
}
152 changes: 130 additions & 22 deletions packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,12 +9,16 @@ 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';
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;
Expand All @@ -28,35 +32,47 @@ export interface StaticBuildOptions {
export async function staticBuild(opts: StaticBuildOptions) {
const { allPages, astroConfig } = opts;

// The pages to be built for rendering purposes.
const pageInput = new Set<string>();

// The JavaScript entrypoints.
const jsInput: Set<string> = new Set();
const jsInput = new Set<string>();

// A map of each page .astro file, to the PageBuildData which contains information
// about that page, such as its paths.
const facadeIdToPageDataMap = new Map<string, PageBuildData>();

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!),
]);

// 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);
for (const specifier of topLevelImports) {
jsInput.add(specifier);
}

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<string>) {
Expand All @@ -67,7 +83,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: {
Expand All @@ -79,7 +95,41 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
target: 'es2020', // must match an esbuild target
},
plugins: [
vitePluginNewBuild(),
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<string>) {
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,
}),
Expand Down Expand Up @@ -124,6 +174,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter

const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
Component,
};
Expand All @@ -136,13 +187,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;

Expand All @@ -151,14 +203,36 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener
route: pageData.route,
routeCache,
logging,
pathname: path,
pathname,
mod,
});

info(logging, 'generate', `Generating: ${path}`);
debug(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<SSRElement>(
linkIds.map((href) => ({
props: {
rel: 'stylesheet',
href,
},
children: '',
}))
);
// Override the `resolve` method so that hydrated components are given the
// hashed filepath to the component.
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');
Expand All @@ -167,7 +241,20 @@ async function generatePath(path: string, opts: StaticBuildOptions, gopts: Gener
}
}

export function vitePluginNewBuild(): 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,
});
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<string>, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin {
return {
name: '@astro/rollup-plugin-new-build',

Expand All @@ -183,13 +270,34 @@ export function vitePluginNewBuild(): VitePlugin {
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;
},

async generateBundle(_options, bundle) {
const promises = [];
const mapping = new Map<string, string>();
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);
}
}
},
};
}
Loading

0 comments on commit ae5255d

Please sign in to comment.