Skip to content

Commit

Permalink
refactor: use SSR manifest in dev (#7587)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico authored Jul 14, 2023
1 parent 7984424 commit 1568cb4
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 97 deletions.
46 changes: 32 additions & 14 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export interface MatchOptions {
}

export class App {
/**
* The current environment of the application
*/
#env: Environment;
#manifest: SSRManifest;
#manifestData: ManifestData;
Expand All @@ -49,7 +52,6 @@ export class App {
dest: consoleLogDestination,
level: 'info',
};
#base: string;
#baseWithoutTrailingSlash: string;

constructor(manifest: SSRManifest, streaming = true) {
Expand All @@ -58,26 +60,41 @@ export class App {
routes: manifest.routes.map((route) => route.routeData),
};
this.#routeDataToRouteInfo = new Map(manifest.routes.map((route) => [route.routeData, route]));
this.#env = createEnvironment({
adapterName: manifest.adapterName,
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
this.#env = this.#createEnvironment(streaming);
}

set setManifest(newManifest: SSRManifest) {
this.#manifest = newManifest;
}

/**
* Creates an environment by reading the stored manifest
*
* @param streaming
* @private
*/
#createEnvironment(streaming = false) {
return createEnvironment({
adapterName: this.#manifest.adapterName,
logging: this.#logging,
markdown: manifest.markdown,
markdown: this.#manifest.markdown,
mode: 'production',
compressHTML: manifest.compressHTML,
renderers: manifest.renderers,
clientDirectives: manifest.clientDirectives,
async resolve(specifier: string) {
if (!(specifier in manifest.entryModules)) {
compressHTML: this.#manifest.compressHTML,
renderers: this.#manifest.renderers,
clientDirectives: this.#manifest.clientDirectives,
resolve: async (specifier: string) => {
if (!(specifier in this.#manifest.entryModules)) {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
const bundlePath = this.#manifest.entryModules[specifier];
switch (true) {
case bundlePath.startsWith('data:'):
case bundlePath.length === 0: {
return bundlePath;
}
default: {
return createAssetLink(bundlePath, manifest.base, manifest.assetsPrefix);
return createAssetLink(bundlePath, this.#manifest.base, this.#manifest.assetsPrefix);
}
}
},
Expand All @@ -86,12 +103,13 @@ export class App {
ssr: true,
streaming,
});
}

this.#base = this.#manifest.base || '/';
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#base);
set setManifestData(newManifestData: ManifestData) {
this.#manifestData = newManifestData;
}
removeBase(pathname: string) {
if (pathname.startsWith(this.#base)) {
if (pathname.startsWith(this.#manifest.base)) {
return pathname.slice(this.#baseWithoutTrailingSlash.length + 1);
}
return pathname;
Expand Down
15 changes: 7 additions & 8 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,27 +163,27 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
}
} else {
const ssrEntry = ssrEntryPage as SinglePageBuiltModule;
const manifest = generateRuntimeManifest(opts.settings, internals, ssrEntry.renderers);
const manifest = createBuildManifest(opts.settings, internals, ssrEntry.renderers);
await generatePage(opts, internals, pageData, ssrEntry, builtPaths, manifest);
}
}
}
for (const pageData of eachRedirectPageData(internals)) {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
const manifest = generateRuntimeManifest(opts.settings, internals, entry.renderers);
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
}
} else {
for (const [pageData, filePath] of eachPageDataFromEntryPoint(internals)) {
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
const manifest = generateRuntimeManifest(opts.settings, internals, entry.renderers);
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);

await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
}
for (const pageData of eachRedirectPageData(internals)) {
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
const manifest = generateRuntimeManifest(opts.settings, internals, entry.renderers);
const manifest = createBuildManifest(opts.settings, internals, entry.renderers);
await generatePage(opts, internals, pageData, entry, builtPaths, manifest);
}
}
Expand Down Expand Up @@ -521,8 +521,7 @@ async function generatePath(
clientDirectives: manifest.clientDirectives,
compressHTML: manifest.compressHTML,
async resolve(specifier: string) {
// NOTE: next PR, borrow logic from build manifest maybe?
const hashedFilePath = internals.entrySpecifierToBundleMap.get(specifier);
const hashedFilePath = manifest.entryModules[specifier];
if (typeof hashedFilePath !== 'string') {
// If no "astro:scripts/before-hydration.js" script exists in the build,
// then we can assume that no before-hydration scripts are needed.
Expand Down Expand Up @@ -656,14 +655,14 @@ async function generatePath(
* @param settings
* @param renderers
*/
export function generateRuntimeManifest(
export function createBuildManifest(
settings: AstroSettings,
internals: BuildInternals,
renderers: SSRLoadedRenderer[]
): SSRManifest {
return {
assets: new Set(),
entryModules: {},
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
routes: [],
adapterName: '',
markdown: settings.config.markdown,
Expand Down
14 changes: 7 additions & 7 deletions packages/astro/src/core/render/dev/environment.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
import type { AstroSettings, RuntimeMode, SSRManifest } from '../../../@types/astro';
import { isServerLikeOutput } from '../../../prerender/utils.js';
import type { LogOptions } from '../../logger/core.js';
import type { ModuleLoader } from '../../module-loader/index';
import type { Environment } from '../index';

import { createEnvironment } from '../index.js';
import { RouteCache } from '../route-cache.js';
import { createResolve } from './resolve.js';
Expand All @@ -14,23 +13,24 @@ export type DevelopmentEnvironment = Environment & {
};

export function createDevelopmentEnvironment(
manifest: SSRManifest,
settings: AstroSettings,
logging: LogOptions,
loader: ModuleLoader
): DevelopmentEnvironment {
const mode: RuntimeMode = 'development';
let env = createEnvironment({
adapterName: settings.adapter?.name,
adapterName: manifest.adapterName,
logging,
markdown: settings.config.markdown,
markdown: manifest.markdown,
mode,
// This will be overridden in the dev server
renderers: [],
clientDirectives: settings.clientDirectives,
compressHTML: settings.config.compressHTML,
clientDirectives: manifest.clientDirectives,
compressHTML: manifest.compressHTML,
resolve: createResolve(loader, settings.config.root),
routeCache: new RouteCache(logging, mode),
site: settings.config.site,
site: manifest.site,
ssr: isServerLikeOutput(settings.config),
streaming: true,
});
Expand Down
54 changes: 44 additions & 10 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type * as vite from 'vite';
import type { AstroSettings, ManifestData } from '../@types/astro';

import type fs from 'fs';
import { patchOverlay } from '../core/errors/overlay.js';
import type { LogOptions } from '../core/logger/core.js';
Expand All @@ -10,6 +9,7 @@ import { createRouteManifest } from '../core/routing/index.js';
import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
import { handleRequest } from './request.js';
import type { SSRManifest } from '../@types/astro';

export interface AstroPluginOptions {
settings: AstroSettings;
Expand All @@ -26,15 +26,16 @@ export default function createVitePluginAstroServer({
name: 'astro:server',
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
let env = createDevelopmentEnvironment(settings, logging, loader);
let manifest: ManifestData = createRouteManifest({ settings, fsMod }, logging);
const serverController = createController({ loader });
const manifest = createDevelopmentManifest(settings);
const env = createDevelopmentEnvironment(manifest, settings, logging, loader);
let manifestData: ManifestData = createRouteManifest({ settings, fsMod }, logging);
const controller = createController({ loader });

/** rebuild the route cache + manifest, as needed. */
function rebuildManifest(needsManifestRebuild: boolean) {
env.routeCache.clearAll();
if (needsManifestRebuild) {
manifest = createRouteManifest({ settings }, logging);
manifestData = createRouteManifest({ settings }, logging);
}
}
// Rebuild route manifest on file change, if needed.
Expand All @@ -51,13 +52,20 @@ export default function createVitePluginAstroServer({
});
}
// Note that this function has a name so other middleware can find it.
viteServer.middlewares.use(async function astroDevHandler(req, res) {
if (req.url === undefined || !req.method) {
res.writeHead(500, 'Incomplete request');
res.end();
viteServer.middlewares.use(async function astroDevHandler(request, response) {
if (request.url === undefined || !request.method) {
response.writeHead(500, 'Incomplete request');
response.end();
return;
}
handleRequest(env, manifest, serverController, req, res);
handleRequest({
env,
manifestData,
controller,
incomingRequest: request,
incomingResponse: response,
manifest,
});
});
};
},
Expand All @@ -70,3 +78,29 @@ export default function createVitePluginAstroServer({
},
};
}

/**
* It creates a `SSRManifest` from the `AstroSettings`.
*
* Renderers needs to be pulled out from the page module emitted during the build.
* @param settings
* @param renderers
*/
export function createDevelopmentManifest(settings: AstroSettings): SSRManifest {
return {
compressHTML: settings.config.compressHTML,
assets: new Set(),
entryModules: {},
routes: [],
adapterName: '',
markdown: settings.config.markdown,
clientDirectives: settings.clientDirectives,
renderers: [],
base: settings.config.base,
assetsPrefix: settings.config.build.assetsPrefix,
site: settings.config.site
? new URL(settings.config.base, settings.config.site).toString()
: settings.config.site,
componentMetadata: new Map(),
};
}
53 changes: 32 additions & 21 deletions packages/astro/src/vite-plugin-astro-server/request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type http from 'http';
import type { ManifestData } from '../@types/astro';
import type { ManifestData, SSRManifest } from '../@types/astro';
import type { DevelopmentEnvironment } from '../core/render/dev/index';
import type { DevServerController } from './controller';

Expand All @@ -14,22 +14,32 @@ import { runWithErrorHandling } from './controller.js';
import { handle500Response } from './response.js';
import { handleRoute, matchRoute } from './route.js';

type HandleRequest = {
env: DevelopmentEnvironment;
manifestData: ManifestData;
controller: DevServerController;
incomingRequest: http.IncomingMessage;
incomingResponse: http.ServerResponse;
manifest: SSRManifest;
};

/** The main logic to route dev server requests to pages in Astro. */
export async function handleRequest(
env: DevelopmentEnvironment,
manifest: ManifestData,
controller: DevServerController,
req: http.IncomingMessage,
res: http.ServerResponse
) {
export async function handleRequest({
env,
manifestData,
controller,
incomingRequest,
incomingResponse,
manifest,
}: HandleRequest) {
const { settings, loader: moduleLoader } = env;
const { config } = settings;
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${req.headers.host}`;
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${incomingRequest.headers.host}`;
const buildingToSSR = isServerLikeOutput(config);

const url = new URL(origin + req.url);
const url = new URL(origin + incomingRequest.url);
let pathname: string;
if (config.trailingSlash === 'never' && !req.url) {
if (config.trailingSlash === 'never' && !incomingRequest.url) {
pathname = '';
} else {
pathname = decodeURI(url.pathname);
Expand All @@ -50,13 +60,13 @@ export async function handleRequest(
}

let body: ArrayBuffer | undefined = undefined;
if (!(req.method === 'GET' || req.method === 'HEAD')) {
if (!(incomingRequest.method === 'GET' || incomingRequest.method === 'HEAD')) {
let bytes: Uint8Array[] = [];
await new Promise((resolve) => {
req.on('data', (part) => {
incomingRequest.on('data', (part) => {
bytes.push(part);
});
req.on('end', resolve);
incomingRequest.on('end', resolve);
});
body = Buffer.concat(bytes);
}
Expand All @@ -65,19 +75,20 @@ export async function handleRequest(
controller,
pathname,
async run() {
const matchedRoute = await matchRoute(pathname, env, manifest);
const matchedRoute = await matchRoute(pathname, env, manifestData);
const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname;
return await handleRoute(
return await handleRoute({
matchedRoute,
url,
resolvedPathname,
pathname: resolvedPathname,
body,
origin,
env,
manifestData,
incomingRequest: incomingRequest,
incomingResponse: incomingResponse,
manifest,
req,
res
);
});
},
onError(_err) {
const err = createSafeError(_err);
Expand All @@ -94,7 +105,7 @@ export async function handleRequest(
telemetry.record(eventError({ cmd: 'dev', err: errorWithMetadata, isFatal: false }));

error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
handle500Response(moduleLoader, res, errorWithMetadata);
handle500Response(moduleLoader, incomingResponse, errorWithMetadata);

return err;
},
Expand Down
Loading

0 comments on commit 1568cb4

Please sign in to comment.