mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
chore: move node and vercel to adapters repo (#11866)
This commit is contained in:
parent
8e5257adda
commit
11ebf3bd15
249 changed files with 66 additions and 10100 deletions
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@benchmark/timer": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"autocannon": "^7.15.0",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/db": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/db": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@types/react": "npm:types-react",
|
||||
"@types/react-dom": "npm:types-react-dom",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"@astrojs/react": "workspace:*",
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/mdx": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@astrojs/react": "workspace:*",
|
||||
"@astrojs/svelte": "workspace:*",
|
||||
"@astrojs/vue": "workspace:*",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
"@astrojs/node": "^8.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@test/static-build-pkg": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/db": "workspace:*",
|
||||
"@astrojs/node": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,38 +1,3 @@
|
|||
# @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/
|
||||
The Node adapter package has moved. Please see [the new repository for the Node adapter](https://github.com/withastro/adapters/tree/main/packages/node).
|
||||
|
|
|
@ -1,55 +1,7 @@
|
|||
{
|
||||
"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/astro.git",
|
||||
"directory": "packages/integrations/node"
|
||||
},
|
||||
"keywords": [
|
||||
"withastro",
|
||||
"astro-adapter"
|
||||
],
|
||||
"bugs": "https://github.com/withastro/astro/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": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"test": "astro-scripts test \"test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"send": "^0.18.0",
|
||||
"server-destroy": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.17.8",
|
||||
"@types/send": "^0.17.4",
|
||||
"@types/server-destroy": "^1.0.4",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"cheerio": "1.0.0",
|
||||
"express": "^4.19.2",
|
||||
"node-mocks-http": "^1.15.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
}
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"keywords": [],
|
||||
"dont_remove": "This is a placeholder for the sake of the docs smoke test"
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
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.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
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';
|
||||
} 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)
|
||||
} 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(
|
||||
protocol: 'http' | 'https' = 'http',
|
||||
hostname: string | undefined,
|
||||
port: number,
|
||||
base?: string,
|
||||
) {
|
||||
const NetworkAddress: NetworkAddressOpt = {
|
||||
local: [],
|
||||
network: [],
|
||||
};
|
||||
Object.values(os.networkInterfaces())
|
||||
.flatMap((nInterface) => nInterface ?? [])
|
||||
.filter(
|
||||
(detail) =>
|
||||
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;
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
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 function (...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);
|
||||
} 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) {
|
||||
res.writeHead(500, `Server error`);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
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 function (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;
|
||||
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?`,
|
||||
);
|
||||
} 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 };
|
|
@ -1,52 +0,0 @@
|
|||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import { NodeApp } from 'astro/app/node';
|
||||
import type { RequestHandler } from './types.js';
|
||||
|
||||
/**
|
||||
* Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware.
|
||||
* If the next callback is provided, it will be called if the request does not have a matching route.
|
||||
* Intended to be used in both standalone and middleware mode.
|
||||
*/
|
||||
export function createAppHandler(app: NodeApp): RequestHandler {
|
||||
/**
|
||||
* Keep track of the current request path using AsyncLocalStorage.
|
||||
* Used to log unhandled rejections with a helpful message.
|
||||
*/
|
||||
const als = new AsyncLocalStorage<string>();
|
||||
const logger = app.getAdapterLogger();
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
const requestUrl = als.getStore();
|
||||
logger.error(`Unhandled rejection while rendering ${requestUrl}`);
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
return async (req, res, next, locals) => {
|
||||
let request: Request;
|
||||
try {
|
||||
request = NodeApp.createRequest(req);
|
||||
} catch (err) {
|
||||
logger.error(`Could not render ${req.url}`);
|
||||
console.error(err);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
|
||||
const routeData = app.match(request);
|
||||
if (routeData) {
|
||||
const response = await als.run(request.url, () =>
|
||||
app.render(request, {
|
||||
addCookieHeader: true,
|
||||
locals,
|
||||
routeData,
|
||||
}),
|
||||
);
|
||||
await NodeApp.writeResponse(response, res);
|
||||
} else if (next) {
|
||||
return next();
|
||||
} else {
|
||||
const response = await app.render(req);
|
||||
await NodeApp.writeResponse(response, res);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
import type { NodeApp } from 'astro/app/node';
|
||||
import send from 'send';
|
||||
import type { Options } from './types.js';
|
||||
|
||||
// check for a dot followed by a extension made up of lowercase characters
|
||||
const isSubresourceRegex = /.+\.[a-z]+$/i;
|
||||
|
||||
/**
|
||||
* Creates a Node.js http listener for static files and prerendered pages.
|
||||
* In standalone mode, the static handler is queried first for the static files.
|
||||
* If one matching the request path is not found, it relegates to the SSR handler.
|
||||
* Intended to be used only in the standalone mode.
|
||||
*/
|
||||
export function createStaticHandler(app: NodeApp, options: Options) {
|
||||
const client = resolveClientDir(options);
|
||||
/**
|
||||
* @param ssr The SSR handler to be called if the static handler does not find a matching file.
|
||||
*/
|
||||
return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => {
|
||||
if (req.url) {
|
||||
const [urlPath, urlQuery] = req.url.split('?');
|
||||
const filePath = path.join(client, app.removeBase(urlPath));
|
||||
|
||||
let pathname: string;
|
||||
let isDirectory = false;
|
||||
try {
|
||||
isDirectory = fs.lstatSync(filePath).isDirectory();
|
||||
} catch {}
|
||||
|
||||
const { trailingSlash = 'ignore' } = options;
|
||||
|
||||
const hasSlash = urlPath.endsWith('/');
|
||||
switch (trailingSlash) {
|
||||
case 'never':
|
||||
if (isDirectory && urlPath != '/' && hasSlash) {
|
||||
pathname = urlPath.slice(0, -1) + (urlQuery ? '?' + urlQuery : '');
|
||||
res.statusCode = 301;
|
||||
res.setHeader('Location', pathname);
|
||||
return res.end();
|
||||
} else pathname = urlPath;
|
||||
// intentionally fall through
|
||||
case 'ignore':
|
||||
{
|
||||
if (isDirectory && !hasSlash) {
|
||||
pathname = urlPath + '/index.html';
|
||||
} else pathname = urlPath;
|
||||
}
|
||||
break;
|
||||
case 'always':
|
||||
// trailing slash is not added to "subresources"
|
||||
if (!hasSlash && !isSubresourceRegex.test(urlPath)) {
|
||||
pathname = urlPath + '/' + (urlQuery ? '?' + urlQuery : '');
|
||||
res.statusCode = 301;
|
||||
res.setHeader('Location', pathname);
|
||||
return res.end();
|
||||
} else pathname = urlPath;
|
||||
break;
|
||||
}
|
||||
// app.removeBase sometimes returns a path without a leading slash
|
||||
pathname = prependForwardSlash(app.removeBase(pathname));
|
||||
|
||||
const stream = send(req, pathname, {
|
||||
root: client,
|
||||
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
|
||||
});
|
||||
|
||||
let forwardError = false;
|
||||
|
||||
stream.on('error', (err) => {
|
||||
if (forwardError) {
|
||||
console.error(err.toString());
|
||||
res.writeHead(500);
|
||||
res.end('Internal server error');
|
||||
return;
|
||||
}
|
||||
// File not found, forward to the SSR handler
|
||||
ssr();
|
||||
});
|
||||
stream.on('headers', (_res: ServerResponse) => {
|
||||
// assets in dist/_astro are hashed and should get the immutable header
|
||||
if (pathname.startsWith(`/${options.assets}/`)) {
|
||||
// This is the "far future" cache header, used for static files whose name includes their digest hash.
|
||||
// 1 year (31,536,000 seconds) is convention.
|
||||
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
|
||||
_res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
});
|
||||
stream.on('file', () => {
|
||||
forwardError = true;
|
||||
});
|
||||
stream.pipe(res);
|
||||
} else {
|
||||
ssr();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveClientDir(options: Options) {
|
||||
const clientURLRaw = new URL(options.client);
|
||||
const serverURLRaw = new URL(options.server);
|
||||
const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw));
|
||||
|
||||
// walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively.
|
||||
const serverFolder = path.basename(options.server);
|
||||
let serverEntryFolderURL = path.dirname(import.meta.url);
|
||||
while (!serverEntryFolderURL.endsWith(serverFolder)) {
|
||||
serverEntryFolderURL = path.dirname(serverEntryFolderURL);
|
||||
}
|
||||
const serverEntryURL = serverEntryFolderURL + '/entry.mjs';
|
||||
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
|
||||
const client = url.fileURLToPath(clientURL);
|
||||
return client;
|
||||
}
|
||||
|
||||
function prependForwardSlash(pth: string) {
|
||||
return pth.startsWith('/') ? pth : '/' + pth;
|
||||
}
|
||||
|
||||
function appendForwardSlash(pth: string) {
|
||||
return pth.endsWith('/') ? pth : pth + '/';
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import type { SSRManifest } from 'astro';
|
||||
import { NodeApp, applyPolyfills } from 'astro/app/node';
|
||||
import createMiddleware from './middleware.js';
|
||||
import { createStandaloneHandler } from './standalone.js';
|
||||
import startServer from './standalone.js';
|
||||
import type { Options } from './types.js';
|
||||
// This needs to run first because some internals depend on `crypto`
|
||||
applyPolyfills();
|
||||
|
||||
// Won't throw if the virtual module is not available because it's not supported in
|
||||
// the users's astro version or if astro:env is not enabled in the project
|
||||
await import('astro/env/setup')
|
||||
.then((mod) => mod.setGetEnv((key) => process.env[key]))
|
||||
.catch(() => {});
|
||||
|
||||
export function createExports(manifest: SSRManifest, options: Options) {
|
||||
const app = new NodeApp(manifest);
|
||||
options.trailingSlash = manifest.trailingSlash;
|
||||
return {
|
||||
options: options,
|
||||
handler:
|
||||
options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options),
|
||||
startServer: () => startServer(app, options),
|
||||
};
|
||||
}
|
||||
|
||||
export function start(manifest: SSRManifest, options: Options) {
|
||||
if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
const app = new NodeApp(manifest);
|
||||
startServer(app, options);
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import type { PreviewServer } from 'astro';
|
||||
import type { NodeApp } from 'astro/app/node';
|
||||
import enableDestroy from 'server-destroy';
|
||||
import { logListeningOn } from './log-listening-on.js';
|
||||
import { createAppHandler } from './serve-app.js';
|
||||
import { createStaticHandler } from './serve-static.js';
|
||||
import type { Options } from './types.js';
|
||||
|
||||
// Used to get Host Value at Runtime
|
||||
export const hostOptions = (host: Options['host']): string => {
|
||||
if (typeof host === 'boolean') {
|
||||
return host ? '0.0.0.0' : 'localhost';
|
||||
}
|
||||
return host;
|
||||
};
|
||||
|
||||
export default function standalone(app: NodeApp, options: Options) {
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : options.port ?? 8080;
|
||||
const host = process.env.HOST ?? hostOptions(options.host);
|
||||
const handler = createStandaloneHandler(app, options);
|
||||
const server = createServer(handler, host, port);
|
||||
server.server.listen(port, host);
|
||||
if (process.env.ASTRO_NODE_LOGGING !== 'disabled') {
|
||||
logListeningOn(app.getAdapterLogger(), server.server, options);
|
||||
}
|
||||
return {
|
||||
server,
|
||||
done: server.closed(),
|
||||
};
|
||||
}
|
||||
|
||||
// also used by server entrypoint
|
||||
export function createStandaloneHandler(app: NodeApp, options: Options) {
|
||||
const appHandler = createAppHandler(app);
|
||||
const staticHandler = createStaticHandler(app, options);
|
||||
return (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
try {
|
||||
// validate request path
|
||||
decodeURI(req.url!);
|
||||
} catch {
|
||||
res.writeHead(400);
|
||||
res.end('Bad request.');
|
||||
return;
|
||||
}
|
||||
staticHandler(req, res, () => appHandler(req, res));
|
||||
};
|
||||
}
|
||||
|
||||
// also used by preview entrypoint
|
||||
export function createServer(listener: http.RequestListener, host: string, port: number) {
|
||||
let httpServer: http.Server | https.Server;
|
||||
|
||||
if (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) {
|
||||
httpServer = https.createServer(
|
||||
{
|
||||
key: fs.readFileSync(process.env.SERVER_KEY_PATH),
|
||||
cert: fs.readFileSync(process.env.SERVER_CERT_PATH),
|
||||
},
|
||||
listener,
|
||||
);
|
||||
} else {
|
||||
httpServer = http.createServer(listener);
|
||||
}
|
||||
enableDestroy(httpServer);
|
||||
|
||||
// Resolves once the server is closed
|
||||
const closed = new Promise<void>((resolve, reject) => {
|
||||
httpServer.addListener('close', resolve);
|
||||
httpServer.addListener('error', reject);
|
||||
});
|
||||
|
||||
const previewable = {
|
||||
host,
|
||||
port,
|
||||
closed() {
|
||||
return closed;
|
||||
},
|
||||
async stop() {
|
||||
await new Promise((resolve, reject) => {
|
||||
httpServer.destroy((err) => (err ? reject(err) : resolve(undefined)));
|
||||
});
|
||||
},
|
||||
} satisfies PreviewServer;
|
||||
|
||||
return {
|
||||
server: httpServer,
|
||||
...previewable,
|
||||
};
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { SSRManifest } from 'astro';
|
||||
import type { NodeApp } from 'astro/app/node';
|
||||
|
||||
export interface UserOptions {
|
||||
/**
|
||||
* Specifies the mode that the adapter builds to.
|
||||
*
|
||||
* - 'middleware' - Build to middleware, to be used within another Node.js server, such as Express.
|
||||
* - 'standalone' - Build to a standalone server. The server starts up just by running the built script.
|
||||
*/
|
||||
mode: 'middleware' | 'standalone';
|
||||
}
|
||||
|
||||
export interface Options extends UserOptions {
|
||||
host: string | boolean;
|
||||
port: number;
|
||||
server: string;
|
||||
client: string;
|
||||
assets: string;
|
||||
trailingSlash?: SSRManifest['trailingSlash'];
|
||||
}
|
||||
|
||||
export interface CreateServerOptions {
|
||||
app: NodeApp;
|
||||
assets: string;
|
||||
client: URL;
|
||||
port: number;
|
||||
host: string | undefined;
|
||||
removeBase: (pathname: string) => string;
|
||||
}
|
||||
|
||||
export type RequestHandler = (...args: RequestHandlerParams) => void | Promise<void>;
|
||||
export type RequestHandlerParams = [
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next?: (err?: unknown) => void,
|
||||
locals?: object,
|
||||
];
|
|
@ -1,153 +0,0 @@
|
|||
import * as assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import nodejs from '../dist/index.js';
|
||||
import { createRequestAndResponse, loadFixture } from './test-utils.js';
|
||||
|
||||
describe('API routes', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('astro/src/@types/astro.js').PreviewServer} */
|
||||
let previewServer;
|
||||
/** @type {URL} */
|
||||
let baseUri;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/api-route/',
|
||||
output: 'server',
|
||||
adapter: nodejs({ mode: 'middleware' }),
|
||||
});
|
||||
await fixture.build();
|
||||
previewServer = await fixture.preview();
|
||||
baseUri = new URL(`http://${previewServer.host ?? 'localhost'}:${previewServer.port}/`);
|
||||
});
|
||||
|
||||
after(() => previewServer.stop());
|
||||
|
||||
it('Can get the request body', async () => {
|
||||
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
|
||||
let { req, res, done } = createRequestAndResponse({
|
||||
method: 'POST',
|
||||
url: '/recipes',
|
||||
});
|
||||
|
||||
req.once('async_iterator', () => {
|
||||
req.send(JSON.stringify({ id: 2 }));
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
|
||||
let [buffer] = await done;
|
||||
|
||||
let json = JSON.parse(buffer.toString('utf-8'));
|
||||
|
||||
assert.equal(json.length, 1);
|
||||
|
||||
assert.equal(json[0].name, 'Broccoli Soup');
|
||||
});
|
||||
|
||||
it('Can get binary data', async () => {
|
||||
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
|
||||
|
||||
let { req, res, done } = createRequestAndResponse({
|
||||
method: 'POST',
|
||||
url: '/binary',
|
||||
});
|
||||
|
||||
req.once('async_iterator', () => {
|
||||
req.send(Buffer.from(new Uint8Array([1, 2, 3, 4, 5])));
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
|
||||
let [out] = await done;
|
||||
let arr = Array.from(new Uint8Array(out.buffer));
|
||||
assert.deepEqual(arr, [5, 4, 3, 2, 1]);
|
||||
});
|
||||
|
||||
it('Can post large binary data', async () => {
|
||||
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
|
||||
|
||||
let { req, res, done } = createRequestAndResponse({
|
||||
method: 'POST',
|
||||
url: '/hash',
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
|
||||
let expectedDigest = null;
|
||||
req.once('async_iterator', () => {
|
||||
// Send 256MB of garbage data in 256KB chunks. This should be fast (< 1sec).
|
||||
let remainingBytes = 256 * 1024 * 1024;
|
||||
const chunkSize = 256 * 1024;
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
while (remainingBytes > 0) {
|
||||
const size = Math.min(remainingBytes, chunkSize);
|
||||
const chunk = Buffer.alloc(size, Math.floor(Math.random() * 256));
|
||||
hash.update(chunk);
|
||||
req.emit('data', chunk);
|
||||
remainingBytes -= size;
|
||||
}
|
||||
|
||||
req.emit('end');
|
||||
expectedDigest = hash.digest();
|
||||
});
|
||||
|
||||
let [out] = await done;
|
||||
assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest));
|
||||
});
|
||||
|
||||
it('Can bail on streaming', async () => {
|
||||
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
|
||||
let { req, res, done } = createRequestAndResponse({
|
||||
url: '/streaming',
|
||||
});
|
||||
|
||||
let locals = { cancelledByTheServer: false };
|
||||
|
||||
handler(req, res, () => {}, locals);
|
||||
req.send();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
res.emit('close');
|
||||
|
||||
await done;
|
||||
|
||||
assert.deepEqual(locals, { cancelledByTheServer: true });
|
||||
});
|
||||
|
||||
it('Can respond with SSR redirect', async () => {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 1000);
|
||||
const response = await fetch(new URL('/redirect', baseUri), {
|
||||
redirect: 'manual',
|
||||
signal: controller.signal,
|
||||
});
|
||||
assert.equal(response.status, 302);
|
||||
assert.equal(response.headers.get('location'), '/destination');
|
||||
});
|
||||
|
||||
it('Can respond with Astro.redirect', async () => {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 1000);
|
||||
const response = await fetch(new URL('/astro-redirect', baseUri), {
|
||||
redirect: 'manual',
|
||||
signal: controller.signal,
|
||||
});
|
||||
assert.equal(response.status, 303);
|
||||
assert.equal(response.headers.get('location'), '/destination');
|
||||
});
|
||||
|
||||
it('Can respond with Response.redirect', async () => {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 1000);
|
||||
const response = await fetch(new URL('/response-redirect', baseUri), {
|
||||
redirect: 'manual',
|
||||
signal: controller.signal,
|
||||
});
|
||||
assert.equal(response.status, 307);
|
||||
assert.equal(response.headers.get('location'), String(new URL('/destination', baseUri)));
|
||||
});
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
import * as assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import * as cheerio from 'cheerio';
|
||||
import nodejs from '../dist/index.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Assets', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
let devPreview;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/image/',
|
||||
output: 'server',
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
vite: {
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
devPreview = await fixture.preview();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devPreview.stop();
|
||||
});
|
||||
|
||||
it('Assets within the _astro folder should be given immutable headers', async () => {
|
||||
let response = await fixture.fetch('/text-file');
|
||||
let cacheControl = response.headers.get('cache-control');
|
||||
assert.equal(cacheControl, null);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Fetch the asset
|
||||
const fileURL = $('a').attr('href');
|
||||
response = await fixture.fetch(fileURL);
|
||||
cacheControl = response.headers.get('cache-control');
|
||||
assert.equal(cacheControl, 'public, max-age=31536000, immutable');
|
||||
});
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
import * as assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import nodejs from '../dist/index.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Bad URLs', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
let devPreview;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/bad-urls/',
|
||||
output: 'server',
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
});
|
||||
await fixture.build();
|
||||
devPreview = await fixture.preview();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devPreview.stop();
|
||||
});
|
||||
|
||||
it('Does not crash on bad urls', async () => {
|
||||
const weirdURLs = [
|
||||
'/\\xfs.bxss.me%3Fastrojs.com/hello-world',
|
||||
'/asdasdasd@ax_zX=.zxczas🐥%/úadasd000%/',
|
||||
'%',
|
||||
'%80',
|
||||
'%c',
|
||||
'%c0%80',
|
||||
'%20foobar%',
|
||||
];
|
||||
|
||||
const statusCodes = [400, 404, 500];
|
||||
for (const weirdUrl of weirdURLs) {
|
||||
const fetchResult = await fixture.fetch(weirdUrl);
|
||||
assert.equal(
|
||||
statusCodes.includes(fetchResult.status),
|
||||
true,
|
||||
`${weirdUrl} returned something else than 400, 404, or 500`,
|
||||
);
|
||||
}
|
||||
const stillWork = await fixture.fetch('/');
|
||||
const text = await stillWork.text();
|
||||
assert.equal(text, '<!DOCTYPE html>Hello!');
|
||||
});
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import * as assert from 'node:assert/strict';
|
||||
import { before, describe, it } from 'node:test';
|
||||
import nodejs from '../dist/index.js';
|
||||
import { createRequestAndResponse, loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Encoded Pathname', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/encoded/',
|
||||
output: 'server',
|
||||
adapter: nodejs({ mode: 'middleware' }),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('Can get an Astro file', async () => {
|
||||
const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs');
|
||||
let { req, res, text } = createRequestAndResponse({
|
||||
url: '/什么',
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
req.send();
|
||||
|
||||
const html = await text();
|
||||
assert.equal(html.includes('什么</h1>'), true);
|
||||
});
|
||||
|
||||
it('Can get a Markdown file', async () => {
|
||||
const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs');
|
||||
|
||||
let { req, res, text } = createRequestAndResponse({
|
||||
url: '/blog/什么',
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
req.send();
|
||||
|
||||
const html = await text();
|
||||
assert.equal(html.includes('什么</h1>'), true);
|
||||
});
|
||||
});
|
|
@ -1,91 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { after, before, describe, it } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import * as cheerio from 'cheerio';
|
||||
import nodejs from '../dist/index.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('Errors', () => {
|
||||
/** @type {import('./test-utils.js').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/errors/',
|
||||
output: 'server',
|
||||
adapter: nodejs({ mode: 'standalone' }),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
let devPreview;
|
||||
|
||||
before(async () => {
|
||||
// The two tests that need the server to run are skipped
|
||||
// devPreview = await fixture.preview();
|
||||
});
|
||||
after(async () => {
|
||||
await devPreview?.stop();
|
||||
});
|
||||
|
||||
it('stays alive after offshoot promise rejections', async () => {
|
||||
// this test needs to happen in a worker because node test runner adds a listener for unhandled rejections in the main thread
|
||||
const url = new URL('./fixtures/errors/dist/server/entry.mjs', import.meta.url);
|
||||
const worker = new Worker(fileURLToPath(url), {
|
||||
type: 'module',
|
||||
env: { ASTRO_NODE_LOGGING: 'enabled' },
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
worker.stdout.on('data', (data) => {
|
||||
setTimeout(() => reject('Server took too long to start'), 1000);
|
||||
if (data.toString().includes('Server listening on http://localhost:4321')) resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await fetch('http://localhost:4321/offshoot-promise-rejection');
|
||||
|
||||
// if there was a crash, it becomes an error here
|
||||
await worker.terminate();
|
||||
});
|
||||
|
||||
it(
|
||||
'rejected promise in template',
|
||||
{ skip: true, todo: 'Review the response from the in-stream' },
|
||||
async () => {
|
||||
const res = await fixture.fetch('/in-stream');
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
assert.equal($('p').text().trim(), 'Internal server error');
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'generator that throws called in template',
|
||||
{ skip: true, todo: 'Review the response from the generator' },
|
||||
async () => {
|
||||
const result = ['<!DOCTYPE html><h1>Astro</h1> 1', 'Internal server error'];
|
||||
|
||||
/** @type {Response} */
|
||||
const res = await fixture.fetch('/generator');
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const chunk1 = await reader.read();
|
||||
const chunk2 = await reader.read();
|
||||
const chunk3 = await reader.read();
|
||||
assert.equal(chunk1.done, false);
|
||||
console.log(chunk1);
|
||||
console.log(chunk2);
|
||||
console.log(chunk3);
|
||||
if (chunk2.done) {
|
||||
assert.equal(decoder.decode(chunk1.value), result.join(''));
|
||||
} else if (chunk3.done) {
|
||||
assert.equal(decoder.decode(chunk1.value), result[0]);
|
||||
assert.equal(decoder.decode(chunk2.value), result[1]);
|
||||
} else {
|
||||
throw new Error('The response should take at most 2 chunks.');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-api-route",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
---
|
||||
return Astro.redirect('/destination', 303);
|
||||
---
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
let body = await request.arrayBuffer();
|
||||
let data = new Uint8Array(body);
|
||||
let r = data.reverse();
|
||||
return new Response(r, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
const iterable = request.body as unknown as AsyncIterable<Uint8Array>;
|
||||
for await (const chunk of iterable) {
|
||||
hash.update(chunk);
|
||||
}
|
||||
|
||||
return new Response(hash.digest(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
export async function POST({ request }) {
|
||||
let body = await request.json();
|
||||
const recipes = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Potato Soup'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Broccoli Soup'
|
||||
}
|
||||
];
|
||||
|
||||
let out = recipes.filter(r => {
|
||||
return r.id === body.id;
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(out), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ redirect }: APIContext) {
|
||||
return redirect('/destination');
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ url: requestUrl }: APIContext) {
|
||||
return Response.redirect(new URL('/destination', requestUrl), 307);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
export const GET = ({ locals }) => {
|
||||
let sentChunks = 0;
|
||||
|
||||
const readableStream = new ReadableStream({
|
||||
async pull(controller) {
|
||||
if (sentChunks === 3) return controller.close();
|
||||
else sentChunks++;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
controller.enqueue(new TextEncoder().encode('hello\n'));
|
||||
},
|
||||
cancel() {
|
||||
locals.cancelledByTheServer = true;
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(readableStream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream"
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-badurls",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Hello!
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-encoded",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
# 什么
|
|
@ -1 +0,0 @@
|
|||
<h1>什么</h1>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-errors",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
function * generator () {
|
||||
yield 1
|
||||
throw Error('ohnoes')
|
||||
}
|
||||
---
|
||||
<h1>Astro</h1>
|
||||
{generator()}
|
||||
<footer>
|
||||
Footer
|
||||
</footer>
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>One</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>One</h1>
|
||||
<p>
|
||||
{Promise.reject('Error in the stream')}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
|
@ -1,2 +0,0 @@
|
|||
{new Promise(async _ => (await {}, Astro.props.undefined.alsoAPropertyOfUndefined))}
|
||||
{Astro.props.undefined.propertyOfUndefined}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-headers",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
Astro.cookies.set('from1', 'astro1');
|
||||
Astro.cookies.set('from2', 'astro2');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
Astro.cookies.set('from1', 'astro1');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
Astro.response.headers.append('set-cookie', 'from1=response1');
|
||||
Astro.response.headers.append('set-cookie', 'from2=response2');
|
||||
Astro.cookies.set('from3', 'astro1');
|
||||
Astro.cookies.set('from4', 'astro2');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
Astro.response.headers.append('set-cookie', 'from1=response1');
|
||||
Astro.cookies.set('from1', 'astro1');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
Astro.response.headers.append('set-cookie', 'from1=value1');
|
||||
Astro.response.headers.append('set-cookie', 'from2=value2');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
Astro.response.headers.append('set-cookie', 'from1=value1');
|
||||
---
|
||||
<p>hello world</p>
|
|
@ -1,9 +0,0 @@
|
|||
import type { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ request, cookies }: APIContext) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
cookies.set('from1', 'astro1');
|
||||
cookies.set('from2', 'astro2');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import type { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ request, cookies }: APIContext) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
cookies.set('from1', 'astro1');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import type { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ request, cookies }: APIContext) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('set-cookie', 'from1=response1');
|
||||
headers.append('set-cookie', 'from2=response2');
|
||||
cookies.set('from3', 'astro1');
|
||||
cookies.set('from4', 'astro2');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import type { APIContext } from 'astro';
|
||||
|
||||
export async function GET({ request, cookies }: APIContext) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('set-cookie', 'from1=response1');
|
||||
cookies.set('from1', 'astro1');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('x-SINGLE', 'single');
|
||||
headers.append('X-triple', 'one');
|
||||
headers.append('x-Triple', 'two');
|
||||
headers.append('x-TRIPLE', 'three');
|
||||
headers.append('SET-cookie', 'hello1=world1');
|
||||
headers.append('Set-Cookie', 'hello2=world2');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('Set-Cookie', 'hello1=world1');
|
||||
headers.append('SET-COOKIE', 'hello2=world2');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('Set-Cookie', 'hello1=world1');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
const headers = new Headers();
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
return new Response('hello world', { headers: undefined });
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export async function GET({ request }: { request: Request }) {
|
||||
const headers = new Headers();
|
||||
headers.append('content-type', 'text/plain;charset=utf-8');
|
||||
headers.append('X-HELLO', 'world');
|
||||
return new Response('hello world', { headers });
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-image",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
this is a text file
|
Binary file not shown.
Before Width: | Height: | Size: 279 KiB |
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import penguin from "../assets/some_penguin.png";
|
||||
---
|
||||
|
||||
<Image src={penguin} alt="Penguins" width={50} />
|
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
import txt from '../assets/file.txt?url';
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing</h1>
|
||||
<main>
|
||||
<a href={txt} download>Download text file</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/locals",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { defineMiddleware } from 'astro:middleware';
|
||||
|
||||
export const onRequest = defineMiddleware(({ url, locals }, next) => {
|
||||
if (url.pathname === "/from-astro-middleware") locals.foo = "baz";
|
||||
return next();
|
||||
})
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
export async function POST({ locals }) {
|
||||
const out = { ...locals };
|
||||
|
||||
return new Response(JSON.stringify(out), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
const { foo } = Astro.locals;
|
||||
---
|
||||
<h1>{foo}</h1>
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
const { foo } = Astro.locals;
|
||||
---
|
||||
<h1>{foo}</h1>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/node-middleware",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404</title>
|
||||
</head>
|
||||
<body>Page does not exist</body>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head><title>node-middleware</title></head>
|
||||
<style>
|
||||
</style>
|
||||
<body>
|
||||
<div>1</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
export async function GET() {
|
||||
let number = Math.random();
|
||||
return Response.json({
|
||||
number,
|
||||
message: `Here's a random number: ${number}`,
|
||||
});
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-prerender-404-500",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
body {
|
||||
background-color: ivory;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// This module is only used by the prerendered 404.astro.
|
||||
// It exhibits different behavior if it's called more than once,
|
||||
// which is detected by a test and interpreted as a failure.
|
||||
|
||||
let usedOnce = false
|
||||
let dynamicMessage = "Page was not prerendered"
|
||||
|
||||
export default function () {
|
||||
if (usedOnce === false) {
|
||||
usedOnce = true
|
||||
return "Page does not exist"
|
||||
}
|
||||
|
||||
dynamicMessage += "+"
|
||||
|
||||
return dynamicMessage
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// This module is only used by the prerendered 500.astro.
|
||||
// It exhibits different behavior if it's called more than once,
|
||||
// which is detected by a test and interpreted as a failure.
|
||||
|
||||
let usedOnce = false
|
||||
let dynamicMessage = "Page was not prerendered"
|
||||
|
||||
export default function () {
|
||||
if (usedOnce === false) {
|
||||
usedOnce = true
|
||||
return "Something went wrong"
|
||||
}
|
||||
|
||||
dynamicMessage += "+"
|
||||
|
||||
return dynamicMessage
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
import message from "../nondeterminism-404"
|
||||
export const prerender = true;
|
||||
---
|
||||
{message()}
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
import "../external-stylesheet.css"
|
||||
import message from "../nondeterminism-500"
|
||||
export const prerender = true
|
||||
---
|
||||
<h1>{message()}</h1>
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
return new Response(null, { status: 500 })
|
||||
---
|
||||
<p>This html will not be served</p>
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Static Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-prerender",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { shared } from './shared';
|
||||
export const onRequest = (ctx, next) => {
|
||||
ctx.locals = {
|
||||
name: shared,
|
||||
};
|
||||
return next();
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>One</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>One</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
import { shared} from "../shared";
|
||||
export const prerender = false;
|
||||
|
||||
const shared = Astro.locals.name;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>One</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{shared}</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
export const prerender = import.meta.env.PRERENDER;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Two</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Two</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
export const shared = 'shared';
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/nodejs-preview-headers",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Hello!
|
|
@ -1,8 +0,0 @@
|
|||
import node from '@astrojs/node'
|
||||
|
||||
export default {
|
||||
base: '/some-base',
|
||||
output: 'hybrid',
|
||||
trailingSlash: 'never',
|
||||
adapter: node({ mode: 'standalone' })
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/node-trailingslash",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
h1 { color: red; }
|
|
@ -1,8 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>One</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>One</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/url",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>URL</title>
|
||||
</head>
|
||||
<body>{Astro.url.href}</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/well-known-locations",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue