From 4c25a1c2eacf897427a7d6dac3bf476ef56799de Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Wed, 16 Mar 2022 12:16:21 -0400 Subject: [PATCH] Implements redirects, headers for SSR (#2798) * Implements redirects, headers for SSR * Move away from an explicit Request * Properly handle endpoint routes in the build * chore(lint): ESLint fix * Update based on review comments Co-authored-by: github-actions[bot] --- .changeset/fuzzy-lies-nail.md | 5 ++ examples/ssr/astro.config.mjs | 8 ++- examples/ssr/package.json | 5 +- examples/ssr/server/api.mjs | 52 +++++++++++++++ examples/ssr/server/server.mjs | 7 ++- examples/ssr/src/api.ts | 51 +++++++++++++-- examples/ssr/src/components/AddToCart.svelte | 9 ++- examples/ssr/src/components/Cart.svelte | 6 +- examples/ssr/src/components/Header.astro | 20 +++++- examples/ssr/src/models/user.ts | 8 +++ examples/ssr/src/pages/cart.astro | 47 ++++++++++++++ examples/ssr/src/pages/login.astro | 30 +++++++++ examples/ssr/src/pages/login.form.js | 10 +++ examples/ssr/src/pages/products/[id].astro | 2 +- examples/ssr/tsconfig.json | 8 +++ packages/astro/src/@types/astro.ts | 12 +--- packages/astro/src/core/app/index.ts | 29 +++++++-- packages/astro/src/core/app/node.ts | 19 ++++-- packages/astro/src/core/build/index.ts | 9 +-- packages/astro/src/core/build/static-build.ts | 35 +++++++++-- packages/astro/src/core/dev/index.ts | 12 ++-- packages/astro/src/core/endpoint/dev/index.ts | 23 +++++++ packages/astro/src/core/endpoint/index.ts | 51 +++++++++++++++ packages/astro/src/core/polyfill.ts | 8 +++ packages/astro/src/core/render/core.ts | 37 ++++++----- packages/astro/src/core/render/dev/index.ts | 36 ++++++++--- packages/astro/src/core/render/request.ts | 51 +++++++++++++++ packages/astro/src/core/render/result.ts | 35 ++++++++--- packages/astro/src/runtime/server/index.ts | 63 ++++++++++++++----- .../src/vite-plugin-astro-server/index.ts | 61 ++++++++++++++++-- .../astro/src/vite-plugin-build-html/index.ts | 10 ++- packages/astro/test/test-utils.js | 2 +- pnpm-lock.yaml | 46 +++++++++++++- 33 files changed, 698 insertions(+), 109 deletions(-) create mode 100644 .changeset/fuzzy-lies-nail.md create mode 100644 examples/ssr/src/models/user.ts create mode 100644 examples/ssr/src/pages/cart.astro create mode 100644 examples/ssr/src/pages/login.astro create mode 100644 examples/ssr/src/pages/login.form.js create mode 100644 examples/ssr/tsconfig.json create mode 100644 packages/astro/src/core/endpoint/dev/index.ts create mode 100644 packages/astro/src/core/endpoint/index.ts create mode 100644 packages/astro/src/core/polyfill.ts create mode 100644 packages/astro/src/core/render/request.ts diff --git a/.changeset/fuzzy-lies-nail.md b/.changeset/fuzzy-lies-nail.md new file mode 100644 index 000000000..07368b458 --- /dev/null +++ b/.changeset/fuzzy-lies-nail.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Implement APIs for headers for SSR flag diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs index d54ab5929..4b01264ec 100644 --- a/examples/ssr/astro.config.mjs +++ b/examples/ssr/astro.config.mjs @@ -4,8 +4,14 @@ export default defineConfig({ renderers: ['@astrojs/renderer-svelte'], vite: { server: { + cors: { + credentials: true + }, proxy: { - '/api': 'http://localhost:8085', + '/api': { + target: 'http://127.0.0.1:8085', + changeOrigin: true, + } }, }, }, diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 5180ae7f9..fe135b832 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "dev-api": "node server/dev-api.mjs", - "dev": "npm run dev-api & astro dev --experimental-ssr", + "dev-server": "astro dev --experimental-ssr", + "dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"", "start": "astro dev", "build": "echo 'Run pnpm run build-ssr instead'", "build-ssr": "node build.mjs", @@ -13,6 +14,8 @@ "devDependencies": { "@astrojs/renderer-svelte": "^0.5.2", "astro": "^0.24.3", + "concurrently": "^7.0.0", + "lightcookie": "^1.0.25", "unocss": "^0.15.6", "vite-imagetools": "^4.0.3" } diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs index 3d2656815..9bb0be72a 100644 --- a/examples/ssr/server/api.mjs +++ b/examples/ssr/server/api.mjs @@ -1,9 +1,14 @@ import fs from 'fs'; +import lightcookie from 'lightcookie'; + const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url)); const db = JSON.parse(dbJSON); const products = db.products; const productMap = new Map(products.map((product) => [product.id, product])); +// Normally this would be in a database. +const userCartItems = new Map(); + const routes = [ { match: /\/api\/products\/([0-9])+/, @@ -32,6 +37,53 @@ const routes = [ res.end(JSON.stringify(products)); }, }, + { + match: /\/api\/cart/, + async handle(req, res) { + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + let cookie = req.headers.cookie; + let userId = cookie ? lightcookie.parse(cookie)['user-id'] : '1'; // default for testing + if(!userId || !userCartItems.has(userId)) { + res.end(JSON.stringify({ items: [] })); + return; + } + let items = userCartItems.get(userId); + let array = Array.from(items.values()); + res.end(JSON.stringify({ items: array })); + } + }, + { + match: /\/api\/add-to-cart/, + async handle(req, res) { + let body = ''; + req.on('data', chunk => body += chunk); + return new Promise(resolve => { + req.on('end', () => { + let cookie = req.headers.cookie; + let userId = lightcookie.parse(cookie)['user-id']; + let msg = JSON.parse(body); + + if(!userCartItems.has(userId)) { + userCartItems.set(userId, new Map()); + } + + let cart = userCartItems.get(userId); + if(cart.has(msg.id)) { + cart.get(msg.id).count++; + } else { + cart.set(msg.id, { id: msg.id, name: msg.name, count: 1 }); + } + + res.writeHead(200, { + 'Content-Type': 'application/json', + }); + res.end(JSON.stringify({ ok: true })); + }); + }); + } + } ]; export async function apiHandler(req, res) { diff --git a/examples/ssr/server/server.mjs b/examples/ssr/server/server.mjs index e760ac2f8..c6f35685e 100644 --- a/examples/ssr/server/server.mjs +++ b/examples/ssr/server/server.mjs @@ -15,9 +15,10 @@ async function handle(req, res) { const route = app.match(req); if (route) { - const html = await app.render(req, route); - - res.writeHead(200, { + /** @type {Response} */ + const response = await app.render(req, route); + const html = await response.text(); + res.writeHead(response.status, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': Buffer.byteLength(html, 'utf-8'), }); diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts index 59619ade6..b71990f3f 100644 --- a/examples/ssr/src/api.ts +++ b/examples/ssr/src/api.ts @@ -5,12 +5,25 @@ interface Product { image: string; } -//let origin: string; -const { mode } = import.meta.env; -const origin = mode === 'develepment' ? `http://localhost:3000` : `http://localhost:8085`; +interface User { + id: number; +} + +interface Cart { + items: Array<{ + id: number; + name: string; + count: number; + }>; +} + +const { MODE } = import.meta.env; +const origin = MODE === 'development' ? `http://127.0.0.1:3000` : `http://127.0.0.1:8085`; async function get(endpoint: string, cb: (response: Response) => Promise): Promise { - const response = await fetch(`${origin}${endpoint}`); + const response = await fetch(`${origin}${endpoint}`, { + credentials: 'same-origin' + }); if (!response.ok) { // TODO make this better... return null; @@ -31,3 +44,33 @@ export async function getProduct(id: number): Promise { return product; }); } + +export async function getUser(): Promise { + return get(`/api/user`, async response => { + const user: User = await response.json(); + return user; + }); +} + +export async function getCart(): Promise { + return get(`/api/cart`, async response => { + const cart: Cart = await response.json(); + return cart; + }); +} + +export async function addToUserCart(id: number | string, name: string): Promise { + await fetch(`${origin}/api/add-to-cart`, { + credentials: 'same-origin', + method: 'POST', + mode: 'no-cors', + headers: { + 'Content-Type': 'application/json', + 'Cache': 'no-cache' + }, + body: JSON.stringify({ + id, + name + }) + }); +} diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte index b03b8180a..0f7a97a93 100644 --- a/examples/ssr/src/components/AddToCart.svelte +++ b/examples/ssr/src/components/AddToCart.svelte @@ -1,11 +1,18 @@ - + diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro index 2839c70d3..c4d925a5f 100644 --- a/examples/ssr/src/components/Header.astro +++ b/examples/ssr/src/components/Header.astro @@ -1,6 +1,10 @@ --- import TextDecorationSkip from './TextDecorationSkip.astro'; import Cart from './Cart.svelte'; +import { getCart } from '../api'; + +const cart = await getCart(); +const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0); ---

diff --git a/examples/ssr/src/models/user.ts b/examples/ssr/src/models/user.ts new file mode 100644 index 000000000..ecd839d46 --- /dev/null +++ b/examples/ssr/src/models/user.ts @@ -0,0 +1,8 @@ +import lightcookie from 'lightcookie'; + + +export function isLoggedIn(request: Request): boolean { + const cookie = request.headers.get('cookie'); + const parsed = lightcookie.parse(cookie); + return 'user-id' in parsed; +} diff --git a/examples/ssr/src/pages/cart.astro b/examples/ssr/src/pages/cart.astro new file mode 100644 index 000000000..e4a00183e --- /dev/null +++ b/examples/ssr/src/pages/cart.astro @@ -0,0 +1,47 @@ +--- +import Header from '../components/Header.astro'; +import Container from '../components/Container.astro'; +import { getCart } from '../api'; +import { isLoggedIn } from '../models/user'; + +if(!isLoggedIn(Astro.request)) { + return Astro.redirect('/'); +} + +// They must be logged in. + +const user = { name: 'test'}; // getUser? +const cart = await getCart(); +--- + + + Cart | Online Store + + + +
+ + +

Cart

+

Hi { user.name }! Here are your cart items:

+ + + + + + + + + {cart.items.map(item => + + + )} + +
ItemCount
{item.name}{item.count}
+
+ + diff --git a/examples/ssr/src/pages/login.astro b/examples/ssr/src/pages/login.astro new file mode 100644 index 000000000..b12a82a5e --- /dev/null +++ b/examples/ssr/src/pages/login.astro @@ -0,0 +1,30 @@ +--- +import Header from '../components/Header.astro'; +import Container from '../components/Container.astro'; +--- + + + Online Store + + + +
+ + +

Login

+
+ + + + + + + +
+
+ + diff --git a/examples/ssr/src/pages/login.form.js b/examples/ssr/src/pages/login.form.js new file mode 100644 index 000000000..9875ae160 --- /dev/null +++ b/examples/ssr/src/pages/login.form.js @@ -0,0 +1,10 @@ + +export function post(params, request) { + return new Response(null, { + status: 301, + headers: { + 'Location': '/', + 'Set-Cookie': 'user-id=1; Path=/; Max-Age=2592000' + } + }); +} diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro index 943f2ab84..9c400c2f1 100644 --- a/examples/ssr/src/pages/products/[id].astro +++ b/examples/ssr/src/pages/products/[id].astro @@ -45,7 +45,7 @@ const product = await getProduct(id);
- +

Description here...

diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json new file mode 100644 index 000000000..e0065a323 --- /dev/null +++ b/examples/ssr/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["ES2015", "DOM"], + "module": "ES2022", + "moduleResolution": "node", + "types": ["astro/env"] + } +} diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 400f5070b..ce089b7e6 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2,6 +2,7 @@ import type * as babel from '@babel/core'; import type { z } from 'zod'; import type { AstroConfigSchema } from '../core/config'; import type { AstroComponentFactory, Metadata } from '../runtime/server'; +import type { AstroRequest } from '../core/render/request'; import type * as vite from 'vite'; export interface AstroBuiltinProps { @@ -43,14 +44,7 @@ export interface AstroGlobal extends AstroGlobalPartial { /** set props for this astro component (along with default values) */ props: Record; /** get information about this page */ - request: { - /** get the current page URL */ - url: URL; - /** get the current canonical URL */ - canonicalURL: URL; - /** get page params (dynamic pages only) */ - params: Params; - }; + request: AstroRequest; /** see if slots are used */ slots: Record & { has(slotName: string): boolean; render(slotName: string): Promise }; } @@ -563,7 +557,7 @@ export interface EndpointOutput { } export interface EndpointHandler { - [method: string]: (params: any) => EndpointOutput; + [method: string]: (params: any, request: AstroRequest) => EndpointOutput | Response; } /** diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index c31c37f31..fc5ffcced 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -27,14 +27,18 @@ export class App { this.#routeCache = new RouteCache(defaultLogOptions); this.#renderersPromise = this.#loadRenderers(); } - match({ pathname }: URL): RouteData | undefined { - return matchRoute(pathname, this.#manifestData); + match(request: Request): RouteData | undefined { + const url = new URL(request.url); + return matchRoute(url.pathname, this.#manifestData); } - async render(url: URL, routeData?: RouteData): Promise { + async render(request: Request, routeData?: RouteData): Promise { if (!routeData) { - routeData = this.match(url); + routeData = this.match(request); if (!routeData) { - return 'Not found'; + return new Response(null, { + status: 404, + statusText: 'Not found' + }); } } @@ -42,10 +46,11 @@ export class App { const info = this.#routeDataToRouteInfo.get(routeData!)!; const [mod, renderers] = await Promise.all([this.#loadModule(info.file), this.#renderersPromise]); + const url = new URL(request.url); const links = createLinkStylesheetElementSet(info.links, manifest.site); const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site); - return render({ + const result = await render({ legacyBuild: false, links, logging: defaultLogOptions, @@ -65,6 +70,18 @@ export class App { route: routeData, routeCache: this.#routeCache, site: this.#manifest.site, + ssr: true, + method: info.routeData.type === 'endpoint' ? '' : 'GET', + headers: request.headers, + }); + + if(result.type === 'response') { + return result.response; + } + + let html = result.html; + return new Response(html, { + status: 200 }); } async #loadRenderers(): Promise { diff --git a/packages/astro/src/core/app/node.ts b/packages/astro/src/core/app/node.ts index d1bcbf46b..46ba710c2 100644 --- a/packages/astro/src/core/app/node.ts +++ b/packages/astro/src/core/app/node.ts @@ -1,20 +1,27 @@ import type { SSRManifest, SerializedSSRManifest } from './types'; +import type { IncomingHttpHeaders } from 'http'; import * as fs from 'fs'; import { App } from './index.js'; import { deserializeManifest } from './common.js'; import { IncomingMessage } from 'http'; -function createURLFromRequest(req: IncomingMessage): URL { - return new URL(`http://${req.headers.host}${req.url}`); +function createRequestFromNodeRequest(req: IncomingMessage): Request { + let url = `http://${req.headers.host}${req.url}`; + const entries = Object.entries(req.headers as Record); + let request = new Request(url, { + method: req.method || 'GET', + headers: new Headers(entries) + }); + return request; } class NodeApp extends App { - match(req: IncomingMessage | URL) { - return super.match(req instanceof URL ? req : createURLFromRequest(req)); + match(req: IncomingMessage | Request) { + return super.match(req instanceof Request ? req : createRequestFromNodeRequest(req)); } - render(req: IncomingMessage | URL) { - return super.render(req instanceof URL ? req : createURLFromRequest(req)); + render(req: IncomingMessage | Request) { + return super.render(req instanceof Request ? req : createRequestFromNodeRequest(req)); } } diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 5f5be3ca7..ece445aa0 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -3,7 +3,7 @@ import type { LogOptions } from '../logger'; import fs from 'fs'; import * as colors from 'kleur/colors'; -import { polyfill } from '@astrojs/webapi'; +import { apply as applyPolyfill } from '../polyfill.js'; import { performance } from 'perf_hooks'; import * as vite from 'vite'; import { createVite, ViteConfigWithSSR } from '../create-vite.js'; @@ -22,11 +22,6 @@ export interface BuildOptions { /** `astro build` */ export default async function build(config: AstroConfig, options: BuildOptions = { logging: defaultLogOptions }): Promise { - // polyfill WebAPIs to globalThis for Node v12, Node v14, and Node v16 - polyfill(globalThis, { - exclude: 'window document', - }); - const builder = new AstroBuilder(config, options); await builder.build(); } @@ -42,6 +37,8 @@ class AstroBuilder { private viteConfig?: ViteConfigWithSSR; constructor(config: AstroConfig, options: BuildOptions) { + applyPolyfill(); + if (!config.buildOptions.site && config.buildOptions.sitemap !== false) { warn(options.logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`); } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 393e61ed2..ea4c89011 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -1,11 +1,12 @@ import type { OutputChunk, OutputAsset, RollupOutput } from 'rollup'; import type { Plugin as VitePlugin, UserConfig, Manifest as ViteManifest } from 'vite'; -import type { AstroConfig, ComponentInstance, ManifestData, Renderer, RouteType } from '../../@types/astro'; +import type { AstroConfig, ComponentInstance, EndpointHandler, ManifestData, Renderer, RouteType } from '../../@types/astro'; import type { AllPagesData } from './types'; import type { LogOptions } from '../logger'; import type { ViteConfigWithSSR } from '../create-vite'; import type { PageBuildData } from './types'; import type { BuildInternals } from '../../core/build/internal.js'; +import type { RenderOptions } from '../../core/render/core'; import type { SerializedSSRManifest, SerializedRouteInfo } from '../app/types'; import fs from 'fs'; @@ -20,9 +21,11 @@ import { createBuildInternals } from '../../core/build/internal.js'; import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js'; import { vitePluginHoistedScripts } from './vite-plugin-hoisted-scripts.js'; import { RouteCache } from '../render/route-cache.js'; +import { call as callEndpoint } from '../endpoint/index.js'; import { serializeRouteData } from '../routing/index.js'; import { render } from '../render/core.js'; import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js'; +import { createRequest } from '../render/request.js'; export interface StaticBuildOptions { allPages: AllPagesData; @@ -134,6 +137,8 @@ export async function staticBuild(opts: StaticBuildOptions) { const topLevelImports = new Set([ // Any component that gets hydrated + // 'components/Counter.jsx' + // { 'components/Counter.jsx': 'counter.hash.js' } ...metadata.hydratedComponentPaths(), // Client-only components ...metadata.clientOnlyComponentPaths(), @@ -373,7 +378,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G const scripts = createModuleScriptElementWithSrcSet(hoistedId ? [hoistedId] : [], site); try { - const html = await render({ + const options: RenderOptions = { legacyBuild: false, links, logging, @@ -392,15 +397,37 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G const fullyRelativePath = relPath[0] === '.' ? relPath : './' + relPath; return fullyRelativePath; }, + method: 'GET', + headers: new Headers(), route: pageData.route, routeCache, site: astroConfig.buildOptions.site, - }); + ssr: opts.astroConfig.buildOptions.experimentalSsr + } + + let body: string; + if(pageData.route.type === 'endpoint') { + + const result = await callEndpoint(mod as unknown as EndpointHandler, options); + + if(result.type === 'response') { + throw new Error(`Returning a Response from an endpoint is not supported in SSG mode.`) + } + body = result.body; + } else { + const result = await render(options); + + // If there's a redirect or something, just do nothing. + if(result.type !== 'html') { + return; + } + body = result.html; + } const outFolder = getOutFolder(astroConfig, pathname, pageData.route.type); const outFile = getOutFile(astroConfig, outFolder, pathname, pageData.route.type); await fs.promises.mkdir(outFolder, { recursive: true }); - await fs.promises.writeFile(outFile, html, 'utf-8'); + await fs.promises.writeFile(outFile, body, 'utf-8'); } catch (err) { error(opts.logging, 'build', `Error rendering:`, err); } diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index a1b48e1d9..be3a7ef7f 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -1,7 +1,8 @@ -import { polyfill } from '@astrojs/webapi'; -import type { AddressInfo } from 'net'; -import { performance } from 'perf_hooks'; import type { AstroConfig } from '../../@types/astro'; +import type { AddressInfo } from 'net'; + +import { performance } from 'perf_hooks'; +import { apply as applyPolyfill } from '../polyfill.js'; import { createVite } from '../create-vite.js'; import { defaultLogOptions, info, warn, LogOptions } from '../logger.js'; import * as vite from 'vite'; @@ -20,10 +21,7 @@ export interface DevServer { /** `astro dev` */ export default async function dev(config: AstroConfig, options: DevOptions = { logging: defaultLogOptions }): Promise { const devStart = performance.now(); - // polyfill WebAPIs for Node.js runtime - polyfill(globalThis, { - exclude: 'window document', - }); + applyPolyfill(); // TODO: remove call once --hostname is baselined const host = getResolvedHostForVite(config); diff --git a/packages/astro/src/core/endpoint/dev/index.ts b/packages/astro/src/core/endpoint/dev/index.ts new file mode 100644 index 000000000..d05ae3f5f --- /dev/null +++ b/packages/astro/src/core/endpoint/dev/index.ts @@ -0,0 +1,23 @@ +import type { EndpointHandler } from '../../../@types/astro'; +import type { SSROptions } from '../../render/dev'; + +import { preload } from '../../render/dev/index.js'; +import { errorHandler } from '../../render/dev/error.js'; +import { call as callEndpoint } from '../index.js'; +import { getParamsAndProps, GetParamsAndPropsError } from '../../render/core.js'; +import { createRequest } from '../../render/request.js'; + + +export async function call(ssrOpts: SSROptions) { + try { + const [, mod] = await preload(ssrOpts); + return await callEndpoint(mod as unknown as EndpointHandler, { + ...ssrOpts, + ssr: ssrOpts.astroConfig.buildOptions.experimentalSsr + }); + + } catch (e: unknown) { + await errorHandler(e, { viteServer: ssrOpts.viteServer, filePath: ssrOpts.filePath }); + throw e; + } +} diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts new file mode 100644 index 000000000..f10efcc9f --- /dev/null +++ b/packages/astro/src/core/endpoint/index.ts @@ -0,0 +1,51 @@ +import type { EndpointHandler } from '../../@types/astro'; +import type { RenderOptions } from '../render/core'; +import { renderEndpoint } from '../../runtime/server/index.js'; +import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js'; +import { createRequest } from '../render/request.js'; + +export type EndpointOptions = Pick; + +type EndpointCallResult = { + type: 'simple', + body: string +} | { + type: 'response', + response: Response +}; + +export async function call(mod: EndpointHandler, opts: EndpointOptions): Promise { + const paramsAndPropsResp = await getParamsAndProps({ ...opts, mod: (mod as any) }); + + if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) { + throw new Error(`[getStaticPath] route pattern matched, but no matching static path found. (${opts.pathname})`); + } + const [params] = paramsAndPropsResp; + const request = createRequest(opts.method, opts.pathname, opts.headers, opts.origin, + opts.site, opts.ssr); + + const response = await renderEndpoint(mod, request, params); + + if(response instanceof Response) { + return { + type: 'response', + response + }; + } + + return { + type: 'simple', + body: response.body + }; +} diff --git a/packages/astro/src/core/polyfill.ts b/packages/astro/src/core/polyfill.ts new file mode 100644 index 000000000..99e0d5cc5 --- /dev/null +++ b/packages/astro/src/core/polyfill.ts @@ -0,0 +1,8 @@ +import { polyfill } from '@astrojs/webapi'; + +export function apply() { + // polyfill WebAPIs for Node.js runtime + polyfill(globalThis, { + exclude: 'window document', + }); +} diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index ccea0c743..71b4d03e7 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,15 +1,15 @@ import type { ComponentInstance, EndpointHandler, MarkdownRenderOptions, Params, Props, Renderer, RouteData, SSRElement } from '../../@types/astro'; import type { LogOptions } from '../logger.js'; +import type { AstroRequest } from './request'; -import { renderEndpoint, renderHead, renderToString } from '../../runtime/server/index.js'; +import { renderHead, renderPage } from '../../runtime/server/index.js'; import { getParams } from '../routing/index.js'; import { createResult } from './result.js'; import { findPathItemByKey, RouteCache, callGetStaticPaths } from './route-cache.js'; -import { warn } from '../logger.js'; interface GetParamsAndPropsOptions { mod: ComponentInstance; - route: RouteData | undefined; + route?: RouteData | undefined; routeCache: RouteCache; pathname: string; logging: LogOptions; @@ -55,7 +55,7 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise return [params, pageProps]; } -interface RenderOptions { +export interface RenderOptions { legacyBuild: boolean; logging: LogOptions; links: Set; @@ -69,10 +69,13 @@ interface RenderOptions { route?: RouteData; routeCache: RouteCache; site?: string; + ssr: boolean; + method: string; + headers: Headers; } -export async function render(opts: RenderOptions): Promise { - const { legacyBuild, links, logging, origin, markdownRender, mod, pathname, scripts, renderers, resolve, route, routeCache, site } = opts; +export async function render(opts: RenderOptions): Promise<{ type: 'html', html: string } | { type: 'response', response: Response }> { + const { headers, legacyBuild, links, logging, origin, markdownRender, method, mod, pathname, scripts, renderers, resolve, route, routeCache, site, ssr } = opts; const paramsAndPropsRes = await getParamsAndProps({ logging, @@ -87,11 +90,6 @@ export async function render(opts: RenderOptions): Promise { } const [params, pageProps] = paramsAndPropsRes; - // For endpoints, render the content immediately without injecting scripts or styles - if (route?.type === 'endpoint') { - return renderEndpoint(mod as any as EndpointHandler, params); - } - // Validate the page component before rendering the page const Component = await mod.default; if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); @@ -109,10 +107,18 @@ export async function render(opts: RenderOptions): Promise { renderers, site, scripts, + ssr, + method, + headers }); - let html = await renderToString(result, Component, pageProps, null); + let page = await renderPage(result, Component, pageProps, null); + if(page.type === 'response') { + return page; + } + + let html = page.html; // handle final head injection if it hasn't happened already if (html.indexOf('') == -1) { html = (await renderHead(result)) + html; @@ -124,6 +130,9 @@ export async function render(opts: RenderOptions): Promise { if (!legacyBuild && !/\n' + html; } - - return html; + + return { + type: 'html', + html + }; } diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index e44ff3072..3d92bf945 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -1,5 +1,7 @@ import type * as vite from 'vite'; import type { AstroConfig, ComponentInstance, Renderer, RouteData, RuntimeMode, SSRElement } from '../../../@types/astro'; +import type { AstroRequest } from '../request'; + import { LogOptions } from '../../logger.js'; import { fileURLToPath } from 'url'; import { getStylesForURL } from './css.js'; @@ -12,7 +14,7 @@ import { prependForwardSlash } from '../../path.js'; import { render as coreRender } from '../core.js'; import { createModuleScriptElementWithSrcSet } from '../ssr-element.js'; -interface SSROptions { +export interface SSROptions { /** an instance of the AstroConfig */ astroConfig: AstroConfig; /** location of file on disk */ @@ -31,10 +33,18 @@ interface SSROptions { routeCache: RouteCache; /** Vite instance */ viteServer: vite.ViteDevServer; + /** Method */ + method: string; + /** Headers */ + headers: Headers; } export type ComponentPreload = [Renderer[], ComponentInstance]; +export type RenderResponse = + { type: 'html', html: string } | + { type: 'response', response: Response }; + const svelteStylesRE = /svelte\?svelte&type=style/; export async function preload({ astroConfig, filePath, viteServer }: Pick): Promise { @@ -47,8 +57,8 @@ export async function preload({ astroConfig, filePath, viteServer }: Pick { - const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts; +export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise { + const { astroConfig, filePath, logging, mode, origin, pathname, method, headers, route, routeCache, viteServer } = ssrOpts; const legacy = astroConfig.buildOptions.legacyBuild; // Add hoisted script tags @@ -113,9 +123,12 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO route, routeCache, site: astroConfig.buildOptions.site, + ssr: astroConfig.buildOptions.experimentalSsr, + method, + headers, }); - if (route?.type === 'endpoint') { + if (route?.type === 'endpoint' || content.type === 'response') { return content; } @@ -158,23 +171,26 @@ export async function render(renderers: Renderer[], mod: ComponentInstance, ssrO } // add injected tags - content = injectTags(content, tags); + let html = injectTags(content.html, tags); // run transformIndexHtml() in dev to run Vite dev transformations if (mode === 'development' && astroConfig.buildOptions.legacyBuild) { const relativeURL = filePath.href.replace(astroConfig.projectRoot.href, '/'); - content = await viteServer.transformIndexHtml(relativeURL, content, pathname); + html = await viteServer.transformIndexHtml(relativeURL, html, pathname); } // inject if missing (TODO: is a more robust check needed for comments, etc.?) - if (!/\n' + content; + if (!/\n' + content; } - return content; + return { + type: 'html', + html + }; } -export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise { +export async function ssr(preloadedComponent: ComponentPreload, ssrOpts: SSROptions): Promise { try { const [renderers, mod] = preloadedComponent; return await render(renderers, mod, ssrOpts); // note(drew): without "await", errors won’t get caught by errorHandler() diff --git a/packages/astro/src/core/render/request.ts b/packages/astro/src/core/render/request.ts new file mode 100644 index 000000000..b969ec4d7 --- /dev/null +++ b/packages/astro/src/core/render/request.ts @@ -0,0 +1,51 @@ +import type { Params } from '../../@types/astro'; +import { canonicalURL as utilCanonicalURL } from '../util.js'; + +type Site = string | undefined; + +export interface AstroRequest { + /** get the current page URL */ + url: URL; + + /** get the current canonical URL */ + canonicalURL: URL; + + /** get page params (dynamic pages only) */ + params: Params; + + headers: Headers; + + method: string; +} + +export type AstroRequestSSR = AstroRequest + +export function createRequest(method: string, pathname: string, headers: Headers, + origin: string, site: Site, ssr: boolean): AstroRequest { + const url = new URL('.' + pathname, new URL(origin)); + + const canonicalURL = utilCanonicalURL('.' + pathname, site ?? url.origin); + + const request: AstroRequest = { + url, + canonicalURL, + params: {}, + headers, + method + }; + + if(!ssr) { + // Headers are only readable if using SSR-mode. If not, make it an empty headers + // object, so you can't do something bad. + request.headers = new Headers(); + + // Disallow using query params. + request.url = new URL(request.url); + + for(const [key] of request.url.searchParams) { + request.url.searchParams.delete(key); + } + } + + return request; +} diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 2175e112e..1086686cc 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -1,13 +1,22 @@ import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, Renderer, SSRElement, SSRResult } from '../../@types/astro'; +import type { AstroRequest } from './request'; import { bold } from 'kleur/colors'; -import { canonicalURL as getCanonicalURL } from '../util.js'; +import { createRequest } from './request.js'; import { isCSSRequest } from './dev/css.js'; import { isScriptRequest } from './script.js'; import { renderSlot } from '../../runtime/server/index.js'; import { warn, LogOptions } from '../logger.js'; +function onlyAvailableInSSR(name: string) { + return function() { + // TODO add more guidance when we have docs and adapters. + throw new Error(`Oops, you are trying to use ${name}, which is only available with SSR.`) + }; +} + export interface CreateResultArgs { + ssr: boolean; legacyBuild: boolean; logging: LogOptions; origin: string; @@ -19,6 +28,8 @@ export interface CreateResultArgs { site: string | undefined; links?: Set; scripts?: Set; + headers: Headers; + method: string; } class Slots { @@ -63,7 +74,10 @@ class Slots { } export function createResult(args: CreateResultArgs): SSRResult { - const { legacyBuild, origin, markdownRender, params, pathname, renderers, resolve, site: buildOptionsSite } = args; + const { legacyBuild, markdownRender, method, origin, headers, params, pathname, renderers, resolve, site } = args; + + const request = createRequest(method, pathname, headers, origin, site, args.ssr); + request.params = params; // Create the result object that will be passed into the render function. // This object starts here as an empty shell (not yet the result) but then @@ -74,19 +88,20 @@ export function createResult(args: CreateResultArgs): SSRResult { links: args.links ?? new Set(), /** This function returns the `Astro` faux-global */ createAstro(astroGlobal: AstroGlobalPartial, props: Record, slots: Record | null) { - const site = new URL(origin); - const url = new URL('.' + pathname, site); - const canonicalURL = getCanonicalURL('.' + pathname, buildOptionsSite || origin); const astroSlots = new Slots(result, slots); return { __proto__: astroGlobal, props, - request: { - canonicalURL, - params, - url, - }, + request, + redirect: args.ssr ? (path: string) => { + return new Response(null, { + status: 301, + headers: { + Location: path + } + }); + } : onlyAvailableInSSR('Astro.redirect'), resolve(path: string) { if (!legacyBuild) { let extra = `This can be replaced with a dynamic import like so: await import("${path}")`; diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index e4307feff..14d9ce256 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -1,5 +1,6 @@ -import type { AstroComponentMetadata, EndpointHandler, Renderer } from '../../@types/astro'; +import type { AstroComponentMetadata, EndpointHandler, Renderer, Params } from '../../@types/astro'; import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/astro'; +import type { AstroRequest } from '../../core/render/request'; import shorthash from 'shorthash'; import { extractDirectives, generateHydrateScript } from './hydration.js'; @@ -80,13 +81,17 @@ export class AstroComponent { } } +function isAstroComponent(obj: any): obj is AstroComponent { + return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object AstroComponent]'; +} + export async function render(htmlParts: TemplateStringsArray, ...expressions: any[]) { return new AstroComponent(htmlParts, expressions); } // The callback passed to to $$createComponent export interface AstroComponentFactory { - (result: any, props: any, slots: any): ReturnType; + (result: any, props: any, slots: any): ReturnType | Response; isAstroComponentFactory?: boolean; } @@ -407,24 +412,19 @@ export function defineScriptVars(vars: Record) { } // Renders an endpoint request to completion, returning the body. -export async function renderEndpoint(mod: EndpointHandler, params: any) { - const method = 'get'; - const handler = mod[method]; +export async function renderEndpoint(mod: EndpointHandler, request: AstroRequest, params: Params) { + const chosenMethod = request.method?.toLowerCase() ?? 'get'; + const handler = mod[chosenMethod]; if (!handler || typeof handler !== 'function') { - throw new Error(`Endpoint handler not found! Expected an exported function for "${method}"`); + throw new Error(`Endpoint handler not found! Expected an exported function for "${chosenMethod}"`); } - const { body } = await mod.get(params); - - return body; + return await handler.call(mod, params, request); } -// Calls a component and renders it into a string of HTML -export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any): Promise { - const Component = await componentFactory(result, props, children); - let template = await renderAstroComponent(Component); - +async function replaceHeadInjection(result: SSRResult, html: string): Promise { + let template = html; // injected by compiler // Must be handled at the end of the rendering process if (template.indexOf('') > -1) { @@ -433,6 +433,41 @@ export async function renderToString(result: SSRResult, componentFactory: AstroC return template; } +// Calls a component and renders it into a string of HTML +export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, + props: any, children: any): Promise { + const Component = await componentFactory(result, props, children); + if(!isAstroComponent(Component)) { + throw new Error('Cannot return a Response from a nested component.'); + } + + let template = await renderAstroComponent(Component); + return replaceHeadInjection(result, template); +} + +export async function renderPage( + result: SSRResult, + componentFactory: AstroComponentFactory, + props: any, + children: any +): Promise<{ type: 'html', html: string } | { type: 'response', response: Response }> { + const response = await componentFactory(result, props, children); + + if(isAstroComponent(response)) { + let template = await renderAstroComponent(response); + const html = await replaceHeadInjection(result, template); + return { + type: 'html', + html + }; + } else { + return { + type: 'response', + response + }; + } +} + // Filter out duplicate elements in our set const uniqueElements = (item: any, index: number, all: any[]) => { const props = JSON.stringify(item.props); diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 33baeed71..32e6adf03 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -1,17 +1,20 @@ import type * as vite from 'vite'; import type http from 'http'; import type { AstroConfig, ManifestData } from '../@types/astro'; +import type { RenderResponse, SSROptions } from '../core/render/dev/index'; import { info, warn, error, LogOptions } from '../core/logger.js'; import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/core.js'; import { createRouteManifest, matchRoute } from '../core/routing/index.js'; import stripAnsi from 'strip-ansi'; import { createSafeError } from '../core/util.js'; import { ssr, preload } from '../core/render/dev/index.js'; +import { call as callEndpoint } from '../core/endpoint/dev/index.js'; import * as msg from '../core/messages.js'; import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js'; import serverErrorTemplate from '../template/5xx.js'; import { RouteCache } from '../core/render/route-cache.js'; +import { AstroRequest } from '../core/render/request.js'; interface AstroPluginOptions { config: AstroConfig; @@ -37,6 +40,33 @@ function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: s res.end(); } +async function writeWebResponse(res: http.ServerResponse, webResponse: Response) { + const { status, headers, body } = webResponse; + res.writeHead(status, Object.fromEntries(headers.entries())); + if(body) { + const reader = body.getReader(); + while(true) { + const { done, value } = await reader.read(); + if(done) break; + if(value) { + res.write(value); + } + } + } + res.end(); +} + +async function writeSSRResult(result: RenderResponse, res: http.ServerResponse, statusCode: 200 | 404) { + if(result.type === 'response') { + const { response } = result; + await writeWebResponse(res, response); + return; + } + + const { html } = result; + writeHtmlResponse(res, statusCode, html); +} + async function handle404Response(origin: string, config: AstroConfig, req: http.IncomingMessage, res: http.ServerResponse) { const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined; const devRoot = site ? site.pathname : '/'; @@ -87,7 +117,8 @@ async function handleRequest( const site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined; const devRoot = site ? site.pathname : '/'; const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`; - const pathname = decodeURI(new URL(origin + req.url).pathname); + const url = new URL(origin + req.url); + const pathname = decodeURI(url.pathname); const rootRelativeUrl = pathname.substring(devRoot.length - 1); try { @@ -129,24 +160,26 @@ async function handleRequest( if (routeCustom404) { const filePathCustom404 = new URL(`./${routeCustom404.component}`, config.projectRoot); const preloadedCompCustom404 = await preload({ astroConfig: config, filePath: filePathCustom404, viteServer }); - const html = await ssr(preloadedCompCustom404, { + const result = await ssr(preloadedCompCustom404, { astroConfig: config, filePath: filePathCustom404, logging, mode: 'development', + method: 'GET', + headers: new Headers(Object.entries(req.headers as Record)), origin, pathname: rootRelativeUrl, route: routeCustom404, routeCache, viteServer, }); - return writeHtmlResponse(res, statusCode, html); + return await writeSSRResult(result, res, statusCode); } else { return handle404Response(origin, config, req, res); } } - const html = await ssr(preloadedComponent, { + const options: SSROptions = { astroConfig: config, filePath, logging, @@ -156,9 +189,25 @@ async function handleRequest( route, routeCache, viteServer, - }); - writeHtmlResponse(res, statusCode, html); + method: req.method || 'GET', + headers: new Headers(Object.entries(req.headers as Record)), + }; + + // Route successfully matched! Render it. + if(route.type === 'endpoint') { + const result = await callEndpoint(options); + if(result.type === 'response') { + await writeWebResponse(res, result.response); + } else { + res.writeHead(200); + res.end(result.body); + } + } else { + const result = await ssr(preloadedComponent, options); + return await writeSSRResult(result, res, statusCode); + } } catch (_err: any) { + debugger; info(logging, 'serve', msg.req({ url: pathname, statusCode: 500 })); const err = createSafeError(_err); error(logging, 'error', msg.err(err)); diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index 8319970cc..3f67d9375 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -83,10 +83,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { for (const pathname of pageData.paths) { pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, '')); const id = ASTRO_PAGE_PREFIX + pathname; - const html = await ssrRender(renderers, mod, { + const response = await ssrRender(renderers, mod, { astroConfig, filePath: new URL(`./${component}`, astroConfig.projectRoot), logging, + headers: new Headers(), + method: 'GET', mode: 'production', origin, pathname, @@ -94,6 +96,12 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { routeCache, viteServer, }); + + if(response.type !== 'html') { + continue; + } + + const html = response.html; renderedPageMap.set(id, html); const document = parse5.parse(html, { diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 340653325..c7060dcc7 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -80,7 +80,7 @@ export async function loadFixture(inlineConfig) { return devResult; }, config, - fetch: (url, init) => fetch(`http://${config.devOptions.hostname}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init), + fetch: (url, init) => fetch(`http://${'127.0.0.1'}:${config.devOptions.port}${url.replace(/^\/?/, '/')}`, init), preview: async (opts = {}) => { const previewServer = await preview(config, { logging: 'error', ...opts }); inlineConfig.devOptions.port = previewServer.port; // update port for fetch diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1e4f0029..512efc508 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,11 +203,15 @@ importers: specifiers: '@astrojs/renderer-svelte': ^0.5.2 astro: ^0.24.3 + concurrently: ^7.0.0 + lightcookie: ^1.0.25 unocss: ^0.15.6 vite-imagetools: ^4.0.3 devDependencies: '@astrojs/renderer-svelte': link:../../packages/renderers/renderer-svelte astro: link:../../packages/astro + concurrently: 7.0.0 + lightcookie: 1.0.25 unocss: 0.15.6 vite-imagetools: 4.0.3 @@ -4636,6 +4640,21 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + /concurrently/7.0.0: + resolution: {integrity: sha512-WKM7PUsI8wyXpF80H+zjHP32fsgsHNQfPLw/e70Z5dYkV7hF+rf8q3D+ScWJIEr57CpkO3OWBko6hwhQLPR8Pw==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.28.0 + lodash: 4.17.21 + rxjs: 6.6.7 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 16.2.0 + dev: true + /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} dev: true @@ -4760,6 +4779,11 @@ packages: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true + /date-fns/2.28.0: + resolution: {integrity: sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==} + engines: {node: '>=0.11'} + dev: true + /debug/4.3.3: resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} engines: {node: '>=6.0'} @@ -6837,6 +6861,10 @@ packages: type-check: 0.4.0 dev: true + /lightcookie/1.0.25: + resolution: {integrity: sha512-SrY/+eBPaKAMnsn7mCsoOMZzoQyCyHHHZlFCu2fjo28XxSyCLjlooKiTxyrXTg8NPaHp1YzWi0lcGG1gDi6KHw==} + dev: true + /lilconfig/2.0.4: resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==} engines: {node: '>=10'} @@ -8572,6 +8600,13 @@ packages: dependencies: queue-microtask: 1.2.3 + /rxjs/6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.14.1 + dev: true + /sade/1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -8867,6 +8902,10 @@ packages: /space-separated-tokens/2.0.1: resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==} + /spawn-command/0.0.2-1: + resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=} + dev: true + /spawndamnit/2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -9318,6 +9357,11 @@ packages: punycode: 2.1.1 dev: true + /tree-kill/1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /trim-newlines/3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -10077,7 +10121,7 @@ packages: /wide-align/1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: - string-width: 1.0.2 + string-width: 4.2.3 dev: true /word-wrap/1.2.3: