Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only resolve inline script specifiers in the static build #2302

Merged
merged 4 commits into from
Jan 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-sloths-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix to allow the static build to build hydrated components
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On L195 I was like "huh" and on L206 I’m like "ahh". 👍

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