summaryrefslogtreecommitdiff
path: root/packages/astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src')
-rw-r--r--packages/astro/src/@types/astro.ts13
-rw-r--r--packages/astro/src/cli/index.ts1
-rw-r--r--packages/astro/src/core/app/common.ts20
-rw-r--r--packages/astro/src/core/app/index.ts100
-rw-r--r--packages/astro/src/core/app/node.ts31
-rw-r--r--packages/astro/src/core/app/types.ts26
-rw-r--r--packages/astro/src/core/build/index.ts7
-rw-r--r--packages/astro/src/core/build/page-data.ts8
-rw-r--r--packages/astro/src/core/build/scan-based-build.ts2
-rw-r--r--packages/astro/src/core/build/static-build.ts242
-rw-r--r--packages/astro/src/core/build/types.d.ts2
-rw-r--r--packages/astro/src/core/config.ts8
-rw-r--r--packages/astro/src/core/render/core.ts119
-rw-r--r--packages/astro/src/core/render/dev/css.ts (renamed from packages/astro/src/core/ssr/css.ts)4
-rw-r--r--packages/astro/src/core/render/dev/error.ts44
-rw-r--r--packages/astro/src/core/render/dev/hmr.ts11
-rw-r--r--packages/astro/src/core/render/dev/html.ts (renamed from packages/astro/src/core/ssr/html.ts)2
-rw-r--r--packages/astro/src/core/render/dev/index.ts158
-rw-r--r--packages/astro/src/core/render/dev/renderers.ts36
-rw-r--r--packages/astro/src/core/render/paginate.ts (renamed from packages/astro/src/core/ssr/paginate.ts)0
-rw-r--r--packages/astro/src/core/render/renderer.ts30
-rw-r--r--packages/astro/src/core/render/result.ts (renamed from packages/astro/src/core/ssr/result.ts)48
-rw-r--r--packages/astro/src/core/render/route-cache.ts (renamed from packages/astro/src/core/ssr/route-cache.ts)4
-rw-r--r--packages/astro/src/core/render/rss.ts (renamed from packages/astro/src/core/ssr/rss.ts)2
-rw-r--r--packages/astro/src/core/render/script.ts (renamed from packages/astro/src/core/ssr/script.ts)0
-rw-r--r--packages/astro/src/core/render/sitemap.ts (renamed from packages/astro/src/core/ssr/sitemap.ts)0
-rw-r--r--packages/astro/src/core/render/ssr-element.ts40
-rw-r--r--packages/astro/src/core/routing/index.ts11
-rw-r--r--packages/astro/src/core/routing/manifest/create.ts (renamed from packages/astro/src/core/ssr/routing.ts)350
-rw-r--r--packages/astro/src/core/routing/manifest/serialization.ts29
-rw-r--r--packages/astro/src/core/routing/match.ts10
-rw-r--r--packages/astro/src/core/routing/params.ts23
-rw-r--r--packages/astro/src/core/routing/validation.ts37
-rw-r--r--packages/astro/src/core/ssr/index.ts300
-rw-r--r--packages/astro/src/runtime/server/index.ts8
-rw-r--r--packages/astro/src/vite-plugin-astro-server/index.ts8
-rw-r--r--packages/astro/src/vite-plugin-astro/styles.ts2
-rw-r--r--packages/astro/src/vite-plugin-build-css/index.ts3
-rw-r--r--packages/astro/src/vite-plugin-build-html/index.ts4
39 files changed, 1124 insertions, 619 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index ea1dc6f4d..598530836 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -28,6 +28,7 @@ export interface CLIFlags {
port?: number;
config?: string;
experimentalStaticBuild?: boolean;
+ experimentalSsr?: boolean;
drafts?: boolean;
}
@@ -102,7 +103,7 @@ export interface AstroUserConfig {
renderers?: string[];
/** Options for rendering markdown content */
markdownOptions?: {
- render?: [string | MarkdownParser, Record<string, any>];
+ render?: MarkdownRenderOptions;
};
/** Options specific to `astro build` */
buildOptions?: {
@@ -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?: {
@@ -224,6 +229,7 @@ export interface ManifestData {
routes: RouteData[];
}
+export type MarkdownRenderOptions = [string | MarkdownParser, Record<string, any>];
export type MarkdownParser = (contents: string, options?: Record<string, any>) => MarkdownParserResponse | PromiseLike<MarkdownParserResponse>;
export interface MarkdownParserResponse {
@@ -341,6 +347,11 @@ export interface RouteData {
type: 'page';
}
+export type SerializedRouteData = Omit<RouteData, 'generate' | 'pattern'> & {
+ generate: undefined;
+ pattern: string;
+};
+
export type RuntimeMode = 'development' | 'production';
/**
diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts
index b37edec1c..ce3be2efb 100644
--- a/packages/astro/src/cli/index.ts
+++ b/packages/astro/src/cli/index.ts
@@ -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.
--drafts Include markdown draft pages in the build.
--verbose Enable verbose logging
--silent Disable logging
diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts
new file mode 100644
index 000000000..ef6d1ae74
--- /dev/null
+++ b/packages/astro/src/core/app/common.ts
@@ -0,0 +1,20 @@
+import type { SSRManifest, SerializedSSRManifest, RouteInfo } from './types';
+import { deserializeRouteData } from '../routing/manifest/serialization.js';
+
+export function deserializeManifest(serializedManifest: SerializedSSRManifest): SSRManifest {
+ const routes: RouteInfo[] = [];
+ for(const serializedRoute of serializedManifest.routes) {
+ routes.push({
+ ...serializedRoute,
+ routeData: deserializeRouteData(serializedRoute.routeData)
+ });
+
+ const route = serializedRoute as unknown as RouteInfo;
+ route.routeData = deserializeRouteData(serializedRoute.routeData);
+ }
+
+ return {
+ ...serializedManifest,
+ routes
+ };
+}
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
new file mode 100644
index 000000000..38e5c3d6f
--- /dev/null
+++ b/packages/astro/src/core/app/index.ts
@@ -0,0 +1,100 @@
+import type { ComponentInstance, ManifestData, RouteData, Renderer } from '../../@types/astro';
+import type {
+ SSRManifest as Manifest, RouteInfo
+} from './types';
+
+import { defaultLogOptions } from '../logger.js';
+import { matchRoute } from '../routing/match.js';
+import { render } from '../render/core.js';
+import { RouteCache } from '../render/route-cache.js';
+import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
+import { createRenderer } from '../render/renderer.js';
+import { prependForwardSlash } from '../path.js';
+
+export class App {
+ #manifest: Manifest;
+ #manifestData: ManifestData;
+ #rootFolder: URL;
+ #routeDataToRouteInfo: Map<RouteData, RouteInfo>;
+ #routeCache: RouteCache;
+ #renderersPromise: Promise<Renderer[]>;
+
+ constructor(manifest: Manifest, rootFolder: URL) {
+ this.#manifest = manifest;
+ this.#manifestData = {
+ routes: manifest.routes.map(route => route.routeData)
+ };
+ this.#rootFolder = rootFolder;
+ this.#routeDataToRouteInfo = new Map(
+ manifest.routes.map(route => [route.routeData, route])
+ );
+ this.#routeCache = new RouteCache(defaultLogOptions);
+ this.#renderersPromise = this.#loadRenderers();
+ }
+ match({ pathname }: URL): RouteData | undefined {
+ return matchRoute(pathname, this.#manifestData);
+ }
+ async render(url: URL, routeData?: RouteData): Promise<string> {
+ if(!routeData) {
+ routeData = this.match(url);
+ if(!routeData) {
+ return 'Not found';
+ }
+ }
+
+ const manifest = this.#manifest;
+ const info = this.#routeDataToRouteInfo.get(routeData!)!;
+ const [mod, renderers] = await Promise.all([
+ this.#loadModule(info.file),
+ this.#renderersPromise
+ ]);
+
+ const links = createLinkStylesheetElementSet(info.links, manifest.site);
+ const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
+
+ return render({
+ experimentalStaticBuild: true,
+ links,
+ logging: defaultLogOptions,
+ markdownRender: manifest.markdown.render,
+ mod,
+ origin: url.origin,
+ pathname: url.pathname,
+ scripts,
+ renderers,
+ async resolve(specifier: string) {
+ if(!(specifier in manifest.entryModules)) {
+ throw new Error(`Unable to resolve [${specifier}]`);
+ }
+ const bundlePath = manifest.entryModules[specifier];
+ return prependForwardSlash(bundlePath);
+ },
+ route: routeData,
+ routeCache: this.#routeCache,
+ site: this.#manifest.site
+ })
+ }
+ async #loadRenderers(): Promise<Renderer[]> {
+ const rendererNames = this.#manifest.renderers;
+ return await Promise.all(rendererNames.map(async (rendererName) => {
+ return createRenderer(rendererName, {
+ renderer(name) {
+ return import(name);
+ },
+ server(entry) {
+ return import(entry);
+ }
+ })
+ }));
+ }
+ async #loadModule(rootRelativePath: string): Promise<ComponentInstance> {
+ let modUrl = new URL(rootRelativePath, this.#rootFolder).toString();
+ let mod: ComponentInstance;
+ try {
+ mod = await import(modUrl);
+ return mod;
+ } catch(err) {
+ throw new Error(`Unable to import ${modUrl}. Does this file exist?`);
+ }
+ }
+}
diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts
new file mode 100644
index 000000000..d1bcbf46b
--- /dev/null
+++ b/packages/astro/src/core/app/node.ts
@@ -0,0 +1,31 @@
+import type { SSRManifest, SerializedSSRManifest } from './types';
+
+import * as fs from 'fs';
+import { App } from './index.js';
+import { deserializeManifest } from './common.js';
+import { IncomingMessage } from 'http';
+
+function createURLFromRequest(req: IncomingMessage): URL {
+ return new URL(`http://${req.headers.host}${req.url}`);
+}
+
+class NodeApp extends App {
+ match(req: IncomingMessage | URL) {
+ return super.match(req instanceof URL ? req : createURLFromRequest(req));
+ }
+ render(req: IncomingMessage | URL) {
+ return super.render(req instanceof URL ? req : createURLFromRequest(req));
+ }
+}
+
+export async function loadManifest(rootFolder: URL): Promise<SSRManifest> {
+ const manifestFile = new URL('./manifest.json', rootFolder);
+ const rawManifest = await fs.promises.readFile(manifestFile, 'utf-8');
+ const serializedManifest: SerializedSSRManifest = JSON.parse(rawManifest);
+ return deserializeManifest(serializedManifest);
+}
+
+export async function loadApp(rootFolder: URL): Promise<NodeApp> {
+ const manifest = await loadManifest(rootFolder);
+ return new NodeApp(manifest, rootFolder);
+}
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
new file mode 100644
index 000000000..8799ef1c9
--- /dev/null
+++ b/packages/astro/src/core/app/types.ts
@@ -0,0 +1,26 @@
+import type { RouteData, SerializedRouteData, MarkdownRenderOptions } from '../../@types/astro';
+
+export interface RouteInfo {
+ routeData: RouteData
+ file: string;
+ links: string[];
+ scripts: string[];
+}
+
+export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
+ routeData: SerializedRouteData;
+}
+
+export interface SSRManifest {
+ routes: RouteInfo[];
+ site?: string;
+ markdown: {
+ render: MarkdownRenderOptions
+ },
+ renderers: string[];
+ entryModules: Record<string, string>;
+}
+
+export type SerializedSSRManifest = Omit<SSRManifest, 'routes'> & {
+ routes: SerializedRouteInfo[];
+}
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index b54e68622..68b12603d 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -8,12 +8,12 @@ import { performance } from 'perf_hooks';
import vite, { ViteDevServer } from '../vite.js';
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
-import { createRouteManifest } from '../ssr/routing.js';
-import { generateSitemap } from '../ssr/sitemap.js';
+import { createRouteManifest } from '../routing/index.js';
+import { generateSitemap } from '../render/sitemap.js';
import { collectPagesData } from './page-data.js';
import { build as scanBasedBuild } from './scan-based-build.js';
import { staticBuild } from './static-build.js';
-import { RouteCache } from '../ssr/route-cache.js';
+import { RouteCache } from '../render/route-cache.js';
export interface BuildOptions {
mode?: string;
@@ -115,6 +115,7 @@ class AstroBuilder {
allPages,
astroConfig: this.config,
logging: this.logging,
+ manifest: this.manifest,
origin: this.origin,
pageNames,
routeCache: this.routeCache,
diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts
index 106e09a05..945423080 100644
--- a/packages/astro/src/core/build/page-data.ts
+++ b/packages/astro/src/core/build/page-data.ts
@@ -1,4 +1,4 @@
-import type { AstroConfig, ComponentInstance, ManifestData, RouteData, RSSResult } from '../../@types/astro';
+import type { AstroConfig, ComponentInstance, ManifestData, RouteData } from '../../@types/astro';
import type { AllPagesData } from './types';
import type { LogOptions } from '../logger';
import type { ViteDevServer } from '../vite.js';
@@ -6,9 +6,9 @@ import type { ViteDevServer } from '../vite.js';
import { fileURLToPath } from 'url';
import * as colors from 'kleur/colors';
import { debug } from '../logger.js';
-import { preload as ssrPreload } from '../ssr/index.js';
-import { generateRssFunction } from '../ssr/rss.js';
-import { callGetStaticPaths, RouteCache, RouteCacheEntry } from '../ssr/route-cache.js';
+import { preload as ssrPreload } from '../render/dev/index.js';
+import { generateRssFunction } from '../render/rss.js';
+import { callGetStaticPaths, RouteCache, RouteCacheEntry } from '../render/route-cache.js';
export interface CollectPagesDataOptions {
astroConfig: AstroConfig;
diff --git a/packages/astro/src/core/build/scan-based-build.ts b/packages/astro/src/core/build/scan-based-build.ts
index c11795fd8..e6d380b61 100644
--- a/packages/astro/src/core/build/scan-based-build.ts
+++ b/packages/astro/src/core/build/scan-based-build.ts
@@ -9,7 +9,7 @@ import vite from '../vite.js';
import { createBuildInternals } from '../../core/build/internal.js';
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
-import { RouteCache } from '../ssr/route-cache.js';
+import { RouteCache } from '../render/route-cache.js';
export interface ScanBasedBuildOptions {
allPages: AllPagesData;
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index 22255148d..3ab3e0cb4 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -1,12 +1,12 @@
-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, ComponentInstance, ManifestData, Renderer } from '../../@types/astro';
import type { AllPagesData } from './types';
import type { LogOptions } from '../logger';
import type { ViteConfigWithSSR } from '../create-vite';
import type { PageBuildData } from './types';
import type { BuildInternals } from '../../core/build/internal.js';
-import type { AstroComponentFactory } from '../../runtime/server';
+import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types';
import fs from 'fs';
import npath from 'path';
@@ -17,17 +17,18 @@ import { debug, error } from '../../core/logger.js';
import { prependForwardSlash, appendForwardSlash } from '../../core/path.js';
import { createBuildInternals } from '../../core/build/internal.js';
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';
+import { emptyDir, prepareOutDir } from './fs.js';
import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js';
-import { RouteCache } from '../ssr/route-cache.js';
+import { RouteCache } from '../render/route-cache.js';
+import { serializeRouteData } from '../routing/index.js';
+import { render } from '../render/core.js';
+import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
export interface StaticBuildOptions {
allPages: AllPagesData;
astroConfig: AstroConfig;
logging: LogOptions;
+ manifest: ManifestData;
origin: string;
pageNames: string[];
routeCache: RouteCache;
@@ -41,6 +42,12 @@ function addPageName(pathname: string, opts: StaticBuildOptions): void {
opts.pageNames.push(pathname.replace(/\/?$/, pathrepl).replace(/^\//, ''));
}
+// Gives back a facadeId that is relative to the root.
+// ie, src/pages/index.astro instead of /Users/name..../src/pages/index.astro
+function rootRelativeFacadeId(facadeId: string, astroConfig: AstroConfig): string {
+ return facadeId.slice(fileURLToPath(astroConfig.projectRoot).length);
+}
+
// Determines of a Rollup chunk is an entrypoint page.
function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk, internals: BuildInternals) {
if (output.type !== 'chunk') {
@@ -48,7 +55,7 @@ function chunkIsPage(astroConfig: AstroConfig, output: OutputAsset | OutputChunk
}
const chunk = output as OutputChunk;
if (chunk.facadeModuleId) {
- const facadeToEntryId = prependForwardSlash(chunk.facadeModuleId.slice(fileURLToPath(astroConfig.projectRoot).length));
+ const facadeToEntryId = prependForwardSlash(rootRelativeFacadeId(chunk.facadeModuleId, astroConfig));
return internals.entrySpecifierToBundleMap.has(facadeToEntryId);
}
return false;
@@ -88,6 +95,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>();
@@ -148,26 +158,38 @@ 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, internals);
+ await ssrMoveAssets(opts);
+ }
}
async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, input: Set<string>) {
const { astroConfig, viteConfig } = opts;
+ const ssr = astroConfig.buildOptions.experimentalSsr;
+ const out = ssr ? getServerRoot(astroConfig) : getOutRoot(astroConfig);
return await vite.build({
logLevel: 'error',
mode: 'production',
build: {
emptyOutDir: false,
+ manifest: ssr,
minify: false,
- outDir: fileURLToPath(getOutRoot(astroConfig)),
+ outDir: fileURLToPath(out),
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
@@ -179,7 +201,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
}),
...(viteConfig.plugins || []),
],
- publicDir: viteConfig.publicDir,
+ publicDir: ssr ? false : viteConfig.publicDir,
root: viteConfig.root,
envPrefix: 'PUBLIC_',
server: viteConfig.server,
@@ -196,17 +218,23 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
return null;
}
+ const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig);
+
return await vite.build({
logLevel: 'error',
mode: 'production',
build: {
emptyOutDir: false,
minify: 'esbuild',
- outDir: fileURLToPath(getOutRoot(astroConfig)),
+ outDir: fileURLToPath(out),
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',
},
@@ -285,14 +313,13 @@ async function generatePage(output: OutputChunk, opts: StaticBuildOptions, inter
const hoistedId = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap) || null;
let compiledModule = await import(url.toString());
- let Component = compiledModule.default;
const generationOptions: Readonly<GeneratePathOptions> = {
pageData,
internals,
linkIds,
hoistedId,
- Component,
+ mod: compiledModule,
renderers,
};
@@ -314,65 +341,48 @@ interface GeneratePathOptions {
internals: BuildInternals;
linkIds: string[];
hoistedId: string | null;
- Component: AstroComponentFactory;
+ mod: ComponentInstance;
renderers: Renderer[];
}
async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: GeneratePathOptions) {
const { astroConfig, logging, origin, routeCache } = opts;
- const { Component, internals, linkIds, hoistedId, pageData, renderers } = gopts;
+ const { mod, internals, linkIds, hoistedId, pageData, renderers } = gopts;
// This adds the page name to the array so it can be shown as part of stats.
addPageName(pathname, opts);
- const [, mod] = pageData.preload;
+ debug('build', `Generating: ${pathname}`);
+
+ const site = astroConfig.buildOptions.site;
+ const links = createLinkStylesheetElementSet(linkIds, site);
+ const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site);
try {
- const [params, pageProps] = await getParamsAndProps({
+ const html = await render({
+ experimentalStaticBuild: true,
+ links,
+ logging,
+ markdownRender: astroConfig.markdownOptions.render,
+ mod,
+ origin,
+ pathname,
+ scripts,
+ renderers,
+ async resolve(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;
+ },
route: pageData.route,
routeCache,
- pathname,
+ site: astroConfig.buildOptions.site,
});
- debug('build', `Generating: ${pathname}`);
-
- const rootpath = appendForwardSlash(new URL(astroConfig.buildOptions.site || 'http://localhost/').pathname);
- const links = new Set<SSRElement>(
- linkIds.map((href) => ({
- props: {
- rel: 'stylesheet',
- href: npath.posix.join(rootpath, href),
- },
- children: '',
- }))
- );
- const scripts = hoistedId
- ? new Set<SSRElement>([
- {
- props: {
- type: 'module',
- src: npath.posix.join(rootpath, hoistedId),
- },
- children: '',
- },
- ])
- : new Set<SSRElement>();
- const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, links, scripts });
-
- // 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 = getOutFolder(astroConfig, pathname);
const outFile = getOutFile(astroConfig, outFolder, pathname);
await fs.promises.mkdir(outFolder, { recursive: true });
@@ -382,11 +392,79 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
}
}
+async function generateManifest(result: RollupOutput, opts: StaticBuildOptions, internals: BuildInternals) {
+ 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);
+
+ const rootRelativeIdToChunkMap = new Map<string, OutputChunk>();
+ for(const output of result.output) {
+ if(chunkIsPage(astroConfig, output, internals)) {
+ const chunk = output as OutputChunk;
+ if(chunk.facadeModuleId) {
+ const id = rootRelativeFacadeId(chunk.facadeModuleId, astroConfig);
+ rootRelativeIdToChunkMap.set(id, chunk);
+ }
+ }
+ }
+
+ const routes: SerializedRouteInfo[] = [];
+
+ for(const routeData of manifest.routes) {
+ const componentPath = routeData.component;
+ const entry = data[componentPath];
+
+ if(!rootRelativeIdToChunkMap.has(componentPath)) {
+ throw new Error('Unable to find chunk for ' + componentPath);
+ }
+
+ const chunk = rootRelativeIdToChunkMap.get(componentPath)!;
+ const facadeId = chunk.facadeModuleId!;
+ const links = getByFacadeId<string[]>(facadeId, internals.facadeIdToAssetsMap) || [];
+ const hoistedScript = getByFacadeId<string>(facadeId, internals.facadeIdToHoistedEntryMap);
+ const scripts = hoistedScript ? [hoistedScript] : [];
+
+ routes.push({
+ file: entry?.file,
+ links,
+ scripts,
+ routeData: serializeRouteData(routeData)
+ });
+ }
+
+ const ssrManifest: SerializedSSRManifest = {
+ routes,
+ site: astroConfig.buildOptions.site,
+ markdown: {
+ render: astroConfig.markdownOptions.render
+ },
+ renderers: astroConfig.renderers,
+ entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries())
+ };
+
+ const outputManifestJSON = JSON.stringify(ssrManifest, null, ' ');
+ 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);
@@ -421,6 +499,34 @@ async function cleanSsrOutput(opts: StaticBuildOptions) {
);
}
+async function ssrMoveAssets(opts: StaticBuildOptions) {
+ const { astroConfig } = opts;
+ const serverRoot = getServerRoot(astroConfig);
+ const clientRoot = getClientRoot(astroConfig);
+ const serverAssets = new URL('./assets/', serverRoot);
+ const clientAssets = new URL('./assets/', clientRoot);
+ const files = await glob('assets/**/*', {
+ cwd: fileURLToPath(serverRoot),
+ });
+
+ // Make the directory
+ await fs.promises.mkdir(clientAssets, { recursive: true });
+
+ await Promise.all(
+ files.map(async (filename) => {
+ const currentUrl = new URL(filename, serverRoot);
+ const clientUrl = new URL(filename, clientRoot);
+ return fs.promises.rename(currentUrl, clientUrl);
+ })
+ );
+
+ await emptyDir(fileURLToPath(serverAssets));
+
+ if(fs.existsSync(serverAssets)) {
+ await fs.promises.rmdir(serverAssets);
+ }
+}
+
export function vitePluginNewBuild(input: Set<string>, internals: BuildInternals, ext: 'js' | 'mjs'): VitePlugin {
return {
name: '@astro/rollup-plugin-new-build',
@@ -451,18 +557,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>();
diff --git a/packages/astro/src/core/build/types.d.ts b/packages/astro/src/core/build/types.d.ts
index 2606075e2..fa37ff888 100644
--- a/packages/astro/src/core/build/types.d.ts
+++ b/packages/astro/src/core/build/types.d.ts
@@ -1,4 +1,4 @@
-import type { ComponentPreload } from '../ssr/index';
+import type { ComponentPreload } from '../render/dev/index';
import type { RouteData } from '../../@types/astro';
export interface PageBuildData {
diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts
index 186677802..a8ddd5b79 100644
--- a/packages/astro/src/core/config.ts
+++ b/packages/astro/src/core/config.ts
@@ -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()
@@ -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,
};
}
@@ -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;
+ }
+ }
if (typeof flags.drafts === 'boolean') astroConfig.buildOptions.drafts = flags.drafts;
return astroConfig;
}
diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts
new file mode 100644
index 000000000..eea5afa33
--- /dev/null
+++ b/packages/astro/src/core/render/core.ts
@@ -0,0 +1,119 @@
+import type { ComponentInstance, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro';
+import type { LogOptions } from '../logger.js';
+
+import { renderPage } from '../../runtime/server/index.js';
+import { getParams } from '../routing/index.js';
+import { createResult } from './result.js';
+import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js';
+import { warn } from '../logger.js';
+
+interface GetParamsAndPropsOptions {
+ mod: ComponentInstance;
+ route: RouteData | undefined;
+ routeCache: RouteCache;
+ pathname: string;
+ logging: LogOptions;
+}
+
+async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise<[Params, Props]> {
+ const { logging, mod, route, routeCache, pathname } = opts;
+ // Handle dynamic routes
+ let params: Params = {};
+ let pageProps: Props;
+ if (route && !route.pathname) {
+ if (route.params.length) {
+ const paramsMatch = route.pattern.exec(pathname);
+ if (paramsMatch) {
+ params = getParams(route.params)(paramsMatch);
+ }
+ }
+ let routeCacheEntry = routeCache.get(route);
+ if (!routeCacheEntry) {
+ warn(logging, 'routeCache', `Internal Warning: getStaticPaths() called twice during the build. (${route.component})`);
+ routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
+ routeCache.set(route, routeCacheEntry);
+ }
+ const paramsKey = JSON.stringify(params);
+ const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, paramsKey);
+ if (!matchedStaticPath) {
+ throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
+ }
+ // This is written this way for performance; instead of spreading the props
+ // which is O(n), create a new object that extends props.
+ pageProps = Object.create(matchedStaticPath.props || Object.prototype);
+ } else {
+ pageProps = {};
+ }
+ return [params, pageProps];
+}
+
+interface RenderOptions {
+ experimentalStaticBuild: boolean;
+ logging: LogOptions,
+ links: Set<SSRElement>;
+ markdownRender: MarkdownRenderOptions,
+ mod: ComponentInstance;
+ origin: string;
+ pathname: string;
+ scripts: Set<SSRElement>;
+ resolve: (s: string) => Promise<string>;
+ renderers: Renderer[];
+ route?: RouteData;
+ routeCache: RouteCache;
+ site?: string;
+}
+
+export async function render(opts: RenderOptions): Promise<string> {
+ const {
+ experimentalStaticBuild,
+ links,
+ logging,
+ origin,
+ markdownRender,
+ mod,
+ pathname,
+ scripts,
+ renderers,
+ resolve,
+ route,
+ routeCache,
+ site
+ } = opts;
+
+ const [params, pageProps] = await getParamsAndProps({
+ logging,
+ mod,
+ route,
+ routeCache,
+ pathname,
+ });
+
+ // Validate the page component before rendering the page
+ const Component = await mod.default;
+ if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
+ if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
+
+
+ const result = createResult({
+ experimentalStaticBuild,
+ links,
+ logging,
+ markdownRender,
+ origin,
+ params,
+ pathname,
+ resolve,
+ renderers,
+ site,
+ scripts
+ });
+
+ let html = await renderPage(result, Component, pageProps, null);
+
+ // inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
+ if (experimentalStaticBuild && !/<!doctype html/i.test(html)) {
+ html = '<!DOCTYPE html>\n' + html;
+ }
+
+ return html;
+}
diff --git a/packages/astro/src/core/ssr/css.ts b/packages/astro/src/core/render/dev/css.ts
index 4ee0e80d8..196fdafd4 100644
--- a/packages/astro/src/core/ssr/css.ts
+++ b/packages/astro/src/core/render/dev/css.ts
@@ -1,7 +1,7 @@
-import type vite from '../vite';
+import type vite from '../../vite';
import path from 'path';
-import { viteID } from '../util.js';
+import { viteID } from '../../util.js';
// https://vitejs.dev/guide/features.html#css-pre-processors
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']);
diff --git a/packages/astro/src/core/render/dev/error.ts b/packages/astro/src/core/render/dev/error.ts
new file mode 100644
index 000000000..aa5a18083
--- /dev/null
+++ b/packages/astro/src/core/render/dev/error.ts
@@ -0,0 +1,44 @@
+import type { BuildResult } from 'esbuild';
+import type vite from '../../vite';
+import type { SSRError } from '../../../@types/astro';
+
+import eol from 'eol';
+import fs from 'fs';
+import { codeFrame } from '../../util.js';
+
+interface ErrorHandlerOptions {
+ filePath: URL;
+ viteServer: vite.ViteDevServer;
+}
+
+export async function errorHandler(e: unknown, { viteServer, filePath }: ErrorHandlerOptions) {
+ // normalize error stack line-endings to \n
+ if ((e as any).stack) {
+ (e as any).stack = eol.lf((e as any).stack);
+ }
+
+ // fix stack trace with Vite (this searches its module graph for matches)
+ if (e instanceof Error) {
+ viteServer.ssrFixStacktrace(e);
+ }
+
+ // Astro error (thrown by esbuild so it needs to be formatted for Vite)
+ if (Array.isArray((e as any).errors)) {
+ const { location, pluginName, text } = (e as BuildResult).errors[0];
+ const err = e as SSRError;
+ if (location) err.loc = { file: location.file, line: location.line, column: location.column };
+ let src = err.pluginCode;
+ if (!src && err.id && fs.existsSync(err.id)) src = await fs.promises.readFile(err.id, 'utf8');
+ if (!src) src = await fs.promises.readFile(filePath, 'utf8');
+ err.frame = codeFrame(src, err.loc);
+ err.id = location?.file;
+ err.message = `${location?.file}: ${text}
+${err.frame}
+`;
+ if (pluginName) err.plugin = pluginName;
+ throw err;
+ }
+
+ // Generic error (probably from Vite, and already formatted)
+ throw e;
+}
diff --git a/packages/astro/src/core/render/dev/hmr.ts b/packages/astro/src/core/render/dev/hmr.ts
new file mode 100644
index 000000000..3c795fdb1
--- /dev/null
+++ b/packages/astro/src/core/render/dev/hmr.ts
@@ -0,0 +1,11 @@
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+
+let hmrScript: string;
+export async function getHmrScript() {
+ if (hmrScript) return hmrScript;
+ const filePath = fileURLToPath(new URL('../../../runtime/client/hmr.js', import.meta.url));
+ const content = await fs.promises.readFile(filePath);
+ hmrScript = content.toString();
+ return hmrScript;
+}
diff --git a/packages/astro/src/core/ssr/html.ts b/packages/astro/src/core/render/dev/html.ts
index eb429b927..2ae147ade 100644
--- a/packages/astro/src/core/ssr/html.ts
+++ b/packages/astro/src/core/render/dev/html.ts
@@ -1,4 +1,4 @@
-import type vite from '../vite';
+import type vite from '../../vite';
import htmlparser2 from 'htmlparser2';
diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts
new file mode 100644
index 000000000..70f142c33
--- /dev/null
+++ b/packages/astro/src/core/render/dev/index.ts
@@ -0,0 +1,158 @@
+import type vite from '../../vite';
+import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode } from '../../../@types/astro';
+import { LogOptions } from '../../logger.js';
+import { fileURLToPath } from 'url';
+import { getStylesForURL } from './css.js';
+import { injectTags } from './html.js';
+import { RouteCache } from '../route-cache.js';
+import { resolveRenderers } from './renderers.js';
+import { errorHandler } from './error.js';
+import { getHmrScript } from './hmr.js';
+import { render as coreRender } from '../core.js';
+import { createModuleScriptElementWithSrcSet } from '../ssr-element.js';
+
+interface SSROptions {
+ /** an instance of the AstroConfig */
+ astroConfig: AstroConfig;
+ /** location of file on disk */
+ filePath: URL;
+ /** logging options */
+ logging: LogOptions;
+ /** "development" or "production" */
+ mode: RuntimeMode;
+ /** production website, needed for some RSS & Sitemap functions */
+ origin: string;
+ /** the web request (needed for dynamic routes) */
+ pathname: string;
+ /** optional, in case we need to render something outside of a dev server */
+ route?: RouteData;
+ /** pass in route cache because SSR can’t manage cache-busting */
+ routeCache: RouteCache;
+ /** Vite instance */
+ viteServer: vite.ViteDevServer;
+}
+
+export type ComponentPreload = [Renderer[], ComponentInstance];
+
+const svelteStylesRE = /svelte\?svelte&type=style/;
+
+export async function preload({ astroConfig, filePath, viteServer }: SSROptions): Promise<ComponentPreload> {
+ // Important: This needs to happen first, in case a renderer provides polyfills.
+ const renderers = await resolveRenderers(viteServer, astroConfig);
+ // Load the module from the Vite SSR Runtime.
+ const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
+
+ return [renderers, mod];
+}
+
+/** use Vite to SSR */
+export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
+ const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
+
+ // Add hoisted script tags
+ const scripts = createModuleScriptElementWithSrcSet(astroConfig.buildOptions.experimentalStaticBuild ?
+ Array.from(mod.$$metadata.hoistedScriptPaths()) :
+ []
+ );
+
+ // Inject HMR scripts
+ if (mode === 'development' && astroConfig.buildOptions.experimentalStaticBuild) {
+ scripts.add({
+ props: { type: 'module', src: '/@vite/client' },
+ children: '',
+ });
+ scripts.add({
+ props: { type: 'module', src: new URL('../../runtime/client/hmr.js', import.meta.url).pathname },
+ children: '',
+ });
+ }
+
+ let html = await coreRender({
+ experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
+ links: new Set(),
+ logging,
+ markdownRender: astroConfig.markdownOptions.render,
+ mod,
+ origin,
+ pathname,
+ scripts,
+ // Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
+ async resolve(s: string) {
+ // The legacy build needs these to remain unresolved so that vite HTML
+ // Can do the resolution. Without this condition the build output will be
+ // broken in the legacy build. This can be removed once the legacy build is removed.
+ if (astroConfig.buildOptions.experimentalStaticBuild) {
+ const [, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
+ return resolvedPath;
+ } else {
+ return s;
+ }
+ },
+ renderers,
+ route,
+ routeCache,
+ site: astroConfig.buildOptions.site,
+ });
+
+ // inject tags
+ const tags: vite.HtmlTagDescriptor[] = [];
+
+ // dev only: inject Astro HMR client
+ if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
+ tags.push({
+ tag: 'script',
+ attrs: { type: 'module' },
+ // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure
+ // `import.meta.hot` is properly handled by Vite
+ children: await getHmrScript(),
+ injectTo: 'head',
+ });
+ }
+
+ // inject CSS
+ [...getStylesForURL(filePath, viteServer)].forEach((href) => {
+ if (mode === 'development' && svelteStylesRE.test(href)) {
+ tags.push({
+ tag: 'script',
+ attrs: { type: 'module', src: href },
+ injectTo: 'head',
+ });
+ } else {
+ tags.push({
+ tag: 'link',
+ attrs: {
+ rel: 'stylesheet',
+ href,
+ 'data-astro-injected': true,
+ },
+ injectTo: 'head',
+ });
+ }
+ });
+
+ // add injected tags
+ html = injectTags(html, tags);
+
+ // run transformIndexHtml() in dev to run Vite dev transformations
+ if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
+ const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
+ html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
+ }
+
+ // inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
+ if (!/<!doctype html/i.test(html)) {
+ html = '<!DOCTYPE html>\n' + html;
+ }
+
+ return html;
+}
+
+export async function ssr(ssrOpts: SSROptions): Promise<string> {
+ try {
+ const [renderers, mod] = await preload(ssrOpts);
+ return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
+ } catch (e: unknown) {
+ await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath });
+ throw e;
+ }
+}
diff --git a/packages/astro/src/core/render/dev/renderers.ts b/packages/astro/src/core/render/dev/renderers.ts
new file mode 100644
index 000000000..abe22b3ca
--- /dev/null
+++ b/packages/astro/src/core/render/dev/renderers.ts
@@ -0,0 +1,36 @@
+import type vite from '../../vite';
+import type { AstroConfig, Renderer } from '../../../@types/astro';
+
+import { resolveDependency } from '../../util.js';
+import { createRenderer } from '../renderer.js';
+
+const cache = new Map<string, Promise<Renderer>>();
+
+async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string, astroConfig: AstroConfig): Promise<Renderer> {
+ const resolvedRenderer: Renderer = await createRenderer(renderer, {
+ renderer(name) {
+ return import(resolveDependency(name, astroConfig));
+ },
+ async server(entry) {
+ const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(entry);
+ const mod = await viteServer.ssrLoadModule(url);
+ return mod;
+ }
+ });
+
+ return resolvedRenderer;
+}
+
+export async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<Renderer[]> {
+ const ids: string[] = astroConfig.renderers;
+ const renderers = await Promise.all(
+ ids.map((renderer) => {
+ if (cache.has(renderer)) return cache.get(renderer)!;
+ let promise = resolveRenderer(viteServer, renderer, astroConfig);
+ cache.set(renderer, promise);
+ return promise;
+ })
+ );
+
+ return renderers;
+}
diff --git a/packages/astro/src/core/ssr/paginate.ts b/packages/astro/src/core/render/paginate.ts
index 96d8a435a..96d8a435a 100644
--- a/packages/astro/src/core/ssr/paginate.ts
+++ b/packages/astro/src/core/render/paginate.ts
diff --git a/packages/astro/src/core/render/renderer.ts b/packages/astro/src/core/render/renderer.ts
new file mode 100644
index 000000000..42025cfc0
--- /dev/null
+++ b/packages/astro/src/core/render/renderer.ts
@@ -0,0 +1,30 @@
+import type { Renderer } from '../../@types/astro';
+
+import npath from 'path';
+
+interface RendererResolverImplementation {
+ renderer: (name: string) => Promise<any>;
+ server: (entry: string) => Promise<any>;
+}
+
+export async function createRenderer(renderer: string, impl: RendererResolverImplementation) {
+ const resolvedRenderer: any = {};
+ // We can dynamically import the renderer by itself because it shouldn't have
+ // any non-standard imports, the index is just meta info.
+ // The other entrypoints need to be loaded through Vite.
+ const {
+ default: { name, client, polyfills, hydrationPolyfills, server },
+ } = await impl.renderer(renderer) //await import(resolveDependency(renderer, astroConfig));
+
+ resolvedRenderer.name = name;
+ if (client) resolvedRenderer.source = npath.posix.join(renderer, client);
+ resolvedRenderer.serverEntry = npath.posix.join(renderer, server);
+ if (Array.isArray(hydrationPolyfills)) resolvedRenderer.hydrationPolyfills = hydrationPolyfills.map((src: string) => npath.posix.join(renderer, src));
+ if (Array.isArray(polyfills)) resolvedRenderer.polyfills = polyfills.map((src: string) => npath.posix.join(renderer, src));
+
+ const { default: rendererSSR } = await impl.server(resolvedRenderer.serverEntry);
+ resolvedRenderer.ssr = rendererSSR;
+
+ const completedRenderer: Renderer = resolvedRenderer;
+ return completedRenderer;
+}
diff --git a/packages/astro/src/core/ssr/result.ts b/packages/astro/src/core/render/result.ts
index 5a03ab769..9775a0949 100644
--- a/packages/astro/src/core/ssr/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -1,25 +1,37 @@
-import type { AstroConfig, AstroGlobal, AstroGlobalPartial, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
+import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro';
import { bold } from 'kleur/colors';
import { canonicalURL as getCanonicalURL } from '../util.js';
-import { isCSSRequest } from './css.js';
+import { isCSSRequest } from './dev/css.js';
import { isScriptRequest } from './script.js';
import { renderSlot } from '../../runtime/server/index.js';
import { warn, LogOptions } from '../logger.js';
export interface CreateResultArgs {
- astroConfig: AstroConfig;
+ experimentalStaticBuild: boolean;
logging: LogOptions;
origin: string;
+ markdownRender: MarkdownRenderOptions;
params: Params;
pathname: string;
renderers: Renderer[];
+ resolve: (s: string) => Promise<string>;
+ site: string | undefined;
links?: Set<SSRElement>;
scripts?: Set<SSRElement>;
}
export function createResult(args: CreateResultArgs): SSRResult {
- const { astroConfig, origin, params, pathname, renderers } = args;
+ const {
+ experimentalStaticBuild,
+ origin,
+ markdownRender,
+ params,
+ pathname,
+ renderers,
+ resolve,
+ site: buildOptionsSite
+ } = args;
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
@@ -32,7 +44,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
createAstro(astroGlobal: AstroGlobalPartial, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
- const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
+ const canonicalURL = getCanonicalURL('.' + pathname, buildOptionsSite || origin);
return {
__proto__: astroGlobal,
props,
@@ -42,7 +54,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
url,
},
resolve(path: string) {
- if (astroConfig.buildOptions.experimentalStaticBuild) {
+ if (experimentalStaticBuild) {
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
if (isCSSRequest(path)) {
extra = `It looks like you are resolving styles. If you are adding a link tag, replace with this:
@@ -83,33 +95,37 @@ ${extra}`
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
- let mdRender = astroConfig.markdownOptions.render;
- let renderOpts = {};
+ let [mdRender, renderOpts] = markdownRender;
+ let parser: MarkdownParser | null = null;
+ //let renderOpts = {};
if (Array.isArray(mdRender)) {
renderOpts = mdRender[1];
mdRender = mdRender[0];
}
// ['rehype-toc', opts]
if (typeof mdRender === 'string') {
- ({ default: mdRender } = await import(mdRender));
+ const mod: { default: MarkdownParser } = await import(mdRender);
+ parser = mod.default;
}
// [import('rehype-toc'), opts]
else if (mdRender instanceof Promise) {
- ({ default: mdRender } = await mdRender);
+ const mod: { default: MarkdownParser } = await mdRender;
+ parser = mod.default;
+ } else if(typeof mdRender === 'function') {
+ parser = mdRender;
+ } else {
+ throw new Error('No Markdown parser found.');
}
- const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
+ const { code } = await parser(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
- // This is a stub and will be implemented by dev and build.
- async resolve(s: string): Promise<string> {
- return '';
- },
+ resolve,
_metadata: {
renderers,
pathname,
- experimentalStaticBuild: astroConfig.buildOptions.experimentalStaticBuild,
+ experimentalStaticBuild,
},
};
diff --git a/packages/astro/src/core/ssr/route-cache.ts b/packages/astro/src/core/render/route-cache.ts
index 11988d36b..889c64a48 100644
--- a/packages/astro/src/core/ssr/route-cache.ts
+++ b/packages/astro/src/core/render/route-cache.ts
@@ -1,8 +1,8 @@
import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetStaticPathsResultKeyed, RouteData, RSS } from '../../@types/astro';
import { LogOptions, warn, debug } from '../logger.js';
-import { generatePaginateFunction } from '../ssr/paginate.js';
-import { validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
+import { generatePaginateFunction } from './paginate.js';
+import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../routing/index.js';
type RSSFn = (...args: any[]) => any;
diff --git a/packages/astro/src/core/ssr/rss.ts b/packages/astro/src/core/render/rss.ts
index 18cce36a1..1e77dff35 100644
--- a/packages/astro/src/core/ssr/rss.ts
+++ b/packages/astro/src/core/render/rss.ts
@@ -1,4 +1,4 @@
-import type { RSSFunction, RSS, RSSResult, FeedResult, RouteData } from '../../@types/astro';
+import type { RSSFunction, RSS, RSSResult, RouteData } from '../../@types/astro';
import { XMLValidator } from 'fast-xml-parser';
import { canonicalURL, isValidURL, PRETTY_FEED_V3 } from '../util.js';
diff --git a/packages/astro/src/core/ssr/script.ts b/packages/astro/src/core/render/script.ts
index a91391963..a91391963 100644
--- a/packages/astro/src/core/ssr/script.ts
+++ b/packages/astro/src/core/render/script.ts
diff --git a/packages/astro/src/core/ssr/sitemap.ts b/packages/astro/src/core/render/sitemap.ts
index a5ef54f6a..a5ef54f6a 100644
--- a/packages/astro/src/core/ssr/sitemap.ts
+++ b/packages/astro/src/core/render/sitemap.ts
diff --git a/packages/astro/src/core/render/ssr-element.ts b/packages/astro/src/core/render/ssr-element.ts
new file mode 100644
index 000000000..5fbd3b115
--- /dev/null
+++ b/packages/astro/src/core/render/ssr-element.ts
@@ -0,0 +1,40 @@
+import type { SSRElement } from '../../@types/astro';
+
+import npath from 'path';
+import { appendForwardSlash } from '../../core/path.js';
+
+function getRootPath(site?: string): string {
+ return appendForwardSlash(new URL(site || 'http://localhost/').pathname)
+}
+
+function joinToRoot(href: string, site?: string): string {
+ return npath.posix.join(getRootPath(site), href);
+}
+
+export function createLinkStylesheetElement(href: string, site?: string): SSRElement {
+ return {
+ props: {
+ rel: 'stylesheet',
+ href: joinToRoot(href, site)
+ },
+ children: '',
+ };
+}
+
+export function createLinkStylesheetElementSet(hrefs: string[], site?: string) {
+ return new Set<SSRElement>(hrefs.map(href => createLinkStylesheetElement(href, site)));
+}
+
+export function createModuleScriptElementWithSrc(src: string, site?: string): SSRElement {
+ return {
+ props: {
+ type: 'module',
+ src: joinToRoot(src, site),
+ },
+ children: '',
+ }
+}
+
+export function createModuleScriptElementWithSrcSet(srces: string[], site?: string): Set<SSRElement> {
+ return new Set<SSRElement>(srces.map(src => createModuleScriptElementWithSrc(src, site)));
+}
diff --git a/packages/astro/src/core/routing/index.ts b/packages/astro/src/core/routing/index.ts
new file mode 100644
index 000000000..2bc9be954
--- /dev/null
+++ b/packages/astro/src/core/routing/index.ts
@@ -0,0 +1,11 @@
+export { createRouteManifest } from './manifest/create.js';
+export {
+ serializeRouteData,
+ deserializeRouteData
+} from './manifest/serialization.js';
+export { matchRoute } from './match.js';
+export { getParams } from './params.js';
+export {
+ validateGetStaticPathsModule,
+ validateGetStaticPathsResult
+} from './validation.js';
diff --git a/packages/astro/src/core/ssr/routing.ts b/packages/astro/src/core/routing/manifest/create.ts
index b6a2cf1a4..5456938ee 100644
--- a/packages/astro/src/core/ssr/routing.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -1,69 +1,16 @@
-import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, Params, RouteData } from '../../@types/astro';
-import type { LogOptions } from '../logger';
+import type {
+ AstroConfig,
+ ManifestData,
+ RouteData
+} from '../../../@types/astro';
+import type { LogOptions } from '../../logger';
import fs from 'fs';
import path from 'path';
import { compile } from 'path-to-regexp';
import slash from 'slash';
import { fileURLToPath } from 'url';
-import { warn } from '../logger.js';
-
-/**
- * given an array of params like `['x', 'y', 'z']` for
- * src/routes/[x]/[y]/[z]/svelte, create a function
- * that turns a RegExpExecArray into ({ x, y, z })
- */
-export function getParams(array: string[]) {
- const fn = (match: RegExpExecArray) => {
- const params: Params = {};
- array.forEach((key, i) => {
- if (key.startsWith('...')) {
- params[key.slice(3)] = match[i + 1] ? decodeURIComponent(match[i + 1]) : undefined;
- } else {
- params[key] = decodeURIComponent(match[i + 1]);
- }
- });
- return params;
- };
-
- return fn;
-}
-
-/** Find matching route from pathname */
-export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
- return manifest.routes.find((route) => route.pattern.test(pathname));
-}
-
-/** Throw error for deprecated/malformed APIs */
-export function validateGetStaticPathsModule(mod: ComponentInstance) {
- if ((mod as any).createCollection) {
- throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
- }
- if (!mod.getStaticPaths) {
- throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
- }
-}
-
-/** Throw error for malformed getStaticPaths() response */
-export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
- if (!Array.isArray(result)) {
- throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`);
- }
- result.forEach((pathObject) => {
- if (!pathObject.params) {
- warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`);
- return;
- }
- for (const [key, val] of Object.entries(pathObject.params)) {
- if (!(typeof val === 'undefined' || typeof val === 'string')) {
- warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`);
- }
- if (val === '') {
- warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`);
- }
- }
- });
-}
+import { warn } from '../../logger.js';
interface Part {
content: string;
@@ -82,6 +29,148 @@ interface Item {
routeSuffix: string;
}
+function countOccurrences(needle: string, haystack: string) {
+ let count = 0;
+ for (let i = 0; i < haystack.length; i += 1) {
+ if (haystack[i] === needle) count += 1;
+ }
+ return count;
+}
+
+function getParts(part: string, file: string) {
+ const result: Part[] = [];
+ part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
+ if (!str) return;
+ const dynamic = i % 2 === 1;
+
+ const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
+
+ if (!content || (dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content))) {
+ throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
+ }
+
+ result.push({
+ content,
+ dynamic,
+ spread: dynamic && /^\.{3}.+$/.test(content),
+ });
+ });
+
+ return result;
+}
+
+function getPattern(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
+ const pathname = segments
+ .map((segment) => {
+ return segment[0].spread
+ ? '(?:\\/(.*?))?'
+ : '\\/' +
+ segment
+ .map((part) => {
+ if (part)
+ return part.dynamic
+ ? '([^/]+?)'
+ : part.content
+ .normalize()
+ .replace(/\?/g, '%3F')
+ .replace(/#/g, '%23')
+ .replace(/%5B/g, '[')
+ .replace(/%5D/g, ']')
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ })
+ .join('');
+ })
+ .join('');
+
+ const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
+ return new RegExp(`^${pathname || '\\/'}${trailing}`);
+}
+
+
+function getTrailingSlashPattern(addTrailingSlash: AstroConfig['devOptions']['trailingSlash']): string {
+ if (addTrailingSlash === 'always') {
+ return '\\/$';
+ }
+ if (addTrailingSlash === 'never') {
+ return '$';
+ }
+ return '\\/?$';
+}
+
+function getGenerator(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
+ const template = segments
+ .map((segment) => {
+ return segment[0].spread
+ ? `/:${segment[0].content.substr(3)}(.*)?`
+ : '/' +
+ segment
+ .map((part) => {
+ if (part)
+ return part.dynamic
+ ? `:${part.content}`
+ : part.content
+ .normalize()
+ .replace(/\?/g, '%3F')
+ .replace(/#/g, '%23')
+ .replace(/%5B/g, '[')
+ .replace(/%5D/g, ']')
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ })
+ .join('');
+ })
+ .join('');
+
+ const trailing = addTrailingSlash !== 'never' && segments.length ? '/' : '';
+ const toPath = compile(template + trailing);
+ return toPath;
+}
+
+function isSpread(str: string) {
+ const spreadPattern = /\[\.{3}/g;
+ return spreadPattern.test(str);
+}
+
+function comparator(a: Item, b: Item) {
+ if (a.isIndex !== b.isIndex) {
+ if (a.isIndex) return isSpread(a.file) ? 1 : -1;
+
+ return isSpread(b.file) ? -1 : 1;
+ }
+
+ const max = Math.max(a.parts.length, b.parts.length);
+
+ for (let i = 0; i < max; i += 1) {
+ const aSubPart = a.parts[i];
+ const bSubPart = b.parts[i];
+
+ if (!aSubPart) return 1; // b is more specific, so goes first
+ if (!bSubPart) return -1;
+
+ // if spread && index, order later
+ if (aSubPart.spread && bSubPart.spread) {
+ return a.isIndex ? 1 : -1;
+ }
+
+ // If one is ...spread order it later
+ if (aSubPart.spread !== bSubPart.spread) return aSubPart.spread ? 1 : -1;
+
+ if (aSubPart.dynamic !== bSubPart.dynamic) {
+ return aSubPart.dynamic ? 1 : -1;
+ }
+
+ if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
+ return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1);
+ }
+ }
+
+ if (a.isPage !== b.isPage) {
+ return a.isPage ? 1 : -1;
+ }
+
+ // otherwise sort alphabetically
+ return a.file < b.file ? -1 : 1;
+}
+
/** Create manifest of all static routes */
export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?: string }, logging: LogOptions): ManifestData {
const components: string[] = [];
@@ -207,144 +296,3 @@ export function createRouteManifest({ config, cwd }: { config: AstroConfig; cwd?
routes,
};
}
-
-function countOccurrences(needle: string, haystack: string) {
- let count = 0;
- for (let i = 0; i < haystack.length; i += 1) {
- if (haystack[i] === needle) count += 1;
- }
- return count;
-}
-
-function isSpread(str: string) {
- const spreadPattern = /\[\.{3}/g;
- return spreadPattern.test(str);
-}
-
-function comparator(a: Item, b: Item) {
- if (a.isIndex !== b.isIndex) {
- if (a.isIndex) return isSpread(a.file) ? 1 : -1;
-
- return isSpread(b.file) ? -1 : 1;
- }
-
- const max = Math.max(a.parts.length, b.parts.length);
-
- for (let i = 0; i < max; i += 1) {
- const aSubPart = a.parts[i];
- const bSubPart = b.parts[i];
-
- if (!aSubPart) return 1; // b is more specific, so goes first
- if (!bSubPart) return -1;
-
- // if spread && index, order later
- if (aSubPart.spread && bSubPart.spread) {
- return a.isIndex ? 1 : -1;
- }
-
- // If one is ...spread order it later
- if (aSubPart.spread !== bSubPart.spread) return aSubPart.spread ? 1 : -1;
-
- if (aSubPart.dynamic !== bSubPart.dynamic) {
- return aSubPart.dynamic ? 1 : -1;
- }
-
- if (!aSubPart.dynamic && aSubPart.content !== bSubPart.content) {
- return bSubPart.content.length - aSubPart.content.length || (aSubPart.content < bSubPart.content ? -1 : 1);
- }
- }
-
- if (a.isPage !== b.isPage) {
- return a.isPage ? 1 : -1;
- }
-
- // otherwise sort alphabetically
- return a.file < b.file ? -1 : 1;
-}
-
-function getParts(part: string, file: string) {
- const result: Part[] = [];
- part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => {
- if (!str) return;
- const dynamic = i % 2 === 1;
-
- const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
-
- if (!content || (dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content))) {
- throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
- }
-
- result.push({
- content,
- dynamic,
- spread: dynamic && /^\.{3}.+$/.test(content),
- });
- });
-
- return result;
-}
-
-function getTrailingSlashPattern(addTrailingSlash: AstroConfig['devOptions']['trailingSlash']): string {
- if (addTrailingSlash === 'always') {
- return '\\/$';
- }
- if (addTrailingSlash === 'never') {
- return '$';
- }
- return '\\/?$';
-}
-
-function getPattern(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
- const pathname = segments
- .map((segment) => {
- return segment[0].spread
- ? '(?:\\/(.*?))?'
- : '\\/' +
- segment
- .map((part) => {
- if (part)
- return part.dynamic
- ? '([^/]+?)'
- : part.content
- .normalize()
- .replace(/\?/g, '%3F')
- .replace(/#/g, '%23')
- .replace(/%5B/g, '[')
- .replace(/%5D/g, ']')
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- })
- .join('');
- })
- .join('');
-
- const trailing = addTrailingSlash && segments.length ? getTrailingSlashPattern(addTrailingSlash) : '$';
- return new RegExp(`^${pathname || '\\/'}${trailing}`);
-}
-
-function getGenerator(segments: Part[][], addTrailingSlash: AstroConfig['devOptions']['trailingSlash']) {
- const template = segments
- .map((segment) => {
- return segment[0].spread
- ? `/:${segment[0].content.substr(3)}(.*)?`
- : '/' +
- segment
- .map((part) => {
- if (part)
- return part.dynamic
- ? `:${part.content}`
- : part.content
- .normalize()
- .replace(/\?/g, '%3F')
- .replace(/#/g, '%23')
- .replace(/%5B/g, '[')
- .replace(/%5D/g, ']')
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- })
- .join('');
- })
- .join('');
-
- const trailing = addTrailingSlash !== 'never' && segments.length ? '/' : '';
- const toPath = compile(template + trailing);
- return toPath;
-}
diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts
new file mode 100644
index 000000000..e751cc517
--- /dev/null
+++ b/packages/astro/src/core/routing/manifest/serialization.ts
@@ -0,0 +1,29 @@
+import type {
+ RouteData,
+ SerializedRouteData
+} from '../../../@types/astro';
+
+function createRouteData(pattern: RegExp, params: string[], component: string, pathname: string | undefined): RouteData {
+ return {
+ type: 'page',
+ pattern,
+ params,
+ component,
+ // TODO bring back
+ generate: () => '',
+ pathname: pathname || undefined,
+ }
+}
+
+export function serializeRouteData(routeData: RouteData): SerializedRouteData {
+ // Is there a better way to do this in TypeScript?
+ const outRouteData = routeData as unknown as SerializedRouteData;
+ outRouteData.pattern = routeData.pattern.source;
+ return outRouteData;
+}
+
+export function deserializeRouteData(rawRouteData: SerializedRouteData) {
+ const { component, params, pathname } = rawRouteData;
+ const pattern = new RegExp(rawRouteData.pattern);
+ return createRouteData(pattern, params, component, pathname);
+}
diff --git a/packages/astro/src/core/routing/match.ts b/packages/astro/src/core/routing/match.ts
new file mode 100644
index 000000000..d5cf4e860
--- /dev/null
+++ b/packages/astro/src/core/routing/match.ts
@@ -0,0 +1,10 @@
+import type {
+ ManifestData,
+ RouteData
+} from '../../@types/astro';
+
+/** Find matching route from pathname */
+export function matchRoute(pathname: string, manifest: ManifestData): RouteData | undefined {
+ return manifest.routes.find((route) => route.pattern.test(pathname));
+}
+
diff --git a/packages/astro/src/core/routing/params.ts b/packages/astro/src/core/routing/params.ts
new file mode 100644
index 000000000..739a99afd
--- /dev/null
+++ b/packages/astro/src/core/routing/params.ts
@@ -0,0 +1,23 @@
+import type { Params } from '../../@types/astro';
+
+/**
+ * given an array of params like `['x', 'y', 'z']` for
+ * src/routes/[x]/[y]/[z]/svelte, create a function
+ * that turns a RegExpExecArray into ({ x, y, z })
+ */
+ export function getParams(array: string[]) {
+ const fn = (match: RegExpExecArray) => {
+ const params: Params = {};
+ array.forEach((key, i) => {
+ if (key.startsWith('...')) {
+ params[key.slice(3)] = match[i + 1] ? decodeURIComponent(match[i + 1]) : undefined;
+ } else {
+ params[key] = decodeURIComponent(match[i + 1]);
+ }
+ });
+ return params;
+ };
+
+ return fn;
+}
+
diff --git a/packages/astro/src/core/routing/validation.ts b/packages/astro/src/core/routing/validation.ts
new file mode 100644
index 000000000..db47f6089
--- /dev/null
+++ b/packages/astro/src/core/routing/validation.ts
@@ -0,0 +1,37 @@
+import type {
+ ComponentInstance,
+ GetStaticPathsResult
+} from '../../@types/astro';
+import type { LogOptions } from '../logger';
+import { warn } from '../logger.js';
+
+/** Throw error for deprecated/malformed APIs */
+export function validateGetStaticPathsModule(mod: ComponentInstance) {
+ if ((mod as any).createCollection) {
+ throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
+ }
+ if (!mod.getStaticPaths) {
+ throw new Error(`[getStaticPaths] getStaticPaths() function is required. Make sure that you \`export\` the function from your component.`);
+ }
+}
+
+/** Throw error for malformed getStaticPaths() response */
+export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
+ if (!Array.isArray(result)) {
+ throw new Error(`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(result)}\`.`);
+ }
+ result.forEach((pathObject) => {
+ if (!pathObject.params) {
+ warn(logging, 'getStaticPaths', `invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(pathObject)}\`. Skipped.`);
+ return;
+ }
+ for (const [key, val] of Object.entries(pathObject.params)) {
+ if (!(typeof val === 'undefined' || typeof val === 'string')) {
+ warn(logging, 'getStaticPaths', `invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(val)}\`.`);
+ }
+ if (val === '') {
+ warn(logging, 'getStaticPaths', `invalid path param: ${key}. \`undefined\` expected for an optional param, but got empty string.`);
+ }
+ }
+ });
+}
diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts
deleted file mode 100644
index c4d214a72..000000000
--- a/packages/astro/src/core/ssr/index.ts
+++ /dev/null
@@ -1,300 +0,0 @@
-import type { BuildResult } from 'esbuild';
-import type vite from '../vite';
-import type { AstroConfig, ComponentInstance, Params, Props, Renderer, RouteData, RuntimeMode, SSRElement, SSRError } from '../../@types/astro';
-import { LogOptions, warn } from '../logger.js';
-
-import eol from 'eol';
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import { renderPage } from '../../runtime/server/index.js';
-import { codeFrame, resolveDependency } from '../util.js';
-import { getStylesForURL } from './css.js';
-import { injectTags } from './html.js';
-import { getParams, validateGetStaticPathsResult } from './routing.js';
-import { createResult } from './result.js';
-import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
-
-const svelteStylesRE = /svelte\?svelte&type=style/;
-
-interface SSROptions {
- /** an instance of the AstroConfig */
- astroConfig: AstroConfig;
- /** location of file on disk */
- filePath: URL;
- /** logging options */
- logging: LogOptions;
- /** "development" or "production" */
- mode: RuntimeMode;
- /** production website, needed for some RSS & Sitemap functions */
- origin: string;
- /** the web request (needed for dynamic routes) */
- pathname: string;
- /** optional, in case we need to render something outside of a dev server */
- route?: RouteData;
- /** pass in route cache because SSR can’t manage cache-busting */
- routeCache: RouteCache;
- /** Vite instance */
- viteServer: vite.ViteDevServer;
-}
-
-const cache = new Map<string, Promise<Renderer>>();
-
-// TODO: improve validation and error handling here.
-async function resolveRenderer(viteServer: vite.ViteDevServer, renderer: string, astroConfig: AstroConfig) {
- const resolvedRenderer: any = {};
- // We can dynamically import the renderer by itself because it shouldn't have
- // any non-standard imports, the index is just meta info.
- // The other entrypoints need to be loaded through Vite.
- const {
- default: { name, client, polyfills, hydrationPolyfills, server },
- } = await import(resolveDependency(renderer, astroConfig));
-
- 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(resolvedRenderer.serverEntry);
- const { default: rendererSSR } = await viteServer.ssrLoadModule(url);
- resolvedRenderer.ssr = rendererSSR;
-
- const completedRenderer: Renderer = resolvedRenderer;
- return completedRenderer;
-}
-
-async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: AstroConfig): Promise<Renderer[]> {
- const ids: string[] = astroConfig.renderers;
- const renderers = await Promise.all(
- ids.map((renderer) => {
- if (cache.has(renderer)) return cache.get(renderer)!;
- let promise = resolveRenderer(viteServer, renderer, astroConfig);
- cache.set(renderer, promise);
- return promise;
- })
- );
-
- return renderers;
-}
-
-interface ErrorHandlerOptions {
- filePath: URL;
- viteServer: vite.ViteDevServer;
-}
-
-async function errorHandler(e: unknown, { viteServer, filePath }: ErrorHandlerOptions) {
- // normalize error stack line-endings to \n
- if ((e as any).stack) {
- (e as any).stack = eol.lf((e as any).stack);
- }
-
- // fix stack trace with Vite (this searches its module graph for matches)
- if (e instanceof Error) {
- viteServer.ssrFixStacktrace(e);
- }
-
- // Astro error (thrown by esbuild so it needs to be formatted for Vite)
- if (Array.isArray((e as any).errors)) {
- const { location, pluginName, text } = (e as BuildResult).errors[0];
- const err = e as SSRError;
- if (location) err.loc = { file: location.file, line: location.line, column: location.column };
- let src = err.pluginCode;
- if (!src && err.id && fs.existsSync(err.id)) src = await fs.promises.readFile(err.id, 'utf8');
- if (!src) src = await fs.promises.readFile(filePath, 'utf8');
- err.frame = codeFrame(src, err.loc);
- err.id = location?.file;
- err.message = `${location?.file}: ${text}
-${err.frame}
-`;
- if (pluginName) err.plugin = pluginName;
- throw err;
- }
-
- // Generic error (probably from Vite, and already formatted)
- throw e;
-}
-
-export type ComponentPreload = [Renderer[], ComponentInstance];
-
-export async function preload({ astroConfig, filePath, viteServer }: SSROptions): Promise<ComponentPreload> {
- // Important: This needs to happen first, in case a renderer provides polyfills.
- const renderers = await resolveRenderers(viteServer, astroConfig);
- // Load the module from the Vite SSR Runtime.
- const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
-
- return [renderers, mod];
-}
-
-export async function getParamsAndProps({ route, routeCache, pathname }: { route: RouteData | undefined; routeCache: RouteCache; pathname: string }): Promise<[Params, Props]> {
- // Handle dynamic routes
- let params: Params = {};
- let pageProps: Props;
- if (route && !route.pathname) {
- if (route.params.length) {
- const paramsMatch = route.pattern.exec(pathname);
- if (paramsMatch) {
- params = getParams(route.params)(paramsMatch);
- }
- }
- const routeCacheEntry = routeCache.get(route);
- if (!routeCacheEntry) {
- throw new Error(`[${route.component}] Internal error: route cache was empty, but expected to be full.`);
- }
- const paramsKey = JSON.stringify(params);
- const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, paramsKey);
- if (!matchedStaticPath) {
- throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
- }
- // This is written this way for performance; instead of spreading the props
- // which is O(n), create a new object that extends props.
- pageProps = Object.create(matchedStaticPath.props || Object.prototype);
- } else {
- pageProps = {};
- }
- return [params, pageProps];
-}
-
-/** use Vite to SSR */
-export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
- const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
-
- // Handle dynamic routes
- let params: Params = {};
- let pageProps: Props = {};
- if (route && !route.pathname) {
- if (route.params.length) {
- const paramsMatch = route.pattern.exec(pathname);
- if (paramsMatch) {
- params = getParams(route.params)(paramsMatch);
- }
- }
- let routeCacheEntry = routeCache.get(route);
- // TODO(fks): All of our getStaticPaths logic should live in a single place,
- // to prevent duplicate runs during the build. This is not expected to run
- // anymore and we should change this check to thrown an internal error.
- if (!routeCacheEntry) {
- warn(logging, 'routeCache', `Internal Warning: getStaticPaths() called twice during the build. (${route.component})`);
- routeCacheEntry = await callGetStaticPaths(mod, route, true, logging);
- routeCache.set(route, routeCacheEntry);
- }
- const matchedStaticPath = routeCacheEntry.staticPaths.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
- if (!matchedStaticPath) {
- throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
- }
- pageProps = { ...matchedStaticPath.props } || {};
- }
-
- // Validate the page component before rendering the page
- const Component = await mod.default;
- if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
- if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
-
- // Add hoisted script tags
- const scripts = astroConfig.buildOptions.experimentalStaticBuild
- ? new Set<SSRElement>(
- Array.from(mod.$$metadata.hoistedScriptPaths()).map((src) => ({
- props: { type: 'module', src },
- children: '',
- }))
- )
- : new Set<SSRElement>();
-
- // Inject HMR scripts
- if (mode === 'development' && astroConfig.buildOptions.experimentalStaticBuild) {
- scripts.add({
- props: { type: 'module', src: '/@vite/client' },
- children: '',
- });
- scripts.add({
- props: { type: 'module', src: new URL('../../runtime/client/hmr.js', import.meta.url).pathname },
- children: '',
- });
- }
-
- const result = createResult({ astroConfig, logging, origin, params, pathname, renderers, scripts });
- // Resolves specifiers in the inline hydrated scripts, such as "@astrojs/renderer-preact/client.js"
- result.resolve = async (s: string) => {
- // The legacy build needs these to remain unresolved so that vite HTML
- // Can do the resolution. Without this condition the build output will be
- // broken in the legacy build. This can be removed once the legacy build is removed.
- if (astroConfig.buildOptions.experimentalStaticBuild) {
- const [, resolvedPath] = await viteServer.moduleGraph.resolveUrl(s);
- return resolvedPath;
- } else {
- return s;
- }
- };
-
- let html = await renderPage(result, Component, pageProps, null);
-
- // inject tags
- const tags: vite.HtmlTagDescriptor[] = [];
-
- // dev only: inject Astro HMR client
- if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
- tags.push({
- tag: 'script',
- attrs: { type: 'module' },
- // HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure
- // `import.meta.hot` is properly handled by Vite
- children: await getHmrScript(),
- injectTo: 'head',
- });
- }
-
- // inject CSS
- [...getStylesForURL(filePath, viteServer)].forEach((href) => {
- if (mode === 'development' && svelteStylesRE.test(href)) {
- tags.push({
- tag: 'script',
- attrs: { type: 'module', src: href },
- injectTo: 'head',
- });
- } else {
- tags.push({
- tag: 'link',
- attrs: {
- rel: 'stylesheet',
- href,
- 'data-astro-injected': true,
- },
- injectTo: 'head',
- });
- }
- });
-
- // add injected tags
- html = injectTags(html, tags);
-
- // run transformIndexHtml() in dev to run Vite dev transformations
- if (mode === 'development' && !astroConfig.buildOptions.experimentalStaticBuild) {
- const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/');
- html = await viteServer.transformIndexHtml(relativeURL, html, pathname);
- }
-
- // inject <!doctype html> if missing (TODO: is a more robust check needed for comments, etc.?)
- if (!/<!doctype html/i.test(html)) {
- html = '<!DOCTYPE html>\n' + html;
- }
-
- return html;
-}
-
-let hmrScript: string;
-async function getHmrScript() {
- if (hmrScript) return hmrScript;
- const filePath = fileURLToPath(new URL('../../runtime/client/hmr.js', import.meta.url));
- const content = await fs.promises.readFile(filePath);
- hmrScript = content.toString();
- return hmrScript;
-}
-
-export async function ssr(ssrOpts: SSROptions): Promise<string> {
- try {
- const [renderers, mod] = await preload(ssrOpts);
- return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler()
- } catch (e: unknown) {
- await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath });
- throw e;
- }
-}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 93c908416..e987db5f6 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -157,7 +157,7 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
}
const probableRendererNames = guessRenderers(metadata.componentUrl);
- if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !HTMLElement.isPrototypeOf(Component as object)) {
+ if (Array.isArray(renderers) && renderers.length === 0 && typeof Component !== 'string' && !componentIsHTMLElement(Component)) {
const message = `Unable to render ${metadata.displayName}!
There are no \`renderers\` set in your \`astro.config.mjs\` file.
@@ -175,7 +175,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
}
}
- if (!renderer && HTMLElement.isPrototypeOf(Component as object)) {
+ if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
const output = renderHTMLElement(result, Component as typeof HTMLElement, _props, slots);
return output;
@@ -465,6 +465,10 @@ export async function renderAstroComponent(component: InstanceType<typeof AstroC
return unescapeHTML(await _render(template));
}
+function componentIsHTMLElement(Component: unknown) {
+ return typeof HTMLElement !== 'undefined' && HTMLElement.isPrototypeOf(Component as object);
+}
+
export async function renderHTMLElement(result: SSRResult, constructor: typeof HTMLElement, props: any, slots: any) {
const name = getHTMLElementName(constructor);
diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts
index de57e1593..eb08ac8a0 100644
--- a/packages/astro/src/vite-plugin-astro-server/index.ts
+++ b/packages/astro/src/vite-plugin-astro-server/index.ts
@@ -2,17 +2,16 @@ import type vite from '../core/vite';
import type http from 'http';
import type { AstroConfig, ManifestData, RouteData } from '../@types/astro';
import { info, LogOptions } from '../core/logger.js';
-import { fileURLToPath } from 'url';
-import { createRouteManifest, matchRoute } from '../core/ssr/routing.js';
+import { createRouteManifest, matchRoute } from '../core/routing/index.js';
import mime from 'mime';
import stripAnsi from 'strip-ansi';
import { createSafeError } from '../core/util.js';
-import { ssr } from '../core/ssr/index.js';
+import { ssr } from '../core/render/dev/index.js';
import * as msg from '../core/messages.js';
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
import serverErrorTemplate from '../template/5xx.js';
-import { RouteCache } from '../core/ssr/route-cache.js';
+import { RouteCache } from '../core/render/route-cache.js';
interface AstroPluginOptions {
config: AstroConfig;
@@ -126,7 +125,6 @@ export default function createPlugin({ config, logging }: AstroPluginOptions): v
return {
name: 'astro:server',
configureServer(viteServer) {
- const pagesDirectory = fileURLToPath(config.pages);
let routeCache = new RouteCache(logging);
let manifest: ManifestData = createRouteManifest({ config: config }, logging);
/** rebuild the route cache + manifest, as needed. */
diff --git a/packages/astro/src/vite-plugin-astro/styles.ts b/packages/astro/src/vite-plugin-astro/styles.ts
index 6ebcd0e0d..b49ce6e9b 100644
--- a/packages/astro/src/vite-plugin-astro/styles.ts
+++ b/packages/astro/src/vite-plugin-astro/styles.ts
@@ -1,6 +1,6 @@
import type vite from '../core/vite';
-import { STYLE_EXTENSIONS } from '../core/ssr/css.js';
+import { STYLE_EXTENSIONS } from '../core/render/dev/css.js';
export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>;
diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts
index 155fdc8ed..de7933f7d 100644
--- a/packages/astro/src/vite-plugin-build-css/index.ts
+++ b/packages/astro/src/vite-plugin-build-css/index.ts
@@ -1,10 +1,9 @@
-import type { RenderedChunk } from 'rollup';
import type { BuildInternals } from '../core/build/internal';
import * as path from 'path';
import esbuild from 'esbuild';
import { Plugin as VitePlugin } from '../core/vite';
-import { isCSSRequest } from '../core/ssr/css.js';
+import { isCSSRequest } from '../core/render/dev/css.js';
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts
index 48bb617c9..87cc46779 100644
--- a/packages/astro/src/vite-plugin-build-html/index.ts
+++ b/packages/astro/src/vite-plugin-build-html/index.ts
@@ -12,10 +12,10 @@ import { getAttribute, hasAttribute, insertBefore, remove, createScript, createE
import { addRollupInput } from './add-rollup-input.js';
import { findAssets, findExternalScripts, findInlineScripts, findInlineStyles, getTextContent, getAttributes } from './extract-assets.js';
import { isBuildableImage, isBuildableLink, isHoistedScript, isInSrcDirectory, hasSrcSet } from './util.js';
-import { render as ssrRender } from '../core/ssr/index.js';
+import { render as ssrRender } from '../core/render/dev/index.js';
import { getAstroStyleId, getAstroPageStyleId } from '../vite-plugin-build-css/index.js';
import { prependDotSlash, removeEndingForwardSlash } from '../core/path.js';
-import { RouteCache } from '../core/ssr/route-cache.js';
+import { RouteCache } from '../core/render/route-cache.js';
// This package isn't real ESM, so have to coerce it
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;