Node.js standalone mode + support for astro preview (#5056)
* wip * Deprecate buildConfig and move to config.build * Implement the standalone server * Stay backwards compat * Add changesets * correctly merge URLs * Get config earlier * update node tests * Return the preview server * update remaining tests * swap usage and config ordering * Update packages/astro/src/@types/astro.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/metal-pumas-walk.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/metal-pumas-walk.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/stupid-points-refuse.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update .changeset/stupid-points-refuse.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Link to build.server config Co-authored-by: Fred K. Schott <fkschott@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
2b7fb848bb
commit
e55af8a232
34 changed files with 1094 additions and 361 deletions
38
.changeset/cyan-paws-fry.md
Normal file
38
.changeset/cyan-paws-fry.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
'astro': minor
|
||||
'@astrojs/node': minor
|
||||
---
|
||||
|
||||
# Adapter support for `astro preview`
|
||||
|
||||
Adapters are now about to support the `astro preview` command via a new integration option. The Node.js adapter `@astrojs/node` is the first of the built-in adapters to gain support for this. What this means is that if you are using `@astrojs/node` you can new preview your SSR app by running:
|
||||
|
||||
```shell
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Adapter API
|
||||
|
||||
We will be updating the other first party Astro adapters to support preview over time. Adapters can opt-in to this feature by providing the `previewEntrypoint` via the `setAdapter` function in `astro:config:done` hook. The Node.js adapter's code looks like this:
|
||||
|
||||
```diff
|
||||
export default function() {
|
||||
return {
|
||||
name: '@astrojs/node',
|
||||
hooks: {
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
setAdapter({
|
||||
name: '@astrojs/node',
|
||||
serverEntrypoint: '@astrojs/node/server.js',
|
||||
+ previewEntrypoint: '@astrojs/node/preview.js',
|
||||
exports: ['handler'],
|
||||
});
|
||||
|
||||
// more here
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The `previewEntrypoint` is a module in the adapter's package that is a Node.js script. This script is run when `astro preview` is run and is charged with starting up the built server. See the Node.js implementation in `@astrojs/node` to see how that is implemented.
|
43
.changeset/metal-pumas-walk.md
Normal file
43
.changeset/metal-pumas-walk.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
'@astrojs/node': major
|
||||
---
|
||||
|
||||
# Standalone mode for the Node.js adapter
|
||||
|
||||
New in `@astrojs/node` is support for __standalone mode__. With standalone mode you can start your production server without needing to write any server JavaScript yourself. The server starts simply by running the script like so:
|
||||
|
||||
```shell
|
||||
node ./dist/server/entry.mjs
|
||||
```
|
||||
|
||||
To enable standalone mode, set the new `mode` to `'standalone'` option in your Astro config:
|
||||
|
||||
```js
|
||||
import { defineConfig } from 'astro/config';
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: nodejs({
|
||||
mode: 'standalone'
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
See the @astrojs/node documentation to learn all of the options available in standalone mode.
|
||||
|
||||
## Breaking change
|
||||
|
||||
This is a semver major change because the new `mode` option is required. Existing @astrojs/node users who are using their own HTTP server framework such as Express can upgrade by setting the `mode` option to `'middleware'` in order to build to a middleware mode, which is the same behavior and API as before.
|
||||
|
||||
```js
|
||||
import { defineConfig } from 'astro/config';
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: nodejs({
|
||||
mode: 'middleware'
|
||||
})
|
||||
});
|
||||
```
|
49
.changeset/stupid-points-refuse.md
Normal file
49
.changeset/stupid-points-refuse.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
'astro': minor
|
||||
'@astrojs/cloudflare': minor
|
||||
'@astrojs/deno': minor
|
||||
'@astrojs/image': minor
|
||||
'@astrojs/netlify': minor
|
||||
'@astrojs/node': minor
|
||||
'@astrojs/vercel': minor
|
||||
---
|
||||
|
||||
# New build configuration
|
||||
|
||||
The ability to customize SSR build configuration more granularly is now available in Astro. You can now customize the output folder for `server` (the server code for SSR), `client` (your client-side JavaScript and assets), and `serverEntry` (the name of the entrypoint server module). Here are the defaults:
|
||||
|
||||
```js
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
build: {
|
||||
server: './dist/server/',
|
||||
client: './dist/client/',
|
||||
serverEntry: 'entry.mjs',
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
These new configuration options are only supported in SSR mode and are ignored when building to SSG (a static site).
|
||||
|
||||
## Integration hook change
|
||||
|
||||
The integration hook `astro:build:start` includes a param `buildConfig` which includes all of these same options. You can continue to use this param in Astro 1.x, but it is deprecated in favor of the new `build.config` options. All of the built-in adapters have been updated to the new format. If you have an integration that depends on this param we suggest upgrading to do this instead:
|
||||
|
||||
```js
|
||||
export default function myIntegration() {
|
||||
return {
|
||||
name: 'my-integration',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ updateConfig }) => {
|
||||
updateConfig({
|
||||
build: {
|
||||
server: '...'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -5,6 +5,8 @@ import node from '@astrojs/node';
|
|||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node(),
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
}),
|
||||
integrations: [svelte()],
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"server": "node server/server.mjs"
|
||||
"server": "node dist/server/entry.mjs"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { createServer } from 'http';
|
||||
import fs from 'fs';
|
||||
import mime from 'mime';
|
||||
import { handler as ssrHandler } from '../dist/server/entry.mjs';
|
||||
|
||||
const clientRoot = new URL('../dist/client/', import.meta.url);
|
||||
|
||||
async function handle(req, res) {
|
||||
ssrHandler(req, res, async (err) => {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end(err.stack);
|
||||
return;
|
||||
}
|
||||
|
||||
let local = new URL('.' + req.url, clientRoot);
|
||||
try {
|
||||
const data = await fs.promises.readFile(local);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': mime.getType(req.url),
|
||||
});
|
||||
res.end(data);
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
handle(req, res).catch((err) => {
|
||||
console.error(err);
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/plain',
|
||||
});
|
||||
res.end(err.toString());
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(8085);
|
||||
console.log('Serving at http://localhost:8085');
|
||||
|
||||
// Silence weird <time> warning
|
||||
console.error = () => {};
|
|
@ -83,8 +83,17 @@ export interface CLIFlags {
|
|||
}
|
||||
|
||||
export interface BuildConfig {
|
||||
/**
|
||||
* @deprecated Use config.build.client instead.
|
||||
*/
|
||||
client: URL;
|
||||
/**
|
||||
* @deprecated Use config.build.server instead.
|
||||
*/
|
||||
server: URL;
|
||||
/**
|
||||
* @deprecated Use config.build.serverEntry instead.
|
||||
*/
|
||||
serverEntry: string;
|
||||
}
|
||||
|
||||
|
@ -381,6 +390,7 @@ export interface AstroUserConfig {
|
|||
* @name outDir
|
||||
* @type {string}
|
||||
* @default `"./dist"`
|
||||
* @see build.server
|
||||
* @description Set the directory that `astro build` writes your final build to.
|
||||
*
|
||||
* The value can be either an absolute file system path or a path relative to the project root.
|
||||
|
@ -526,6 +536,68 @@ export interface AstroUserConfig {
|
|||
* This means that when you create relative URLs using `new URL('./relative', Astro.url)`, you will get consistent behavior between dev and build.
|
||||
*/
|
||||
format?: 'file' | 'directory';
|
||||
/**
|
||||
* @docs
|
||||
* @name build.client
|
||||
* @type {string}
|
||||
* @default `'./dist/client'`
|
||||
* @description
|
||||
* Controls the output directory of your client-side CSS and JavaScript when `output: 'server'` only.
|
||||
* `outDir` controls where the code is built to.
|
||||
*
|
||||
* This value is relative to the `outDir`.
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* output: 'server',
|
||||
* build: {
|
||||
* client: './client'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
client?: string;
|
||||
/**
|
||||
* @docs
|
||||
* @name build.server
|
||||
* @type {string}
|
||||
* @default `'./dist/server'`
|
||||
* @description
|
||||
* Controls the output directory of server JavaScript when building to SSR.
|
||||
*
|
||||
* This value is relative to the `outDir`.
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* build: {
|
||||
* server: './server'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
server?: string;
|
||||
/**
|
||||
* @docs
|
||||
* @name build.serverEntry
|
||||
* @type {string}
|
||||
* @default `'entry.mjs'`
|
||||
* @description
|
||||
* Specifies the file name of the server entrypoint when building to SSR.
|
||||
* This entrypoint is usually dependent on which host you are deploying to and
|
||||
* will be set by your adapter for you.
|
||||
*
|
||||
* Note that it is recommended that this file ends with `.mjs` so that the runtime
|
||||
* detects that the file is a JavaScript module.
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* build: {
|
||||
* serverEntry: 'main.mjs'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
serverEntry?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1073,6 +1145,7 @@ export type Params = Record<string, string | number | undefined>;
|
|||
export interface AstroAdapter {
|
||||
name: string;
|
||||
serverEntrypoint?: string;
|
||||
previewEntrypoint?: string;
|
||||
exports?: string[];
|
||||
args?: any;
|
||||
}
|
||||
|
@ -1234,7 +1307,7 @@ export interface AstroIntegration {
|
|||
hooks: {
|
||||
'astro:config:setup'?: (options: {
|
||||
config: AstroConfig;
|
||||
command: 'dev' | 'build';
|
||||
command: 'dev' | 'build' | 'preview';
|
||||
isRestart: boolean;
|
||||
updateConfig: (newConfig: Record<string, any>) => void;
|
||||
addRenderer: (renderer: AstroRenderer) => void;
|
||||
|
@ -1332,3 +1405,25 @@ export interface SSRResult {
|
|||
}
|
||||
|
||||
export type MarkdownAstroData = { frontmatter: object };
|
||||
|
||||
/* Preview server stuff */
|
||||
export interface PreviewServer {
|
||||
host?: string;
|
||||
port: number;
|
||||
closed(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PreviewServerParams {
|
||||
outDir: URL;
|
||||
client: URL;
|
||||
serverEntrypoint: URL;
|
||||
host: string | undefined;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export type CreatePreviewServer = (params: PreviewServerParams) => PreviewServer | Promise<PreviewServer>;
|
||||
|
||||
export interface PreviewModule {
|
||||
default: CreatePreviewServer;
|
||||
}
|
||||
|
|
|
@ -87,9 +87,9 @@ class AstroBuilder {
|
|||
/** Run the build logic. build() is marked private because usage should go through ".run()" */
|
||||
private async build({ viteConfig }: { viteConfig: vite.InlineConfig }) {
|
||||
const buildConfig: BuildConfig = {
|
||||
client: new URL('./client/', this.settings.config.outDir),
|
||||
server: new URL('./server/', this.settings.config.outDir),
|
||||
serverEntry: 'entry.mjs',
|
||||
client: this.settings.config.build.client,
|
||||
server: this.settings.config.build.server,
|
||||
serverEntry: this.settings.config.build.serverEntry,
|
||||
};
|
||||
await runHookBuildStart({ config: this.settings.config, buildConfig, logging: this.logging });
|
||||
this.validateConfig();
|
||||
|
|
|
@ -10,7 +10,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
|
|||
import * as vite from 'vite';
|
||||
import { mergeConfig as mergeViteConfig } from 'vite';
|
||||
import { LogOptions } from '../logger/core.js';
|
||||
import { arraify, isObject } from '../util.js';
|
||||
import { arraify, isObject, isURL } from '../util.js';
|
||||
import { createRelativeSchema } from './schema.js';
|
||||
|
||||
load.use([loadTypeScript]);
|
||||
|
@ -346,6 +346,10 @@ function mergeConfigRecursively(
|
|||
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
|
||||
continue;
|
||||
}
|
||||
if(isURL(existing) && isURL(value)) {
|
||||
merged[key] = value;
|
||||
continue;
|
||||
}
|
||||
if (isObject(existing) && isObject(value)) {
|
||||
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
|
||||
continue;
|
||||
|
|
|
@ -17,7 +17,12 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
|||
outDir: './dist',
|
||||
base: '/',
|
||||
trailingSlash: 'ignore',
|
||||
build: { format: 'directory' },
|
||||
build: {
|
||||
format: 'directory',
|
||||
client: './dist/client/',
|
||||
server: './dist/server/',
|
||||
serverEntry: 'entry.mjs'
|
||||
},
|
||||
server: {
|
||||
host: false,
|
||||
port: 3000,
|
||||
|
@ -97,6 +102,20 @@ export const AstroConfigSchema = z.object({
|
|||
.union([z.literal('file'), z.literal('directory')])
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.format),
|
||||
client: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.client)
|
||||
.transform((val) => new URL(val)),
|
||||
server: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.server)
|
||||
.transform((val) => new URL(val)),
|
||||
serverEntry: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
@ -233,6 +252,28 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
|||
.string()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.outDir)
|
||||
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
|
||||
build: z.object({
|
||||
format: z
|
||||
.union([z.literal('file'), z.literal('directory')])
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.format),
|
||||
client: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.client)
|
||||
.transform(val => new URL(val, fileProtocolRoot)),
|
||||
server: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.server)
|
||||
.transform(val => new URL(val, fileProtocolRoot)),
|
||||
serverEntry: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
server: z.preprocess(
|
||||
// preprocess
|
||||
(val) =>
|
||||
|
@ -265,6 +306,16 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
|||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
}).transform(config => {
|
||||
// If the user changed outDir but not build.server, build.config, adjust so those
|
||||
// are relative to the outDir, as is the expected default.
|
||||
if(!config.build.server.toString().startsWith(config.outDir.toString()) && config.build.server.toString().endsWith('dist/server/')) {
|
||||
config.build.server = new URL('./dist/server/', config.outDir);
|
||||
}
|
||||
if(!config.build.client.toString().startsWith(config.outDir.toString()) && config.build.client.toString().endsWith('dist/client/')) {
|
||||
config.build.client = new URL('./dist/client/', config.outDir);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
return AstroConfigRelativeSchema;
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
import type { AstroTelemetry } from '@astrojs/telemetry';
|
||||
import type { AddressInfo } from 'net';
|
||||
import type { AstroSettings } from '../../@types/astro';
|
||||
import type { AstroSettings, PreviewModule, PreviewServer } from '../../@types/astro';
|
||||
import { runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js';
|
||||
import type { LogOptions } from '../logger/core';
|
||||
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import { performance } from 'perf_hooks';
|
||||
import sirv from 'sirv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
|
||||
import { error, info } from '../logger/core.js';
|
||||
import * as msg from '../messages.js';
|
||||
import createStaticPreviewServer from './static-preview-server.js';
|
||||
import { createRequire } from 'module';
|
||||
import { getResolvedHostForHttpServer } from './util.js';
|
||||
|
||||
interface PreviewOptions {
|
||||
|
@ -18,162 +11,48 @@ interface PreviewOptions {
|
|||
telemetry: AstroTelemetry;
|
||||
}
|
||||
|
||||
export interface PreviewServer {
|
||||
host?: string;
|
||||
port: number;
|
||||
server: http.Server;
|
||||
closed(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
|
||||
|
||||
/** The primary dev action */
|
||||
export default async function preview(
|
||||
settings: AstroSettings,
|
||||
_settings: AstroSettings,
|
||||
{ logging }: PreviewOptions
|
||||
): Promise<PreviewServer> {
|
||||
if (settings.config.output === 'server') {
|
||||
throw new Error(
|
||||
`[preview] 'output: server' not supported. Use your deploy platform's preview command directly instead, if one exists. (ex: 'netlify dev', 'vercel dev', 'wrangler', etc.)`
|
||||
);
|
||||
}
|
||||
const startServerTime = performance.now();
|
||||
const defaultOrigin = 'http://localhost';
|
||||
const trailingSlash = settings.config.trailingSlash;
|
||||
/** Base request URL. */
|
||||
let baseURL = new URL(settings.config.base, new URL(settings.config.site || '/', defaultOrigin));
|
||||
const staticFileServer = sirv(fileURLToPath(settings.config.outDir), {
|
||||
dev: true,
|
||||
etag: true,
|
||||
maxAge: 0,
|
||||
const settings = await runHookConfigSetup({
|
||||
settings: _settings,
|
||||
command: 'preview',
|
||||
logging: logging,
|
||||
});
|
||||
// Create the preview server, send static files out of the `dist/` directory.
|
||||
const server = http.createServer((req, res) => {
|
||||
const requestURL = new URL(req.url as string, defaultOrigin);
|
||||
|
||||
// respond 404 to requests outside the base request directory
|
||||
if (!requestURL.pathname.startsWith(baseURL.pathname)) {
|
||||
res.statusCode = 404;
|
||||
res.end(subpathNotUsedTemplate(baseURL.pathname, requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
|
||||
/** Relative request path. */
|
||||
const pathname = requestURL.pathname.slice(baseURL.pathname.length - 1);
|
||||
|
||||
const isRoot = pathname === '/';
|
||||
const hasTrailingSlash = isRoot || pathname.endsWith('/');
|
||||
|
||||
function sendError(message: string) {
|
||||
res.statusCode = 404;
|
||||
res.end(notFoundTemplate(pathname, message));
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case hasTrailingSlash && trailingSlash == 'never' && !isRoot:
|
||||
sendError('Not Found (trailingSlash is set to "never")');
|
||||
return;
|
||||
case !hasTrailingSlash &&
|
||||
trailingSlash == 'always' &&
|
||||
!isRoot &&
|
||||
!HAS_FILE_EXTENSION_REGEXP.test(pathname):
|
||||
sendError('Not Found (trailingSlash is set to "always")');
|
||||
return;
|
||||
default: {
|
||||
// HACK: rewrite req.url so that sirv finds the file
|
||||
req.url = '/' + req.url?.replace(baseURL.pathname, '');
|
||||
staticFileServer(req, res, () => {
|
||||
const errorPagePath = fileURLToPath(settings.config.outDir + '/404.html');
|
||||
if (fs.existsSync(errorPagePath)) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader('Content-Type', 'text/html;charset=utf-8');
|
||||
res.end(fs.readFileSync(errorPagePath));
|
||||
} else {
|
||||
staticFileServer(req, res, () => {
|
||||
sendError('Not Found');
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let { port } = settings.config.server;
|
||||
await runHookConfigDone({ settings: settings, logging: logging });
|
||||
const host = getResolvedHostForHttpServer(settings.config.server.host);
|
||||
const { port } = settings.config.server;
|
||||
|
||||
let httpServer: http.Server;
|
||||
if (settings.config.output === 'static') {
|
||||
const server = await createStaticPreviewServer(settings, { logging, host, port });
|
||||
return server;
|
||||
}
|
||||
if (!settings.adapter) {
|
||||
throw new Error(`[preview] No adapter found.`);
|
||||
}
|
||||
if (!settings.adapter.previewEntrypoint) {
|
||||
throw new Error(`[preview] adapter does not have previewEntrypoint.`);
|
||||
}
|
||||
// We need to use require.resolve() here so that advanced package managers like pnpm
|
||||
// don't treat this as a dependency of Astro itself. This correctly resolves the
|
||||
// preview entrypoint of the integration package, relative to the user's project root.
|
||||
const require = createRequire(settings.config.root);
|
||||
const previewEntrypoint = require.resolve(settings.adapter.previewEntrypoint);
|
||||
|
||||
/** Expose dev server to `port` */
|
||||
function startServer(timerStart: number): Promise<void> {
|
||||
let showedPortTakenMsg = false;
|
||||
let showedListenMsg = false;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const listen = () => {
|
||||
httpServer = server.listen(port, host, async () => {
|
||||
if (!showedListenMsg) {
|
||||
const resolvedUrls = msg.resolveServerUrls({
|
||||
address: server.address() as AddressInfo,
|
||||
host: settings.config.server.host,
|
||||
https: false,
|
||||
});
|
||||
info(
|
||||
logging,
|
||||
null,
|
||||
msg.serverStart({
|
||||
startupTime: performance.now() - timerStart,
|
||||
resolvedUrls,
|
||||
host: settings.config.server.host,
|
||||
site: baseURL,
|
||||
})
|
||||
);
|
||||
}
|
||||
showedListenMsg = true;
|
||||
resolve();
|
||||
});
|
||||
httpServer?.on('error', onError);
|
||||
};
|
||||
|
||||
const onError = (err: NodeJS.ErrnoException) => {
|
||||
if (err.code && err.code === 'EADDRINUSE') {
|
||||
if (!showedPortTakenMsg) {
|
||||
info(logging, 'astro', msg.portInUse({ port }));
|
||||
showedPortTakenMsg = true; // only print this once
|
||||
}
|
||||
port++;
|
||||
return listen(); // retry
|
||||
} else {
|
||||
error(logging, 'astro', err.stack || err.message);
|
||||
httpServer?.removeListener('error', onError);
|
||||
reject(err); // reject
|
||||
}
|
||||
};
|
||||
|
||||
listen();
|
||||
});
|
||||
const previewModule = (await import(previewEntrypoint)) as Partial<PreviewModule>;
|
||||
if(typeof previewModule.default !== 'function') {
|
||||
throw new Error(`[preview] ${settings.adapter.name} cannot preview your app.`);
|
||||
}
|
||||
|
||||
// Start listening on `hostname:port`.
|
||||
await startServer(startServerTime);
|
||||
|
||||
// Resolves once the server is closed
|
||||
function closed() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
httpServer!.addListener('close', resolve);
|
||||
httpServer!.addListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
const server = await previewModule.default({
|
||||
outDir: settings.config.outDir,
|
||||
client: settings.config.build.client,
|
||||
serverEntrypoint: new URL(settings.config.build.serverEntry, settings.config.build.server),
|
||||
host,
|
||||
port,
|
||||
closed,
|
||||
server: httpServer!,
|
||||
stop: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
|
||||
});
|
||||
},
|
||||
};
|
||||
port
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
164
packages/astro/src/core/preview/static-preview-server.ts
Normal file
164
packages/astro/src/core/preview/static-preview-server.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
import type { AddressInfo } from 'net';
|
||||
import type { AstroSettings } from '../../@types/astro';
|
||||
import type { LogOptions } from '../logger/core';
|
||||
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import { performance } from 'perf_hooks';
|
||||
import sirv from 'sirv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js';
|
||||
import { error, info } from '../logger/core.js';
|
||||
import * as msg from '../messages.js';
|
||||
|
||||
export interface PreviewServer {
|
||||
host?: string;
|
||||
port: number;
|
||||
server: http.Server;
|
||||
closed(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
|
||||
|
||||
/** The primary dev action */
|
||||
export default async function createStaticPreviewServer(
|
||||
settings: AstroSettings,
|
||||
{ logging, host, port }: { logging: LogOptions; host: string | undefined; port: number }
|
||||
): Promise<PreviewServer> {
|
||||
const startServerTime = performance.now();
|
||||
const defaultOrigin = 'http://localhost';
|
||||
const trailingSlash = settings.config.trailingSlash;
|
||||
/** Base request URL. */
|
||||
let baseURL = new URL(settings.config.base, new URL(settings.config.site || '/', defaultOrigin));
|
||||
const staticFileServer = sirv(fileURLToPath(settings.config.outDir), {
|
||||
dev: true,
|
||||
etag: true,
|
||||
maxAge: 0,
|
||||
});
|
||||
// Create the preview server, send static files out of the `dist/` directory.
|
||||
const server = http.createServer((req, res) => {
|
||||
const requestURL = new URL(req.url as string, defaultOrigin);
|
||||
|
||||
// respond 404 to requests outside the base request directory
|
||||
if (!requestURL.pathname.startsWith(baseURL.pathname)) {
|
||||
res.statusCode = 404;
|
||||
res.end(subpathNotUsedTemplate(baseURL.pathname, requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
|
||||
/** Relative request path. */
|
||||
const pathname = requestURL.pathname.slice(baseURL.pathname.length - 1);
|
||||
|
||||
const isRoot = pathname === '/';
|
||||
const hasTrailingSlash = isRoot || pathname.endsWith('/');
|
||||
|
||||
function sendError(message: string) {
|
||||
res.statusCode = 404;
|
||||
res.end(notFoundTemplate(pathname, message));
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case hasTrailingSlash && trailingSlash == 'never' && !isRoot:
|
||||
sendError('Not Found (trailingSlash is set to "never")');
|
||||
return;
|
||||
case !hasTrailingSlash &&
|
||||
trailingSlash == 'always' &&
|
||||
!isRoot &&
|
||||
!HAS_FILE_EXTENSION_REGEXP.test(pathname):
|
||||
sendError('Not Found (trailingSlash is set to "always")');
|
||||
return;
|
||||
default: {
|
||||
// HACK: rewrite req.url so that sirv finds the file
|
||||
req.url = '/' + req.url?.replace(baseURL.pathname, '');
|
||||
staticFileServer(req, res, () => {
|
||||
const errorPagePath = fileURLToPath(settings.config.outDir + '/404.html');
|
||||
if (fs.existsSync(errorPagePath)) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader('Content-Type', 'text/html;charset=utf-8');
|
||||
res.end(fs.readFileSync(errorPagePath));
|
||||
} else {
|
||||
staticFileServer(req, res, () => {
|
||||
sendError('Not Found');
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let httpServer: http.Server;
|
||||
|
||||
/** Expose dev server to `port` */
|
||||
function startServer(timerStart: number): Promise<void> {
|
||||
let showedPortTakenMsg = false;
|
||||
let showedListenMsg = false;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const listen = () => {
|
||||
httpServer = server.listen(port, host, async () => {
|
||||
if (!showedListenMsg) {
|
||||
const resolvedUrls = msg.resolveServerUrls({
|
||||
address: server.address() as AddressInfo,
|
||||
host: settings.config.server.host,
|
||||
https: false,
|
||||
});
|
||||
info(
|
||||
logging,
|
||||
null,
|
||||
msg.serverStart({
|
||||
startupTime: performance.now() - timerStart,
|
||||
resolvedUrls,
|
||||
host: settings.config.server.host,
|
||||
site: baseURL,
|
||||
})
|
||||
);
|
||||
}
|
||||
showedListenMsg = true;
|
||||
resolve();
|
||||
});
|
||||
httpServer?.on('error', onError);
|
||||
};
|
||||
|
||||
const onError = (err: NodeJS.ErrnoException) => {
|
||||
if (err.code && err.code === 'EADDRINUSE') {
|
||||
if (!showedPortTakenMsg) {
|
||||
info(logging, 'astro', msg.portInUse({ port }));
|
||||
showedPortTakenMsg = true; // only print this once
|
||||
}
|
||||
port++;
|
||||
return listen(); // retry
|
||||
} else {
|
||||
error(logging, 'astro', err.stack || err.message);
|
||||
httpServer?.removeListener('error', onError);
|
||||
reject(err); // reject
|
||||
}
|
||||
};
|
||||
|
||||
listen();
|
||||
});
|
||||
}
|
||||
|
||||
// Start listening on `hostname:port`.
|
||||
await startServer(startServerTime);
|
||||
|
||||
// Resolves once the server is closed
|
||||
function closed() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
httpServer!.addListener('close', resolve);
|
||||
httpServer!.addListener('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
closed,
|
||||
server: httpServer!,
|
||||
stop: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -13,6 +13,11 @@ export function isObject(value: unknown): value is Record<string, any> {
|
|||
return typeof value === 'object' && value != null;
|
||||
}
|
||||
|
||||
/** Cross-realm compatible URL */
|
||||
export function isURL(value: unknown): value is URL {
|
||||
return Object.prototype.toString.call(value) === '[object URL]';
|
||||
}
|
||||
|
||||
/** Wraps an object in an array. If an array is passed, ignore it. */
|
||||
export function arraify<T>(target: T | T[]): T[] {
|
||||
return Array.isArray(target) ? target : [target];
|
||||
|
|
|
@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import type { InlineConfig, ViteDevServer } from 'vite';
|
||||
import {
|
||||
AstroConfig,
|
||||
AstroIntegration,
|
||||
AstroRenderer,
|
||||
AstroSettings,
|
||||
BuildConfig,
|
||||
|
@ -13,7 +14,7 @@ import {
|
|||
import type { SerializedSSRManifest } from '../core/app/types';
|
||||
import type { PageBuildData } from '../core/build/types';
|
||||
import { mergeConfig } from '../core/config/config.js';
|
||||
import { info, LogOptions } from '../core/logger/core.js';
|
||||
import { info, LogOptions, warn } from '../core/logger/core.js';
|
||||
|
||||
async function withTakingALongTimeMsg<T>({
|
||||
name,
|
||||
|
@ -41,7 +42,7 @@ export async function runHookConfigSetup({
|
|||
isRestart = false,
|
||||
}: {
|
||||
settings: AstroSettings;
|
||||
command: 'dev' | 'build';
|
||||
command: 'dev' | 'build' | 'preview';
|
||||
logging: LogOptions;
|
||||
isRestart?: boolean;
|
||||
}): Promise<AstroSettings> {
|
||||
|
@ -211,13 +212,40 @@ export async function runHookBuildStart({
|
|||
buildConfig: BuildConfig;
|
||||
logging: LogOptions;
|
||||
}) {
|
||||
function warnDeprecated(integration: AstroIntegration, prop: 'server' | 'client' | 'serverEntry') {
|
||||
let value: any = Reflect.get(buildConfig, prop);
|
||||
Object.defineProperty(buildConfig, prop, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newValue) {
|
||||
value = newValue;
|
||||
warn(logging, 'astro:build:start', `Your adapter ${bold(integration.name)} is using a deprecated API, buildConfig. ${bold(prop)} config should be set via config.build.${prop} instead.`);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
Object.defineProperty(buildConfig, prop, {
|
||||
enumerable: true,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const integration of config.integrations) {
|
||||
if (integration?.hooks?.['astro:build:start']) {
|
||||
const undoClientWarning = warnDeprecated(integration, 'client');
|
||||
const undoServerWarning = warnDeprecated(integration, 'server');
|
||||
const undoServerEntryWarning = warnDeprecated(integration, 'serverEntry');
|
||||
await withTakingALongTimeMsg({
|
||||
name: integration.name,
|
||||
hookResult: integration.hooks['astro:build:start']({ buildConfig }),
|
||||
logging,
|
||||
});
|
||||
undoClientWarning();
|
||||
undoServerEntryWarning();
|
||||
undoServerWarning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@ import nodejs from '@astrojs/node';
|
|||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: nodejs(),
|
||||
adapter: nodejs({ mode: 'middleware' }),
|
||||
});
|
||||
|
|
|
@ -2,6 +2,6 @@ import { defineConfig } from 'astro/config';
|
|||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: nodejs(),
|
||||
adapter: nodejs({ mode: 'middleware' }),
|
||||
output: 'server',
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroAdapter, AstroConfig, AstroIntegration, BuildConfig } from 'astro';
|
||||
import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro';
|
||||
import esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
@ -7,6 +7,12 @@ type Options = {
|
|||
mode: 'directory' | 'advanced';
|
||||
};
|
||||
|
||||
interface BuildConfig {
|
||||
server: URL;
|
||||
client: URL;
|
||||
serverEntry: string;
|
||||
}
|
||||
|
||||
export function getAdapter(isModeDirectory: boolean): AstroAdapter {
|
||||
return isModeDirectory
|
||||
? {
|
||||
|
@ -29,14 +35,26 @@ const SHIM = `globalThis.process = {
|
|||
export default function createIntegration(args?: Options): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
let _buildConfig: BuildConfig;
|
||||
let needsBuildConfig = false;
|
||||
const isModeDirectory = args?.mode === 'directory';
|
||||
|
||||
return {
|
||||
name: '@astrojs/cloudflare',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ config, updateConfig }) => {
|
||||
needsBuildConfig = !config.build.client;
|
||||
updateConfig({
|
||||
build: {
|
||||
client: new URL('./static/', config.outDir),
|
||||
server: new URL('./', config.outDir),
|
||||
serverEntry: '_worker.js',
|
||||
}
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
setAdapter(getAdapter(isModeDirectory));
|
||||
_config = config;
|
||||
_buildConfig = config.build;
|
||||
|
||||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
|
@ -45,12 +63,6 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
`);
|
||||
}
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
_buildConfig = buildConfig;
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
buildConfig.serverEntry = '_worker.js';
|
||||
buildConfig.server = new URL('./', _config.outDir);
|
||||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
vite.resolve = vite.resolve || {};
|
||||
|
@ -69,6 +81,14 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
vite.ssr.target = vite.ssr.target || 'webworker';
|
||||
}
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
// Backwards compat
|
||||
if(needsBuildConfig) {
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
buildConfig.server = new URL('./', _config.outDir);
|
||||
buildConfig.serverEntry = '_worker.js';
|
||||
}
|
||||
},
|
||||
'astro:build:done': async () => {
|
||||
const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server);
|
||||
const pkg = fileURLToPath(entryUrl);
|
||||
|
|
|
@ -4,6 +4,11 @@ import * as fs from 'fs';
|
|||
import * as npath from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
interface BuildConfig {
|
||||
server: URL;
|
||||
serverEntry: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
|
@ -24,13 +29,16 @@ export function getAdapter(args?: Options): AstroAdapter {
|
|||
}
|
||||
|
||||
export default function createIntegration(args?: Options): AstroIntegration {
|
||||
let _buildConfig: any;
|
||||
let _buildConfig: BuildConfig;
|
||||
let _vite: any;
|
||||
let needsBuildConfig = false;
|
||||
return {
|
||||
name: '@astrojs/deno',
|
||||
hooks: {
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
needsBuildConfig = !config.build.client;
|
||||
setAdapter(getAdapter(args));
|
||||
_buildConfig = config.build;
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/deno] \`output: "server"\` is required to use this adapter.`);
|
||||
|
@ -40,7 +48,10 @@ export default function createIntegration(args?: Options): AstroIntegration {
|
|||
}
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
_buildConfig = buildConfig;
|
||||
// Backwards compat
|
||||
if(needsBuildConfig) {
|
||||
_buildConfig = buildConfig;
|
||||
}
|
||||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroConfig, AstroIntegration, BuildConfig } from 'astro';
|
||||
import type { AstroConfig, AstroIntegration } from 'astro';
|
||||
import { ssgBuild } from './build/ssg.js';
|
||||
import type { ImageService, SSRImageService, TransformOptions } from './loaders/index.js';
|
||||
import type { LoggerLevel } from './utils/logger.js';
|
||||
|
@ -12,6 +12,11 @@ export { getPicture } from './lib/get-picture.js';
|
|||
const PKG_NAME = '@astrojs/image';
|
||||
const ROUTE_PATTERN = '/_image';
|
||||
|
||||
interface BuildConfig {
|
||||
client: URL;
|
||||
server: URL;
|
||||
}
|
||||
|
||||
interface ImageIntegration {
|
||||
loader?: ImageService;
|
||||
defaultLoader: SSRImageService;
|
||||
|
@ -42,6 +47,7 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
|
||||
let _config: AstroConfig;
|
||||
let _buildConfig: BuildConfig;
|
||||
let needsBuildConfig = false;
|
||||
|
||||
// During SSG builds, this is used to track all transformed images required.
|
||||
const staticImages = new Map<string, Map<string, TransformOptions>>();
|
||||
|
@ -67,8 +73,8 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
name: PKG_NAME,
|
||||
hooks: {
|
||||
'astro:config:setup': async ({ command, config, updateConfig, injectRoute }) => {
|
||||
needsBuildConfig = !config.build?.server;
|
||||
_config = config;
|
||||
|
||||
updateConfig({ vite: getViteConfiguration() });
|
||||
|
||||
if (command === 'dev' || config.output === 'server') {
|
||||
|
@ -88,8 +94,15 @@ export default function integration(options: IntegrationOptions = {}): AstroInte
|
|||
defaultLoader,
|
||||
};
|
||||
},
|
||||
'astro:build:start': async ({ buildConfig }) => {
|
||||
_buildConfig = buildConfig;
|
||||
'astro:config:done': ({ config }) => {
|
||||
_config = config;
|
||||
_buildConfig = config.build;
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
// Backwards compat
|
||||
if(needsBuildConfig) {
|
||||
_buildConfig = buildConfig;
|
||||
}
|
||||
},
|
||||
'astro:build:setup': async () => {
|
||||
// Used to cache all images rendered to HTML
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AstroAdapter, AstroConfig, AstroIntegration, BuildConfig, RouteData } from 'astro';
|
||||
import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro';
|
||||
import esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as npath from 'path';
|
||||
|
@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
|
|||
import type { Plugin as VitePlugin } from 'vite';
|
||||
import { createRedirects } from './shared.js';
|
||||
|
||||
interface BuildConfig {
|
||||
server: URL;
|
||||
client: URL;
|
||||
serverEntry: string;
|
||||
}
|
||||
|
||||
const SHIM = `globalThis.process = {
|
||||
argv: [],
|
||||
env: {},
|
||||
|
@ -74,8 +80,8 @@ async function createEdgeManifest(routes: RouteData[], entryFile: string, dir: U
|
|||
await fs.promises.writeFile(manifestURL, _manifest, 'utf-8');
|
||||
}
|
||||
|
||||
async function bundleServerEntry(buildConfig: BuildConfig, vite: any) {
|
||||
const entryUrl = new URL(buildConfig.serverEntry, buildConfig.server);
|
||||
async function bundleServerEntry({ serverEntry, server }: BuildConfig, vite: any) {
|
||||
const entryUrl = new URL(serverEntry, server);
|
||||
const pth = fileURLToPath(entryUrl);
|
||||
await esbuild.build({
|
||||
target: 'es2020',
|
||||
|
@ -96,7 +102,7 @@ async function bundleServerEntry(buildConfig: BuildConfig, vite: any) {
|
|||
const chunkFileNames =
|
||||
vite?.build?.rollupOptions?.output?.chunkFileNames ?? 'chunks/chunk.[hash].mjs';
|
||||
const chunkPath = npath.dirname(chunkFileNames);
|
||||
const chunksDirUrl = new URL(chunkPath + '/', buildConfig.server);
|
||||
const chunksDirUrl = new URL(chunkPath + '/', server);
|
||||
await fs.promises.rm(chunksDirUrl, { recursive: true, force: true });
|
||||
} catch {}
|
||||
}
|
||||
|
@ -105,17 +111,13 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
let _config: AstroConfig;
|
||||
let entryFile: string;
|
||||
let _buildConfig: BuildConfig;
|
||||
let needsBuildConfig = false;
|
||||
let _vite: any;
|
||||
return {
|
||||
name: '@astrojs/netlify/edge-functions',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ config, updateConfig }) => {
|
||||
if (dist) {
|
||||
config.outDir = dist;
|
||||
} else {
|
||||
config.outDir = new URL('./dist/', config.root);
|
||||
}
|
||||
|
||||
needsBuildConfig = !config.build.client;
|
||||
// Add a plugin that shims the global environment.
|
||||
const injectPlugin: VitePlugin = {
|
||||
name: '@astrojs/netlify/plugin-inject',
|
||||
|
@ -128,8 +130,14 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
}
|
||||
},
|
||||
};
|
||||
|
||||
const outDir = dist ?? new URL('./dist/', config.root);
|
||||
updateConfig({
|
||||
outDir,
|
||||
build: {
|
||||
client: outDir,
|
||||
server: new URL('./.netlify/edge-functions/', config.root),
|
||||
serverEntry: 'entry.js',
|
||||
},
|
||||
vite: {
|
||||
plugins: [injectPlugin],
|
||||
},
|
||||
|
@ -138,6 +146,8 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
'astro:config:done': ({ config, setAdapter }) => {
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
_buildConfig = config.build;
|
||||
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/netlify] \`output: "server"\` is required to use this adapter.`);
|
||||
|
@ -146,12 +156,14 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
);
|
||||
}
|
||||
},
|
||||
'astro:build:start': async ({ buildConfig }) => {
|
||||
_buildConfig = buildConfig;
|
||||
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
|
||||
buildConfig.client = _config.outDir;
|
||||
buildConfig.server = new URL('./.netlify/edge-functions/', _config.root);
|
||||
buildConfig.serverEntry = 'entry.js';
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
if(needsBuildConfig) {
|
||||
buildConfig.client = _config.outDir;
|
||||
buildConfig.server = new URL('./.netlify/edge-functions/', _config.root);
|
||||
buildConfig.serverEntry = 'entry.js';
|
||||
_buildConfig = buildConfig;
|
||||
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
|
||||
}
|
||||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
|
|
|
@ -22,19 +22,25 @@ function netlifyFunctions({
|
|||
}: NetlifyFunctionsOptions = {}): AstroIntegration {
|
||||
let _config: AstroConfig;
|
||||
let entryFile: string;
|
||||
let needsBuildConfig = false;
|
||||
return {
|
||||
name: '@astrojs/netlify',
|
||||
hooks: {
|
||||
'astro:config:setup': ({ config }) => {
|
||||
if (dist) {
|
||||
config.outDir = dist;
|
||||
} else {
|
||||
config.outDir = new URL('./dist/', config.root);
|
||||
}
|
||||
'astro:config:setup': ({ config, updateConfig }) => {
|
||||
needsBuildConfig = !config.build.client;
|
||||
const outDir = dist ?? new URL('./dist/', config.root);
|
||||
updateConfig({
|
||||
outDir,
|
||||
build: {
|
||||
client: outDir,
|
||||
server: new URL('./.netlify/functions-internal/', config.root),
|
||||
}
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ config, setAdapter }) => {
|
||||
setAdapter(getAdapter({ binaryMediaTypes }));
|
||||
_config = config;
|
||||
entryFile = config.build.serverEntry.replace(/\.m?js/, '');
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/netlify] \`output: "server"\` is required to use this adapter.`);
|
||||
|
@ -43,10 +49,12 @@ function netlifyFunctions({
|
|||
);
|
||||
}
|
||||
},
|
||||
'astro:build:start': async ({ buildConfig }) => {
|
||||
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
|
||||
buildConfig.client = _config.outDir;
|
||||
buildConfig.server = new URL('./.netlify/functions-internal/', _config.root);
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
if(needsBuildConfig) {
|
||||
buildConfig.client = _config.outDir;
|
||||
buildConfig.server = new URL('./.netlify/functions-internal/', _config.root);
|
||||
entryFile = buildConfig.serverEntry.replace(/\.m?js/, '');
|
||||
}
|
||||
},
|
||||
'astro:build:done': async ({ routes, dir }) => {
|
||||
await createRedirects(routes, dir, entryFile, false);
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
# @astrojs/node 🔲
|
||||
# @astrojs/node
|
||||
|
||||
This adapter allows Astro to deploy your SSR site to Node targets.
|
||||
|
||||
- <strong>[Why Astro Node](#why-astro-node)</strong>
|
||||
- <strong>[Installation](#installation)</strong>
|
||||
- <strong>[Usage](#usage)</strong>
|
||||
- <strong>[Configuration](#configuration)</strong>
|
||||
- <strong>[Usage](#usage)</strong>
|
||||
- <strong>[Troubleshooting](#troubleshooting)</strong>
|
||||
- <strong>[Contributing](#contributing)</strong>
|
||||
- <strong>[Changelog](#changelog)</strong>
|
||||
|
||||
|
||||
## Why Astro Node
|
||||
## Why @astrojs/node
|
||||
|
||||
If you're using Astro as a static site builder—its behavior out of the box—you don't need an adapter.
|
||||
|
||||
If you wish to [use server-side rendering (SSR)](https://docs.astro.build/en/guides/server-side-rendering/), Astro requires an adapter that matches your deployment runtime.
|
||||
|
||||
[Node](https://nodejs.org/en/) is a JavaScript runtime for server-side code. Frameworks like [Express](https://expressjs.com/) are built on top of it and make it easier to write server applications in Node. This adapter provides access to Node's API and creates a script to run your Astro project that can be utilized in Node applications.
|
||||
[Node.js](https://nodejs.org/en/) is a JavaScript runtime for server-side code. @astrojs/node can be used either in standalone mode or as middleware for other http servers, such as [Express](https://expressjs.com/).
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -42,23 +42,47 @@ If you prefer to install the adapter manually instead, complete the following tw
|
|||
|
||||
1. Add two new lines to your `astro.config.mjs` project configuration file.
|
||||
|
||||
```js title="astro.config.mjs" ins={2, 5-6}
|
||||
```js title="astro.config.mjs" ins={2, 5-8}
|
||||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node(),
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@astrojs/node can be configured by passing options into the adapter function. The following options are available:
|
||||
|
||||
### Mode
|
||||
|
||||
Controls whether the adapter builds to `middleware` or `standalone` mode.
|
||||
|
||||
- `middleware` mode allows the built output to be used as middleware for another Node.js server, like Express.js or Fastify.
|
||||
```js
|
||||
import { defineConfig } from 'astro/config';
|
||||
import nodejs from '@astrojs/node';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'middleware'
|
||||
}),
|
||||
});
|
||||
```
|
||||
- `standalone` mode builds to server that automatically starts with the entry module is run. This allows you to more easily deploy your build to a host without any additional code.
|
||||
|
||||
## Usage
|
||||
|
||||
After [performing a build](https://docs.astro.build/en/guides/deploy/#building-your-site-locally) there will be a `dist/server/entry.mjs` module that exposes a `handler` function. This works like a [middleware](https://expressjs.com/en/guide/using-middleware.html) function: it can handle incoming requests and respond accordingly.
|
||||
First, [performing a build](https://docs.astro.build/en/guides/deploy/#building-your-site-locally). Depending on which `mode` selected (see above) follow the appropriate steps below:
|
||||
|
||||
### Middleware
|
||||
|
||||
### Using a middleware framework
|
||||
You can use this `handler` with any framework that supports the Node `request` and `response` objects.
|
||||
The server entrypoint is built to `./dist/server/entry.mjs` by default. This module exports a `handler` function that can be used with any framework that supports the Node `request` and `response` objects.
|
||||
|
||||
For example, with Express:
|
||||
|
||||
|
@ -73,40 +97,27 @@ app.use(ssrHandler);
|
|||
app.listen(8080);
|
||||
```
|
||||
|
||||
Note that middleware mode does not do file servering. You'll need to configure your HTTP framework to do that for you. By default the client assets are written to `./dist/client/`.
|
||||
|
||||
### Using `http`
|
||||
### Standalone
|
||||
|
||||
This output script does not require you use Express and can work with even the built-in `http` and `https` node modules. The handler does follow the convention calling an error function when either
|
||||
In standalone mode a server starts when the server entrypoint is run. By default it is built to `./dist/server/entry.mjs`. You can run it with:
|
||||
|
||||
- A route is not found for the request.
|
||||
- There was an error rendering.
|
||||
|
||||
You can use these to implement your own 404 behavior like so:
|
||||
|
||||
```js
|
||||
import http from 'http';
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
ssrHandler(req, res, err => {
|
||||
if(err) {
|
||||
res.writeHead(500);
|
||||
res.end(err.toString());
|
||||
} else {
|
||||
// Serve your static assets here maybe?
|
||||
// 404?
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}).listen(8080);
|
||||
```shell
|
||||
node ./dist/server/entry.mjs
|
||||
```
|
||||
|
||||
For standalone mode the server handles file servering in addition to the page and API routes.
|
||||
|
||||
#### HTTPS
|
||||
|
||||
## Configuration
|
||||
By default the standalone server uses HTTP. This works well if you have a proxy server in front of it that does HTTPS. If you need the standalone server to run HTTPS itself you need to provide your SSL key and certificate.
|
||||
|
||||
This adapter does not expose any configuration options.
|
||||
You can pass the path to your key and certification via the environment variables `SERVER_CERT_PATH` and `SERVER_KEY_PATH`. This is how you might pass them in bash:
|
||||
|
||||
```bash
|
||||
SERVER_KEY_PATH=./private/key.pem SERVER_CERT_PATH=./private/cert.pem node ./dist/server/entry.mjs
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./server.js": "./dist/server.js",
|
||||
"./preview.js": "./dist/preview.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -29,9 +30,11 @@
|
|||
"test": "mocha --exit --timeout 20000 test/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/webapi": "^1.1.0"
|
||||
"@astrojs/webapi": "^1.1.0",
|
||||
"send": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/send": "^0.17.1",
|
||||
"astro": "workspace:*",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
|
|
77
packages/integrations/node/src/http-server.ts
Normal file
77
packages/integrations/node/src/http-server.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { fileURLToPath } from 'url';
|
||||
import send from 'send';
|
||||
|
||||
interface CreateServerOptions {
|
||||
client: URL;
|
||||
port: number;
|
||||
host: string | undefined;
|
||||
}
|
||||
|
||||
export function createServer({ client, port, host }: CreateServerOptions, handler: http.RequestListener) {
|
||||
const listener: http.RequestListener = (req, res) => {
|
||||
if(req.url) {
|
||||
const fileURL = new URL('.' + req.url, client);
|
||||
|
||||
const stream = send(req, fileURLToPath(fileURL), {
|
||||
dotfiles: 'deny',
|
||||
});
|
||||
|
||||
let forwardError = false;
|
||||
|
||||
stream.on('error', err => {
|
||||
if(forwardError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.toString());
|
||||
res.writeHead(500);
|
||||
res.end('Internal server error');
|
||||
return;
|
||||
}
|
||||
// File not found, forward to the SSR handler
|
||||
handler(req, res);
|
||||
});
|
||||
|
||||
stream.on('file', () => {
|
||||
forwardError = true;
|
||||
});
|
||||
stream.pipe(res);
|
||||
} else {
|
||||
handler(req, res);
|
||||
}
|
||||
};
|
||||
|
||||
let httpServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> |
|
||||
https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
||||
|
||||
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);
|
||||
}
|
||||
httpServer.listen(port, host);
|
||||
|
||||
// Resolves once the server is closed
|
||||
const closed = new Promise<void>((resolve, reject) => {
|
||||
httpServer.addListener('close', resolve);
|
||||
httpServer.addListener('error', reject);
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
closed() {
|
||||
return closed;
|
||||
},
|
||||
server: httpServer,
|
||||
stop: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
httpServer.close((err) => (err ? reject(err) : resolve(undefined)));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,24 +1,48 @@
|
|||
import type { AstroAdapter, AstroIntegration } from 'astro';
|
||||
import type { Options, UserOptions } from './types';
|
||||
|
||||
export function getAdapter(): AstroAdapter {
|
||||
export function getAdapter(options: Options): AstroAdapter {
|
||||
return {
|
||||
name: '@astrojs/node',
|
||||
serverEntrypoint: '@astrojs/node/server.js',
|
||||
previewEntrypoint: '@astrojs/node/preview.js',
|
||||
exports: ['handler'],
|
||||
args: options
|
||||
};
|
||||
}
|
||||
|
||||
export default function createIntegration(): AstroIntegration {
|
||||
export default function createIntegration(userOptions: UserOptions): AstroIntegration {
|
||||
if(!userOptions?.mode) {
|
||||
throw new Error(`[@astrojs/node] Setting the 'mode' option is required.`)
|
||||
}
|
||||
|
||||
let needsBuildConfig = false;
|
||||
let _options: Options;
|
||||
return {
|
||||
name: '@astrojs/node',
|
||||
hooks: {
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
setAdapter(getAdapter());
|
||||
needsBuildConfig = !config.build?.server;
|
||||
_options = {
|
||||
...userOptions,
|
||||
client: config.build.client?.toString(),
|
||||
server: config.build.server?.toString(),
|
||||
host: config.server.host,
|
||||
port: config.server.port,
|
||||
};
|
||||
setAdapter(getAdapter(_options));
|
||||
|
||||
if (config.output === 'static') {
|
||||
console.warn(`[@astrojs/node] \`output: "server"\` is required to use this adapter.`);
|
||||
}
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
// Backwards compat
|
||||
if(needsBuildConfig) {
|
||||
_options.client = buildConfig.client.toString();
|
||||
_options.server = buildConfig.server.toString();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
53
packages/integrations/node/src/middleware.ts
Normal file
53
packages/integrations/node/src/middleware.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { NodeApp } from 'astro/app/node';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
export default function(app: NodeApp) {
|
||||
return async function(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||
try {
|
||||
const route = app.match(req);
|
||||
|
||||
if (route) {
|
||||
try {
|
||||
const response = await app.render(req);
|
||||
await writeWebResponse(app, res, response);
|
||||
} catch (err: unknown) {
|
||||
if (next) {
|
||||
next(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else if (next) {
|
||||
return next();
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, `Server error`);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: Response) {
|
||||
const { status, headers, body } = webResponse;
|
||||
|
||||
if (app.setCookieHeaders) {
|
||||
const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse));
|
||||
if (setCookieHeaders.length) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||
if (body) {
|
||||
for await (const chunk of body as unknown as Readable) {
|
||||
res.write(chunk);
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
}
|
54
packages/integrations/node/src/preview.ts
Normal file
54
packages/integrations/node/src/preview.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import type { CreatePreviewServer } from 'astro';
|
||||
import type { createExports } from './server';
|
||||
import http from 'http';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createServer } from './http-server.js';
|
||||
|
||||
const preview: CreatePreviewServer = async function({
|
||||
client,
|
||||
serverEntrypoint,
|
||||
host,
|
||||
port,
|
||||
}) {
|
||||
type ServerModule = ReturnType<typeof createExports>;
|
||||
type MaybeServerModule = Partial<ServerModule>;
|
||||
let ssrHandler: ServerModule['handler'];
|
||||
try {
|
||||
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
|
||||
const ssrModule: MaybeServerModule = await import(serverEntrypoint.toString());
|
||||
if(typeof ssrModule.handler === 'function') {
|
||||
ssrHandler = ssrModule.handler;
|
||||
} else {
|
||||
throw new Error(`The server entrypoint doesn't have a handler. Are you sure this is the right file?`);
|
||||
}
|
||||
} catch(_err) {
|
||||
throw new Error(`The server entrypoint ${fileURLToPath} does not exist. Have you ran a build yet?`);
|
||||
}
|
||||
|
||||
const handler: http.RequestListener = (req, res) => {
|
||||
ssrHandler(req, res, (ssrErr: any) => {
|
||||
if (ssrErr) {
|
||||
res.writeHead(500);
|
||||
res.end(ssrErr.toString());
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const server = createServer({
|
||||
client,
|
||||
port,
|
||||
host,
|
||||
}, handler);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Preview server listening on http://${host}:${port}`);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export {
|
||||
preview as default
|
||||
};
|
|
@ -1,8 +1,9 @@
|
|||
import { polyfill } from '@astrojs/webapi';
|
||||
import type { SSRManifest } from 'astro';
|
||||
import type { Options } from './types';
|
||||
import { polyfill } from '@astrojs/webapi';
|
||||
import { NodeApp } from 'astro/app/node';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import type { Readable } from 'stream';
|
||||
import middleware from './middleware.js';
|
||||
import startServer from './standalone.js';
|
||||
|
||||
polyfill(globalThis, {
|
||||
exclude: 'window document',
|
||||
|
@ -11,49 +12,15 @@ polyfill(globalThis, {
|
|||
export function createExports(manifest: SSRManifest) {
|
||||
const app = new NodeApp(manifest);
|
||||
return {
|
||||
async handler(req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) {
|
||||
try {
|
||||
const route = app.match(req);
|
||||
|
||||
if (route) {
|
||||
try {
|
||||
const response = await app.render(req);
|
||||
await writeWebResponse(app, res, response);
|
||||
} catch (err: unknown) {
|
||||
if (next) {
|
||||
next(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else if (next) {
|
||||
return next();
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, `Server error`);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: middleware(app)
|
||||
};
|
||||
}
|
||||
|
||||
async function writeWebResponse(app: NodeApp, res: ServerResponse, webResponse: Response) {
|
||||
const { status, headers, body } = webResponse;
|
||||
|
||||
if (app.setCookieHeaders) {
|
||||
const setCookieHeaders: Array<string> = Array.from(app.setCookieHeaders(webResponse));
|
||||
if (setCookieHeaders.length) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
export function start(manifest: SSRManifest, options: Options) {
|
||||
if(options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(status, Object.fromEntries(headers.entries()));
|
||||
if (body) {
|
||||
for await (const chunk of body as unknown as Readable) {
|
||||
res.write(chunk);
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
const app = new NodeApp(manifest);
|
||||
startServer(app, options);
|
||||
}
|
||||
|
|
53
packages/integrations/node/src/standalone.ts
Normal file
53
packages/integrations/node/src/standalone.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { NodeApp } from 'astro/app/node';
|
||||
import type { Options } from './types';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import middleware from './middleware.js';
|
||||
import { createServer } from './http-server.js';
|
||||
|
||||
function resolvePaths(options: Options) {
|
||||
const clientURLRaw = new URL(options.client);
|
||||
const serverURLRaw = new URL(options.server);
|
||||
const rel = path.relative(fileURLToPath(serverURLRaw), fileURLToPath(clientURLRaw));
|
||||
|
||||
const serverEntryURL = new URL(import.meta.url);
|
||||
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
|
||||
|
||||
return {
|
||||
client: clientURL,
|
||||
};
|
||||
}
|
||||
|
||||
function appendForwardSlash(pth: string) {
|
||||
return pth.endsWith('/') ? pth : pth + '/';
|
||||
}
|
||||
|
||||
export function getResolvedHostForHttpServer(host: string | boolean) {
|
||||
if (host === false) {
|
||||
// Use a secure default
|
||||
return '127.0.0.1';
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
export default function startServer(app: NodeApp, options: Options) {
|
||||
const port = process.env.PORT ? Number(process.env.port) : (options.port ?? 8080);
|
||||
const { client } = resolvePaths(options);
|
||||
const handler = middleware(app);
|
||||
|
||||
const host = getResolvedHostForHttpServer(options.host);
|
||||
const server = createServer({
|
||||
client,
|
||||
port,
|
||||
host,
|
||||
}, handler);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Server listening on http://${host}:${port}`);
|
||||
|
||||
return server.closed();
|
||||
}
|
17
packages/integrations/node/src/types.ts
Normal file
17
packages/integrations/node/src/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
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;
|
||||
}
|
|
@ -10,7 +10,7 @@ describe('API routes', () => {
|
|||
fixture = await loadFixture({
|
||||
root: './fixtures/api-route/',
|
||||
output: 'server',
|
||||
adapter: nodejs(),
|
||||
adapter: nodejs({ mode: 'middleware' }),
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
|
|
@ -17,16 +17,35 @@ export default function vercelEdge(): AstroIntegration {
|
|||
let _config: AstroConfig;
|
||||
let functionFolder: URL;
|
||||
let serverEntry: string;
|
||||
let needsBuildConfig = false;
|
||||
|
||||
return {
|
||||
name: PACKAGE_NAME,
|
||||
hooks: {
|
||||
'astro:config:setup': ({ config }) => {
|
||||
config.outDir = getVercelOutput(config.root);
|
||||
'astro:config:setup': ({ config, updateConfig }) => {
|
||||
needsBuildConfig = !config.build.client;
|
||||
const outDir = getVercelOutput(config.root);
|
||||
updateConfig({
|
||||
outDir,
|
||||
build: {
|
||||
serverEntry: 'entry.mjs',
|
||||
client: new URL('./static/', outDir),
|
||||
server: new URL('./functions/render.func/', config.outDir),
|
||||
}
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
serverEntry = config.build.serverEntry;
|
||||
functionFolder = config.build.server;
|
||||
},
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
if(needsBuildConfig) {
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
serverEntry = buildConfig.serverEntry = 'entry.mjs';
|
||||
functionFolder = buildConfig.server = new URL('./functions/render.func/', _config.outDir);
|
||||
}
|
||||
},
|
||||
'astro:build:setup': ({ vite, target }) => {
|
||||
if (target === 'server') {
|
||||
|
@ -49,11 +68,6 @@ export default function vercelEdge(): AstroIntegration {
|
|||
};
|
||||
}
|
||||
},
|
||||
'astro:build:start': async ({ buildConfig }) => {
|
||||
buildConfig.serverEntry = serverEntry = 'entry.mjs';
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
buildConfig.server = functionFolder = new URL('./functions/render.func/', _config.outDir);
|
||||
},
|
||||
'astro:build:done': async ({ routes }) => {
|
||||
// Edge function config
|
||||
// https://vercel.com/docs/build-output-api/v3#vercel-primitives/edge-functions/configuration
|
||||
|
|
|
@ -19,16 +19,29 @@ export default function vercelEdge(): AstroIntegration {
|
|||
let buildTempFolder: URL;
|
||||
let functionFolder: URL;
|
||||
let serverEntry: string;
|
||||
let needsBuildConfig = false;
|
||||
|
||||
return {
|
||||
name: PACKAGE_NAME,
|
||||
hooks: {
|
||||
'astro:config:setup': ({ config }) => {
|
||||
config.outDir = getVercelOutput(config.root);
|
||||
'astro:config:setup': ({ config, updateConfig }) => {
|
||||
needsBuildConfig = !config.build.client;
|
||||
const outDir = getVercelOutput(config.root);
|
||||
updateConfig({
|
||||
outDir,
|
||||
build: {
|
||||
serverEntry: 'entry.js',
|
||||
client: new URL('./static/', outDir),
|
||||
server: new URL('./dist/', config.root),
|
||||
}
|
||||
});
|
||||
},
|
||||
'astro:config:done': ({ setAdapter, config }) => {
|
||||
setAdapter(getAdapter());
|
||||
_config = config;
|
||||
buildTempFolder = config.build.server;
|
||||
functionFolder = new URL('./functions/render.func/', config.outDir);
|
||||
serverEntry = config.build.serverEntry;
|
||||
|
||||
if (config.output === 'static') {
|
||||
throw new Error(`
|
||||
|
@ -37,11 +50,12 @@ export default function vercelEdge(): AstroIntegration {
|
|||
`);
|
||||
}
|
||||
},
|
||||
'astro:build:start': async ({ buildConfig }) => {
|
||||
buildConfig.serverEntry = serverEntry = 'entry.js';
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
buildConfig.server = buildTempFolder = new URL('./dist/', _config.root);
|
||||
functionFolder = new URL('./functions/render.func/', _config.outDir);
|
||||
'astro:build:start': ({ buildConfig }) => {
|
||||
if(needsBuildConfig) {
|
||||
buildConfig.client = new URL('./static/', _config.outDir);
|
||||
buildTempFolder = buildConfig.server = new URL('./dist/', _config.root);
|
||||
serverEntry = buildConfig.serverEntry = 'entry.js';
|
||||
}
|
||||
},
|
||||
'astro:build:done': async ({ routes }) => {
|
||||
// Copy necessary files (e.g. node_modules/)
|
||||
|
|
|
@ -2916,14 +2916,18 @@ importers:
|
|||
packages/integrations/node:
|
||||
specifiers:
|
||||
'@astrojs/webapi': ^1.1.0
|
||||
'@types/send': ^0.17.1
|
||||
astro: workspace:*
|
||||
astro-scripts: workspace:*
|
||||
chai: ^4.3.6
|
||||
mocha: ^9.2.2
|
||||
node-mocks-http: ^1.11.0
|
||||
send: ^0.18.0
|
||||
dependencies:
|
||||
'@astrojs/webapi': link:../../webapi
|
||||
'@astrojs/webapi': 1.1.0
|
||||
send: 0.18.0
|
||||
devDependencies:
|
||||
'@types/send': 0.17.1
|
||||
astro: link:../../astro
|
||||
astro-scripts: link:../../../scripts
|
||||
chai: 4.3.6
|
||||
|
@ -3785,6 +3789,13 @@ packages:
|
|||
vfile-message: 3.1.2
|
||||
dev: false
|
||||
|
||||
/@astrojs/webapi/1.1.0:
|
||||
resolution: {integrity: sha512-yLSksFKv9kRbI3WWPuRvbBjS+J5ZNmZHacJ6Io8XQleKIHHHcw7RoNcrLK0s+9iwVPhqMYIzja6HJuvnO93oFw==}
|
||||
dependencies:
|
||||
global-agent: 3.0.0
|
||||
node-fetch: 3.2.10
|
||||
dev: false
|
||||
|
||||
/@babel/code-frame/7.18.6:
|
||||
resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -11193,6 +11204,17 @@ packages:
|
|||
engines: {node: '>=0.11'}
|
||||
dev: false
|
||||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: false
|
||||
|
||||
/debug/4.3.3_supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
@ -11338,7 +11360,6 @@ packages:
|
|||
/depd/2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/deprecation/2.3.1:
|
||||
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
|
||||
|
@ -11348,6 +11369,11 @@ packages:
|
|||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
/destroy/1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: false
|
||||
|
||||
/detect-indent/6.1.0:
|
||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -11468,6 +11494,10 @@ packages:
|
|||
sigmund: 1.0.1
|
||||
dev: true
|
||||
|
||||
/ee-first/1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
dev: false
|
||||
|
||||
/ejs/3.1.8:
|
||||
resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -11493,6 +11523,11 @@ packages:
|
|||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
dev: false
|
||||
|
||||
/encodeurl/1.0.2:
|
||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/end-of-stream/1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
|
@ -12164,6 +12199,10 @@ packages:
|
|||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
/escape-html/1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
dev: false
|
||||
|
||||
/escape-string-regexp/1.0.5:
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
@ -12391,6 +12430,11 @@ packages:
|
|||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/etag/1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/event-target-shim/5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -12613,7 +12657,6 @@ packages:
|
|||
/fresh/0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/fs-constants/1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
@ -13179,7 +13222,6 @@ packages:
|
|||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
dev: true
|
||||
|
||||
/http-proxy-agent/4.0.1:
|
||||
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
|
||||
|
@ -14502,7 +14544,6 @@ packages:
|
|||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/mime/3.0.0:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
|
@ -14673,12 +14714,15 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: false
|
||||
|
||||
/ms/2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
|
||||
/ms/2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/mustache/4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
|
@ -14886,6 +14930,13 @@ packages:
|
|||
has-symbols: 1.0.3
|
||||
object-keys: 1.1.1
|
||||
|
||||
/on-finished/2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
dev: false
|
||||
|
||||
/once/1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
|
@ -15805,7 +15856,6 @@ packages:
|
|||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/raw-body/2.5.1:
|
||||
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
|
||||
|
@ -16429,6 +16479,27 @@ packages:
|
|||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
/send/0.18.0:
|
||||
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.0
|
||||
mime: 1.6.0
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/serialize-error/7.0.1:
|
||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -16456,7 +16527,6 @@ packages:
|
|||
|
||||
/setprototypeof/1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
dev: true
|
||||
|
||||
/sharp/0.31.1:
|
||||
resolution: {integrity: sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==}
|
||||
|
@ -16731,7 +16801,6 @@ packages:
|
|||
/statuses/2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/stream-transform/2.1.3:
|
||||
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
|
||||
|
@ -17136,7 +17205,6 @@ packages:
|
|||
/toidentifier/1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: true
|
||||
|
||||
/totalist/1.1.0:
|
||||
resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==}
|
||||
|
|
Loading…
Reference in a new issue