0
Fork 0
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:
Alexander Niebuhr 2024-08-30 19:43:43 +02:00 committed by GitHub
parent 8e5257adda
commit 11ebf3bd15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
249 changed files with 66 additions and 10100 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"
}

View file

@ -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:*",

View file

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/node": "workspace:*",
"@astrojs/node": "^8.3.3",
"astro": "workspace:*"
}
}

View file

@ -4,6 +4,6 @@
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
"@astrojs/node": "^8.3.3"
}
}

View file

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/node": "workspace:*",
"@astrojs/node": "^8.3.3",
"astro": "workspace:*"
}
}

View file

@ -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:*"
}

View file

@ -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

View file

@ -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) 2023present [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).

View file

@ -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"
}

View file

@ -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.`,
);
}
},
},
};
}

View file

@ -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;
}

View file

@ -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();
}
}
};
}

View file

@ -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 };

View file

@ -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);
}
};
}

View file

@ -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 + '/';
}

View file

@ -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);
}

View file

@ -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,
};
}

View file

@ -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,
];

View file

@ -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)));
});
});

View file

@ -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');
});
});

View file

@ -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!');
});
});

View file

@ -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);
});
});

View file

@ -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.');
}
},
);
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-api-route",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,3 +0,0 @@
---
return Astro.redirect('/destination', 303);
---

View file

@ -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'
}
});
}

View file

@ -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'
}
});
}

View file

@ -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'
}
});
}

View file

@ -1,5 +0,0 @@
import { APIContext } from 'astro';
export async function GET({ redirect }: APIContext) {
return redirect('/destination');
}

View file

@ -1,5 +0,0 @@
import { APIContext } from 'astro';
export async function GET({ url: requestUrl }: APIContext) {
return Response.redirect(new URL('/destination', requestUrl), 307);
}

View file

@ -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"
}
});
}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-badurls",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-encoded",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1 +0,0 @@
<h1>什么</h1>

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-errors",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,11 +0,0 @@
---
function * generator () {
yield 1
throw Error('ohnoes')
}
---
<h1>Astro</h1>
{generator()}
<footer>
Footer
</footer>

View file

@ -1,13 +0,0 @@
---
---
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
<p>
{Promise.reject('Error in the stream')}
</p>
</body>
</html>

View file

@ -1,2 +0,0 @@
{new Promise(async _ => (await {}, Astro.props.undefined.alsoAPropertyOfUndefined))}
{Astro.props.undefined.propertyOfUndefined}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,5 +0,0 @@
---
Astro.cookies.set('from1', 'astro1');
Astro.cookies.set('from2', 'astro2');
---
<p>hello world</p>

View file

@ -1,4 +0,0 @@
---
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>

View file

@ -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>

View file

@ -1,5 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=response1');
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>

View file

@ -1,5 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
Astro.response.headers.append('set-cookie', 'from2=value2');
---
<p>hello world</p>

View file

@ -1,4 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
---
<p>hello world</p>

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -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 });
}

View file

@ -1,4 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
return new Response('hello world', { headers });
}

View file

@ -1,3 +0,0 @@
export async function GET({ request }: { request: Request }) {
return new Response('hello world', { headers: undefined });
}

View file

@ -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 });
}

View file

@ -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"
}
}

View file

@ -1 +0,0 @@
this is a text file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

View file

@ -1,6 +0,0 @@
---
import { Image } from "astro:assets";
import penguin from "../assets/some_penguin.png";
---
<Image src={penguin} alt="Penguins" width={50} />

View file

@ -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>

View file

@ -1,9 +0,0 @@
{
"name": "@test/locals",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -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();
})

View file

@ -1,10 +0,0 @@
export async function POST({ locals }) {
const out = { ...locals };
return new Response(JSON.stringify(out), {
headers: {
'Content-Type': 'application/json'
}
});
}

View file

@ -1,4 +0,0 @@
---
const { foo } = Astro.locals;
---
<h1>{foo}</h1>

View file

@ -1,4 +0,0 @@
---
const { foo } = Astro.locals;
---
<h1>{foo}</h1>

View file

@ -1,9 +0,0 @@
{
"name": "@test/node-middleware",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -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>

View file

@ -1,11 +0,0 @@
---
---
<html lang="en">
<head><title>node-middleware</title></head>
<style>
</style>
<body>
<div>1</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
export async function GET() {
let number = Math.random();
return Response.json({
number,
message: `Here's a random number: ${number}`,
});
}

View file

@ -1,10 +0,0 @@
{
"name": "@test/nodejs-prerender-404-500",
"version": "0.0.0",
"private": true,
"type": "module",
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,3 +0,0 @@
body {
background-color: ivory;
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1,5 +0,0 @@
---
import message from "../nondeterminism-404"
export const prerender = true;
---
{message()}

View file

@ -1,6 +0,0 @@
---
import "../external-stylesheet.css"
import message from "../nondeterminism-500"
export const prerender = true
---
<h1>{message()}</h1>

View file

@ -1,4 +0,0 @@
---
return new Response(null, { status: 500 })
---
<p>This html will not be served</p>

View file

@ -1,12 +0,0 @@
---
export const prerender = true;
---
<html>
<head>
<title>Static Page</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-prerender",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,7 +0,0 @@
import { shared } from './shared';
export const onRequest = (ctx, next) => {
ctx.locals = {
name: shared,
};
return next();
};

View file

@ -1,10 +0,0 @@
---
---
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>

View file

@ -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>

View file

@ -1,11 +0,0 @@
---
export const prerender = import.meta.env.PRERENDER;
---
<html>
<head>
<title>Two</title>
</head>
<body>
<h1>Two</h1>
</body>
</html>

View file

@ -1 +0,0 @@
export const shared = 'shared';

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-preview-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,8 +0,0 @@
import node from '@astrojs/node'
export default {
base: '/some-base',
output: 'hybrid',
trailingSlash: 'never',
adapter: node({ mode: 'standalone' })
};

View file

@ -1,9 +0,0 @@
{
"name": "@test/node-trailingslash",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1 +0,0 @@
h1 { color: red; }

View file

@ -1,8 +0,0 @@
<html>
<head>
<title>Index</title>
</head>
<body>
<h1>Index</h1>
</body>
</html>

View file

@ -1,11 +0,0 @@
---
export const prerender = true;
---
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>

View file

@ -1,9 +0,0 @@
{
"name": "@test/url",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,9 +0,0 @@
---
---
<html lang="en">
<head>
<title>URL</title>
</head>
<body>{Astro.url.href}</body>
</html>

View file

@ -1,9 +0,0 @@
{
"name": "@test/well-known-locations",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

Some files were not shown because too many files have changed in this diff Show more