diff --git a/cli/serve.ts b/cli/serve.ts index e41fff9e..90577386 100644 --- a/cli/serve.ts +++ b/cli/serve.ts @@ -12,8 +12,6 @@ * @see LICENSE.md in the project root for further licensing information. */ -import * as http from 'node:http' -import { Socket } from 'node:net' import * as path from 'node:path' import type { Argv } from 'yargs' import { KeyworkResourceError, Status } from '../errors/index.js' @@ -21,7 +19,7 @@ import { KeyworkLogger } from '../logging/index.js' import { NodeStaticFileRouter, applyNodeKeyworkPolyfills, - createNodeServerHandler, + createNodeKeyworkServer, projectRootPathBuilder, } from '../node/index.js' import { FetcherModuleExports, RequestRouter } from '../router/index.js' @@ -80,16 +78,16 @@ export async function serveBuilder({ port, host, ...serveArgs }: ServeArgs) { ) } - const fetcher = scriptModule.default + const router = scriptModule.default - if (!fetcher || !RequestRouter.assertIsInstanceOf(fetcher)) { + if (!router || !RequestRouter.assertIsInstanceOf(router)) { throw new KeyworkResourceError( `Router script ${absoluteScriptPath} does not export a valid request router.`, Status.InternalServerError ) } - fetcher.get( + router.get( '/dist/*', new NodeStaticFileRouter({ filesDirectoryPath: projectRootPathBuilder('dist'), @@ -97,37 +95,15 @@ export async function serveBuilder({ port, host, ...serveArgs }: ServeArgs) { }).fetchStaticFile ) - fetcher.use( + router.use( new NodeStaticFileRouter({ filesDirectoryPath: absolutePublicDirPath, }) ) - fetcher.$prettyPrintRoutes() + router.$prettyPrintRoutes() - // And then wrap the router with `createServerHandler` - const server = http.createServer(createNodeServerHandler(fetcher)) - - const connections = new Set() - server.on('connection', (connection) => { - connections.add(connection) - connection.on('close', () => connections.delete(connection)) - }) - - server.on('close', () => fetcher.dispose('Shutting down...')) - - const onProcessEnd = () => { - for (const connection of connections) { - connection.destroy() - } - - connections.clear() - - server.close(() => process.exit(0)) - } - - process.on('SIGINT', onProcessEnd) - process.on('SIGTERM', onProcessEnd) + const server = createNodeKeyworkServer(router) logger.info('Starting...') diff --git a/events/FetchEventProvider.tsx b/events/FetchEventProvider.tsx index 90776c82..bea40512 100644 --- a/events/FetchEventProvider.tsx +++ b/events/FetchEventProvider.tsx @@ -12,7 +12,7 @@ * @see LICENSE.md in the project root for further licensing information. */ -import { IsomorphicFetchEvent } from '../events/index.js' +import { IsomorphicFetchEvent, SSRDocumentContext } from '../events/index.js' import { RequestContext } from '../http/index.js' import { KeyworkLogger, KeyworkLoggerContext } from '../logging/index.js' import { URLPatternResultContext } from '../uri/index.js' @@ -50,7 +50,9 @@ export const FetchEventProvider: React.FC = ({ logger, - {children} + + {children} + diff --git a/events/SSRDocument.tsx b/events/SSRDocument.tsx index 2d9ab163..4ce1a74b 100644 --- a/events/SSRDocument.tsx +++ b/events/SSRDocument.tsx @@ -12,7 +12,7 @@ * @see LICENSE.md in the project root for further licensing information. */ -import { HtmlHTMLAttributes } from 'react' +import { HtmlHTMLAttributes, createContext, useContext } from 'react' import { ImportMap } from '../files/index.js' /** @@ -111,7 +111,8 @@ export interface SSRDocument { /** * Whether to omit the React hydration script from the document. - * This is useful when you want to render a static HTML page. + * This is useful when you want to render a static HTML page, + * or use your own hydration script. * * @default false */ @@ -123,3 +124,13 @@ export interface DocumentImage { height?: number url: string } + +/** + * @internal + */ +export const SSRDocumentContext = createContext(undefined as any) +SSRDocumentContext.displayName = 'SSRDocumentContext' + +export function useSSRDocument() { + return useContext(SSRDocumentContext) +} diff --git a/files/extensions/Images.ts b/files/extensions/Images.ts index 9ec4481e..42b9a2b7 100644 --- a/files/extensions/Images.ts +++ b/files/extensions/Images.ts @@ -31,3 +31,8 @@ export const PNG = { extension: 'png', mimeType: 'image/png', } as const + +export const WEBP = { + extension: 'webp', + mimeType: 'image/webp', +} as const diff --git a/files/extensions/Styles.ts b/files/extensions/Styles.ts new file mode 100644 index 00000000..09b1dbe5 --- /dev/null +++ b/files/extensions/Styles.ts @@ -0,0 +1,18 @@ +/** + * @file This file is part of the Keywork project. + * @copyright Nirrius, LLC. All rights reserved. + * @author Teffen Ellis, et al. + * @license AGPL-3.0 + * + * @remarks Keywork is free software for non-commercial purposes. + * You can be released from the requirements of the license by purchasing a commercial license. + * Buying such a license is mandatory as soon as you develop commercial activities + * involving the Keywork software without disclosing the source code of your own applications. + * + * @see LICENSE.md in the project root for further licensing information. + */ + +export const CSS = { + extension: 'css', + mimeType: 'text/css; charset=utf-8', +} as const diff --git a/files/extensions/index.ts b/files/extensions/index.ts index 35dfde41..20236d6f 100644 --- a/files/extensions/index.ts +++ b/files/extensions/index.ts @@ -16,4 +16,5 @@ export * from './Forms.js' export * from './Images.js' export * from './JavaScript.js' export * from './PlainText.js' +export * from './Styles.js' export * from './XML.js' diff --git a/node/createServerHandler.ts b/node/createServerHandler.ts index 2751d770..da08398e 100644 --- a/node/createServerHandler.ts +++ b/node/createServerHandler.ts @@ -12,7 +12,8 @@ * @see LICENSE.md in the project root for further licensing information. */ -import type { IncomingMessage, ServerResponse } from 'node:http' +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' +import type { Socket } from 'node:net' import type { RequestRouter } from '../router/index.js' import { respondWithRouter } from './respondWithRouter.js' @@ -38,6 +39,40 @@ export type ServerHandler = (req: IncomingMessage, res: ServerResponse) => void * @see {respondWithRouter} * @beta Node support is currently experimental and may change in the near future. */ -export function createNodeServerHandler(router: RequestRouter): ServerHandler { - return (req, res) => respondWithRouter(router, req, res) +export function createNodeServerHandler( + router: RequestRouter, + env?: BoundAliases +): ServerHandler { + return (req, res) => respondWithRouter(router, req, res, env) +} + +/** + * Given a `RequestRouter`, creates a Node-compatible server. + */ +export function createNodeKeyworkServer( + router: RequestRouter, + server = createServer(createNodeServerHandler(router)) +) { + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => connections.delete(connection)) + }) + + server.on('close', () => router.dispose('Shutting down...')) + + const onProcessEnd = () => { + for (const connection of connections) { + connection.destroy() + } + + connections.clear() + + server.close(() => process.exit(0)) + } + + process.on('SIGINT', onProcessEnd) + process.on('SIGTERM', onProcessEnd) + + return server } diff --git a/node/respondWithRouter.ts b/node/respondWithRouter.ts index 7d6eeb5d..9352991c 100644 --- a/node/respondWithRouter.ts +++ b/node/respondWithRouter.ts @@ -43,10 +43,10 @@ function readNodeEnv(): BoundAliases { export async function respondWithRouter( router: RequestRouter, incomingMessage: IncomingMessage, - serverResponse: ServerResponse + serverResponse: ServerResponse, + env = readNodeEnv() ): Promise { const request = transformIncomingMessageToRequest(incomingMessage) - const env = readNodeEnv() const event = new IsomorphicFetchEvent('fetch', { request, originalURL: request.url, env }) const fetchResponse = await router.fetch(request, env, event) diff --git a/package.json b/package.json index 246c4194..f8ef5ed4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keywork", - "version": "8.1.14", + "version": "8.1.17", "license": "AGPL-3.0", "author": "Teffen Ellis ", "description": "A batteries-included, magic-free, library for building web apps in V8 Isolates.", @@ -89,7 +89,7 @@ "react": ">=18.2", "react-dom": ">=18.2", "undici": ">=5.25.2", - "urlpattern-polyfill": "^5.0.5" + "urlpattern-polyfill": "^9.0.0" }, "peerDependenciesMeta": { "react": { diff --git a/router/MiddlewareFetch.ts b/router/MiddlewareFetch.ts index 2bd4676e..99f11310 100644 --- a/router/MiddlewareFetch.ts +++ b/router/MiddlewareFetch.ts @@ -13,6 +13,7 @@ */ /* eslint-disable no-restricted-globals */ +import type { IsomorphicFetchEvent } from '../events/IsomorphicFetchEvent.js' import type { RouteMatch } from './RouteMatch.js' /** @ignore */ @@ -51,7 +52,7 @@ export interface MiddlewareFetch, /** * When invoked, will execute a route handler defined after the current. * diff --git a/router/RequestRouter.ts b/router/RequestRouter.ts index 33780396..d3179f06 100644 --- a/router/RequestRouter.ts +++ b/router/RequestRouter.ts @@ -29,7 +29,13 @@ import { import { IDisposable } from '../lifecycle/index.js' import { KeyworkLogger } from '../logging/index.js' import { ReactRendererOptions, renderReactStream } from '../ssr/index.js' -import { PatternRouteComponentMap, URLPatternLike, isKeyworkRouteComponent, normalizeURLPattern } from '../uri/index.js' +import { + PatternRouteComponentMap, + URLPatternLike, + isKeyworkRouteComponent, + normalizeURLPattern, + normalizeURLPatternInput, +} from '../uri/index.js' import { Fetcher } from './Fetcher.js' import { FetcherLike } from './FetcherLike.js' import { MiddlewareFetch } from './MiddlewareFetch.js' @@ -489,7 +495,7 @@ export class RequestRouter implements Fetcher, parsedRoutes: ParsedRoute[], matchingAgainst: URLPatternLike ): RouteMatch[] { - const matchInput = normalizeURLPattern(matchingAgainst) + const matchInput = normalizeURLPatternInput(matchingAgainst) const matchedRoutes: RouteMatch[] = [] for (const parsedRoute of parsedRoutes) { @@ -554,6 +560,7 @@ export class RequestRouter implements Fetcher, const pathnameGroups = match.pathname.groups['0'] if (pathnameGroups) { + console.log('>>>> Normalizing', normalizedURL.pathname, pathnameGroups) normalizedURL.pathname = pathnameGroups } // Update the URL params... @@ -590,7 +597,7 @@ export class RequestRouter implements Fetcher, ) try { if (parsedRoute.kind === 'routeHandler') { - possibleResponse = await parsedRoute.fetch(event, next as any) + possibleResponse = await parsedRoute.fetch(event, next) } else { possibleResponse = await parsedRoute.fetcher.fetch(event.request, env, event, next) } diff --git a/ssr/RequestDocument.tsx b/ssr/RequestDocument.tsx index 3ca06bd4..e63f2e47 100644 --- a/ssr/RequestDocument.tsx +++ b/ssr/RequestDocument.tsx @@ -12,6 +12,7 @@ * @see LICENSE.md in the project root for further licensing information. */ +import { SSRPropsByPath, SSRPropsProvider } from '../client/SSRPropsProvider.js' import { FetchEventProvider, IsomorphicFetchEvent } from '../events/index.js' import { ImportMap } from '../files/index.js' import { PageElementComponent } from '../http/index.js' @@ -54,15 +55,20 @@ export const RequestDocument: React.FC = ({ }) => { const staticProps = pageElement.props + const initialNavigatorURL = new URL(event.request.url) + const initialPropsByPath: SSRPropsByPath = new Map([[initialNavigatorURL.pathname, staticProps]]) + const appDocument = ( - - - - {pageElement} - - - - + + + + + {pageElement} + + + + + ) return appDocument