diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs index f6aba20cef..70e06ea3ae 100644 --- a/examples/ssr/astro.config.mjs +++ b/examples/ssr/astro.config.mjs @@ -1,10 +1,10 @@ import { defineConfig } from 'astro/config'; import svelte from '@astrojs/svelte'; -import nodejs from '@astrojs/node'; +import deno from '@astrojs/deno'; // https://astro.build/config export default defineConfig({ - adapter: nodejs(), + adapter: deno(), integrations: [svelte()], vite: { server: { diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 240157e475..5e93e69fd3 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@astrojs/svelte": "^0.0.2-next.0", - "@astrojs/node": "^0.0.2-next.0", + "@astrojs/deno": "^0.0.2-next.0", "astro": "^0.25.0-next.3", "concurrently": "^7.0.0", "lightcookie": "^1.0.25", diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro index c4d925a5f7..469ec16829 100644 --- a/examples/ssr/src/components/Header.astro +++ b/examples/ssr/src/components/Header.astro @@ -3,7 +3,7 @@ import TextDecorationSkip from './TextDecorationSkip.astro'; import Cart from './Cart.svelte'; import { getCart } from '../api'; -const cart = await getCart(); +const cart = { items: [] };// await getCart(); const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0); --- + + + +
+
+

+ + + + + + + + + + + + + + + + + + + Web Feed Preview +

+

+

+ + + + + Visit Website → + +
+

Recent Items

+ +
+

+ + + + + + +

+ + Published: + +
+
+
+ + + +`; diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 6f03d48061..3de99bd54e 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -2,8 +2,7 @@ import { bold } from 'kleur/colors'; import type { AstroGlobal, AstroGlobalPartial, MarkdownParser, MarkdownRenderOptions, Params, SSRElement, SSRLoadedRenderer, SSRResult } from '../../@types/astro'; import { renderSlot } from '../../runtime/server/index.js'; import { LogOptions, warn } from '../logger.js'; -import { isCSSRequest } from './dev/css.js'; -import { canonicalURL as utilCanonicalURL } from '../util.js'; +import { createCanonicalURL, isCSSRequest } from './util.js'; import { isScriptRequest } from './script.js'; function onlyAvailableInSSR(name: string) { @@ -71,10 +70,10 @@ class Slots { } export function createResult(args: CreateResultArgs): SSRResult { - const { legacyBuild, markdownRender, origin, params, pathname, renderers, request, resolve, site } = args; + const { legacyBuild, markdownRender, params, pathname, renderers, request, resolve, site } = args; const url = new URL(request.url); - const canonicalURL = utilCanonicalURL('.' + pathname, site ?? url.origin); + const canonicalURL = createCanonicalURL('.' + pathname, site ?? url.origin); // 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 diff --git a/packages/astro/src/core/render/route-cache.ts b/packages/astro/src/core/render/route-cache.ts index d181d1b1c8..909227f327 100644 --- a/packages/astro/src/core/render/route-cache.ts +++ b/packages/astro/src/core/render/route-cache.ts @@ -2,7 +2,7 @@ import type { ComponentInstance, GetStaticPathsItem, GetStaticPathsResult, GetSt import { LogOptions, warn, debug } from '../logger.js'; import { generatePaginateFunction } from './paginate.js'; -import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../routing/index.js'; +import { validateGetStaticPathsModule, validateGetStaticPathsResult } from '../routing/validation.js'; type RSSFn = (...args: any[]) => any; diff --git a/packages/astro/src/core/render/rss.ts b/packages/astro/src/core/render/rss.ts index 1e77dff353..e02a6155e4 100644 --- a/packages/astro/src/core/render/rss.ts +++ b/packages/astro/src/core/render/rss.ts @@ -1,7 +1,8 @@ import type { RSSFunction, RSS, RSSResult, RouteData } from '../../@types/astro'; import { XMLValidator } from 'fast-xml-parser'; -import { canonicalURL, isValidURL, PRETTY_FEED_V3 } from '../util.js'; +import { PRETTY_FEED_V3 } from './pretty-feed.js'; +import { createCanonicalURL, isValidURL } from './util.js'; /** Validates getStaticPaths.rss */ export function validateRSS(args: GenerateRSSArgs): void { @@ -38,7 +39,7 @@ export function generateRSS(args: GenerateRSSArgs): string { // title, description, customData xml += `<![CDATA[${rssData.title}]]>`; xml += ``; - xml += `${canonicalURL(site).href}`; + xml += `${createCanonicalURL(site).href}`; if (typeof rssData.customData === 'string') xml += rssData.customData; // items for (const result of rssData.items) { @@ -49,7 +50,7 @@ export function generateRSS(args: GenerateRSSArgs): string { if (!result.link) throw new Error(`[${srcFile}] rss.items required "link" property is missing. got: "${JSON.stringify(result)}"`); xml += `<![CDATA[${result.title}]]>`; // If the item's link is already a valid URL, don't mess with it. - const itemLink = isValidURL(result.link) ? result.link : canonicalURL(result.link, site).href; + const itemLink = isValidURL(result.link) ? result.link : createCanonicalURL(result.link, site).href; xml += `${itemLink}`; xml += `${itemLink}`; if (result.description) xml += ``; diff --git a/packages/astro/src/core/render/util.ts b/packages/astro/src/core/render/util.ts new file mode 100644 index 0000000000..58fbcf736f --- /dev/null +++ b/packages/astro/src/core/render/util.ts @@ -0,0 +1,29 @@ +import npath from 'path'; + +/** Normalize URL to its canonical form */ +export function createCanonicalURL(url: string, base?: string): URL { + let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical + pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections) + if (!npath.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension + pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t) + return new URL(pathname, base); +} + +/** Check if a URL is already valid */ +export function isValidURL(url: string): boolean { + try { + new URL(url); + return true; + } catch (e) {} + return false; +} + +// https://vitejs.dev/guide/features.html#css-pre-processors +export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.postcss', '.scss', '.sass', '.styl', '.stylus', '.less']); + +const cssRe = new RegExp( + `\\.(${Array.from(STYLE_EXTENSIONS) + .map((s) => s.slice(1)) + .join('|')})($|\\?)` +); +export const isCSSRequest = (request: string): boolean => cssRe.test(request); diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 6787f177bc..8e48b3055a 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -7,24 +7,6 @@ import fs from 'fs'; import { fileURLToPath, pathToFileURL } from 'url'; import resolve from 'resolve'; -/** Normalize URL to its canonical form */ -export function canonicalURL(url: string, base?: string): URL { - let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical - pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections) - if (!path.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension - pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() won’t) - return new URL(pathname, base); -} - -/** Check if a URL is already valid */ -export function isValidURL(url: string): boolean { - try { - new URL(url); - return true; - } catch (e) {} - return false; -} - /** Returns true if argument is an object of any prototype/class (but not null). */ export function isObject(value: unknown): value is Record { return typeof value === 'object' && value != null; @@ -137,105 +119,3 @@ export function isBuildingToSSR(config: AstroConfig): boolean { return !!config._ctx.adapter?.serverEntrypoint; } -// Vendored from https://github.com/genmon/aboutfeeds/blob/main/tools/pretty-feed-v3.xsl -/** Basic stylesheet for RSS feeds */ -export const PRETTY_FEED_V3 = ` - - - - - - - <xsl:value-of select="/rss/channel/title"/> Web Feed - - - - - - -
-
-

- - - - - - - - - - - - - - - - - - - Web Feed Preview -

-

-

- - - - - Visit Website → - -
-

Recent Items

- -
-

- - - - - - -

- - Published: - -
-
-
- - -
-
`; diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index b04caeaf17..f2d2003941 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -3,6 +3,7 @@ import type { ViteDevServer } from 'vite'; import { AstroConfig, AstroRenderer, BuildConfig, RouteData } from '../@types/astro.js'; import { mergeConfig } from '../core/config.js'; import ssgAdapter from '../adapter-ssg/index.js'; +import type { ViteConfigWithSSR } from '../core/create-vite.js'; export async function runHookConfigSetup({ config: _config, command }: { config: AstroConfig; command: 'dev' | 'build' }): Promise { if (_config.adapter) { @@ -91,6 +92,14 @@ export async function runHookBuildStart({ config, buildConfig }: { config: Astro } } +export async function runHookBuildServerSetup({ config, vite }: { config: AstroConfig, vite: ViteConfigWithSSR }) { + for (const integration of config.integrations) { + if (integration.hooks['astro:build:server:setup']) { + await integration.hooks['astro:build:server:setup']({ vite }); + } + } +} + export async function runHookBuildDone({ config, pages, routes }: { config: AstroConfig; pages: string[]; routes: RouteData[] }) { for (const integration of config.integrations) { if (integration.hooks['astro:build:done']) { diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index c963e42c82..0fa3f8f67e 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,12 +1,11 @@ import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro'; import type { SSRElement, SSRResult } from '../../@types/astro'; import { hydrationSpecifier, serializeListValue } from './util.js'; -import serializeJavaScript from 'serialize-javascript'; // Serializes props passed into a component so that they can be reused during hydration. // The value is any export function serializeProps(value: any) { - return serializeJavaScript(value); + return JSON.stringify(value); } const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only']; diff --git a/packages/astro/src/vite-plugin-astro/styles.ts b/packages/astro/src/vite-plugin-astro/styles.ts index d344298ea0..85467506e0 100644 --- a/packages/astro/src/vite-plugin-astro/styles.ts +++ b/packages/astro/src/vite-plugin-astro/styles.ts @@ -1,6 +1,6 @@ import type * as vite from 'vite'; -import { STYLE_EXTENSIONS } from '../core/render/dev/css.js'; +import { STYLE_EXTENSIONS } from '../core/render/util.js'; export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise; diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts index 7de50af4ec..96d878a6ae 100644 --- a/packages/astro/src/vite-plugin-build-css/index.ts +++ b/packages/astro/src/vite-plugin-build-css/index.ts @@ -3,7 +3,7 @@ import type { BuildInternals } from '../core/build/internal'; import * as path from 'path'; import esbuild from 'esbuild'; import { Plugin as VitePlugin } from 'vite'; -import { isCSSRequest } from '../core/render/dev/css.js'; +import { isCSSRequest } from '../core/render/util.js'; import { getPageDatasByChunk } from '../core/build/internal.js'; const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css'; diff --git a/packages/integrations/deno/CHANGELOG.md b/packages/integrations/deno/CHANGELOG.md new file mode 100644 index 0000000000..31593ed2b9 --- /dev/null +++ b/packages/integrations/deno/CHANGELOG.md @@ -0,0 +1,7 @@ +# @astrojs/node + +## 0.0.2-next.0 + +### Patch Changes + +- [#2873](https://github.com/withastro/astro/pull/2873) [`e4025d1f`](https://github.com/withastro/astro/commit/e4025d1f530310d6ab951109f4f53878a307471a) Thanks [@matthewp](https://github.com/matthewp)! - Improves the build by building to a single file for rendering diff --git a/packages/integrations/deno/package.json b/packages/integrations/deno/package.json new file mode 100644 index 0000000000..9b4c92a8c5 --- /dev/null +++ b/packages/integrations/deno/package.json @@ -0,0 +1,34 @@ +{ + "name": "@astrojs/deno", + "description": "Deploy your site to a Deno server", + "version": "0.0.2-next.0", + "type": "module", + "types": "./dist/index.d.ts", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/astro.git", + "directory": "packages/integrations/deno" + }, + "bugs": "https://github.com/withastro/astro/issues", + "homepage": "https://astro.build", + "exports": { + ".": "./dist/index.js", + "./server.js": "./dist/server.js", + "./package.json": "./package.json" + }, + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "dev": "astro-scripts dev \"src/**/*.ts\"" + }, + "dependencies": { + "path-browserify": "^1.0.1", + "tty-browserify": "^0.0.1", + "events-browserify-mfsu": "^3.2.0" + }, + "devDependencies": { + "astro": "workspace:*", + "astro-scripts": "workspace:*" + } +} diff --git a/packages/integrations/deno/readme.md b/packages/integrations/deno/readme.md new file mode 100644 index 0000000000..0114852786 --- /dev/null +++ b/packages/integrations/deno/readme.md @@ -0,0 +1,53 @@ +# @astrojs/node + +An experimental static-side rendering adapter for use with Node.js servers. + +In your astro.config.mjs use: + +```js +import nodejs from '@astrojs/node'; + +export default { + adapter: nodejs() +} +``` + +After performing a build there will be a `dist/server/entry.mjs` module that works like a middleware function. You can use with any framework that supports the Node `request` and `response` objects. For example, with Express you can do: + +```js +import express from 'express'; +import { handler as ssrHandler } from './dist/server/entry.mjs'; + +const app = express(); +app.use(ssrHandler); + +app.listen(8080); +``` + +# Using `http` + +This adapter does not require you use Express and can work with even the `http` and `https` modules. The adapter does following the Expression convention of calling a function when either + +- 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); +``` diff --git a/packages/integrations/deno/src/index.ts b/packages/integrations/deno/src/index.ts new file mode 100644 index 0000000000..dce43d2e81 --- /dev/null +++ b/packages/integrations/deno/src/index.ts @@ -0,0 +1,43 @@ +import type { AstroAdapter, AstroIntegration } from 'astro'; + + + +export function getAdapter(): AstroAdapter { + return { + name: '@astrojs/deno', + serverEntrypoint: '@astrojs/deno/server.js', + }; +} + +export default function createIntegration(): AstroIntegration { + return { + name: '@astrojs/deno', + hooks: { + 'astro:config:setup': ({ config, command }) => { + }, + 'astro:config:done': ({ setAdapter }) => { + setAdapter(getAdapter()); + }, + 'astro:build:server:setup': ({ vite }) => { + Object.assign(vite, { + resolve: { + ...(vite.resolve ?? {}), + alias: { + ...(vite.resolve?.alias ?? {}), + 'events': 'events-browserify-mfsu', + 'path': 'path-browserify', + 'tty': 'tty-browserify' + } + }, + ssr: { + ...(vite.ssr ?? {}) + }, + }) + vite.ssr = { + noExternal: true + }; + + } + }, + }; +} diff --git a/packages/integrations/deno/src/server.ts b/packages/integrations/deno/src/server.ts new file mode 100644 index 0000000000..0da1f2721d --- /dev/null +++ b/packages/integrations/deno/src/server.ts @@ -0,0 +1,35 @@ +import './shim.js'; +import type { SSRManifest } from 'astro'; +import { App } from 'astro/app'; + +export async function start(manifest: SSRManifest) { + const app = new App(manifest); + + // Start listening on port 8080 of localhost. + const server = Deno.listen({ port: 8085 }); + console.log(`HTTP webserver running. Access it at: http://127.0.0.1:8085/`); + + // Connections to the server will be yielded up as an async iterable. + for await (const conn of server) { + // In order to not be blocking, we need to handle each connection individually + // without awaiting the function + serveHttp(conn, app); + } + +} + +async function render(request: Request, app: App) { + const response = await app.render(request); + console.log(response) + return response; +} + +async function serveHttp(conn: Deno.Conn, app: App) { + // This "upgrades" a network connection into an HTTP connection. + const httpConn = Deno.serveHttp(conn); + // Each request sent over the HTTP connection will be yielded as an async + // iterator from the HTTP connection. + for await (const requestEvent of httpConn) { + requestEvent.respondWith(render(requestEvent.request, app)) + } +} diff --git a/packages/integrations/deno/src/shim.ts b/packages/integrations/deno/src/shim.ts new file mode 100644 index 0000000000..11c1c912b2 --- /dev/null +++ b/packages/integrations/deno/src/shim.ts @@ -0,0 +1,5 @@ + +(globalThis as any).process = { + argv: [], + env: {} +}; diff --git a/packages/integrations/deno/tsconfig.json b/packages/integrations/deno/tsconfig.json new file mode 100644 index 0000000000..44baf375c8 --- /dev/null +++ b/packages/integrations/deno/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "allowJs": true, + "module": "ES2020", + "outDir": "./dist", + "target": "ES2020" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20e1d39d9e..100118a572 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,7 +286,7 @@ importers: examples/ssr: specifiers: - '@astrojs/node': ^0.0.2-next.0 + '@astrojs/deno': ^0.0.2-next.0 '@astrojs/svelte': ^0.0.2-next.0 astro: ^0.25.0-next.3 concurrently: ^7.0.0 @@ -297,7 +297,7 @@ importers: dependencies: svelte: 3.46.4 devDependencies: - '@astrojs/node': link:../../packages/integrations/node + '@astrojs/deno': link:../../packages/integrations/deno '@astrojs/svelte': link:../../packages/integrations/svelte astro: link:../../packages/astro concurrently: 7.0.0 @@ -463,6 +463,7 @@ importers: '@types/mime': ^2.0.3 '@types/mocha': ^9.1.0 '@types/parse5': ^6.0.3 + '@types/readable-stream': ^2.3.13 '@types/resolve': ^1.20.1 '@types/rimraf': ^3.0.2 '@types/send': ^0.17.1 @@ -494,6 +495,7 @@ importers: postcss: ^8.4.12 postcss-load-config: ^3.1.3 prismjs: ^1.27.0 + readable-stream: ^3.6.0 rehype-slug: ^5.0.1 resolve: ^1.22.0 rollup: ^2.70.1 @@ -576,6 +578,7 @@ importers: '@types/mime': 2.0.3 '@types/mocha': 9.1.0 '@types/parse5': 6.0.3 + '@types/readable-stream': 2.3.13 '@types/resolve': 1.20.1 '@types/rimraf': 3.0.2 '@types/send': 0.17.1 @@ -585,6 +588,7 @@ importers: cheerio: 1.0.0-rc.10 execa: 6.1.0 mocha: 9.2.2 + readable-stream: 3.6.0 sass: 1.49.9 packages/astro-prism: @@ -1166,6 +1170,21 @@ importers: astro-scripts: link:../../scripts uvu: 0.5.3 + packages/integrations/deno: + specifiers: + astro: workspace:* + astro-scripts: workspace:* + events-browserify-mfsu: ^3.2.0 + path-browserify: ^1.0.1 + tty-browserify: ^0.0.1 + dependencies: + events-browserify-mfsu: 3.2.0 + path-browserify: 1.0.1 + tty-browserify: 0.0.1 + devDependencies: + astro: link:../../astro + astro-scripts: link:../../../scripts + packages/integrations/lit: specifiers: '@lit-labs/ssr': ^2.0.4 @@ -3962,6 +3981,13 @@ packages: csstype: 3.0.11 dev: false + /@types/readable-stream/2.3.13: + resolution: {integrity: sha512-4JSCx8EUzaW9Idevt+9lsRAt1lcSccoQfE+AouM1gk8sFxnnytKNIO3wTl9Dy+4m6jRJ1yXhboLHHT/LXBQiEw==} + dependencies: + '@types/node': 17.0.23 + safe-buffer: 5.2.1 + dev: true + /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: @@ -6111,6 +6137,11 @@ packages: engines: {node: '>=10.13.0'} dev: true + /events-browserify-mfsu/3.2.0: + resolution: {integrity: sha512-d7B0Bt1LhUXUmWmyhByqE0pbuWKUUp3QVkpfqPs8z3LWNLscd+mnq7PMRYNRVoEm1Yh67JwkYnT6RbDg8VlJdQ==} + engines: {node: '>=0.8.x'} + dev: false + /execa/5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -9884,6 +9915,10 @@ packages: typescript: 4.6.3 dev: true + /tty-browserify/0.0.1: + resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + dev: false + /tty-table/2.8.13: resolution: {integrity: sha512-eVV/+kB6fIIdx+iUImhXrO22gl7f6VmmYh0Zbu6C196fe1elcHXd7U6LcLXu0YoVPc2kNesWiukYcdK8ZmJ6aQ==} engines: {node: '>=8.16.0'}