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:
parent
ff9dbc6927
commit
2aa5ba5c52
8 changed files with 136 additions and 7 deletions
5
.changeset/long-eagles-care.md
Normal file
5
.changeset/long-eagles-care.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fixes use of framework renderers in the static build
|
|
@ -279,6 +279,8 @@ export interface Renderer {
|
|||
name: string;
|
||||
/** Import statement for renderer */
|
||||
source?: string;
|
||||
/** Import statement for the server renderer */
|
||||
serverEntry: string;
|
||||
/** Scripts to be injected before component */
|
||||
polyfills?: string[];
|
||||
/** Polyfills that need to run before hydration ever occurs */
|
||||
|
|
28
packages/astro/src/core/build/fs.ts
Normal file
28
packages/astro/src/core/build/fs.ts
Normal 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']));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import type { OutputChunk, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup';
|
||||
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 { LogOptions } from '../logger';
|
||||
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 { createResult } from '../ssr/result.js';
|
||||
import { renderPage } from '../../runtime/server/index.js';
|
||||
import { prepareOutDir } from './fs.js';
|
||||
|
||||
export interface StaticBuildOptions {
|
||||
allPages: AllPagesData;
|
||||
|
@ -35,6 +36,8 @@ function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
|||
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Determines of a Rollup chunk is an entrypoint page.
|
||||
function chunkIsPage(output: OutputAsset | OutputChunk, internals: BuildInternals) {
|
||||
if (output.type !== 'chunk') {
|
||||
|
@ -82,6 +85,11 @@ export async function staticBuild(opts: StaticBuildOptions) {
|
|||
// Build internals needed by the CSS plugin
|
||||
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
|
||||
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',
|
||||
mode: 'production',
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
emptyOutDir: false,
|
||||
minify: false,
|
||||
outDir: fileURLToPath(astroConfig.dist),
|
||||
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>) {
|
||||
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 = [];
|
||||
for (let output of result.output) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let url = new URL('./' + output.fileName, astroConfig.dist);
|
||||
|
@ -198,6 +229,7 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
|
|||
internals,
|
||||
linkIds,
|
||||
Component,
|
||||
renderers,
|
||||
};
|
||||
|
||||
const renderPromises = pageData.paths.map((path) => {
|
||||
|
@ -211,16 +243,17 @@ interface GeneratePathOptions {
|
|||
internals: BuildInternals;
|
||||
linkIds: string[];
|
||||
Component: AstroComponentFactory;
|
||||
renderers: Renderer[];
|
||||
}
|
||||
|
||||
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
|
||||
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.
|
||||
addPageName(pathname, opts);
|
||||
|
||||
const [renderers, mod] = pageData.preload;
|
||||
const [,mod] = pageData.preload;
|
||||
|
||||
try {
|
||||
const [params, pageProps] = await getParamsAndProps({
|
||||
|
|
|
@ -52,9 +52,10 @@ async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string,
|
|||
|
||||
resolvedRenderer.name = name;
|
||||
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(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);
|
||||
resolvedRenderer.ssr = rendererSSR;
|
||||
|
||||
|
|
19
packages/astro/test/fixtures/static-build-frameworks/src/components/PCounter.jsx
vendored
Normal file
19
packages/astro/test/fixtures/static-build-frameworks/src/components/PCounter.jsx
vendored
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
12
packages/astro/test/fixtures/static-build-frameworks/src/pages/index.astro
vendored
Normal file
12
packages/astro/test/fixtures/static-build-frameworks/src/pages/index.astro
vendored
Normal 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>
|
29
packages/astro/test/static-build-frameworks.test.js
Normal file
29
packages/astro/test/static-build-frameworks.test.js
Normal 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');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue