Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js standalone mode + support for astro preview #5056

Merged
merged 18 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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:
matthewp marked this conversation as resolved.
Show resolved Hide resolved

```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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking more about this, if you default to middleware and make this option required, do you still need the major version?

Copy link
Contributor Author

@matthewp matthewp Oct 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't be required if it had a default, no?

We could default to middleware. My thought was that these two modes are pretty significantly different and it didn't feel right to "prefer" one over the other. Kind of like Vercel serverless vs edge, just completely different things.

As far as semver, given that this is an integration I didn't think it mattered as much to do a semver major change.

But I don't feel too strongly on either of these things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, agreed that because its an integration this doesn't matter as much / I'm okay with the plan for a major. And I agree it makes more sense as required, or as defaulting to standalone. Defaulting to middleware is probably the least natural of the 3...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think we should do a major in that case and require the option. We can track usage in the future and if standalone is overwhelming (and once it becomes more stable) we can make it the default if we want to.


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'` which builds to a middleware mode, which is the same behavior and API as before.
matthewp marked this conversation as resolved.
Show resolved Hide resolved

```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 granular 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:
matthewp marked this conversation as resolved.
Show resolved Hide resolved

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

export default defineConfig({
output: 'server',
build: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! This makes a lot of sense!

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 if 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:
matthewp marked this conversation as resolved.
Show resolved Hide resolved

```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 @@ -533,6 +542,69 @@ 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 code, both CSS and JavaScript.
* Note that this config option is only used when `output: 'server'`. In SSG mode
* `outDir` controls where the code is built to.
matthewp marked this conversation as resolved.
Show resolved Hide resolved
*
* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Note that it is recommended that this file ends with `.mjs` so that the runtime
* Note that it is recommended that this file ends with `.mjs` so that the runtime

Just checking this is intentional, because I think I've only ever heard "runtime" as an adjective "runtime environment, runtime system, runtime library". If this is a noun, then it's fine!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've definitely heard it used as a noun. In fact, nodejs.org's <meta description> tag uses it as one! view-source:https://nodejs.org/en/

* detects that the file is a JavaScript module.
*
* ```js
* {
* build: {
* serverEntry: 'main.mjs'
* }
* }
* ```
*/
serverEntry?: string;
};

/**
Expand Down Expand Up @@ -1081,6 +1153,7 @@ export type Props = Record<string, unknown>;
export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
previewEntrypoint?: string;
exports?: string[];
args?: any;
}
Expand Down Expand Up @@ -1141,7 +1214,7 @@ export interface AstroIntegration {
hooks: {
'astro:config:setup'?: (options: {
config: AstroConfig;
command: 'dev' | 'build';
command: 'dev' | 'build' | 'preview';
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
Expand Down Expand Up @@ -1237,3 +1310,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