Fix use of frameworks in the static build (#2367)

* Fix use of frameworks in the static build

* Adding a changeset

* fix typescript

* Empty out the directory before running the builds

* Use a util to empty the directory

* Only empty the outdir if needed

* Move prepareOutDir to its own module

* Prepare outDir is actually sync
This commit is contained in:
Matthew Phillips 2022-01-13 09:23:03 -05:00 committed by GitHub
parent ff9dbc6927
commit 2aa5ba5c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes use of framework renderers in the static build

View file

@ -279,6 +279,8 @@ export interface Renderer {
name: string; name: string;
/** Import statement for renderer */ /** Import statement for renderer */
source?: string; source?: string;
/** Import statement for the server renderer */
serverEntry: string;
/** Scripts to be injected before component */ /** Scripts to be injected before component */
polyfills?: string[]; polyfills?: string[];
/** Polyfills that need to run before hydration ever occurs */ /** Polyfills that need to run before hydration ever occurs */

View file

@ -0,0 +1,28 @@
import type { AstroConfig } from '../../@types/astro';
import fs from 'fs';
import npath from 'path';
import { fileURLToPath } from 'url';
export function emptyDir(dir: string, skip?: Set<string>): void {
for (const file of fs.readdirSync(dir)) {
if (skip?.has(file)) {
continue
}
const abs = npath.resolve(dir, file)
// baseline is Node 12 so can't use rmSync :(
if (fs.lstatSync(abs).isDirectory()) {
emptyDir(abs)
fs.rmdirSync(abs)
} else {
fs.unlinkSync(abs)
}
}
}
export function prepareOutDir(astroConfig: AstroConfig) {
const outDir = fileURLToPath(astroConfig.dist);
if (fs.existsSync(outDir)) {
return emptyDir(outDir, new Set(['.git']));
}
}

View file

@ -1,6 +1,6 @@
import type { OutputChunk, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup'; import type { OutputChunk, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin, UserConfig } from '../vite'; import type { Plugin as VitePlugin, UserConfig } from '../vite';
import type { AstroConfig, RouteCache, SSRElement } from '../../@types/astro'; import type { AstroConfig, Renderer, RouteCache, SSRElement } from '../../@types/astro';
import type { AllPagesData } from './types'; import type { AllPagesData } from './types';
import type { LogOptions } from '../logger'; import type { LogOptions } from '../logger';
import type { ViteConfigWithSSR } from '../create-vite'; import type { ViteConfigWithSSR } from '../create-vite';
@ -19,6 +19,7 @@ import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'
import { getParamsAndProps } from '../ssr/index.js'; import { getParamsAndProps } from '../ssr/index.js';
import { createResult } from '../ssr/result.js'; import { createResult } from '../ssr/result.js';
import { renderPage } from '../../runtime/server/index.js'; import { renderPage } from '../../runtime/server/index.js';
import { prepareOutDir } from './fs.js';
export interface StaticBuildOptions { export interface StaticBuildOptions {
allPages: AllPagesData; allPages: AllPagesData;
@ -35,6 +36,8 @@ function addPageName(pathname: string, opts: StaticBuildOptions): void {
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, '')); opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
} }
// Determines of a Rollup chunk is an entrypoint page. // Determines of a Rollup chunk is an entrypoint page.
function chunkIsPage(output: OutputAsset | OutputChunk, internals: BuildInternals) { function chunkIsPage(output: OutputAsset | OutputChunk, internals: BuildInternals) {
if (output.type !== 'chunk') { if (output.type !== 'chunk') {
@ -82,6 +85,11 @@ export async function staticBuild(opts: StaticBuildOptions) {
// Build internals needed by the CSS plugin // Build internals needed by the CSS plugin
const internals = createBuildInternals(); const internals = createBuildInternals();
// Empty out the dist folder, if needed. Vite has a config for doing this
// but because we are running 2 vite builds in parallel, that would cause a race
// condition, so we are doing it ourselves
prepareOutDir(astroConfig);
// Run the SSR build and client build in parallel // Run the SSR build and client build in parallel
const [ssrResult] = (await Promise.all([ssrBuild(opts, internals, pageInput), clientBuild(opts, internals, jsInput)])) as RollupOutput[]; const [ssrResult] = (await Promise.all([ssrBuild(opts, internals, pageInput), clientBuild(opts, internals, jsInput)])) as RollupOutput[];
@ -97,7 +105,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
logLevel: 'error', logLevel: 'error',
mode: 'production', mode: 'production',
build: { build: {
emptyOutDir: true, emptyOutDir: false,
minify: false, minify: false,
outDir: fileURLToPath(astroConfig.dist), outDir: fileURLToPath(astroConfig.dist),
ssr: true, ssr: true,
@ -163,18 +171,41 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
}); });
} }
async function collectRenderers(opts: StaticBuildOptions): Promise<Renderer[]> {
// All of the PageDatas have the same renderers, so just grab one.
const pageData = Object.values(opts.allPages)[0];
// These renderers have been loaded through Vite. To generate pages
// we need the ESM loaded version. This creates that.
const viteLoadedRenderers = pageData.preload[0];
const renderers = await Promise.all(viteLoadedRenderers.map(async r => {
const mod = await import(r.serverEntry);
return Object.create(r, {
ssr: {
value: mod.default
}
}) as Renderer;
}));
return renderers;
}
async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { async function generatePages(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) {
debug(opts.logging, 'generate', 'End build step, now generating'); debug(opts.logging, 'generate', 'End build step, now generating');
// Get renderers to be shared for each page generation.
const renderers = await collectRenderers(opts);
const generationPromises = []; const generationPromises = [];
for (let output of result.output) { for (let output of result.output) {
if (chunkIsPage(output, internals)) { if (chunkIsPage(output, internals)) {
generationPromises.push(generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap)); generationPromises.push(generatePage(output as OutputChunk, opts, internals, facadeIdToPageDataMap, renderers));
} }
} }
await Promise.all(generationPromises); await Promise.all(generationPromises);
} }
async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>) { async function generatePage(output: OutputChunk, opts: StaticBuildOptions, internals: BuildInternals, facadeIdToPageDataMap: Map<string, PageBuildData>, renderers: Renderer[]) {
const { astroConfig } = opts; const { astroConfig } = opts;
let url = new URL('./' + output.fileName, astroConfig.dist); let url = new URL('./' + output.fileName, astroConfig.dist);
@ -198,6 +229,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
internals, internals,
linkIds, linkIds,
Component, Component,
renderers,
}; };
const renderPromises = pageData.paths.map((path) => { const renderPromises = pageData.paths.map((path) => {
@ -211,16 +243,17 @@ interface GeneratePathOptions {
internals: BuildInternals; internals: BuildInternals;
linkIds: string[]; linkIds: string[];
Component: AstroComponentFactory; Component: AstroComponentFactory;
renderers: Renderer[];
} }
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) { async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
const { astroConfig, logging, origin, pageNames, routeCache } = opts; const { astroConfig, logging, origin, pageNames, routeCache } = opts;
const { Component, internals, linkIds, pageData } = gopts; const { Component, internals, linkIds, pageData, renderers } = gopts;
// This adds the page name to the array so it can be shown as part of stats. // This adds the page name to the array so it can be shown as part of stats.
addPageName(pathname, opts); addPageName(pathname, opts);
const [renderers, mod] = pageData.preload; const [,mod] = pageData.preload;
try { try {
const [params, pageProps] = await getParamsAndProps({ const [params, pageProps] = await getParamsAndProps({

View file

@ -52,9 +52,10 @@ async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string,
resolvedRenderer.name = name; resolvedRenderer.name = name;
if (client) resolvedRenderer.source = path.posix.join(renderer, client); if (client) resolvedRenderer.source = path.posix.join(renderer, client);
resolvedRenderer.serverEntry = path.posix.join(renderer, server);
if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => path.posix.join(renderer, src)); if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => path.posix.join(renderer, src));
if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => path.posix.join(renderer, src)); if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => path.posix.join(renderer, src));
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(path.posix.join(renderer, server)); const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(resolvedRenderer.serverEntry);
const { default: rendererSSR } = await viteServer.ssrLoadModule(url); const { default: rendererSSR } = await viteServer.ssrLoadModule(url);
resolvedRenderer.ssr = rendererSSR; resolvedRenderer.ssr = rendererSSR;

View file

@ -0,0 +1,19 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
export default function Counter({ children }) {
const [count, setCount] = useState(0);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
return (
<>
<div class="counter">
<button onClick={subtract}>-</button>
<pre>{count}</pre>
<button onClick={add}>+</button>
</div>
<div class="counter-message">{children}</div>
</>
);
}

View file

@ -0,0 +1,12 @@
---
import PCounter from '../components/PCounter.jsx';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<PCounter client:load />
</body>
</html>

View file

@ -0,0 +1,29 @@
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
function addLeadingSlash(path) {
return path.startsWith('/') ? path : '/' + path;
}
describe('Static build - frameworks', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
projectRoot: './fixtures/static-build-frameworks/',
renderers: [
'@astrojs/renderer-preact'
],
buildOptions: {
experimentalStaticBuild: true,
},
});
await fixture.build();
});
it('can build preact', async () => {
const html = await fixture.readFile('/index.html');
expect(html).to.be.a('string');
});
});