Skip to content

Commit e55af8a

Browse files
matthewpFredKSchottsarah11918
authored
Node.js standalone mode + support for astro preview (#5056)
* 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>
1 parent 2b7fb84 commit e55af8a

File tree

34 files changed

+1094
-361
lines changed

34 files changed

+1094
-361
lines changed

.changeset/cyan-paws-fry.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'astro': minor
3+
'@astrojs/node': minor
4+
---
5+
6+
# Adapter support for `astro preview`
7+
8+
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:
9+
10+
```shell
11+
npm run preview
12+
```
13+
14+
## Adapter API
15+
16+
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:
17+
18+
```diff
19+
export default function() {
20+
return {
21+
name: '@astrojs/node',
22+
hooks: {
23+
'astro:config:done': ({ setAdapter, config }) => {
24+
setAdapter({
25+
name: '@astrojs/node',
26+
serverEntrypoint: '@astrojs/node/server.js',
27+
+ previewEntrypoint: '@astrojs/node/preview.js',
28+
exports: ['handler'],
29+
});
30+
31+
// more here
32+
}
33+
}
34+
};
35+
}
36+
```
37+
38+
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.

.changeset/metal-pumas-walk.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@astrojs/node': major
3+
---
4+
5+
# Standalone mode for the Node.js adapter
6+
7+
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:
8+
9+
```shell
10+
node ./dist/server/entry.mjs
11+
```
12+
13+
To enable standalone mode, set the new `mode` to `'standalone'` option in your Astro config:
14+
15+
```js
16+
import { defineConfig } from 'astro/config';
17+
import nodejs from '@astrojs/node';
18+
19+
export default defineConfig({
20+
output: 'server',
21+
adapter: nodejs({
22+
mode: 'standalone'
23+
})
24+
});
25+
```
26+
27+
See the @astrojs/node documentation to learn all of the options available in standalone mode.
28+
29+
## Breaking change
30+
31+
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.
32+
33+
```js
34+
import { defineConfig } from 'astro/config';
35+
import nodejs from '@astrojs/node';
36+
37+
export default defineConfig({
38+
output: 'server',
39+
adapter: nodejs({
40+
mode: 'middleware'
41+
})
42+
});
43+
```

.changeset/stupid-points-refuse.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'astro': minor
3+
'@astrojs/cloudflare': minor
4+
'@astrojs/deno': minor
5+
'@astrojs/image': minor
6+
'@astrojs/netlify': minor
7+
'@astrojs/node': minor
8+
'@astrojs/vercel': minor
9+
---
10+
11+
# New build configuration
12+
13+
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:
14+
15+
```js
16+
import { defineConfig } from 'astro/config';
17+
18+
export default defineConfig({
19+
output: 'server',
20+
build: {
21+
server: './dist/server/',
22+
client: './dist/client/',
23+
serverEntry: 'entry.mjs',
24+
}
25+
});
26+
```
27+
28+
These new configuration options are only supported in SSR mode and are ignored when building to SSG (a static site).
29+
30+
## Integration hook change
31+
32+
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:
33+
34+
```js
35+
export default function myIntegration() {
36+
return {
37+
name: 'my-integration',
38+
hooks: {
39+
'astro:config:setup': ({ updateConfig }) => {
40+
updateConfig({
41+
build: {
42+
server: '...'
43+
}
44+
});
45+
}
46+
}
47+
}
48+
}
49+
```

examples/ssr/astro.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import node from '@astrojs/node';
55
// https://astro.build/config
66
export default defineConfig({
77
output: 'server',
8-
adapter: node(),
8+
adapter: node({
9+
mode: 'standalone'
10+
}),
911
integrations: [svelte()],
1012
});

examples/ssr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "astro build",
1010
"preview": "astro preview",
1111
"astro": "astro",
12-
"server": "node server/server.mjs"
12+
"server": "node dist/server/entry.mjs"
1313
},
1414
"devDependencies": {},
1515
"dependencies": {

examples/ssr/server/server.mjs

Lines changed: 0 additions & 44 deletions
This file was deleted.

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

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,17 @@ export interface CLIFlags {
8383
}
8484

8585
export interface BuildConfig {
86+
/**
87+
* @deprecated Use config.build.client instead.
88+
*/
8689
client: URL;
90+
/**
91+
* @deprecated Use config.build.server instead.
92+
*/
8793
server: URL;
94+
/**
95+
* @deprecated Use config.build.serverEntry instead.
96+
*/
8897
serverEntry: string;
8998
}
9099

@@ -381,6 +390,7 @@ export interface AstroUserConfig {
381390
* @name outDir
382391
* @type {string}
383392
* @default `"./dist"`
393+
* @see build.server
384394
* @description Set the directory that `astro build` writes your final build to.
385395
*
386396
* The value can be either an absolute file system path or a path relative to the project root.
@@ -526,6 +536,68 @@ export interface AstroUserConfig {
526536
* This means that when you create relative URLs using `new URL('./relative', Astro.url)`, you will get consistent behavior between dev and build.
527537
*/
528538
format?: 'file' | 'directory';
539+
/**
540+
* @docs
541+
* @name build.client
542+
* @type {string}
543+
* @default `'./dist/client'`
544+
* @description
545+
* Controls the output directory of your client-side CSS and JavaScript when `output: 'server'` only.
546+
* `outDir` controls where the code is built to.
547+
*
548+
* This value is relative to the `outDir`.
549+
*
550+
* ```js
551+
* {
552+
* output: 'server',
553+
* build: {
554+
* client: './client'
555+
* }
556+
* }
557+
* ```
558+
*/
559+
client?: string;
560+
/**
561+
* @docs
562+
* @name build.server
563+
* @type {string}
564+
* @default `'./dist/server'`
565+
* @description
566+
* Controls the output directory of server JavaScript when building to SSR.
567+
*
568+
* This value is relative to the `outDir`.
569+
*
570+
* ```js
571+
* {
572+
* build: {
573+
* server: './server'
574+
* }
575+
* }
576+
* ```
577+
*/
578+
server?: string;
579+
/**
580+
* @docs
581+
* @name build.serverEntry
582+
* @type {string}
583+
* @default `'entry.mjs'`
584+
* @description
585+
* Specifies the file name of the server entrypoint when building to SSR.
586+
* This entrypoint is usually dependent on which host you are deploying to and
587+
* will be set by your adapter for you.
588+
*
589+
* Note that it is recommended that this file ends with `.mjs` so that the runtime
590+
* detects that the file is a JavaScript module.
591+
*
592+
* ```js
593+
* {
594+
* build: {
595+
* serverEntry: 'main.mjs'
596+
* }
597+
* }
598+
* ```
599+
*/
600+
serverEntry?: string;
529601
};
530602

531603
/**
@@ -1073,6 +1145,7 @@ export type Params = Record<string, string | number | undefined>;
10731145
export interface AstroAdapter {
10741146
name: string;
10751147
serverEntrypoint?: string;
1148+
previewEntrypoint?: string;
10761149
exports?: string[];
10771150
args?: any;
10781151
}
@@ -1234,7 +1307,7 @@ export interface AstroIntegration {
12341307
hooks: {
12351308
'astro:config:setup'?: (options: {
12361309
config: AstroConfig;
1237-
command: 'dev' | 'build';
1310+
command: 'dev' | 'build' | 'preview';
12381311
isRestart: boolean;
12391312
updateConfig: (newConfig: Record<string, any>) => void;
12401313
addRenderer: (renderer: AstroRenderer) => void;
@@ -1332,3 +1405,25 @@ export interface SSRResult {
13321405
}
13331406

13341407
export type MarkdownAstroData = { frontmatter: object };
1408+
1409+
/* Preview server stuff */
1410+
export interface PreviewServer {
1411+
host?: string;
1412+
port: number;
1413+
closed(): Promise<void>;
1414+
stop(): Promise<void>;
1415+
}
1416+
1417+
export interface PreviewServerParams {
1418+
outDir: URL;
1419+
client: URL;
1420+
serverEntrypoint: URL;
1421+
host: string | undefined;
1422+
port: number;
1423+
}
1424+
1425+
export type CreatePreviewServer = (params: PreviewServerParams) => PreviewServer | Promise<PreviewServer>;
1426+
1427+
export interface PreviewModule {
1428+
default: CreatePreviewServer;
1429+
}

packages/astro/src/core/build/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ class AstroBuilder {
8787
/** Run the build logic. build() is marked private because usage should go through ".run()" */
8888
private async build({ viteConfig }: { viteConfig: vite.InlineConfig }) {
8989
const buildConfig: BuildConfig = {
90-
client: new URL('./client/', this.settings.config.outDir),
91-
server: new URL('./server/', this.settings.config.outDir),
92-
serverEntry: 'entry.mjs',
90+
client: this.settings.config.build.client,
91+
server: this.settings.config.build.server,
92+
serverEntry: this.settings.config.build.serverEntry,
9393
};
9494
await runHookBuildStart({ config: this.settings.config, buildConfig, logging: this.logging });
9595
this.validateConfig();

packages/astro/src/core/config/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
1010
import * as vite from 'vite';
1111
import { mergeConfig as mergeViteConfig } from 'vite';
1212
import { LogOptions } from '../logger/core.js';
13-
import { arraify, isObject } from '../util.js';
13+
import { arraify, isObject, isURL } from '../util.js';
1414
import { createRelativeSchema } from './schema.js';
1515

1616
load.use([loadTypeScript]);
@@ -346,6 +346,10 @@ function mergeConfigRecursively(
346346
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
347347
continue;
348348
}
349+
if(isURL(existing) && isURL(value)) {
350+
merged[key] = value;
351+
continue;
352+
}
349353
if (isObject(existing) && isObject(value)) {
350354
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
351355
continue;

0 commit comments

Comments
 (0)