Skip to content

Commit

Permalink
Adapters v0 (#2855)
Browse files Browse the repository at this point in the history
* Adapter v0

* Finalizing adapters

* Update the lockfile

* Add the default adapter after config setup is called

* Create the default adapter in config:done

* Fix lint error

* Remove unused callConfigSetup

* remove unused export

* Use a test adapter to test SSR

* Adds a changeset

* Updated based on feedback

* Updated the lockfile

* Only throw if set to a different adapter

* Clean up outdated comments

* Move the adapter to an  config option

* Make adapter optional

* Update the docs/changeset to reflect config API change

* Clarify regular Node usage
  • Loading branch information
matthewp committed Mar 24, 2022
1 parent 5c96145 commit 5e52814
Show file tree
Hide file tree
Showing 29 changed files with 886 additions and 496 deletions.
29 changes: 29 additions & 0 deletions .changeset/hot-plants-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'astro': patch
---

Adds support for the Node adapter (SSR)

This provides the first SSR adapter available using the `integrations` API. It is a Node.js adapter that can be used with the `http` module or any framework that wraps it, like Express.

In your astro.config.mjs use:

```js
import nodejs from '@astrojs/node';

export default {
adapter: nodejs()
}
```

After performing a build there will be a `dist/server/entry.mjs` module that works like a middleware function. You can use with any framework that supports the Node `request` and `response` objects. For example, with Express you can do:

```js
import express from 'express';
import { handler as ssrHandler } from '@astrojs/node';

const app = express();
app.use(handler);

app.listen(8080);
```
2 changes: 2 additions & 0 deletions examples/ssr/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
import nodejs from '@astrojs/node';

// https://astro.build/config
export default defineConfig({
adapter: nodejs(),
integrations: [svelte()],
vite: {
server: {
Expand Down
12 changes: 0 additions & 12 deletions examples/ssr/build.mjs

This file was deleted.

4 changes: 2 additions & 2 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"dev-server": "astro dev --experimental-ssr",
"dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"",
"start": "astro dev",
"build": "echo 'Run pnpm run build-ssr instead'",
"build-ssr": "node build.mjs",
"build": "astro build --experimental-ssr",
"server": "node server/server.mjs"
},
"devDependencies": {
"@astrojs/svelte": "^0.0.2-next.0",
"@astrojs/node": "^0.0.1",
"astro": "^0.25.0-next.2",
"concurrently": "^7.0.0",
"lightcookie": "^1.0.25",
Expand Down
48 changes: 18 additions & 30 deletions examples/ssr/server/server.mjs
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
import { createServer } from 'http';
import fs from 'fs';
import mime from 'mime';
import { loadApp } from 'astro/app/node';
import { polyfill } from '@astrojs/webapi';
import { apiHandler } from './api.mjs';

polyfill(globalThis);
import { handler as ssrHandler } from '../dist/server/entry.mjs';

const clientRoot = new URL('../dist/client/', import.meta.url);
const serverRoot = new URL('../dist/server/', import.meta.url);
const app = await loadApp(serverRoot);

async function handle(req, res) {
const route = app.match(req);
ssrHandler(req, res, async () => {
// Did not match an SSR route

if (route) {
/** @type {Response} */
const response = await app.render(req, route);
const html = await response.text();
res.writeHead(response.status, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(html, 'utf-8'),
});
res.end(html);
} else if (/^\/api\//.test(req.url)) {
return apiHandler(req, res);
} else {
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
if (/^\/api\//.test(req.url)) {
return apiHandler(req, res);
} else {
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
}
}
});
}

const server = createServer((req, res) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://astro.build",
"types": "./dist/types/@types/astro.d.ts",
"typesVersions": {
"*": { "app/*": ["./dist/types/core/app/*"] }
},
"exports": {
".": "./astro.js",
"./env": "./env.d.ts",
"./config": "./config.mjs",
"./internal": "./internal.js",
"./app": "./dist/core/app/index.js",
"./app/node": "./dist/core/app/node.js",
"./client/*": "./dist/runtime/client/*",
"./components": "./components/index.js",
Expand Down
27 changes: 25 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request';
export type { SSRManifest } from '../core/app/types';

export interface AstroBuiltinProps {
'client:load'?: boolean;
Expand Down Expand Up @@ -37,6 +38,10 @@ export interface CLIFlags {
drafts?: boolean;
}

export interface BuildConfig {
staticMode: boolean | undefined;
}

/**
* Astro.* available in all components
* Docs: https://docs.astro.build/reference/api-reference/#astro-global
Expand Down Expand Up @@ -154,6 +159,16 @@ export interface AstroUserConfig {
*/
integrations?: AstroIntegration[];

/**
* @docs
* @name adapter
* @type {AstroIntegration}
* @default `undefined`
* @description
* Add an adapter to build for SSR (server-side rendering). An adapter makes it easy to connect a deployed Astro app to a hosting provider or runtime environment.
*/
adapter?: AstroIntegration;

/** @deprecated - Use "integrations" instead. Run Astro to learn more about migrating. */
renderers?: string[];

Expand Down Expand Up @@ -461,11 +476,13 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// This is a more detailed type than zod validation gives us.
// TypeScript still confirms zod validation matches this type.
integrations: AstroIntegration[];
adapter?: AstroIntegration;
// Private:
// We have a need to pass context based on configured state,
// that is different from the user-exposed configuration.
// TODO: Create an AstroConfig class to manage this, long-term.
_ctx: {
adapter: AstroAdapter | undefined;
renderers: AstroRenderer[];
scripts: { stage: InjectedScriptStage; content: string }[];
};
Expand Down Expand Up @@ -596,6 +613,12 @@ export type Props = Record<string, unknown>;

type Body = string;

export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
exports?: string[];
}

export interface EndpointOutput<Output extends Body = Body> {
body: Output;
}
Expand Down Expand Up @@ -642,11 +665,11 @@ export interface AstroIntegration {
// more generalized. Consider the SSR use-case as well.
// injectElement: (stage: vite.HtmlTagDescriptor, element: string) => void;
}) => void;
'astro:config:done'?: (options: { config: AstroConfig }) => void | Promise<void>;
'astro:config:done'?: (options: {config: AstroConfig, setAdapter: (adapter: AstroAdapter) => void; }) => void | Promise<void>;
'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise<void>;
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>;
'astro:build:start'?: () => void | Promise<void>;
'astro:build:start'?: (options: { buildConfig: BuildConfig }) => void | Promise<void>;
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL }) => void | Promise<void>;
};
}
Expand Down
23 changes: 23 additions & 0 deletions packages/astro/src/adapter-ssg/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { AstroAdapter, AstroIntegration } from '../@types/astro';

export function getAdapter(): AstroAdapter {
return {
name: '@astrojs/ssg',
// This one has no server entrypoint and is mostly just an integration
//serverEntrypoint: '@astrojs/ssg/server.js',
};
}

export default function createIntegration(): AstroIntegration {
return {
name: '@astrojs/ssg',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter(getAdapter());
},
'astro:build:start': ({ buildConfig }) => {
buildConfig.staticMode = true;
}
}
};
}
3 changes: 2 additions & 1 deletion packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentInstance, ManifestData, RouteData, SSRLoadedRenderer } fr
import type { SSRManifest as Manifest, RouteInfo } from './types';

import { defaultLogOptions } from '../logger.js';
export { deserializeManifest } from './common.js';
import { matchRoute } from '../routing/match.js';
import { render } from '../render/core.js';
import { RouteCache } from '../render/route-cache.js';
Expand Down Expand Up @@ -64,7 +65,7 @@ export class App {
throw new Error(`Unable to resolve [${specifier}]`);
}
const bundlePath = manifest.entryModules[specifier];
return prependForwardSlash(bundlePath);
return bundlePath.startsWith('data:') ? bundlePath : prependForwardSlash(bundlePath);
},
route: routeData,
routeCache: this.#routeCache,
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/core/app/node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { SSRManifest, SerializedSSRManifest } from './types';
import type { IncomingHttpHeaders } from 'http';

import * as fs from 'fs';
import { App } from './index.js';
Expand All @@ -16,7 +15,7 @@ function createRequestFromNodeRequest(req: IncomingMessage): Request {
return request;
}

class NodeApp extends App {
export class NodeApp extends App {
match(req: IncomingMessage | Request) {
return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req));
}
Expand Down
64 changes: 64 additions & 0 deletions packages/astro/src/core/build/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { AstroConfig, RouteType } from '../../@types/astro';
import npath from 'path';
import { appendForwardSlash } from '../../core/path.js';

const STATUS_CODE_PAGES = new Set(['/404', '/500']);

export function getOutRoot(astroConfig: AstroConfig): URL {
return new URL('./', astroConfig.dist);
}

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

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

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

// This is the root folder to write to.
switch (routeType) {
case 'endpoint':
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
case 'page':
switch (astroConfig.buildOptions.pageUrlFormat) {
case 'directory': {
if (STATUS_CODE_PAGES.has(pathname)) {
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
}
return new URL('.' + appendForwardSlash(pathname), outRoot);
}
case 'file': {
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
}
}
}
}

export function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL {
switch (routeType) {
case 'endpoint':
return new URL(npath.basename(pathname), outFolder);
case 'page':
switch (astroConfig.buildOptions.pageUrlFormat) {
case 'directory': {
if (STATUS_CODE_PAGES.has(pathname)) {
const baseName = npath.basename(pathname);
return new URL('./' + (baseName || 'index') + '.html', outFolder);
}
return new URL('./index.html', outFolder);
}
case 'file': {
const baseName = npath.basename(pathname);
return new URL('./' + (baseName || 'index') + '.html', outFolder);
}
}
}
}

0 comments on commit 5e52814

Please sign in to comment.