Skip to content

Commit

Permalink
Merge vercel and node into main #366
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderniebuhr authored Aug 29, 2024
2 parents 18bfd51 + 8906c64 commit 89927b1
Show file tree
Hide file tree
Showing 240 changed files with 10,553 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/cloudflare/test/fixtures/astro-env/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference path="../.astro/env.d.ts" />
/// <reference types="astro/client" />

Expand Down
899 changes: 899 additions & 0 deletions packages/node/CHANGELOG.md

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions packages/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# @astrojs/node

This adapter allows Astro to deploy your SSR site to Node targets.

## Documentation

Read the [`@astrojs/node` docs][docs]

## Support

- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more!

- Check our [Astro Integration Documentation][astro-integration] for more on integrations.

- Submit bug reports and feature requests as [GitHub issues][issues].

## Contributing

This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started:

- [Contributor Manual][contributing]
- [Code of Conduct][coc]
- [Community Guide][community]

## License

MIT

Copyright (c) 2023–present [Astro][astro]

[astro]: https://astro.build/
[docs]: https://docs.astro.build/en/guides/integrations-guide/node/
[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md
[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md
[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md
[discord]: https://astro.build/chat/
[issues]: https://github.com/withastro/astro/issues
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
49 changes: 49 additions & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@astrojs/node",
"description": "Deploy your site to a Node.js server",
"version": "8.3.3",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/adapters.git",
"directory": "packages/node"
},
"keywords": ["withastro", "astro-adapter"],
"bugs": "https://github.com/withastro/adapters/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/node/",
"exports": {
".": "./dist/index.js",
"./server.js": "./dist/server.js",
"./preview.js": "./dist/preview.js",
"./package.json": "./package.json"
},
"files": ["dist"],
"scripts": {
"build": "tsc",
"test": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
"send": "^0.18.0",
"server-destroy": "^1.0.1"
},
"peerDependencies": {
"astro": "^4.2.0"
},
"devDependencies": {
"@astrojs/test-utils": "workspace:*",
"@types/node": "^18.17.8",
"@types/send": "^0.17.4",
"@types/server-destroy": "^1.0.4",
"astro": "^4.14.6",
"astro-scripts": "workspace:*",
"cheerio": "1.0.0",
"express": "^4.19.2",
"node-mocks-http": "^1.15.1"
},
"publishConfig": {
"provenance": true
}
}
82 changes: 82 additions & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { AstroAdapter, AstroIntegration } from 'astro';
import { AstroError } from 'astro/errors';
import type { Options, UserOptions } from './types.js';

export function getAdapter(options: Options): AstroAdapter {
return {
name: '@astrojs/node',
serverEntrypoint: '@astrojs/node/server.js',
previewEntrypoint: '@astrojs/node/preview.js',
exports: ['handler', 'startServer', 'options'],
args: options,
supportedAstroFeatures: {
hybridOutput: 'stable',
staticOutput: 'stable',
serverOutput: 'stable',
assets: {
supportKind: 'stable',
isSharpCompatible: true,
isSquooshCompatible: true,
},
i18nDomains: 'experimental',
envGetSecret: 'experimental',
},
};
}

// TODO: remove once we don't use a TLA anymore
async function shouldExternalizeAstroEnvSetup() {
try {
await import('astro/env/setup');
return false;
} catch {
return true;
}
}

export default function createIntegration(userOptions: UserOptions): AstroIntegration {
if (!userOptions?.mode) {
throw new AstroError(`Setting the 'mode' option is required.`);
}

let _options: Options;
return {
name: '@astrojs/node',
hooks: {
'astro:config:setup': async ({ updateConfig, config }) => {
updateConfig({
image: {
endpoint: config.image.endpoint ?? 'astro/assets/endpoint/node',
},
vite: {
ssr: {
noExternal: ['@astrojs/node'],
...((await shouldExternalizeAstroEnvSetup())
? {
external: ['astro/env/setup'],
}
: {}),
},
},
});
},
'astro:config:done': ({ setAdapter, config, logger }) => {
_options = {
...userOptions,
client: config.build.client?.toString(),
server: config.build.server?.toString(),
host: config.server.host,
port: config.server.port,
assets: config.build.assets,
};
setAdapter(getAdapter(_options));

if (config.output === 'static') {
logger.warn(
`\`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`
);
}
},
},
};
}
93 changes: 93 additions & 0 deletions packages/node/src/log-listening-on.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type http from 'node:http';
import https from 'node:https';
import type { AddressInfo } from 'node:net';
import os from 'node:os';
import type { AstroIntegrationLogger } from 'astro';
import type { Options } from './types.js';

export async function logListeningOn(
logger: AstroIntegrationLogger,
server: http.Server | https.Server,
options: Pick<Options, 'host'>
) {
await new Promise<void>((resolve) => server.once('listening', resolve));
const protocol = server instanceof https.Server ? 'https' : 'http';
// Allow to provide host value at runtime
const host = getResolvedHostForHttpServer(
process.env.HOST !== undefined && process.env.HOST !== '' ? process.env.HOST : options.host
);
const { port } = server.address() as AddressInfo;
const address = getNetworkAddress(protocol, host, port);

if (host === undefined) {
logger.info(
`Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n`
);
} else {
logger.info(`Server listening on ${address.local[0]}`);
}
}

function getResolvedHostForHttpServer(host: string | boolean) {
if (host === false) {
// Use a secure default
return 'localhost';
// biome-ignore lint/style/noUselessElse: <explanation>
} else if (host === true) {
// If passed --host in the CLI without arguments
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
// biome-ignore lint/style/noUselessElse: <explanation>
} else {
return host;
}
}

interface NetworkAddressOpt {
local: string[];
network: string[];
}

const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']);

// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914
export function getNetworkAddress(
// biome-ignore lint/style/useDefaultParameterLast: <explanation>
protocol: 'http' | 'https' = 'http',
hostname: string | undefined,
port: number,
base?: string
) {
const NetworkAddress: NetworkAddressOpt = {
local: [],
network: [],
};
// biome-ignore lint/complexity/noForEach: <explanation>
Object.values(os.networkInterfaces())
.flatMap((nInterface) => nInterface ?? [])
.filter(
(detail) =>
// biome-ignore lint/complexity/useOptionalChain: <explanation>
detail &&
detail.address &&
(detail.family === 'IPv4' ||
// @ts-expect-error Node 18.0 - 18.3 returns number
detail.family === 4)
)
.forEach((detail) => {
let host = detail.address.replace(
'127.0.0.1',
hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname
);
// ipv6 host
if (host.includes(':')) {
host = `[${host}]`;
}
const url = `${protocol}://${host}:${port}${base ? base : ''}`;
if (detail.address.includes('127.0.0.1')) {
NetworkAddress.local.push(url);
} else {
NetworkAddress.network.push(url);
}
});
return NetworkAddress;
}
43 changes: 43 additions & 0 deletions packages/node/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { NodeApp } from 'astro/app/node';
import { createAppHandler } from './serve-app.js';
import type { RequestHandler } from './types.js';

/**
* Creates a middleware that can be used with Express, Connect, etc.
*
* Similar to `createAppHandler` but can additionally be placed in the express
* chain as an error middleware.
*
* https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
*/
export default function createMiddleware(app: NodeApp): RequestHandler {
const handler = createAppHandler(app);
const logger = app.getAdapterLogger();
// using spread args because express trips up if the function's
// stringified body includes req, res, next, locals directly
return async (...args) => {
// assume normal invocation at first
const [req, res, next, locals] = args;
// short circuit if it is an error invocation
if (req instanceof Error) {
const error = req;
if (next) {
return next(error);
// biome-ignore lint/style/noUselessElse: <explanation>
} else {
throw error;
}
}
try {
await handler(req, res, next, locals);
} catch (err) {
logger.error(`Could not render ${req.url}`);
console.error(err);
if (!res.headersSent) {
// biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
res.writeHead(500, `Server error`);
res.end();
}
}
};
}
63 changes: 63 additions & 0 deletions packages/node/src/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fileURLToPath } from 'node:url';
import type { CreatePreviewServer } from 'astro';
import { AstroError } from 'astro/errors';
import { logListeningOn } from './log-listening-on.js';
import type { createExports } from './server.js';
import { createServer } from './standalone.js';

type ServerModule = ReturnType<typeof createExports>;
type MaybeServerModule = Partial<ServerModule>;

const createPreviewServer: CreatePreviewServer = async (preview) => {
let ssrHandler: ServerModule['handler'];
let options: ServerModule['options'];
try {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
const ssrModule: MaybeServerModule = await import(preview.serverEntrypoint.toString());
if (typeof ssrModule.handler === 'function') {
ssrHandler = ssrModule.handler;
// biome-ignore lint/style/noNonNullAssertion: <explanation>
options = ssrModule.options!;
} else {
throw new AstroError(
`The server entrypoint doesn't have a handler. Are you sure this is the right file?`
);
}
} catch (err) {
if ((err as any).code === 'ERR_MODULE_NOT_FOUND') {
throw new AstroError(
`The server entrypoint ${fileURLToPath(
preview.serverEntrypoint
)} does not exist. Have you ran a build yet?`
);
// biome-ignore lint/style/noUselessElse: <explanation>
} else {
throw err;
}
}
const host = preview.host ?? 'localhost';
const port = preview.port ?? 4321;
const server = createServer(ssrHandler, host, port);

// If user specified custom headers append a listener
// to the server to add those headers to response
if (preview.headers) {
server.server.addListener('request', (_, res) => {
if (res.statusCode === 200) {
for (const [name, value] of Object.entries(preview.headers ?? {})) {
if (value) res.setHeader(name, value);
}
}
});
}

logListeningOn(preview.logger, server.server, options);
await new Promise<void>((resolve, reject) => {
server.server.once('listening', resolve);
server.server.once('error', reject);
server.server.listen(port, host);
});
return server;
};

export { createPreviewServer as default };
Loading

0 comments on commit 89927b1

Please sign in to comment.