Skip to content

Commit

Permalink
Netlify adapter (#2879)
Browse files Browse the repository at this point in the history
* Netlify adapter

* Remove package.json export that doesnt exist

* Fix out path

* Make netlifyFunctions the default

* Make the dist configurable

* Add an export for the functions

* Append of the file exists
  • Loading branch information
matthewp committed Mar 25, 2022
1 parent df8fac5 commit 80034c6
Show file tree
Hide file tree
Showing 19 changed files with 290 additions and 36 deletions.
18 changes: 18 additions & 0 deletions .changeset/blue-rocks-smoke.md
@@ -0,0 +1,18 @@
---
'astro': patch
'@astrojs/netlify': patch
'@astrojs/node': patch
---

Netlify Adapter

This change adds a Netlify adapter that uses Netlify Functions. You can use it like so:

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

export default defineConfig({
adapter: netlify()
});
```
3 changes: 3 additions & 0 deletions packages/astro/package.json
Expand Up @@ -15,6 +15,9 @@
"types": "./dist/types/@types/astro.d.ts",
"typesVersions": {
"*": {
"app": [
"./dist/types/core/app/index"
],
"app/*": [
"./dist/types/core/app/*"
]
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/@types/astro.ts
Expand Up @@ -39,6 +39,9 @@ export interface CLIFlags {
}

export interface BuildConfig {
client: URL;
server: URL;
serverEntry: string;
staticMode: boolean | undefined;
}

Expand Down Expand Up @@ -617,6 +620,7 @@ export interface AstroAdapter {
name: string;
serverEntrypoint?: string;
exports?: string[];
args?: any;
}

export interface EndpointOutput<Output extends Body = Body> {
Expand Down Expand Up @@ -670,7 +674,7 @@ export interface AstroIntegration {
'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise<void>;
'astro:server:done'?: () => void | Promise<void>;
'astro:build:start'?: (options: { buildConfig: BuildConfig }) => void | Promise<void>;
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL }) => void | Promise<void>;
'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL; routes: RouteData[] }) => void | Promise<void>;
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/app/types.ts
Expand Up @@ -27,3 +27,5 @@ export interface SSRManifest {
export type SerializedSSRManifest = Omit<SSRManifest, 'routes'> & {
routes: SerializedRouteInfo[];
};

export type AdapterCreateExports<T = any> = (manifest: SSRManifest, args?: T) => Record<string, any>;
13 changes: 0 additions & 13 deletions packages/astro/src/core/build/common.ts
@@ -1,5 +1,4 @@
import type { AstroConfig, RouteType } from '../../@types/astro';
import type { StaticBuildOptions } from './types';
import npath from 'path';
import { appendForwardSlash } from '../../core/path.js';

Expand All @@ -9,18 +8,6 @@ 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);

Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/core/build/generate.ts
Expand Up @@ -13,7 +13,7 @@ import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { call as callEndpoint } from '../endpoint/index.js';
import { render } from '../render/core.js';
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
import { getOutFile, getOutRoot, getOutFolder, getServerRoot } from './common.js';
import { getOutFile, getOutRoot, getOutFolder } from './common.js';
import { getPageDataByComponent, eachPageData } from './internal.js';
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
import { getTimeStat } from './util.js';
Expand Down Expand Up @@ -70,8 +70,9 @@ export async function generatePages(result: RollupOutput, opts: StaticBuildOptio
info(opts.logging, null, `\n${bgMagenta(black(' generating static routes '))}\n`);

const ssr = !!opts.astroConfig._ctx.adapter?.serverEntrypoint;
const outFolder = ssr ? getServerRoot(opts.astroConfig) : getOutRoot(opts.astroConfig);
const ssrEntryURL = new URL(`./entry.mjs?time=${Date.now()}`, outFolder);
const serverEntry = opts.buildConfig.serverEntry;
const outFolder = ssr ? opts.buildConfig.server : opts.astroConfig.dist;
const ssrEntryURL = new URL('./' + serverEntry + `?time=${Date.now()}`, outFolder);
const ssrEntry = await import(ssrEntryURL.toString());

for (const pageData of eachPageData(internals)) {
Expand Down
12 changes: 9 additions & 3 deletions packages/astro/src/core/build/index.ts
Expand Up @@ -7,7 +7,7 @@ import { apply as applyPolyfill } from '../polyfill.js';
import { performance } from 'perf_hooks';
import * as vite from 'vite';
import { createVite, ViteConfigWithSSR } from '../create-vite.js';
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
import { debug, defaultLogOptions, info, levels, timerMessage, warn, warnIfUsingExperimentalSSR } from '../logger.js';
import { createRouteManifest } from '../routing/index.js';
import { generateSitemap } from '../render/sitemap.js';
import { collectPagesData } from './page-data.js';
Expand Down Expand Up @@ -73,11 +73,17 @@ class AstroBuilder {
{ astroConfig: this.config, logging, mode: 'build' }
);
await runHookConfigDone({ config: this.config });
warnIfUsingExperimentalSSR(logging, this.config);
this.viteConfig = viteConfig;
const viteServer = await vite.createServer(viteConfig);
this.viteServer = viteServer;
debug('build', timerMessage('Vite started', timer.viteStart));
const buildConfig: BuildConfig = { staticMode: undefined };
const buildConfig: BuildConfig = {
client: new URL('./client/', this.config.dist),
server: new URL('./server/', this.config.dist),
serverEntry: 'entry.mjs',
staticMode: undefined
};
await runHookBuildStart({ config: this.config, buildConfig });

info(this.logging, 'build', 'Collecting page data...');
Expand Down Expand Up @@ -167,7 +173,7 @@ class AstroBuilder {

// You're done! Time to clean up.
await viteServer.close();
await runHookBuildDone({ config: this.config, pages: pageNames });
await runHookBuildDone({ config: this.config, pages: pageNames, routes: Object.values(allPages).map(pd => pd.route) });

if (logging.level && levels[logging.level] <= levels['info']) {
const buildMode = this.config.buildOptions.experimentalSsr ? 'ssr' : 'static';
Expand Down
16 changes: 7 additions & 9 deletions packages/astro/src/core/build/static-build.ts
Expand Up @@ -19,7 +19,6 @@ import { vitePluginSSR } from './vite-plugin-ssr.js';
import { vitePluginPages } from './vite-plugin-pages.js';
import { generatePages } from './generate.js';
import { trackPageData } from './internal.js';
import { getClientRoot, getServerRoot, getOutRoot } from './common.js';
import { isBuildingToSSR } from '../util.js';
import { getTimeStat } from './util.js';

Expand Down Expand Up @@ -114,7 +113,7 @@ export async function staticBuild(opts: StaticBuildOptions) {
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);
const out = ssr ? opts.buildConfig.server : astroConfig.dist;
// TODO: use vite.mergeConfig() here?
return await vite.build({
logLevel: 'error',
Expand All @@ -130,9 +129,10 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
input: [],
output: {
format: 'esm',
entryFileNames: 'entry.mjs',
entryFileNames: opts.buildConfig.serverEntry,
chunkFileNames: 'chunks/chunk.[hash].mjs',
assetFileNames: 'assets/asset.[hash][extname]',
inlineDynamicImports: true,
},
},
// must match an esbuild target
Expand Down Expand Up @@ -170,12 +170,11 @@ async function clientBuild(opts: StaticBuildOptions, internals: BuildInternals,
return null;
}

const out = astroConfig.buildOptions.experimentalSsr ? getClientRoot(astroConfig) : getOutRoot(astroConfig);

// TODO: use vite.mergeConfig() here?

info(opts.logging, null, `\n${bgGreen(black(' building resources '))}\n`);

const out = isBuildingToSSR(astroConfig) ? opts.buildConfig.client : astroConfig.dist;

const buildResult = await vite.build({
logLevel: 'info',
mode: 'production',
Expand Down Expand Up @@ -229,9 +228,8 @@ async function cleanSsrOutput(opts: StaticBuildOptions) {

async function ssrMoveAssets(opts: StaticBuildOptions) {
info(opts.logging, 'build', 'Rearranging server assets...');
const { astroConfig } = opts;
const serverRoot = getServerRoot(astroConfig);
const clientRoot = getClientRoot(astroConfig);
const serverRoot = opts.buildConfig.staticMode ? opts.buildConfig.client : opts.buildConfig.server;
const clientRoot = opts.buildConfig.client;
const serverAssets = new URL('./assets/', serverRoot);
const clientAssets = new URL('./assets/', clientRoot);
const files = await glob('assets/**/*', {
Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/core/build/vite-plugin-ssr.ts
Expand Up @@ -35,23 +35,24 @@ const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), {
pageMap: _main.pageMap,
renderers: _main.renderers
});
const _args = ${adapter.args ? JSON.stringify(adapter.args) : 'undefined'};
${
adapter.exports
? `const _exports = adapter.createExports(_manifest);
? `const _exports = adapter.createExports(_manifest, _args);
${adapter.exports.map((name) => `export const ${name} = _exports['${name}'];`).join('\n')}
`
: ''
}
const _start = 'start';
if(_start in adapter) {
adapter[_start](_manifest);
adapter[_start](_manifest, _args);
}`;
}
return void 0;
},

generateBundle(opts, bundle) {
generateBundle(_opts, bundle) {
const manifest = buildManifest(buildOpts, internals);

for (const [_chunkName, chunk] of Object.entries(bundle)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/dev/index.ts
Expand Up @@ -4,7 +4,7 @@ import * as vite from 'vite';
import type { AstroConfig } from '../../@types/astro';
import { runHookConfigDone, runHookConfigSetup, runHookServerDone, runHookServerSetup, runHookServerStart } from '../../integrations/index.js';
import { createVite } from '../create-vite.js';
import { defaultLogOptions, info, LogOptions, warn } from '../logger.js';
import { defaultLogOptions, info, LogOptions, warn, warnIfUsingExperimentalSSR } from '../logger.js';
import * as msg from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { getResolvedHostForVite } from './util.js';
Expand Down Expand Up @@ -32,6 +32,7 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
{ astroConfig: config, logging: options.logging, mode: 'dev' }
);
await runHookConfigDone({ config });
warnIfUsingExperimentalSSR(options.logging, config);
const viteServer = await vite.createServer(viteConfig);
runHookServerSetup({ config, server: viteServer });
await viteServer.listen(config.devOptions.port);
Expand Down
11 changes: 11 additions & 0 deletions packages/astro/src/core/logger.ts
@@ -1,10 +1,12 @@
import type { AstroConfig } from '../@types/astro';
import { bold, cyan, dim, red, yellow, reset } from 'kleur/colors';
import { performance } from 'perf_hooks';
import { Writable } from 'stream';
import stringWidth from 'string-width';
import * as readline from 'readline';
import debugPackage from 'debug';
import { format as utilFormat } from 'util';
import { isBuildingToSSR } from './util.js';

type ConsoleStream = Writable & {
fd: 1 | 2;
Expand Down Expand Up @@ -211,3 +213,12 @@ export function timerMessage(message: string, startTime: number = performance.no
let timeDisplay = timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`;
return `${message} ${dim(timeDisplay)}`;
}

/**
* A warning that SSR is experimental. Remove when we can.
*/
export function warnIfUsingExperimentalSSR(opts: LogOptions, config: AstroConfig) {
if(isBuildingToSSR(config)) {
warn(opts, 'warning', bold(`Warning:`), ` SSR support is still experimental and subject to API changes. If using in production pin your dependencies to prevent accidental breakage.`);
}
}
6 changes: 3 additions & 3 deletions packages/astro/src/integrations/index.ts
@@ -1,6 +1,6 @@
import type { AddressInfo } from 'net';
import type { ViteDevServer } from 'vite';
import { AstroConfig, AstroRenderer, BuildConfig } from '../@types/astro.js';
import { AstroConfig, AstroRenderer, BuildConfig, RouteData } from '../@types/astro.js';
import { mergeConfig } from '../core/config.js';
import ssgAdapter from '../adapter-ssg/index.js';

Expand Down Expand Up @@ -91,10 +91,10 @@ export async function runHookBuildStart({ config, buildConfig }: { config: Astro
}
}

export async function runHookBuildDone({ config, pages }: { config: AstroConfig; pages: string[] }) {
export async function runHookBuildDone({ config, pages, routes }: { config: AstroConfig; pages: string[], routes: RouteData[] }) {
for (const integration of config.integrations) {
if (integration.hooks['astro:build:done']) {
await integration.hooks['astro:build:done']({ pages: pages.map((p) => ({ pathname: p })), dir: config.dist });
await integration.hooks['astro:build:done']({ pages: pages.map((p) => ({ pathname: p })), dir: config.dist, routes });
}
}
}
34 changes: 34 additions & 0 deletions packages/integrations/netlify/package.json
@@ -0,0 +1,34 @@
{
"name": "@astrojs/netlify",
"description": "Deploy your site to Netlify",
"version": "0.0.1",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/netlify"
},
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://astro.build",
"exports": {
".": "./dist/index.js",
"./functions": "./dist/integration-functions.js",
"./netlify-functions.js": "./dist/netlify-functions.js",
"./package.json": "./package.json"
},
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"dev": "astro-scripts dev \"src/**/*.ts\""
},
"dependencies": {
"@astrojs/webapi": "^0.11.0"
},
"devDependencies": {
"@netlify/functions": "^1.0.0",
"astro": "workspace:*",
"astro-scripts": "workspace:*"
}
}
44 changes: 44 additions & 0 deletions packages/integrations/netlify/readme.md
@@ -0,0 +1,44 @@
# @astrojs/netlify

Deploy your server-side rendered (SSR) Astro app to [Netlify](https://www.netlify.com/).

Use this adapter in your Astro configuration file:

```js
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';

export default defineConfig({
adapter: netlify()
});
```

After you build your site the `netlify/` folder will contain [Netlify Functions](https://docs.netlify.com/functions/overview/) in the `netlify/functions/` folder.

Now you can deploy!

```shell
netlify deploy
```

## Configuration

The output folder is configuration with the `dist` property when creating the adapter.

```js
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';

export default defineConfig({
adapter: netlify({
dist: new URL('./dist/', import.meta.url)
})
});
```
And then point to the dist in your `netlify.toml`:
```toml
[functions]
directory = "dist/functions"
```

0 comments on commit 80034c6

Please sign in to comment.