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

Flagged SSR support #2548

Merged
merged 14 commits into from
Feb 14, 2022
Next Next commit
Checkpoint, basics are working
  • Loading branch information
matthewp committed Feb 10, 2022
commit 78211110d9d01645a7b6c0bf7964790a6e9e6a0d
5 changes: 5 additions & 0 deletions examples/ssr/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @ts-check

export default /** @type {import('astro').AstroUserConfig} */ ({
renderers: ['@astrojs/renderer-react'],
});
17 changes: 17 additions & 0 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@example/ssr",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev --experimental-ssr",
"start": "astro dev",
"build": "astro build --experimental-ssr",
"preview": "astro preview"
},
"devDependencies": {
"astro": "^0.23.0-next.0",
"preact": "~10.5.15",
"unocss": "^0.15.5",
"vite-imagetools": "^4.0.1"
}
}
71 changes: 71 additions & 0 deletions examples/ssr/server/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createServer } from 'http';
import { deserializeManifestData, matchRoute } from 'astro/routing';
import { createResult } from 'astro/ssr/result.js';
import { renderPage } from 'astro/server/index.js';
import reactRenderer from '@astrojs/renderer-react';
import reactServerRenderer from '@astrojs/renderer-react/server.js';
import fs from 'fs';

const renderers = [
Object.create(reactRenderer, {
ssr: {
value: reactServerRenderer
}
})
];

const serverRoot = new URL('../dist/server/', import.meta.url);
const manifestData = JSON.parse(await fs.promises.readFile(new URL('./manifest.json', serverRoot)));
const manifest = deserializeManifestData(manifestData);

async function handle(req, res) {
const route = matchRoute(req.url, manifest);
if(route) {
const modFile = new URL('./' + route.distEntry, serverRoot);
const mod = await import(modFile);
const { default: Component } = mod;
// TODO get params

const result = createResult({
astroConfig: {
buildOptions: {}
},
logging: {},
origin: 'http:https://example.com',
params: {},
pathname: req.url,
renderers,
links: undefined, // TODO
scripts: undefined // TODO
});

const pageProps = {};

let html = await renderPage(result, Component, pageProps, null);


res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(html)
} else {
res.writeHead(404);
res.end();
}
}

const server = createServer((req, res) => {
handle(req, res).catch(err => {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(err.toString());
})
});

server.listen(8080);
console.log('Serving at http:https://localhost:8080');

// Silence weird <time> warning
console.error = () => {};
matthewp marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions examples/ssr/src/components/Time.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export default function({ time }) {
const iso = time.toISOString();
const formatted = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(time);

return <time datetime={iso}>{ formatted }</time>
}
12 changes: 12 additions & 0 deletions examples/ssr/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import Time from '../components/Time.jsx';
---
<html>
<head>
<title>SSR App</title>
</head>
<body>
<h1>SSR App</h1>
<p>Time: <Time time={new Date()} /></p>
</body>
</html>
2 changes: 2 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"./internal/*": "./dist/runtime/server/*",
"./package.json": "./package.json",
"./runtime/*": "./dist/runtime/*",
"./routing": "./dist/core/ssr/routing.js",
"./ssr/*": "./dist/core/ssr/*",
matthewp marked this conversation as resolved.
Show resolved Hide resolved
"./server/*": "./dist/runtime/server/*",
"./vite-plugin-astro": "./dist/vite-plugin-astro/index.js",
"./vite-plugin-astro/*": "./dist/vite-plugin-astro/*",
Expand Down
15 changes: 15 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface CLIFlags {
port?: number;
config?: string;
experimentalStaticBuild?: boolean;
experimentalSsr?: boolean;
drafts?: boolean;
}

Expand Down Expand Up @@ -132,6 +133,10 @@ export interface AstroUserConfig {
* Default: false
*/
experimentalStaticBuild?: boolean;
/**
* Enable a build for SSR support.
*/
experimentalSsr?: boolean;
};
/** Options for the development server run with `astro dev`. */
devOptions?: {
Expand Down Expand Up @@ -224,6 +229,10 @@ export interface ManifestData {
routes: RouteData[];
}

export type SerializedManifestData = Omit<ManifestData, 'routes'> & {
routes: SerializedRouteData[];
};

export type MarkdownParser = (contents: string, options?: Record<string, any>) => MarkdownParserResponse | PromiseLike<MarkdownParserResponse>;

export interface MarkdownParserResponse {
Expand Down Expand Up @@ -339,8 +348,14 @@ export interface RouteData {
pathname?: string;
pattern: RegExp;
type: 'page';
distEntry?: string;
}

export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
generate: undefined;
pattern: string;
};

export type RuntimeMode = 'development' | 'production';

/**
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function printHelp() {
--project-root <path> Specify the path to the project root folder.
--no-sitemap Disable sitemap generation (build only).
--experimental-static-build A more performant build that expects assets to be define statically.
--experimental-ssr Enable SSR compilation.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix indentation here

--drafts Include markdown draft pages in the build.
--verbose Enable verbose logging
--silent Disable logging
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class AstroBuilder {
allPages,
astroConfig: this.config,
logging: this.logging,
manifest: this.manifest,
origin: this.origin,
pageNames,
routeCache: this.routeCache,
Expand Down
74 changes: 54 additions & 20 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, OutputAsset, PreRenderedChunk, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin, UserConfig } from '../vite';
import type { AstroConfig, Renderer, SSRElement } from '../../@types/astro';
import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup';
import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from '../vite';
import type { AstroConfig, ManifestData, Renderer, SSRElement } from '../../@types/astro';
import type { AllPagesData } from './types';
import type { LogOptions } from '../logger';
import type { ViteConfigWithSSR } from '../create-vite';
Expand All @@ -23,11 +23,13 @@ import { renderPage } from '../../runtime/server/index.js';
import { prepareOutDir } from './fs.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
import { RouteCache } from '../ssr/route-cache.js';
import { serializeManifestData } from '../ssr/routing.js';

export interface StaticBuildOptions {
allPages: AllPagesData;
astroConfig: AstroConfig;
logging: LogOptions;
manifest: ManifestData;
origin: string;
pageNames: string[];
routeCache: RouteCache;
Expand Down Expand Up @@ -88,6 +90,9 @@ function getByFacadeId<T>(facadeId: string, map: Map<string, T>): T | undefined
export async function staticBuild(opts: StaticBuildOptions) {
const { allPages, astroConfig } = opts;

// Basic options
const staticMode = !astroConfig.buildOptions.experimentalSsr;

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

Expand Down Expand Up @@ -148,9 +153,14 @@ export async function staticBuild(opts: StaticBuildOptions) {
// 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(ssrResult, opts, internals, facadeIdToPageDataMap);
await cleanSsrOutput(opts);
// SSG mode, generate pages.
if(staticMode) {
// Generate each of the pages.
await generatePages(ssrResult, opts, internals, facadeIdToPageDataMap);
await cleanSsrOutput(opts);
} else {
await generateManifest(ssrResult, opts);
}
}

async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) {
Expand All @@ -161,13 +171,17 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
mode: 'production',
build: {
emptyOutDir: false,
manifest: true,
minify: false,
outDir: fileURLToPath(getOutRoot(astroConfig)),
outDir: fileURLToPath(getServerRoot(astroConfig)),
ssr: true,
rollupOptions: {
input: Array.from(input),
output: {
format: 'esm',
entryFileNames: '[name].[hash].mjs',
chunkFileNames: 'chunks/[name].[hash].mjs',
assetFileNames: 'assets/[name].[hash][extname]'
},
},
target: 'esnext', // must match an esbuild target
Expand Down Expand Up @@ -202,11 +216,15 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
build: {
emptyOutDir: false,
minify: 'esbuild',
outDir: fileURLToPath(getOutRoot(astroConfig)),
outDir: fileURLToPath(getClientRoot(astroConfig)),
rollupOptions: {
input: Array.from(input),
output: {
format: 'esm',
entryFileNames: '[name].[hash].js',
chunkFileNames: 'chunks/[name].[hash].js',
assetFileNames: 'assets/[name].[hash][extname]'

},
preserveEntrySignatures: 'exports-only',
},
Expand Down Expand Up @@ -382,11 +400,39 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
}
}

async function generateManifest(result: RollupOutput, opts: StaticBuildOptions) {
const { astroConfig, manifest } = opts;
const manifestFile = new URL('./manifest.json', getServerRoot(astroConfig));

const inputManifestJSON = await fs.promises.readFile(manifestFile, 'utf-8');
const data: ViteManifest = JSON.parse(inputManifestJSON);

for(const routeData of manifest.routes) {
const entry = data[routeData.component];
routeData.distEntry = entry?.file;
}

const outputManifestJSON = serializeManifestData(manifest);
await fs.promises.writeFile(manifestFile, outputManifestJSON, 'utf-8');
}

function getOutRoot(astroConfig: AstroConfig): URL {
const rootPathname = appendForwardSlash(astroConfig.buildOptions.site ? new URL(astroConfig.buildOptions.site).pathname : '/');
return new URL('.' + rootPathname, astroConfig.dist);
}

function getServerRoot(astroConfig: AstroConfig): URL {
const rootFolder = getOutRoot(astroConfig);
const serverFolder = new URL('./server/', rootFolder);
return serverFolder;
}

function getClientRoot(astroConfig: AstroConfig): URL {
const rootFolder = getOutRoot(astroConfig);
const serverFolder = new URL('./client/', rootFolder);
return serverFolder;
}

function getOutFolder(astroConfig: AstroConfig, pathname: string): URL {
const outRoot = getOutRoot(astroConfig);

Expand Down Expand Up @@ -451,18 +497,6 @@ export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals
}
},

outputOptions(outputOptions) {
Object.assign(outputOptions, {
entryFileNames(_chunk: PreRenderedChunk) {
return 'assets/[name].[hash].' + ext;
},
chunkFileNames(_chunk: PreRenderedChunk) {
return 'assets/[name].[hash].' + ext;
},
});
return outputOptions;
},

async generateBundle(_options, bundle) {
const promises = [];
const mapping = new Map<string, string>();
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const AstroConfigSchema = z.object({
.optional()
.default('directory'),
experimentalStaticBuild: z.boolean().optional().default(false),
experimentalSsr: z.boolean().optional().default(false),
drafts: z.boolean().optional().default(false),
})
.optional()
Expand Down Expand Up @@ -130,6 +131,7 @@ function resolveFlags(flags: Partial<Flags>): CLIFlags {
config: typeof flags.config === 'string' ? flags.config : undefined,
hostname: typeof flags.hostname === 'string' ? flags.hostname : undefined,
experimentalStaticBuild: typeof flags.experimentalStaticBuild === 'boolean' ? flags.experimentalStaticBuild : false,
experimentalSsr: typeof flags.experimentalSsr === 'boolean' ? flags.experimentalSsr : false,
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : false,
};
}
Expand All @@ -143,6 +145,12 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
if (typeof flags.port === 'number') astroConfig.devOptions.port = flags.port;
if (typeof flags.hostname === 'string') astroConfig.devOptions.hostname = flags.hostname;
if (typeof flags.experimentalStaticBuild === 'boolean') astroConfig.buildOptions.experimentalStaticBuild = flags.experimentalStaticBuild;
if (typeof flags.experimentalSsr === 'boolean') {
astroConfig.buildOptions.experimentalSsr = flags.experimentalSsr;
if(flags.experimentalSsr) {
astroConfig.buildOptions.experimentalStaticBuild = true;
}
matthewp marked this conversation as resolved.
Show resolved Hide resolved
}
if (typeof flags.drafts === 'boolean') astroConfig.buildOptions.drafts = flags.drafts;
return astroConfig;
}
Expand Down
Loading