Skip to content

Commit

Permalink
Node.js standalone mode + support for astro preview (#5056)
Browse files Browse the repository at this point in the history
* wip

* Deprecate buildConfig and move to config.build

* Implement the standalone server

* Stay backwards compat

* Add changesets

* correctly merge URLs

* Get config earlier

* update node tests

* Return the preview server

* update remaining tests

* swap usage and config ordering

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/metal-pumas-walk.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/metal-pumas-walk.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/stupid-points-refuse.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/stupid-points-refuse.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Link to build.server config

Co-authored-by: Fred K. Schott <fkschott@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
3 people authored Oct 12, 2022
1 parent 2b7fb84 commit e55af8a
Show file tree
Hide file tree
Showing 34 changed files with 1,094 additions and 361 deletions.
38 changes: 38 additions & 0 deletions .changeset/cyan-paws-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'astro': minor
'@astrojs/node': minor
---

# Adapter support for `astro preview`

Adapters are now about to support the `astro preview` command via a new integration option. The Node.js adapter `@astrojs/node` is the first of the built-in adapters to gain support for this. What this means is that if you are using `@astrojs/node` you can new preview your SSR app by running:

```shell
npm run preview
```

## Adapter API

We will be updating the other first party Astro adapters to support preview over time. Adapters can opt-in to this feature by providing the `previewEntrypoint` via the `setAdapter` function in `astro:config:done` hook. The Node.js adapter's code looks like this:

```diff
export default function() {
return {
name: '@astrojs/node',
hooks: {
'astro:config:done': ({ setAdapter, config }) => {
setAdapter({
name: '@astrojs/node',
serverEntrypoint: '@astrojs/node/server.js',
+ previewEntrypoint: '@astrojs/node/preview.js',
exports: ['handler'],
});

// more here
}
}
};
}
```

The `previewEntrypoint` is a module in the adapter's package that is a Node.js script. This script is run when `astro preview` is run and is charged with starting up the built server. See the Node.js implementation in `@astrojs/node` to see how that is implemented.
43 changes: 43 additions & 0 deletions .changeset/metal-pumas-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@astrojs/node': major
---

# Standalone mode for the Node.js adapter

New in `@astrojs/node` is support for __standalone mode__. With standalone mode you can start your production server without needing to write any server JavaScript yourself. The server starts simply by running the script like so:

```shell
node ./dist/server/entry.mjs
```

To enable standalone mode, set the new `mode` to `'standalone'` option in your Astro config:

```js
import { defineConfig } from 'astro/config';
import nodejs from '@astrojs/node';

export default defineConfig({
output: 'server',
adapter: nodejs({
mode: 'standalone'
})
});
```

See the @astrojs/node documentation to learn all of the options available in standalone mode.

## Breaking change

This is a semver major change because the new `mode` option is required. Existing @astrojs/node users who are using their own HTTP server framework such as Express can upgrade by setting the `mode` option to `'middleware'` in order to build to a middleware mode, which is the same behavior and API as before.

```js
import { defineConfig } from 'astro/config';
import nodejs from '@astrojs/node';

export default defineConfig({
output: 'server',
adapter: nodejs({
mode: 'middleware'
})
});
```
49 changes: 49 additions & 0 deletions .changeset/stupid-points-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
'astro': minor
'@astrojs/cloudflare': minor
'@astrojs/deno': minor
'@astrojs/image': minor
'@astrojs/netlify': minor
'@astrojs/node': minor
'@astrojs/vercel': minor
---

# New build configuration

The ability to customize SSR build configuration more granularly is now available in Astro. You can now customize the output folder for `server` (the server code for SSR), `client` (your client-side JavaScript and assets), and `serverEntry` (the name of the entrypoint server module). Here are the defaults:

```js
import { defineConfig } from 'astro/config';

export default defineConfig({
output: 'server',
build: {
server: './dist/server/',
client: './dist/client/',
serverEntry: 'entry.mjs',
}
});
```

These new configuration options are only supported in SSR mode and are ignored when building to SSG (a static site).

## Integration hook change

The integration hook `astro:build:start` includes a param `buildConfig` which includes all of these same options. You can continue to use this param in Astro 1.x, but it is deprecated in favor of the new `build.config` options. All of the built-in adapters have been updated to the new format. If you have an integration that depends on this param we suggest upgrading to do this instead:

```js
export default function myIntegration() {
return {
name: 'my-integration',
hooks: {
'astro:config:setup': ({ updateConfig }) => {
updateConfig({
build: {
server: '...'
}
});
}
}
}
}
```
4 changes: 3 additions & 1 deletion examples/ssr/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: node(),
adapter: node({
mode: 'standalone'
}),
integrations: [svelte()],
});
2 changes: 1 addition & 1 deletion examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"server": "node server/server.mjs"
"server": "node dist/server/entry.mjs"
},
"devDependencies": {},
"dependencies": {
Expand Down
44 changes: 0 additions & 44 deletions examples/ssr/server/server.mjs

This file was deleted.

97 changes: 96 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,17 @@ export interface CLIFlags {
}

export interface BuildConfig {
/**
* @deprecated Use config.build.client instead.
*/
client: URL;
/**
* @deprecated Use config.build.server instead.
*/
server: URL;
/**
* @deprecated Use config.build.serverEntry instead.
*/
serverEntry: string;
}

Expand Down Expand Up @@ -381,6 +390,7 @@ export interface AstroUserConfig {
* @name outDir
* @type {string}
* @default `"./dist"`
* @see build.server
* @description Set the directory that `astro build` writes your final build to.
*
* The value can be either an absolute file system path or a path relative to the project root.
Expand Down Expand Up @@ -526,6 +536,68 @@ export interface AstroUserConfig {
* This means that when you create relative URLs using `new URL('./relative', Astro.url)`, you will get consistent behavior between dev and build.
*/
format?: 'file' | 'directory';
/**
* @docs
* @name build.client
* @type {string}
* @default `'./dist/client'`
* @description
* Controls the output directory of your client-side CSS and JavaScript when `output: 'server'` only.
* `outDir` controls where the code is built to.
*
* This value is relative to the `outDir`.
*
* ```js
* {
* output: 'server',
* build: {
* client: './client'
* }
* }
* ```
*/
client?: string;
/**
* @docs
* @name build.server
* @type {string}
* @default `'./dist/server'`
* @description
* Controls the output directory of server JavaScript when building to SSR.
*
* This value is relative to the `outDir`.
*
* ```js
* {
* build: {
* server: './server'
* }
* }
* ```
*/
server?: string;
/**
* @docs
* @name build.serverEntry
* @type {string}
* @default `'entry.mjs'`
* @description
* Specifies the file name of the server entrypoint when building to SSR.
* This entrypoint is usually dependent on which host you are deploying to and
* will be set by your adapter for you.
*
* Note that it is recommended that this file ends with `.mjs` so that the runtime
* detects that the file is a JavaScript module.
*
* ```js
* {
* build: {
* serverEntry: 'main.mjs'
* }
* }
* ```
*/
serverEntry?: string;
};

/**
Expand Down Expand Up @@ -1073,6 +1145,7 @@ export type Params = Record<string, string | number | undefined>;
export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
previewEntrypoint?: string;
exports?: string[];
args?: any;
}
Expand Down Expand Up @@ -1234,7 +1307,7 @@ export interface AstroIntegration {
hooks: {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
Expand Down Expand Up @@ -1332,3 +1405,25 @@ export interface SSRResult {
}

export type MarkdownAstroData = { frontmatter: object };

/* Preview server stuff */
export interface PreviewServer {
host?: string;
port: number;
closed(): Promise<void>;
stop(): Promise<void>;
}

export interface PreviewServerParams {
outDir: URL;
client: URL;
serverEntrypoint: URL;
host: string | undefined;
port: number;
}

export type CreatePreviewServer = (params: PreviewServerParams) => PreviewServer | Promise<PreviewServer>;

export interface PreviewModule {
default: CreatePreviewServer;
}
6 changes: 3 additions & 3 deletions packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ class AstroBuilder {
/** Run the build logic. build() is marked private because usage should go through ".run()" */
private async build({ viteConfig }: { viteConfig: vite.InlineConfig }) {
const buildConfig: BuildConfig = {
client: new URL('./client/', this.settings.config.outDir),
server: new URL('./server/', this.settings.config.outDir),
serverEntry: 'entry.mjs',
client: this.settings.config.build.client,
server: this.settings.config.build.server,
serverEntry: this.settings.config.build.serverEntry,
};
await runHookBuildStart({ config: this.settings.config, buildConfig, logging: this.logging });
this.validateConfig();
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
import * as vite from 'vite';
import { mergeConfig as mergeViteConfig } from 'vite';
import { LogOptions } from '../logger/core.js';
import { arraify, isObject } from '../util.js';
import { arraify, isObject, isURL } from '../util.js';
import { createRelativeSchema } from './schema.js';

load.use([loadTypeScript]);
Expand Down Expand Up @@ -346,6 +346,10 @@ function mergeConfigRecursively(
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
continue;
}
if(isURL(existing) && isURL(value)) {
merged[key] = value;
continue;
}
if (isObject(existing) && isObject(value)) {
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
continue;
Expand Down
Loading

0 comments on commit e55af8a

Please sign in to comment.