From c44322efc44012e20e71e6104b44257b17e1cd14 Mon Sep 17 00:00:00 2001 From: reaper Date: Fri, 10 Apr 2026 10:35:47 +0530 Subject: [PATCH 1/7] feat: migrate to Fetch-native API handler and explicit adapter pattern - Rewrite runtime/handler.js: API handlers receive Request, return Response - Add nodeRequestToFetch/fetchResponseToNode bridge functions to adex/src/http.js - Remove adexDevServer and adapterMap from vite.js; extract generateServerEntry helper - Add adex-manifest-sidecar plugin to emit adex.manifest.json in server build - Fully rewrite adex-adapter-node: node() factory, createNodeDevServerPlugin, Fetch bridge inlined - Migrate all playground API routes to Fetch API signature - Update test fixtures to use adapter: node() and add adex-adapter-node dep - Add 10 unit tests for HTTP bridge functions and 2 handler integration tests (28 total passing) --- adex/runtime/handler.js | 94 +++--- .../tests/minimal-no-ssr.spec.snap.cjs | 10 +- .../tests/minimal-tailwind.spec.snap.cjs | 10 +- adex/snapshots/tests/minimal.spec.snap.cjs | 10 +- adex/src/hook.d.ts | 17 +- adex/src/http.d.ts | 15 + adex/src/http.js | 59 ++++ adex/src/vite.d.ts | 23 +- adex/src/vite.js | 295 +++++------------- .../fixtures/minimal-no-ssr/package.json | 1 + .../fixtures/minimal-no-ssr/vite.config.js | 2 + .../fixtures/minimal-tailwind/package.json | 1 + .../fixtures/minimal-tailwind/vite.config.js | 2 + adex/tests/fixtures/minimal/package.json | 1 + adex/tests/fixtures/minimal/src/api/ping.js | 9 + adex/tests/fixtures/minimal/vite.config.js | 2 + adex/tests/http-bridge.spec.js | 179 +++++++++++ adex/tests/minimal.spec.js | 17 + packages/adapters/node/lib/index.d.ts | 47 ++- packages/adapters/node/lib/index.js | 258 ++++++++++++--- playground/src/api/$id/hello.js | 12 +- playground/src/api/$id/json.js | 12 +- playground/src/api/hello.js | 8 +- playground/src/api/html.js | 9 +- playground/src/api/random-data.js | 7 +- playground/src/api/status-helpers.js | 49 ++- playground/vite.config.js | 2 + pnpm-lock.yaml | 9 + 28 files changed, 792 insertions(+), 368 deletions(-) create mode 100644 adex/tests/fixtures/minimal/src/api/ping.js create mode 100644 adex/tests/http-bridge.spec.js diff --git a/adex/runtime/handler.js b/adex/runtime/handler.js index f5c60be..b8c2a4b 100644 --- a/adex/runtime/handler.js +++ b/adex/runtime/handler.js @@ -1,5 +1,4 @@ import { CONSTANTS, emitToHooked } from 'adex/hook' -import { prepareRequest, prepareResponse } from 'adex/http' import { toStatic } from 'adex/ssr' import { renderToStringAsync } from 'adex/utils/isomorphic' import { h } from 'preact' @@ -14,14 +13,17 @@ import { routes as pageRoutes } from '~routes' const html = String.raw -export async function handler(req, res) { - res.statusCode = 200 - - prepareRequest(req) - prepareResponse(res) - - const [url, search] = req.url.split('?') - const baseURL = normalizeRequestUrl(url) +/** + * Core request handler — Fetch API native. + * Receives a standard Request, returns a standard Response. + * Page responses carry an `x-adex-page-route` header so the adapter + * kernel can inject manifest assets before sending to the client. + * @param {Request} request + * @returns {Promise} + */ +export async function handler(request) { + const { pathname } = new URL(request.url) + const baseURL = normalizeRequestUrl(pathname) const { metas, links, title, lang } = toStatic() @@ -29,32 +31,24 @@ export async function handler(req, res) { const matchedInAPI = apiRoutes.find(d => { return d.regex.pattern.test(baseURL) }) + if (matchedInAPI) { const module = await matchedInAPI.module() - const routeParams = getRouteParams(baseURL, matchedInAPI) - req.params = routeParams - const modifiableContext = { - req: req, - res: res, - } - await emitToHooked(CONSTANTS.beforeApiCall, modifiableContext) + const context = { request } + await emitToHooked(CONSTANTS.beforeApiCall, context) const handlerFn = - 'default' in module ? module.default : (_, res) => res.end() - const serverHandler = async (req, res) => { - await handlerFn(req, res) - await emitToHooked(CONSTANTS.afterApiCall, { req, res }) - } - return { - serverHandler, - } - } - return { - serverHandler: async (_, res) => { - res.statusCode = 404 - res.end('Not found') - await emitToHooked(CONSTANTS.afterApiCall, { req, res }) - }, + 'default' in module + ? module.default + : () => new Response('Not found', { status: 404 }) + const response = await handlerFn(context.request) + await emitToHooked(CONSTANTS.afterApiCall, { + request: context.request, + response, + }) + return response } + + return new Response('Not found', { status: 404 }) } const matchedInPages = pageRoutes.find(d => { @@ -65,15 +59,13 @@ export async function handler(req, res) { const routeParams = getRouteParams(baseURL, matchedInPages) // @ts-expect-error - global.location = new URL(req.url, 'http://localhost') + global.location = new URL(request.url) - const modifiableContext = { - req: req, - } - await emitToHooked(CONSTANTS.beforePageRender, modifiableContext) + const context = { request } + await emitToHooked(CONSTANTS.beforePageRender, context) const rendered = await renderToStringAsync( - h(App, { url: modifiableContext.req.url }), + h(App, { url: new URL(context.request.url).pathname }), {} ) @@ -89,24 +81,32 @@ export async function handler(req, res) { body: rendered, }) - modifiableContext.html = htmlString - await emitToHooked(CONSTANTS.afterPageRender, modifiableContext) - htmlString = modifiableContext.html - return { - html: htmlString, - pageRoute: matchedInPages.route, - } + const pageContext = { request: context.request, html: htmlString } + await emitToHooked(CONSTANTS.afterPageRender, pageContext) + htmlString = pageContext.html + + return new Response(htmlString, { + status: 200, + headers: { + 'content-type': 'text/html', + 'x-adex-page-route': matchedInPages.route, + }, + }) } - return { - html: HTMLTemplate({ + return new Response( + HTMLTemplate({ metas, links, title, lang, body: '404 | Not Found', }), - } + { + status: 404, + headers: { 'content-type': 'text/html' }, + } + ) } function HTMLTemplate({ diff --git a/adex/snapshots/tests/minimal-no-ssr.spec.snap.cjs b/adex/snapshots/tests/minimal-no-ssr.spec.snap.cjs index 39011de..a4a4d75 100644 --- a/adex/snapshots/tests/minimal-no-ssr.spec.snap.cjs +++ b/adex/snapshots/tests/minimal-no-ssr.spec.snap.cjs @@ -11,7 +11,10 @@ exports["devMode ssr minimal > gives a static response 1"] = `" - + + + +

Hello World

@@ -31,7 +34,10 @@ exports["devMode ssr minimal > gives a static response 2"] = `" - + + + +

About

diff --git a/adex/snapshots/tests/minimal-tailwind.spec.snap.cjs b/adex/snapshots/tests/minimal-tailwind.spec.snap.cjs index 7db8699..d5c31d2 100644 --- a/adex/snapshots/tests/minimal-tailwind.spec.snap.cjs +++ b/adex/snapshots/tests/minimal-tailwind.spec.snap.cjs @@ -11,7 +11,10 @@ exports["devMode ssr minimal with styles > gives a non-static ssr response 1"] = - + + + +

Hello World

@@ -31,7 +34,10 @@ exports["devMode ssr minimal with styles > gives a static SSR response 1"] = `" - + + + +

About

diff --git a/adex/snapshots/tests/minimal.spec.snap.cjs b/adex/snapshots/tests/minimal.spec.snap.cjs index 9308110..053dcd9 100644 --- a/adex/snapshots/tests/minimal.spec.snap.cjs +++ b/adex/snapshots/tests/minimal.spec.snap.cjs @@ -11,7 +11,10 @@ exports["devMode ssr minimal > gives a non-static ssr response 1"] = `" - + + + +

Hello World

@@ -38,7 +41,10 @@ exports["devMode ssr minimal > gives a static SSR response 1"] = `" - + + + +

About

diff --git a/adex/src/hook.d.ts b/adex/src/hook.d.ts index e8e4f92..8e92290 100644 --- a/adex/src/hook.d.ts +++ b/adex/src/hook.d.ts @@ -1,12 +1,11 @@ -import { IncomingMessage } from 'node:http' - -export type Context = { - req: IncomingMessage - html: string +export type PageRenderContext = { + request: Request + html?: string } export type APIContext = { - req: IncomingMessage + request: Request + response?: Response } export declare const CONSTANTS: { @@ -22,15 +21,15 @@ export declare function hook( ): void export declare function beforePageRender( - fn: (ctx: Omit) => void + fn: (ctx: Omit) => void ): Promise export declare function afterPageRender( - fn: (ctx: Context) => void + fn: (ctx: PageRenderContext) => void ): Promise export declare function beforeAPICall( - fn: (ctx: APIContext) => void + fn: (ctx: Omit) => void ): Promise export declare function afterAPICall( diff --git a/adex/src/http.d.ts b/adex/src/http.d.ts index 71429ff..f2823b2 100644 --- a/adex/src/http.d.ts +++ b/adex/src/http.d.ts @@ -21,3 +21,18 @@ export type ServerResponse = HTTPServerResponse & { export function prepareRequest(req: IncomingMessage): void export function prepareResponse(res: ServerResponse): void + +/** + * Convert a Node.js IncomingMessage to a Fetch API Request. + * Used by adapter kernels to bridge from Node HTTP to Fetch. + */ +export function nodeRequestToFetch(req: HTTPIncomingMessage): Promise + +/** + * Write a Fetch API Response to a Node.js ServerResponse. + * Skips internal x-adex-* headers. Used by adapter kernels. + */ +export function fetchResponseToNode( + response: Response, + res: HTTPServerResponse +): Promise diff --git a/adex/src/http.js b/adex/src/http.js index 31bfe83..7212916 100644 --- a/adex/src/http.js +++ b/adex/src/http.js @@ -28,6 +28,65 @@ export function prepareRequest(req) { } } +/** + * Convert a Node.js IncomingMessage to a Fetch API Request. + * Reconstructs the full URL from req.url + Host header. + * Reads and buffers the body stream. + * @param {import("./http.js").IncomingMessage} req + * @returns {Promise} + */ +export async function nodeRequestToFetch(req) { + const protocol = req.socket?.encrypted ? 'https' : 'http' + const host = req.headers['host'] ?? 'localhost' + const url = new URL(req.url, `${protocol}://${host}`) + + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) { + for (const v of value) headers.append(key, v) + } else if (value != null) { + headers.set(key, value) + } + } + + const hasBody = req.method !== 'GET' && req.method !== 'HEAD' + let body = undefined + if (hasBody) { + body = await new Promise((resolve, reject) => { + const chunks = [] + req.on('data', chunk => chunks.push(chunk)) + req.on('end', () => resolve(Buffer.concat(chunks))) + req.on('error', reject) + }) + } + + return new Request(url.href, { + method: req.method, + headers, + body: body ?? null, + }) +} + +/** + * Write a Fetch API Response to a Node.js ServerResponse. + * Copies status, headers (skipping x-adex-* internal headers), and streams body. + * @param {Response} response + * @param {import("./http.js").ServerResponse} res + * @returns {Promise} + */ +export async function fetchResponseToNode(response, res) { + res.statusCode = response.status + for (const [key, value] of response.headers.entries()) { + if (key.startsWith('x-adex-')) continue + res.setHeader(key, value) + } + if (response.body) { + const buf = Buffer.from(await response.arrayBuffer()) + res.write(buf) + } + res.end() +} + /** * @param {import("./http.js").ServerResponse} res */ diff --git a/adex/src/vite.d.ts b/adex/src/vite.d.ts index a52cf1f..1c8879c 100644 --- a/adex/src/vite.d.ts +++ b/adex/src/vite.d.ts @@ -1,17 +1,34 @@ import { UserConfig, Plugin } from 'vite' import type { Options as FontOptions } from './fonts.js' -export type Adapters = 'node' +export interface AdapterClientInfo { + bundle: boolean + islands: boolean + manifestPath: string + outDir: string +} + +export interface AdapterConfig { + /** npm package name — added to ssr.noExternal so it bundles into the server output */ + name: string + /** the import specifier used in the generated virtual:adex:server entry */ + module: string + /** + * Returns a Vite plugin that handles dev-mode request serving for this adapter. + * Called by the core adex() plugin with the same islands flag. + */ + devServerPlugin: (options: { islands: boolean }) => Plugin +} export interface AdexOptions { fonts?: FontOptions islands?: boolean - adapter?: Adapters + adapter?: AdapterConfig ssr?: boolean __clientConfig?: UserConfig } -export function adex(options: AdexOptions): Plugin[] +export function adex(options?: AdexOptions): Plugin[] declare module 'vite' { interface Plugin { diff --git a/adex/src/vite.js b/adex/src/vite.js index 25cdbe0..62ad3fe 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -23,20 +23,65 @@ const cwd = process.cwd() const islandsDir = join(cwd, '.islands') let runningIslandBuild = false -const adapterMap = { - node: 'adex-adapter-node', +/** + * Generate the virtual:adex:server entry code for a given adapter module. + * Reads adex.manifest.json at runtime to discover client build info. + * @param {string} adapterModule - the import specifier for the adapter + * @returns {string} + */ +function generateServerEntry(adapterModule) { + return `import { createServer } from '${adapterModule}' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' +import { env } from 'adex/env' + +import 'virtual:adex:font.css' +import 'virtual:adex:global.css' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const PORT = parseInt(env.get('PORT', '3000'), 10) +const HOST = env.get('HOST', 'localhost') + +function readJSON(p) { + try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} } +} + +const adexManifest = readJSON(join(__dirname, 'adex.manifest.json')) +const serverManifest = readJSON(join(__dirname, 'manifest.json')) +const clientManifest = adexManifest?.client?.bundle + ? readJSON(join(__dirname, adexManifest.client.manifestPath)) + : {} + +const paths = { + assets: join(__dirname, './assets'), + islands: join(__dirname, './islands'), + client: join(__dirname, adexManifest?.client?.outDir ?? '../client'), +} + +const server = createServer({ + port: PORT, + host: HOST, + adex: { + manifests: { server: serverManifest, client: clientManifest }, + paths, + client: adexManifest?.client ?? { bundle: false, islands: false }, + }, +}) + +if ('run' in server) { server.run() } +export default server.fetch +` } /** * @param {import("./vite.js").AdexOptions} [options] * @returns {(import("vite").Plugin)[]} */ -export function adex({ - fonts, - islands = false, - ssr = true, - adapter: adapter = 'node', -} = {}) { +export function adex({ fonts, islands = false, ssr = true, adapter } = {}) { + const adapterModule = adapter?.module ?? 'adex-adapter-node' + // @ts-expect-error probably because of the `.filter` return [ preactPages({ @@ -68,71 +113,10 @@ export function adex({ ), createVirtualModule( 'virtual:adex:server', - `import { createServer } from '${adapterMap[adapter]}' - import { dirname, join } from 'node:path' - import { fileURLToPath } from 'node:url' - import { existsSync, readFileSync } from 'node:fs' - import { env } from 'adex/env' - - import 'virtual:adex:font.css' - import 'virtual:adex:global.css' - - const __dirname = dirname(fileURLToPath(import.meta.url)) - - const PORT = parseInt(env.get('PORT', '3000'), 10) - const HOST = env.get('HOST', 'localhost') - - const paths = { - assets: join(__dirname, './assets'), - islands: join(__dirname, './islands'), - client: join(__dirname, '../client'), - } - - function getServerManifest() { - const manifestPath = join(__dirname, 'manifest.json') - if (existsSync(manifestPath)) { - const manifestFile = readFileSync(manifestPath, 'utf8') - return parseManifest(manifestFile) - } - return {} - } - - function getClientManifest() { - const manifestPath = join(__dirname, '../client/manifest.json') - if (existsSync(manifestPath)) { - const manifestFile = readFileSync(manifestPath, 'utf8') - return parseManifest(manifestFile) - } - return {} - } - - function parseManifest(manifestString) { - try { - const manifestJSON = JSON.parse(manifestString) - return manifestJSON - } catch (err) { - return {} - } - } - - const server = createServer({ - port: PORT, - host: HOST, - adex:{ - manifests:{server:getServerManifest(),client:getClientManifest()}, - paths, - } - }) - - if ('run' in server) { - server.run() - } - - export default server.fetch - ` + generateServerEntry(adapterModule) ), addFontsPlugin(fonts), - adexDevServer({ islands }), + adapter?.devServerPlugin?.({ islands }), adexBuildPrep({ islands }), adexClientBuilder({ ssr, islands }), islands && adexIslandsBuilder(), @@ -494,93 +478,10 @@ function adexClientSSRBuilder(opts) { } } -/** - * @returns {import("vite").Plugin} - */ -function adexDevServer({ islands = false } = {}) { - const devCSSMap = new Map() - let cfg - return { - name: 'adex-dev-server', - apply: 'serve', - enforce: 'pre', - config() { - return { - ssr: { - noExternal: ['adex/app'], - }, - } - }, - configResolved(_cfg) { - cfg = _cfg - }, - async resolveId(id, importer, meta) { - if (id.endsWith('.css')) { - if (!importer) return - const importerFromRoot = importer.replace(resolve(cfg.root), '') - const resolvedCss = await this.resolve(id, importer, meta) - if (resolvedCss) { - devCSSMap.set( - importerFromRoot, - (devCSSMap.get(importer) ?? []).concat(resolvedCss.id) - ) - } - return - } - }, - configureServer(server) { - return () => { - server.middlewares.use(async function (req, res, next) { - const module = await server.ssrLoadModule('virtual:adex:handler') - if (!module) { - return next() - } - try { - const { html, serverHandler, pageRoute } = await module.handler( - req, - res - ) - if (serverHandler) { - return serverHandler(req, res) - } - const cssLinks = devCSSMap.get(pageRoute) ?? [] - let renderedHTML = html.replace( - '', - ` - - ${cssLinks.map(d => { - return `` - })} - - ` - ) - if (!islands) { - renderedHTML = html.replace( - '', - `` - ) - } - const finalRenderedHTML = await server.transformIndexHtml( - req.url, - renderedHTML - ) - res.setHeader('content-type', 'text/html') - res.write(finalRenderedHTML) - return res.end() - } catch (err) { - server.ssrFixStacktrace(err) - next(err) - } - }) - } - }, - } -} - /** * @param {object} options * @param {import("./fonts.js").Options} options.fonts - * @param {string} options.adapter + * @param {import("./vite.js").AdapterConfig} options.adapter * @param {boolean} options.islands * @returns {import("vite").Plugin} */ @@ -616,7 +517,7 @@ function adexServerBuilder({ fonts, adapter, islands }) { configFile: false, ssr: { external: ['preact', 'adex', 'preact-render-to-string'], - noExternal: Object.values(adapterMap), + noExternal: adapter?.name ? [adapter.name] : [], }, resolve: cfg.resolve, appType: 'custom', @@ -651,69 +552,27 @@ function adexServerBuilder({ fonts, adapter, islands }) { ), createVirtualModule( 'virtual:adex:server', - `import { createServer } from '${adapterMap[adapter]}' - import { dirname, join } from 'node:path' - import { fileURLToPath } from 'node:url' - import { existsSync, readFileSync } from 'node:fs' - import { env } from 'adex/env' - - import 'virtual:adex:font.css' - import 'virtual:adex:global.css' - - const __dirname = dirname(fileURLToPath(import.meta.url)) - - const PORT = parseInt(env.get('PORT', '3000'), 10) - const HOST = env.get('HOST', 'localhost') - - const paths = { - assets: join(__dirname, './assets'), - islands: join(__dirname, './islands'), - client: join(__dirname, '../client'), - } - - function getServerManifest() { - const manifestPath = join(__dirname, 'manifest.json') - if (existsSync(manifestPath)) { - const manifestFile = readFileSync(manifestPath, 'utf8') - return parseManifest(manifestFile) - } - return {} - } - - function getClientManifest() { - const manifestPath = join(__dirname, '../client/manifest.json') - if (existsSync(manifestPath)) { - const manifestFile = readFileSync(manifestPath, 'utf8') - return parseManifest(manifestFile) - } - return {} - } - - function parseManifest(manifestString) { - try { - const manifestJSON = JSON.parse(manifestString) - return manifestJSON - } catch (err) { - return {} - } - } - - const server = createServer({ - port: PORT, - host: HOST, - adex:{ - manifests:{server:getServerManifest(),client:getClientManifest()}, - paths, - } - }) - - if ('run' in server) { - server.run() - } - - export default server.fetch - ` + generateServerEntry(adapter?.module ?? 'adex-adapter-node') ), + // Emit adex.manifest.json into the server output so the adapter + // kernel knows at runtime whether a client bundle was built. + { + name: 'adex-manifest-sidecar', + generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'adex.manifest.json', + source: JSON.stringify({ + client: { + bundle: !islands && existsSync(join(defOut, 'client')), + islands: !!islands, + manifestPath: '../client/manifest.json', + outDir: '../client', + }, + }), + }) + }, + }, addFontsPlugin(fonts), islands && adexIslandsBuilder(), ...sanitizedPlugins, diff --git a/adex/tests/fixtures/minimal-no-ssr/package.json b/adex/tests/fixtures/minimal-no-ssr/package.json index 0391cdf..e158d5b 100644 --- a/adex/tests/fixtures/minimal-no-ssr/package.json +++ b/adex/tests/fixtures/minimal-no-ssr/package.json @@ -5,6 +5,7 @@ "version": "0.0.0", "dependencies": { "adex": "workspace:*", + "adex-adapter-node": "workspace:*", "preact": "catalog:", "@preact/preset-vite": "catalog:" }, diff --git a/adex/tests/fixtures/minimal-no-ssr/vite.config.js b/adex/tests/fixtures/minimal-no-ssr/vite.config.js index c23ef59..c93159e 100644 --- a/adex/tests/fixtures/minimal-no-ssr/vite.config.js +++ b/adex/tests/fixtures/minimal-no-ssr/vite.config.js @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import { adex } from 'adex' +import { node } from 'adex-adapter-node' import preact from '@preact/preset-vite' export default defineConfig({ @@ -7,6 +8,7 @@ export default defineConfig({ adex({ islands: false, ssr: false, + adapter: node(), }), preact(), ], diff --git a/adex/tests/fixtures/minimal-tailwind/package.json b/adex/tests/fixtures/minimal-tailwind/package.json index a727196..5520956 100644 --- a/adex/tests/fixtures/minimal-tailwind/package.json +++ b/adex/tests/fixtures/minimal-tailwind/package.json @@ -6,6 +6,7 @@ "dependencies": { "@preact/preset-vite": "catalog:", "adex": "workspace:*", + "adex-adapter-node": "workspace:*", "preact": "catalog:" }, "devDependencies": { diff --git a/adex/tests/fixtures/minimal-tailwind/vite.config.js b/adex/tests/fixtures/minimal-tailwind/vite.config.js index 59f14af..d4d548f 100644 --- a/adex/tests/fixtures/minimal-tailwind/vite.config.js +++ b/adex/tests/fixtures/minimal-tailwind/vite.config.js @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import { adex } from 'adex' +import { node } from 'adex-adapter-node' import preact from '@preact/preset-vite' export default defineConfig({ @@ -7,6 +8,7 @@ export default defineConfig({ adex({ islands: false, ssr: true, + adapter: node(), }), preact(), ], diff --git a/adex/tests/fixtures/minimal/package.json b/adex/tests/fixtures/minimal/package.json index 3645fd8..4edf1e2 100644 --- a/adex/tests/fixtures/minimal/package.json +++ b/adex/tests/fixtures/minimal/package.json @@ -5,6 +5,7 @@ "version": "0.0.0", "dependencies": { "adex": "workspace:*", + "adex-adapter-node": "workspace:*", "preact": "catalog:", "@preact/preset-vite": "catalog:" }, diff --git a/adex/tests/fixtures/minimal/src/api/ping.js b/adex/tests/fixtures/minimal/src/api/ping.js new file mode 100644 index 0000000..142f4d0 --- /dev/null +++ b/adex/tests/fixtures/minimal/src/api/ping.js @@ -0,0 +1,9 @@ +/** + * Simple test API route for the minimal fixture. + * Used by integration tests to verify Fetch-based API handler behavior. + * @param {Request} request + * @returns {Response} + */ +export default function handler(request) { + return Response.json({ ok: true, method: request.method }) +} diff --git a/adex/tests/fixtures/minimal/vite.config.js b/adex/tests/fixtures/minimal/vite.config.js index 59f14af..d4d548f 100644 --- a/adex/tests/fixtures/minimal/vite.config.js +++ b/adex/tests/fixtures/minimal/vite.config.js @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import { adex } from 'adex' +import { node } from 'adex-adapter-node' import preact from '@preact/preset-vite' export default defineConfig({ @@ -7,6 +8,7 @@ export default defineConfig({ adex({ islands: false, ssr: true, + adapter: node(), }), preact(), ], diff --git a/adex/tests/http-bridge.spec.js b/adex/tests/http-bridge.spec.js new file mode 100644 index 0000000..d695acc --- /dev/null +++ b/adex/tests/http-bridge.spec.js @@ -0,0 +1,179 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { EventEmitter } from 'node:events' +import { nodeRequestToFetch, fetchResponseToNode } from '../src/http.js' + +/** + * Minimal mock of Node's IncomingMessage, enough for nodeRequestToFetch. + */ +function mockIncomingMessage({ + method = 'GET', + url = '/', + headers = { host: 'localhost' }, + body = null, + encrypted = false, +} = {}) { + const req = new EventEmitter() + req.method = method + req.url = url + req.headers = headers + req.socket = { encrypted } + + // Emit body chunks on next tick so callers can attach listeners first + if (body != null && method !== 'GET' && method !== 'HEAD') { + process.nextTick(() => { + req.emit('data', Buffer.from(body)) + req.emit('end') + }) + } else { + process.nextTick(() => req.emit('end')) + } + + return req +} + +/** + * Minimal mock of Node's ServerResponse. + */ +class MockServerResponse { + constructor() { + this.statusCode = 200 + this.headers = {} + this.chunks = [] + this.ended = false + } + setHeader(name, value) { + this.headers[name.toLowerCase()] = value + } + write(data) { + this.chunks.push(data) + } + end() { + this.ended = true + } + body() { + return Buffer.concat(this.chunks.map(c => Buffer.from(c))).toString('utf8') + } +} + +describe('nodeRequestToFetch', () => { + it('converts a GET request with no body', async () => { + const req = mockIncomingMessage({ method: 'GET', url: '/hello' }) + const fetchReq = await nodeRequestToFetch(req) + + assert.strictEqual(fetchReq.method, 'GET') + assert.ok(fetchReq.url.endsWith('/hello')) + assert.strictEqual(fetchReq.body, null) + }) + + it('reconstructs URL from req.url and Host header', async () => { + const req = mockIncomingMessage({ + method: 'GET', + url: '/path?q=1', + headers: { host: 'example.com' }, + }) + const fetchReq = await nodeRequestToFetch(req) + + assert.strictEqual(new URL(fetchReq.url).host, 'example.com') + assert.strictEqual(new URL(fetchReq.url).pathname, '/path') + assert.strictEqual(new URL(fetchReq.url).search, '?q=1') + }) + + it('uses https scheme when socket is encrypted', async () => { + const req = mockIncomingMessage({ + method: 'GET', + url: '/', + headers: { host: 'secure.example.com' }, + encrypted: true, + }) + const fetchReq = await nodeRequestToFetch(req) + + assert.ok(fetchReq.url.startsWith('https://')) + }) + + it('copies request headers', async () => { + const req = mockIncomingMessage({ + method: 'GET', + url: '/', + headers: { + 'host': 'localhost', + 'x-custom-header': 'my-value', + 'accept': 'application/json', + }, + }) + const fetchReq = await nodeRequestToFetch(req) + + assert.strictEqual(fetchReq.headers.get('x-custom-header'), 'my-value') + assert.strictEqual(fetchReq.headers.get('accept'), 'application/json') + }) + + it('buffers POST body into the Request', async () => { + const payload = JSON.stringify({ hello: 'world' }) + const req = mockIncomingMessage({ + method: 'POST', + url: '/api/data', + headers: { 'host': 'localhost', 'content-type': 'application/json' }, + body: payload, + }) + const fetchReq = await nodeRequestToFetch(req) + + assert.strictEqual(fetchReq.method, 'POST') + const text = await fetchReq.text() + assert.strictEqual(text, payload) + }) + + it('HEAD request has no body', async () => { + const req = mockIncomingMessage({ method: 'HEAD', url: '/' }) + const fetchReq = await nodeRequestToFetch(req) + + assert.strictEqual(fetchReq.method, 'HEAD') + assert.strictEqual(fetchReq.body, null) + }) +}) + +describe('fetchResponseToNode', () => { + it('copies status code', async () => { + const response = new Response('', { status: 201 }) + const res = new MockServerResponse() + await fetchResponseToNode(response, res) + + assert.strictEqual(res.statusCode, 201) + assert.strictEqual(res.ended, true) + }) + + it('copies response headers, skipping x-adex-* headers', async () => { + const response = new Response('', { + status: 200, + headers: { + 'content-type': 'application/json', + 'x-adex-page-route': '/should-be-skipped', + 'x-custom': 'kept', + }, + }) + const res = new MockServerResponse() + await fetchResponseToNode(response, res) + + assert.strictEqual(res.headers['content-type'], 'application/json') + assert.strictEqual(res.headers['x-custom'], 'kept') + assert.strictEqual(res.headers['x-adex-page-route'], undefined) + }) + + it('writes body buffer to res', async () => { + const response = new Response('hello adex', { status: 200 }) + const res = new MockServerResponse() + await fetchResponseToNode(response, res) + + assert.strictEqual(res.body(), 'hello adex') + assert.strictEqual(res.ended, true) + }) + + it('handles a 404 response with no body', async () => { + const response = new Response(null, { status: 404 }) + const res = new MockServerResponse() + await fetchResponseToNode(response, res) + + assert.strictEqual(res.statusCode, 404) + assert.strictEqual(res.ended, true) + assert.strictEqual(res.body(), '') + }) +}) diff --git a/adex/tests/minimal.spec.js b/adex/tests/minimal.spec.js index 2e003aa..bdde94d 100644 --- a/adex/tests/minimal.spec.js +++ b/adex/tests/minimal.spec.js @@ -1,4 +1,5 @@ import { after, before, describe, it } from 'node:test' +import assert from 'node:assert' import { devServerURL, launchDemoDevServer } from './utils.js' import { snapshot } from '@barelyhuman/node-snapshot' @@ -36,4 +37,20 @@ describe('devMode ssr minimal', async () => { ) ) }) + + await it('API route returns JSON with 200', async () => { + const response = await fetch(new URL('/api/ping', devServerURL)) + const json = await response.json() + + assert.strictEqual(response.status, 200) + assert.strictEqual(json.ok, true) + assert.strictEqual(json.method, 'GET') + }) + + await it('unknown route returns 404', async () => { + const response = await fetch( + new URL('/this-route-does-not-exist', devServerURL) + ) + assert.strictEqual(response.status, 404) + }) }) diff --git a/packages/adapters/node/lib/index.d.ts b/packages/adapters/node/lib/index.d.ts index dd955a7..d92ae20 100644 --- a/packages/adapters/node/lib/index.d.ts +++ b/packages/adapters/node/lib/index.d.ts @@ -1,6 +1,45 @@ -type ServerOut = { - run: () => any - fetch: undefined +import type { Plugin } from 'vite' + +export interface AdapterClientInfo { + /** true when a full client bundle was emitted to dist/client/ */ + bundle: boolean + /** true when islands were built to dist/server/islands/ */ + islands: boolean +} + +export interface AdexRuntimeConfig { + manifests: { server: object; client: object } + paths: { assets: string; islands: string; client: string } + client: AdapterClientInfo +} + +export interface NodeAdapterOptions { + port?: number | string + host?: string +} + +export interface AdapterConfig { + /** npm package name — added to ssr.noExternal so it bundles into the server output */ + name: string + /** the import specifier used in the generated virtual:adex:server entry */ + module: string + /** + * Returns a Vite plugin that handles dev-mode request serving. + * Called by the core adex() plugin with the same islands flag. + */ + devServerPlugin: (options: { islands: boolean }) => Plugin } -export const createServer: ({ port: number, host: string }) => ServerOut +/** + * Adapter factory — pass to adex({ adapter: node() }) in vite.config.js + */ +export declare function node(options?: NodeAdapterOptions): AdapterConfig + +/** + * Runtime server factory — called by the generated virtual:adex:server entry + */ +export declare const createServer: (options?: { + port?: number | string + host?: string + adex?: AdexRuntimeConfig +}) => { run: () => void; fetch: undefined } diff --git a/packages/adapters/node/lib/index.js b/packages/adapters/node/lib/index.js index efe2f1b..e08d0a1 100644 --- a/packages/adapters/node/lib/index.js +++ b/packages/adapters/node/lib/index.js @@ -1,20 +1,198 @@ import { existsSync } from 'node:fs' import http from 'node:http' +import { resolve } from 'node:path' -import { sirv, useMiddleware } from 'adex/ssr' +let islandMode = false -import { handler } from 'virtual:adex:handler' +/** + * Convert a Node.js IncomingMessage to a Fetch API Request. + * @param {import('node:http').IncomingMessage} req + * @returns {Promise} + */ +async function nodeRequestToFetch(req) { + const protocol = req.socket?.encrypted ? 'https' : 'http' + const host = req.headers['host'] ?? 'localhost' + const url = new URL(req.url, `${protocol}://${host}`) -let islandMode = false + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (Array.isArray(value)) { + for (const v of value) headers.append(key, v) + } else if (value != null) { + headers.set(key, value) + } + } + + const hasBody = req.method !== 'GET' && req.method !== 'HEAD' + let body = undefined + if (hasBody) { + body = await new Promise((resolve, reject) => { + const chunks = [] + req.on('data', chunk => chunks.push(chunk)) + req.on('end', () => resolve(Buffer.concat(chunks))) + req.on('error', reject) + }) + } + + return new Request(url.href, { + method: req.method, + headers, + body: body ?? null, + }) +} + +/** + * Write a Fetch API Response to a Node.js ServerResponse. + * Skips x-adex-* internal headers. + * @param {Response} response + * @param {import('node:http').ServerResponse} res + * @returns {Promise} + */ +async function fetchResponseToNode(response, res) { + res.statusCode = response.status + for (const [key, value] of response.headers.entries()) { + if (key.startsWith('x-adex-')) continue + res.setHeader(key, value) + } + if (response.body) { + const buf = Buffer.from(await response.arrayBuffer()) + res.write(buf) + } + res.end() +} + +/** + * Adapter factory — pass to adex({ adapter: node() }) in vite.config.js + * Returns an AdapterConfig object the core framework uses at build time. + * @param {import('./index.d.ts').NodeAdapterOptions} [options] + * @returns {import('./index.d.ts').AdapterConfig} + */ +export function node(options = {}) { + return { + name: 'adex-adapter-node', + module: 'adex-adapter-node', + devServerPlugin({ islands }) { + return createNodeDevServerPlugin({ islands }) + }, + } +} + +/** + * Creates the Vite dev server plugin for the node adapter. + * This is the relocated + updated equivalent of the old adexDevServer plugin + * that used to live in adex/src/vite.js. It owns how requests are served + * in dev mode for a Node.js environment. + * + * @param {{ islands: boolean }} options + * @returns {import('vite').Plugin} + */ +function createNodeDevServerPlugin({ islands = false } = {}) { + const devCSSMap = new Map() + let cfg + + return { + name: 'adex-dev-server', + apply: 'serve', + enforce: 'pre', + config() { + return { + ssr: { + noExternal: ['adex/app'], + }, + } + }, + configResolved(_cfg) { + cfg = _cfg + }, + async resolveId(id, importer, meta) { + if (id.endsWith('.css')) { + if (!importer) return + const importerFromRoot = importer.replace(resolve(cfg.root), '') + const resolvedCss = await this.resolve(id, importer, meta) + if (resolvedCss) { + devCSSMap.set( + importerFromRoot, + (devCSSMap.get(importer) ?? []).concat(resolvedCss.id) + ) + } + return + } + }, + configureServer(server) { + return () => { + server.middlewares.use(async function (req, res, next) { + const module = await server.ssrLoadModule('virtual:adex:handler') + if (!module) { + return next() + } + try { + const fetchRequest = await nodeRequestToFetch(req) + const response = await module.handler(fetchRequest) + const pageRoute = response.headers.get('x-adex-page-route') + + if (!pageRoute) { + // API response or 404 — write directly to node res + await fetchResponseToNode(response, res) + return + } + + // Page response — inject dev CSS preload links + HMR client script + const cssLinks = devCSSMap.get(pageRoute) ?? [] + let html = await response.text() + + html = html.replace( + '', + ` + + ${cssLinks + .map( + d => + `` + ) + .join('')} + ` + ) + + if (!islands) { + html = html.replace( + '', + `` + ) + } + + const finalHTML = await server.transformIndexHtml(req.url, html) + res.setHeader('content-type', 'text/html') + res.write(finalHTML) + res.end() + } catch (err) { + server.ssrFixStacktrace(err) + next(err) + } + }) + } + }, + } +} + +/** + * @param {{ manifests: object, paths: object, client: import('./index.d.ts').AdapterClientInfo }} adexConfig + */ +async function createHandler({ + manifests, + paths, + client = { bundle: true, islands: false }, +}) { + const { sirv, useMiddleware } = await import('adex/ssr') + // @ts-expect-error injected by vite + const { handler } = await import('virtual:adex:handler') -function createHandler({ manifests, paths }) { const serverAssets = sirv(paths.assets, { maxAge: 31536000, immutable: true, onNoMatch: defaultHandler, }) - let islandsWereGenerated = existsSync(paths.islands) + let islandsWereGenerated = client.islands && existsSync(paths.islands) // @ts-ignore let islandAssets = (req, res, next) => { @@ -30,7 +208,7 @@ function createHandler({ manifests, paths }) { }) } - let clientWasGenerated = existsSync(paths.client) + let clientWasGenerated = client.bundle && existsSync(paths.client) // @ts-ignore let clientAssets = (req, res, next) => { @@ -46,19 +224,24 @@ function createHandler({ manifests, paths }) { } async function defaultHandler(req, res) { - const { html: template, pageRoute, serverHandler } = await handler(req, res) - if (serverHandler) { - return serverHandler(req, res) + const fetchRequest = await nodeRequestToFetch(req) + const response = await handler(fetchRequest) + const pageRoute = response.headers.get('x-adex-page-route') + + if (!pageRoute) { + // API response or 404 — write directly + await fetchResponseToNode(response, res) + return } - const templateWithDeps = addDependencyAssets( - template, + // Page response — inject manifest CSS/JS assets + const html = await response.text() + const finalHTML = addDependencyAssets( + html, pageRoute, manifests.server, manifests.client ) - - const finalHTML = templateWithDeps res.setHeader('content-type', 'text/html') res.write(finalHTML) res.end() @@ -88,40 +271,11 @@ function createHandler({ manifests, paths }) { ) } -// function parseManifest(manifestString) { -// try { -// const manifestJSON = JSON.parse(manifestString) -// return manifestJSON -// } catch (err) { -// return {} -// } -// } - -// function getServerManifest() { -// const manifestPath = join(__dirname, 'manifest.json') -// if (existsSync(manifestPath)) { -// const manifestFile = readFileSync(manifestPath, 'utf8') -// return parseManifest(manifestFile) -// } -// return {} -// } - -// function getClientManifest() { -// const manifestPath = join(__dirname, '../client/manifest.json') -// if (existsSync(manifestPath)) { -// const manifestFile = readFileSync(manifestPath, 'utf8') -// return parseManifest(manifestFile) -// } -// return {} -// } - function manifestToHTML(manifest, filePath) { let links = [] let scripts = [] - // TODO: move it up the chain const rootServerFile = 'virtual:adex:server' - // if root manifest, also add it's css imports in if (manifest[rootServerFile]) { const graph = manifest[rootServerFile] links = links.concat( @@ -135,9 +289,7 @@ function manifestToHTML(manifest, filePath) { ) } - // TODO: move it up the chain const rootClientFile = 'virtual:adex:client' - // if root manifest, also add it's css imports in if (!islandMode && manifest[rootClientFile]) { const graph = manifest[rootClientFile] links = links.concat( @@ -229,14 +381,24 @@ export const createServer = ({ client: {}, }, paths: {}, + client: { bundle: true, islands: false }, }, } = {}) => { - const handler = createHandler(adex) - const server = http.createServer(handler) + // createHandler is async (uses dynamic imports); wrap in a lazy-init server + let server + + async function getServer() { + if (!server) { + const handler = await createHandler(adex) + server = http.createServer(handler) + } + return server + } return { - run() { - return server.listen(port, host, () => { + async run() { + const s = await getServer() + return s.listen(port, host, () => { console.log(`Listening on ${host}:${port}`) }) }, diff --git a/playground/src/api/$id/hello.js b/playground/src/api/$id/hello.js index b274d28..fd515b3 100644 --- a/playground/src/api/$id/hello.js +++ b/playground/src/api/$id/hello.js @@ -1,7 +1,11 @@ /** - * @param {import("adex/http").IncomingMessage} req - * @param {import("adex/http").ServerResponse} res + * @param {Request} request */ -export default (req, res) => { - return res.text(`Hello from ${req.params.id}`) +export default request => { + const { pathname } = new URL(request.url) + // route: /api/:id/hello — id is the second path segment + const id = pathname.split('/')[2] + return new Response(`Hello from ${id}`, { + headers: { 'content-type': 'text/plain' }, + }) } diff --git a/playground/src/api/$id/json.js b/playground/src/api/$id/json.js index a09c90d..34f36d4 100644 --- a/playground/src/api/$id/json.js +++ b/playground/src/api/$id/json.js @@ -1,9 +1,9 @@ /** - * @param {import("adex/http").IncomingMessage} req - * @param {import("adex/http").ServerResponse} res + * @param {Request} request */ -export default (req, res) => { - return res.json({ - message: `Hello in ${req.params.id}`, - }) +export default request => { + const { pathname } = new URL(request.url) + // route: /api/:id/json — id is the second path segment + const id = pathname.split('/')[2] + return Response.json({ message: `Hello in ${id}` }) } diff --git a/playground/src/api/hello.js b/playground/src/api/hello.js index c1ed97d..5d2f9e4 100644 --- a/playground/src/api/hello.js +++ b/playground/src/api/hello.js @@ -1,10 +1,10 @@ import { env } from 'adex/env' + /** - * @param {import("adex/http").IncomingMessage} req - * @param {import("adex/http").ServerResponse} res + * @param {Request} request */ -export default (req, res) => { - return res.json({ +export default request => { + return Response.json({ pong: true, appUrl: env.get('APP_URL'), }) diff --git a/playground/src/api/html.js b/playground/src/api/html.js index 291ce05..bf77345 100644 --- a/playground/src/api/html.js +++ b/playground/src/api/html.js @@ -5,9 +5,10 @@ afterAPICall(ctx => { }) /** - * @param {import("adex/http").IncomingMessage} req - * @param {import("adex/http").ServerResponse} res + * @param {Request} request */ -export default (req, res) => { - return res.html(`

Html Response

`) +export default request => { + return new Response(`

Html Response

`, { + headers: { 'content-type': 'text/html' }, + }) } diff --git a/playground/src/api/random-data.js b/playground/src/api/random-data.js index cb7e11f..0cb2f18 100644 --- a/playground/src/api/random-data.js +++ b/playground/src/api/random-data.js @@ -1,5 +1,8 @@ -export default function (req, res) { - return res.json( +/** + * @param {Request} request + */ +export default function (request) { + return Response.json( Array.from({ length: 3 }) .fill(0) .map((d, i) => (i + 1) * Math.random()) diff --git a/playground/src/api/status-helpers.js b/playground/src/api/status-helpers.js index 1f4f532..5ac563b 100644 --- a/playground/src/api/status-helpers.js +++ b/playground/src/api/status-helpers.js @@ -1,27 +1,50 @@ /** - * @param {import("adex/http").IncomingMessage} req - * @param {import("adex/http").ServerResponse} res + * @param {Request} request */ -export default (req, res) => { - const { pathname, searchParams } = new URL(req.url, 'http://localhost') +export default request => { + const { searchParams } = new URL(request.url) const type = searchParams.get('type') const message = searchParams.get('message') + const errorBody = msg => (msg ? JSON.stringify({ error: msg }) : null) + const jsonHeaders = { 'content-type': 'application/json' } switch (type) { case 'badRequest': - return res.badRequest(message) + return new Response(errorBody(message), { + status: 400, + headers: jsonHeaders, + }) case 'unauthorized': - return res.unauthorized(message) + return new Response(errorBody(message), { + status: 401, + headers: jsonHeaders, + }) case 'forbidden': - return res.forbidden(message) + return new Response(errorBody(message), { + status: 403, + headers: jsonHeaders, + }) case 'notFound': - return res.notFound(message) + return new Response(errorBody(message), { + status: 404, + headers: jsonHeaders, + }) case 'internalServerError': - return res.internalServerError(message) + return new Response(errorBody(message), { + status: 500, + headers: jsonHeaders, + }) default: - return res.json({ - usage: 'Add ?type=badRequest&message=Custom%20message to test status helpers', - available: ['badRequest', 'unauthorized', 'forbidden', 'notFound', 'internalServerError'] + return Response.json({ + usage: + 'Add ?type=badRequest&message=Custom%20message to test status helpers', + available: [ + 'badRequest', + 'unauthorized', + 'forbidden', + 'notFound', + 'internalServerError', + ], }) } -} \ No newline at end of file +} diff --git a/playground/vite.config.js b/playground/vite.config.js index 2ddcaeb..418da50 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -1,6 +1,7 @@ import { defineConfig } from 'vite' import preact from '@preact/preset-vite' import { adex } from 'adex' +import { node } from 'adex-adapter-node' import { providers } from 'adex/fonts' // https://vitejs.dev/config/ @@ -8,6 +9,7 @@ export default defineConfig({ plugins: [ adex({ islands: false, + adapter: node(), fonts: { providers: [providers.google()], families: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c21832..32ade60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: adex: specifier: workspace:* version: link:../../.. + adex-adapter-node: + specifier: workspace:* + version: link:../../../../packages/adapters/node preact: specifier: 'catalog:' version: 10.24.2 @@ -166,6 +169,9 @@ importers: adex: specifier: workspace:* version: link:../../.. + adex-adapter-node: + specifier: workspace:* + version: link:../../../../packages/adapters/node preact: specifier: 'catalog:' version: 10.24.2 @@ -182,6 +188,9 @@ importers: adex: specifier: workspace:* version: link:../../.. + adex-adapter-node: + specifier: workspace:* + version: link:../../../../packages/adapters/node preact: specifier: 'catalog:' version: 10.24.2 From 09ec5302f36a4aae17310a35e63d77ced9c108ff Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 11 Apr 2026 19:06:42 +0530 Subject: [PATCH 2/7] refactor: move serverEntry ownership to adapter, remove generateServerEntry from core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add serverEntry({ islands }) to node() factory — adapter now owns its own bootstrap template (filesystem reads, port/host, manifest loading) - Remove generateServerEntry() from adex/src/vite.js entirely - Remove 'module' field from AdapterConfig; core no longer needs the adapter's import specifier since the entry string is fully owned by the adapter - Add ensureAdapter() helper: defaults to adex-adapter-node if no adapter passed, throws a clear install message if the package is not found - Add createAdapterDevServerPlugin() proxy: lazily resolves the adapter and forwards all Vite hooks (config, configResolved, resolveId, configureServer) with correct 'this' binding so the inner plugin's context is preserved - virtual:adex:server load hook is now lazy (async) in both dev and build paths - All 28 tests pass --- adex/src/vite.d.ts | 7 +- adex/src/vite.js | 158 +++++++++++++++++--------- packages/adapters/node/lib/index.d.ts | 7 +- packages/adapters/node/lib/index.js | 46 +++++++- 4 files changed, 157 insertions(+), 61 deletions(-) diff --git a/adex/src/vite.d.ts b/adex/src/vite.d.ts index 1c8879c..c9a85b9 100644 --- a/adex/src/vite.d.ts +++ b/adex/src/vite.d.ts @@ -11,13 +11,16 @@ export interface AdapterClientInfo { export interface AdapterConfig { /** npm package name — added to ssr.noExternal so it bundles into the server output */ name: string - /** the import specifier used in the generated virtual:adex:server entry */ - module: string /** * Returns a Vite plugin that handles dev-mode request serving for this adapter. * Called by the core adex() plugin with the same islands flag. */ devServerPlugin: (options: { islands: boolean }) => Plugin + /** + * Returns the source code string for the virtual:adex:server entry point. + * Core injects this verbatim — all runtime bootstrap logic lives here. + */ + serverEntry: (options: { islands: boolean }) => string } export interface AdexOptions { diff --git a/adex/src/vite.js b/adex/src/vite.js index 62ad3fe..625baf9 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -24,55 +24,72 @@ const islandsDir = join(cwd, '.islands') let runningIslandBuild = false /** - * Generate the virtual:adex:server entry code for a given adapter module. - * Reads adex.manifest.json at runtime to discover client build info. - * @param {string} adapterModule - the import specifier for the adapter - * @returns {string} + * Resolve the adapter, defaulting to adex-adapter-node if none is provided. + * Cached after first resolution so it is only imported once per Vite session. + * Throws a clear, actionable error if the default cannot be found. + * @param {import("./vite.js").AdapterConfig | undefined} adapter + * @returns {Promise} */ -function generateServerEntry(adapterModule) { - return `import { createServer } from '${adapterModule}' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { readFileSync } from 'node:fs' -import { env } from 'adex/env' - -import 'virtual:adex:font.css' -import 'virtual:adex:global.css' - -const __dirname = dirname(fileURLToPath(import.meta.url)) - -const PORT = parseInt(env.get('PORT', '3000'), 10) -const HOST = env.get('HOST', 'localhost') - -function readJSON(p) { - try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} } +async function ensureAdapter(adapter) { + if (adapter) return adapter + try { + const mod = await import('adex-adapter-node') + return mod.node() + } catch { + throw new Error( + "[adex] No adapter was provided and 'adex-adapter-node' could not be found.\n" + + 'Install it: npm add adex-adapter-node\n' + + 'Or pass it explicitly: adex({ adapter: node() })' + ) + } } -const adexManifest = readJSON(join(__dirname, 'adex.manifest.json')) -const serverManifest = readJSON(join(__dirname, 'manifest.json')) -const clientManifest = adexManifest?.client?.bundle - ? readJSON(join(__dirname, adexManifest.client.manifestPath)) - : {} - -const paths = { - assets: join(__dirname, './assets'), - islands: join(__dirname, './islands'), - client: join(__dirname, adexManifest?.client?.outDir ?? '../client'), -} +/** + * Creates a proxy Vite plugin that forwards all dev-server hooks to the + * adapter's devServerPlugin. The adapter is resolved lazily on first use so + * that the default (adex-adapter-node) can be imported asynchronously when no + * adapter is explicitly provided. + * + * @param {{ adapter: import("./vite.js").AdapterConfig | undefined, islands: boolean }} options + * @returns {import("vite").Plugin} + */ +function createAdapterDevServerPlugin({ adapter, islands }) { + /** @type {import("vite").Plugin | null} */ + let inner = null + + async function getInner() { + if (inner) return inner + const resolved = await ensureAdapter(adapter) + inner = resolved.devServerPlugin({ islands }) + return inner + } -const server = createServer({ - port: PORT, - host: HOST, - adex: { - manifests: { server: serverManifest, client: clientManifest }, - paths, - client: adexManifest?.client ?? { bundle: false, islands: false }, - }, -}) - -if ('run' in server) { server.run() } -export default server.fetch -` + return { + name: 'adex-adapter-dev-server', + apply: 'serve', + enforce: 'pre', + async config(...args) { + const p = await getInner() + return p.config?.call(this, ...args) + }, + async configResolved(...args) { + const p = await getInner() + return p.configResolved?.call(this, ...args) + }, + async resolveId(...args) { + const p = await getInner() + return p.resolveId?.call(this, ...args) + }, + configureServer(server) { + // configureServer must return a function (or void) synchronously in + // Vite's plugin contract, but the return value can itself be async. + return async () => { + const plugin = await getInner() + const result = plugin.configureServer?.call(this, server) + if (typeof result === 'function') await result() + } + }, + } } /** @@ -80,8 +97,6 @@ export default server.fetch * @returns {(import("vite").Plugin)[]} */ export function adex({ fonts, islands = false, ssr = true, adapter } = {}) { - const adapterModule = adapter?.module ?? 'adex-adapter-node' - // @ts-expect-error probably because of the `.filter` return [ preactPages({ @@ -111,12 +126,28 @@ export function adex({ fonts, islands = false, ssr = true, adapter } = {}) { 'virtual:adex:handler', readFileSync(join(__dirname, '../runtime/handler.js'), 'utf8') ), - createVirtualModule( - 'virtual:adex:server', - generateServerEntry(adapterModule) - ), + // virtual:adex:server — content is adapter-owned, resolved lazily so the + // default adapter (adex-adapter-node) can be imported async if not provided. + { + name: 'adex-virtual-server-entry', + enforce: 'pre', + resolveId(id) { + if (id === 'virtual:adex:server' || id === '/virtual:adex:server') { + return '\0virtual:adex:server' + } + }, + async load(id) { + if (id !== '\0virtual:adex:server') return + const resolved = await ensureAdapter(adapter) + return resolved.serverEntry({ islands }) + }, + }, addFontsPlugin(fonts), - adapter?.devServerPlugin?.({ islands }), + // Dev-server plugin — adapter-owned. If no adapter is given, the default + // (adex-adapter-node) is resolved async on first hook invocation. + // We wrap it in a proxy plugin so all hooks (config, configResolved, + // resolveId, configureServer) are forwarded to the real plugin object. + createAdapterDevServerPlugin({ adapter, islands }), adexBuildPrep({ islands }), adexClientBuilder({ ssr, islands }), islands && adexIslandsBuilder(), @@ -550,10 +581,25 @@ function adexServerBuilder({ fonts, adapter, islands }) { 'virtual:adex:handler', readFileSync(join(__dirname, '../runtime/handler.js'), 'utf8') ), - createVirtualModule( - 'virtual:adex:server', - generateServerEntry(adapter?.module ?? 'adex-adapter-node') - ), + // virtual:adex:server — delegate to adapter.serverEntry() so adapters + // own their own bootstrap code. ensureAdapter() handles the default. + { + name: 'adex-virtual-server-entry', + enforce: 'pre', + resolveId(id) { + if ( + id === 'virtual:adex:server' || + id === '/virtual:adex:server' + ) { + return '\0virtual:adex:server' + } + }, + async load(id) { + if (id !== '\0virtual:adex:server') return + const resolved = await ensureAdapter(adapter) + return resolved.serverEntry({ islands }) + }, + }, // Emit adex.manifest.json into the server output so the adapter // kernel knows at runtime whether a client bundle was built. { diff --git a/packages/adapters/node/lib/index.d.ts b/packages/adapters/node/lib/index.d.ts index d92ae20..a20dd9a 100644 --- a/packages/adapters/node/lib/index.d.ts +++ b/packages/adapters/node/lib/index.d.ts @@ -21,13 +21,16 @@ export interface NodeAdapterOptions { export interface AdapterConfig { /** npm package name — added to ssr.noExternal so it bundles into the server output */ name: string - /** the import specifier used in the generated virtual:adex:server entry */ - module: string /** * Returns a Vite plugin that handles dev-mode request serving. * Called by the core adex() plugin with the same islands flag. */ devServerPlugin: (options: { islands: boolean }) => Plugin + /** + * Returns the source code string for the virtual:adex:server entry point. + * Core injects this verbatim — all runtime bootstrap logic lives here. + */ + serverEntry: (options: { islands: boolean }) => string } /** diff --git a/packages/adapters/node/lib/index.js b/packages/adapters/node/lib/index.js index e08d0a1..76be837 100644 --- a/packages/adapters/node/lib/index.js +++ b/packages/adapters/node/lib/index.js @@ -70,10 +70,54 @@ async function fetchResponseToNode(response, res) { export function node(options = {}) { return { name: 'adex-adapter-node', - module: 'adex-adapter-node', devServerPlugin({ islands }) { return createNodeDevServerPlugin({ islands }) }, + serverEntry({ islands }) { + return `import { createServer } from 'adex-adapter-node' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' +import { env } from 'adex/env' + +import 'virtual:adex:font.css' +import 'virtual:adex:global.css' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const PORT = parseInt(env.get('PORT', '3000'), 10) +const HOST = env.get('HOST', 'localhost') + +function readJSON(p) { + try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return {} } +} + +const adexManifest = readJSON(join(__dirname, 'adex.manifest.json')) +const serverManifest = readJSON(join(__dirname, 'manifest.json')) +const clientManifest = adexManifest?.client?.bundle + ? readJSON(join(join(__dirname, adexManifest.client.manifestPath))) + : {} + +const paths = { + assets: join(__dirname, './assets'), + islands: join(__dirname, './islands'), + client: join(__dirname, adexManifest?.client?.outDir ?? '../client'), +} + +const server = createServer({ + port: PORT, + host: HOST, + adex: { + manifests: { server: serverManifest, client: clientManifest }, + paths, + client: adexManifest?.client ?? { bundle: false, islands: false }, + }, +}) + +if ('run' in server) { server.run() } +export default server.fetch +` + }, } } From 92e98d982043b2fcd3cb63eac778e604301dc93b Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 11 Apr 2026 19:29:42 +0530 Subject: [PATCH 3/7] chore: dedupe preact --- pnpm-lock.yaml | 271 +++++++++++-------------------------------------- 1 file changed, 62 insertions(+), 209 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32ade60..c8d7b5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,13 +79,13 @@ importers: version: 1.15.0 preact: specifier: ^10.22.0 - version: 10.24.2 + version: 10.29.1 preact-iso: specifier: ^2.9.0 - version: 2.9.0(preact-render-to-string@6.5.5(preact@10.24.2))(preact@10.24.2) + version: 2.9.0(preact-render-to-string@6.5.5(preact@10.29.1))(preact@10.29.1) preact-render-to-string: specifier: ^6.5.5 - version: 6.5.5(preact@10.24.2) + version: 6.5.5(preact@10.29.1) regexparam: specifier: ^3.0.0 version: 3.0.0 @@ -107,16 +107,16 @@ importers: version: 1.1.0 '@preact/preset-vite': specifier: 'catalog:' - version: 2.10.5(@babel/core@7.24.7)(preact@10.24.2)(rollup@4.60.1)(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.24.7)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) '@types/node': specifier: ^20.14.10 - version: 20.16.10 + version: 20.19.39 adex-adapter-node: specifier: ^0.0.17 version: 0.0.17 autoprefixer: specifier: ^10.4.19 - version: 10.4.20(postcss@8.5.9) + version: 10.4.27(postcss@8.5.9) c8: specifier: 11.0.0 version: 11.0.0 @@ -134,13 +134,13 @@ importers: version: 0.8.0 prettier: specifier: ^3.5.3 - version: 3.5.3 + version: 3.8.1 tailwindcss: specifier: ^3.4.19 version: 3.4.19 vite: specifier: ^8.0.7 - version: 8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + version: 8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) adex/tests/fixtures/minimal: dependencies: @@ -1332,9 +1332,6 @@ packages: '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==, tarball: https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz} - '@types/node@20.16.10': - resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==, tarball: https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz} - '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==, tarball: https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz} @@ -1439,13 +1436,6 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==, tarball: https://registry.npmjs.org/astring/-/astring-1.9.0.tgz} hasBin: true - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==, tarball: https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - autoprefixer@10.4.27: resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==, tarball: https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz} engines: {node: ^10 || ^12 || >=14} @@ -1489,11 +1479,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmjs.org/braces/-/braces-3.0.3.tgz} engines: {node: '>=8'} - browserslist@4.24.0: - resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==, tarball: https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==, tarball: https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1526,9 +1511,6 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==, tarball: https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz} engines: {node: '>= 6'} - caniuse-lite@1.0.30001667: - resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==, tarball: https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz} - caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==, tarball: https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz} @@ -1641,15 +1623,6 @@ packages: engines: {node: '>=4'} hasBin: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.0.tgz} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} engines: {node: '>=6.0'} @@ -1719,9 +1692,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, tarball: https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz} - electron-to-chromium@1.5.32: - resolution: {integrity: sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==, tarball: https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz} - electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==, tarball: https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz} @@ -1752,10 +1722,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz} - engines: {node: '>=6'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz} engines: {node: '>=6'} @@ -1824,9 +1790,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==, tarball: https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz} engines: {node: '>=14'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==, tarball: https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz} - fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==, tarball: https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz} @@ -1887,10 +1850,6 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==, tarball: https://registry.npmjs.org/glob/-/glob-13.0.0.tgz} - engines: {node: 20 || >=22} - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==, tarball: https://registry.npmjs.org/glob/-/glob-13.0.6.tgz} engines: {node: 18 || 20 || >=22} @@ -2183,10 +2142,6 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, tarball: https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz} engines: {node: '>= 12.0.0'} - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz} - engines: {node: '>=14'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz} engines: {node: '>=14'} @@ -2213,10 +2168,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz} - engines: {node: 20 || >=22} - lru-cache@11.3.3: resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz} engines: {node: 20 || >=22} @@ -2285,10 +2236,6 @@ packages: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==, tarball: https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz} engines: {node: '>=8'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz} engines: {node: '>=16 || 14 >=14.17'} @@ -2352,9 +2299,6 @@ packages: node-html-parser@6.1.13: resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==, tarball: https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==, tarball: https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz} - node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==, tarball: https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz} @@ -2375,10 +2319,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, tarball: https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==, tarball: https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz} - engines: {node: '>=0.10.0'} - npm-bundled@5.0.0: resolution: {integrity: sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==, tarball: https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz} engines: {node: ^20.17.0 || >=22.9.0} @@ -2502,17 +2442,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, tarball: https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz} - engines: {node: 20 || >=22} - path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz} engines: {node: 18 || 20 || >=22} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} @@ -2594,11 +2527,6 @@ packages: preact@10.29.1: resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==, tarball: https://registry.npmjs.org/preact/-/preact-10.29.1.tgz} - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==, tarball: https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz} - engines: {node: '>=14'} - hasBin: true - prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, tarball: https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz} engines: {node: '>=14'} @@ -2888,9 +2816,6 @@ packages: unconfig@7.5.0: resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==, tarball: https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz} - undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz} @@ -2911,12 +2836,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, tarball: https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz} engines: {node: '>= 10.0.0'} - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==, tarball: https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, tarball: https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz} hasBin: true @@ -3179,7 +3098,7 @@ snapshots: dependencies: '@babel/compat-data': 7.24.7 '@babel/helper-validator-option': 7.24.7 - browserslist: 4.24.0 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -3549,7 +3468,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@isaacs/string-locale-compare@1.1.0': {} @@ -3795,7 +3714,7 @@ snapshots: dependencies: '@npmcli/name-from-folder': 4.0.0 '@npmcli/package-json': 7.0.5 - glob: 13.0.0 + glob: 13.0.6 minimatch: 10.2.5 '@npmcli/metavuln-calculator@9.0.3': @@ -3815,7 +3734,7 @@ snapshots: '@npmcli/package-json@7.0.5': dependencies: '@npmcli/git': 7.0.2 - glob: 13.0.0 + glob: 13.0.6 hosted-git-info: 9.0.2 json-parse-even-better-errors: 5.0.0 proc-log: 6.1.0 @@ -3930,57 +3849,57 @@ snapshots: - rollup - supports-color - '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.24.2)(rollup@4.60.1)(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': + '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.24.2)(rollup@4.60.1)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.24.7) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.24.7) - '@prefresh/vite': 2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) + '@prefresh/vite': 2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.24.7) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) - vite-prerender-plugin: 0.5.13(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) + vite: 8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact - rollup - supports-color - '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.24.2)(rollup@4.60.1)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': + '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.24.7) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.24.7) - '@prefresh/vite': 2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) + '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.24.7) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) - vite-prerender-plugin: 0.5.13(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) + vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact - rollup - supports-color - '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + '@preact/preset-vite@2.10.5(@babel/core@7.24.7)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.24.7) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.24.7) - '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.24.7) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) - vite-prerender-plugin: 0.5.13(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + vite: 8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -4018,7 +3937,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@prefresh/vite@2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': + '@prefresh/vite@2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@prefresh/babel-plugin': 0.5.3 @@ -4026,23 +3945,23 @@ snapshots: '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 preact: 10.24.2 - vite: 8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + vite: 8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@prefresh/vite@2.4.12(preact@10.24.2)(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': + '@prefresh/vite@2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@prefresh/babel-plugin': 0.5.3 - '@prefresh/core': 1.5.2(preact@10.24.2) + '@prefresh/core': 1.5.2(preact@10.29.1) '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 - preact: 10.24.2 - vite: 8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + preact: 10.29.1 + vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@prefresh/vite@2.4.12(preact@10.29.1)(vite@6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@babel/core': 7.24.7 '@prefresh/babel-plugin': 0.5.3 @@ -4050,7 +3969,7 @@ snapshots: '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 preact: 10.29.1 - vite: 6.4.2(@types/node@20.19.39)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + vite: 8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -4264,10 +4183,6 @@ snapshots: '@types/istanbul-lib-coverage@2.0.6': {} - '@types/node@20.16.10': - dependencies: - undici-types: 6.19.8 - '@types/node@20.19.39': dependencies: undici-types: 6.21.0 @@ -4317,7 +4232,7 @@ snapshots: agent-base@7.1.1: dependencies: - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4348,16 +4263,6 @@ snapshots: astring@1.9.0: {} - autoprefixer@10.4.20(postcss@8.5.9): - dependencies: - browserslist: 4.24.0 - caniuse-lite: 1.0.30001667 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.0 - postcss: 8.5.9 - postcss-value-parser: 4.2.0 - autoprefixer@10.4.27(postcss@8.5.9): dependencies: browserslist: 4.28.2 @@ -4397,13 +4302,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.0: - dependencies: - caniuse-lite: 1.0.30001667 - electron-to-chromium: 1.5.32 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.24.0) - browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.16 @@ -4444,9 +4342,9 @@ snapshots: dependencies: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 - glob: 13.0.0 + glob: 13.0.6 lru-cache: 11.3.3 - minipass: 7.1.2 + minipass: 7.1.3 minipass-collect: 2.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -4455,8 +4353,6 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001667: {} - caniuse-lite@1.0.30001787: {} chokidar@3.6.0: @@ -4581,10 +4477,6 @@ snapshots: cssesc@3.0.0: {} - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -4635,8 +4527,6 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.32: {} - electron-to-chromium@1.5.334: {} emoji-regex@10.4.0: {} @@ -4708,8 +4598,6 @@ snapshots: '@esbuild/win32-x64': 0.27.2 optional: true - escalade@3.1.2: {} - escalade@3.2.0: {} estree-walker@2.0.2: {} @@ -4788,8 +4676,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fraction.js@4.3.7: {} - fraction.js@5.3.4: {} fs-extra@11.3.4: @@ -4800,7 +4686,7 @@ snapshots: fs-minipass@3.0.3: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 fsevents@2.3.3: optional: true @@ -4830,7 +4716,7 @@ snapshots: glob-bin@1.0.0: dependencies: foreground-child: 3.3.1 - glob: 13.0.0 + glob: 13.0.6 jackspeak: 4.1.1 package-json-from-dist: 1.0.1 @@ -4847,15 +4733,9 @@ snapshots: foreground-child: 3.3.1 jackspeak: 4.1.1 minimatch: 10.2.5 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 - - glob@13.0.0: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.2 - path-scurry: 2.0.0 + path-scurry: 2.0.2 glob@13.0.6: dependencies: @@ -4905,14 +4785,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -5099,8 +4979,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lilconfig@3.1.2: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -5121,8 +4999,6 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} - lru-cache@11.3.3: {} lru-cache@5.1.1: @@ -5144,7 +5020,7 @@ snapshots: '@npmcli/redact': 4.0.0 cacache: 20.0.4 http-cache-semantics: 4.1.1 - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch: 5.0.2 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -5175,11 +5051,11 @@ snapshots: minipass-collect@2.0.1: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch@5.0.2: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-sized: 2.0.0 minizlib: 3.1.0 optionalDependencies: @@ -5195,19 +5071,17 @@ snapshots: minipass-sized@2.0.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass@3.3.6: dependencies: yallist: 4.0.0 - minipass@7.1.2: {} - minipass@7.1.3: {} minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mri@1.2.0: {} @@ -5261,8 +5135,6 @@ snapshots: css-select: 5.1.0 he: 1.2.0 - node-releases@2.0.18: {} - node-releases@2.0.37: {} node-stream-zip@1.15.0: {} @@ -5279,8 +5151,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - npm-bundled@5.0.0: dependencies: npm-normalize-package-bin: 5.0.0 @@ -5315,7 +5185,7 @@ snapshots: '@npmcli/redact': 4.0.0 jsonparse: 1.3.1 make-fetch-happen: 15.0.5 - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch: 5.0.2 minizlib: 3.1.0 npm-package-arg: 13.0.2 @@ -5383,7 +5253,7 @@ snapshots: '@npmcli/run-script': 10.0.4 cacache: 20.0.4 fs-minipass: 3.0.3 - minipass: 7.1.2 + minipass: 7.1.3 npm-package-arg: 13.0.2 npm-packlist: 10.0.4 npm-pick-manifest: 11.0.3 @@ -5420,18 +5290,11 @@ snapshots: path-parse@1.0.7: {} - path-scurry@2.0.0: - dependencies: - lru-cache: 11.0.2 - minipass: 7.1.2 - path-scurry@2.0.2: dependencies: lru-cache: 11.3.3 minipass: 7.1.3 - picocolors@1.1.0: {} - picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -5458,7 +5321,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.9): dependencies: - lilconfig: 3.1.2 + lilconfig: 3.1.3 yaml: 2.8.3 optionalDependencies: postcss: 8.5.9 @@ -5486,21 +5349,19 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact-iso@2.9.0(preact-render-to-string@6.5.5(preact@10.24.2))(preact@10.24.2): + preact-iso@2.9.0(preact-render-to-string@6.5.5(preact@10.29.1))(preact@10.29.1): dependencies: - preact: 10.24.2 - preact-render-to-string: 6.5.5(preact@10.24.2) + preact: 10.29.1 + preact-render-to-string: 6.5.5(preact@10.29.1) - preact-render-to-string@6.5.5(preact@10.24.2): + preact-render-to-string@6.5.5(preact@10.29.1): dependencies: - preact: 10.24.2 + preact: 10.29.1 preact@10.24.2: {} preact@10.29.1: {} - prettier@3.5.3: {} - prettier@3.8.1: {} pretty-format@29.7.0: @@ -5658,7 +5519,7 @@ snapshots: socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.3 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -5701,7 +5562,7 @@ snapshots: ssri@13.0.1: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 stack-trace@1.0.0-pre2: {} @@ -5780,7 +5641,7 @@ snapshots: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 - minipass: 7.1.2 + minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 @@ -5850,8 +5711,6 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.5.0 - undici-types@6.19.8: {} - undici-types@6.20.0: optional: true @@ -5871,12 +5730,6 @@ snapshots: universalify@2.0.1: {} - update-browserslist-db@1.1.0(browserslist@4.24.0): - dependencies: - browserslist: 4.24.0 - escalade: 3.1.2 - picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -5918,7 +5771,7 @@ snapshots: stack-trace: 1.0.0-pre2 vite: 6.4.2(@types/node@22.13.16)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) - vite-prerender-plugin@0.5.13(vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)): + vite-prerender-plugin@0.5.13(vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -5926,7 +5779,7 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.4 stack-trace: 1.0.0-pre2 - vite: 8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) + vite: 8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3) vite-prerender-plugin@0.5.13(vite@8.0.7(@types/node@22.13.16)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3)): dependencies: @@ -5968,7 +5821,7 @@ snapshots: lightningcss: 1.32.0 yaml: 2.8.3 - vite@8.0.7(@types/node@20.16.10)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3): + vite@8.0.7(@types/node@20.19.39)(esbuild@0.27.2)(jiti@2.6.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5976,7 +5829,7 @@ snapshots: rolldown: 1.0.0-rc.13 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 20.16.10 + '@types/node': 20.19.39 esbuild: 0.27.2 fsevents: 2.3.3 jiti: 2.6.1 @@ -6075,7 +5928,7 @@ snapshots: yargs@18.0.0: dependencies: cliui: 9.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 string-width: 7.2.0 y18n: 5.0.8 From a7137b8186dafbc845ed5327e272da72d372cac8 Mon Sep 17 00:00:00 2001 From: reaper Date: Sat, 11 Apr 2026 19:56:42 +0530 Subject: [PATCH 4/7] chore: correct bundle check --- adex/src/vite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adex/src/vite.js b/adex/src/vite.js index 625baf9..0404976 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -610,7 +610,7 @@ function adexServerBuilder({ fonts, adapter, islands }) { fileName: 'adex.manifest.json', source: JSON.stringify({ client: { - bundle: !islands && existsSync(join(defOut, 'client')), + bundle: !islands, islands: !!islands, manifestPath: '../client/manifest.json', outDir: '../client', From a161e702cc0861dfa067cfdd70d66f4705527646 Mon Sep 17 00:00:00 2001 From: reaper Date: Sun, 12 Apr 2026 11:15:39 +0530 Subject: [PATCH 5/7] chore: add in a dummy deno adapter --- adex/runtime/handler.js | 6 +- adex/src/vite.d.ts | 8 + adex/src/vite.js | 21 +- packages/adapters/deno/lib/dev.js | 102 ++++++++ packages/adapters/deno/lib/index.d.ts | 61 +++++ packages/adapters/deno/lib/index.js | 320 ++++++++++++++++++++++++++ packages/adapters/deno/package.json | 45 ++++ packages/adapters/node/lib/dev.js | 100 ++++++++ packages/adapters/node/lib/index.js | 157 +------------ packages/adapters/node/package.json | 3 + playground/deno.lock | 70 ++++++ playground/package.json | 1 + playground/vite.config.js | 5 +- pnpm-lock.yaml | 15 +- 14 files changed, 748 insertions(+), 166 deletions(-) create mode 100644 packages/adapters/deno/lib/dev.js create mode 100644 packages/adapters/deno/lib/index.d.ts create mode 100644 packages/adapters/deno/lib/index.js create mode 100644 packages/adapters/deno/package.json create mode 100644 packages/adapters/node/lib/dev.js create mode 100644 playground/deno.lock diff --git a/adex/runtime/handler.js b/adex/runtime/handler.js index b8c2a4b..c51b730 100644 --- a/adex/runtime/handler.js +++ b/adex/runtime/handler.js @@ -59,7 +59,7 @@ export async function handler(request) { const routeParams = getRouteParams(baseURL, matchedInPages) // @ts-expect-error - global.location = new URL(request.url) + globalThis.location = new URL(request.url) const context = { request } await emitToHooked(CONSTANTS.beforePageRender, context) @@ -75,9 +75,7 @@ export async function handler(request) { title, lang, entryPage: matchedInPages.route, - routeParams: Buffer.from(JSON.stringify(routeParams), 'utf8').toString( - 'base64' - ), + routeParams: btoa(JSON.stringify(routeParams)), body: rendered, }) diff --git a/adex/src/vite.d.ts b/adex/src/vite.d.ts index c9a85b9..b94f262 100644 --- a/adex/src/vite.d.ts +++ b/adex/src/vite.d.ts @@ -1,5 +1,6 @@ import { UserConfig, Plugin } from 'vite' import type { Options as FontOptions } from './fonts.js' +import type { RollupOptions } from 'rollup' export interface AdapterClientInfo { bundle: boolean @@ -21,6 +22,13 @@ export interface AdapterConfig { * Core injects this verbatim — all runtime bootstrap logic lives here. */ serverEntry: (options: { islands: boolean }) => string + /** + * Optional hook to extend or override the Rollup options used in the SSR + * server build. The base options are passed in; return the final options. + * Use this to add extra `external` patterns (e.g. /^https?:\/\//) or set + * `output.preserveModules: true` for runtimes like Deno. + */ + rollupOptions?: (base: RollupOptions) => RollupOptions } export interface AdexOptions { diff --git a/adex/src/vite.js b/adex/src/vite.js index 0404976..7351358 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -544,11 +544,25 @@ function adexServerBuilder({ fonts, adapter, islands }) { .filter(d => !d.name.startsWith('vite:')) .filter(d => !d.name.startsWith('adex-')) + // Resolve adapter early so we can call rollupOptions() below. + const resolvedAdapter = await ensureAdapter(adapter) + + // Base Rollup options. The adapter may extend these via rollupOptions(). + const baseRollupOptions = { + input: { + index: input, + }, + external: ['adex/ssr', /^jsr:/, /^npm:/], + } + const finalRollupOptions = resolvedAdapter.rollupOptions + ? resolvedAdapter.rollupOptions(baseRollupOptions) + : baseRollupOptions + await build({ configFile: false, ssr: { external: ['preact', 'adex', 'preact-render-to-string'], - noExternal: adapter?.name ? [adapter.name] : [], + noExternal: resolvedAdapter?.name ? [resolvedAdapter.name] : [], }, resolve: cfg.resolve, appType: 'custom', @@ -596,8 +610,7 @@ function adexServerBuilder({ fonts, adapter, islands }) { }, async load(id) { if (id !== '\0virtual:adex:server') return - const resolved = await ensureAdapter(adapter) - return resolved.serverEntry({ islands }) + return resolvedAdapter.serverEntry({ islands }) }, }, // Emit adex.manifest.json into the server output so the adapter @@ -635,7 +648,7 @@ function adexServerBuilder({ fonts, adapter, islands }) { input: { index: input, }, - external: ['adex/ssr'], + external: ['adex/ssr', /^http:/, /^npm:/, /^jsr:/], }, }, }) diff --git a/packages/adapters/deno/lib/dev.js b/packages/adapters/deno/lib/dev.js new file mode 100644 index 0000000..c934dbf --- /dev/null +++ b/packages/adapters/deno/lib/dev.js @@ -0,0 +1,102 @@ +// Dev-mode Vite plugin for the Deno adapter. +// Vite's dev server always runs on Node.js, so this file uses Node-only APIs +// (adex/http bridge, node:path) and is never imported in the Deno production runtime. + +import { nodeRequestToFetch, fetchResponseToNode } from 'adex/http' +import { resolve } from 'node:path' + +/** + * Creates the Vite dev server plugin for the Deno adapter. + * Even though the production target is Deno, the dev server is always + * Node.js (Vite), so we use the same Node ↔ Fetch bridge as the node adapter. + * + * @param {{ islands: boolean }} options + * @returns {import('vite').Plugin} + */ +export function createDenoDevServerPlugin({ islands = false } = {}) { + const devCSSMap = new Map() + let cfg + + return { + name: 'adex-dev-server', + apply: 'serve', + enforce: 'pre', + config() { + return { + ssr: { + noExternal: ['adex/app'], + }, + } + }, + configResolved(_cfg) { + cfg = _cfg + }, + async resolveId(id, importer, meta) { + if (id.endsWith('.css')) { + if (!importer) return + const importerFromRoot = importer.replace(resolve(cfg.root), '') + const resolvedCss = await this.resolve(id, importer, meta) + if (resolvedCss) { + devCSSMap.set( + importerFromRoot, + (devCSSMap.get(importer) ?? []).concat(resolvedCss.id) + ) + } + return + } + }, + configureServer(server) { + return () => { + server.middlewares.use(async function (req, res, next) { + const module = await server.ssrLoadModule('virtual:adex:handler') + if (!module) { + return next() + } + try { + const fetchRequest = await nodeRequestToFetch(req) + const response = await module.handler(fetchRequest) + const pageRoute = response.headers.get('x-adex-page-route') + + if (!pageRoute) { + // API response or 404 — write directly to node res + await fetchResponseToNode(response, res) + return + } + + // Page response — inject dev CSS preload links + HMR client script + const cssLinks = devCSSMap.get(pageRoute) ?? [] + let html = await response.text() + + html = html.replace( + '', + ` + + ${cssLinks + .map( + d => + `` + ) + .join('')} + ` + ) + + if (!islands) { + html = html.replace( + '', + `` + ) + } + + const finalHTML = await server.transformIndexHtml(req.url, html) + res.setHeader('content-type', 'text/html') + res.write(finalHTML) + res.end() + } catch (err) { + server.ssrFixStacktrace(err) + next(err) + } + }) + } + }, + } +} diff --git a/packages/adapters/deno/lib/index.d.ts b/packages/adapters/deno/lib/index.d.ts new file mode 100644 index 0000000..7c4911d --- /dev/null +++ b/packages/adapters/deno/lib/index.d.ts @@ -0,0 +1,61 @@ +import type { Plugin } from 'vite' +import type { RollupOptions } from 'rollup' + +export interface AdapterClientInfo { + /** true when a full client bundle was emitted to dist/client/ */ + bundle: boolean + /** true when islands were built to dist/server/islands/ */ + islands: boolean +} + +export interface AdexRuntimeConfig { + manifests: { server: object; client: object } + paths: { assets: string; islands: string; client: string } + client: AdapterClientInfo +} + +export interface DenoAdapterOptions { + port?: number | string + hostname?: string +} + +export interface DenoServerOptions { + port?: number | string + hostname?: string + adex?: AdexRuntimeConfig +} + +export interface AdapterConfig { + /** npm package name — added to ssr.noExternal so it bundles into the server output */ + name: string + /** + * Returns a Vite plugin that handles dev-mode request serving. + * Called by the core adex() plugin with the same islands flag. + */ + devServerPlugin: (options: { islands: boolean }) => Plugin + /** + * Returns the source code string for the virtual:adex:server entry point. + * Core injects this verbatim — all runtime bootstrap logic lives here. + */ + serverEntry: (options: { islands: boolean }) => string + /** + * Optional hook to extend/override the Rollup options for the SSR server + * build. The Deno adapter uses this to enable preserveModules and add + * https:// / node: specifiers to external. + */ + rollupOptions?: (base: RollupOptions) => RollupOptions +} + +/** + * Adapter factory — pass to adex({ adapter: deno() }) in vite.config.js + */ +export declare function deno(options?: DenoAdapterOptions): AdapterConfig + +/** + * Runtime server factory — called by the generated virtual:adex:server entry. + * Uses Deno.serve() for the production HTTP server. + */ +export declare const createServer: (options?: DenoServerOptions) => { + run(): void + fetch: (req: Request) => Promise +} diff --git a/packages/adapters/deno/lib/index.js b/packages/adapters/deno/lib/index.js new file mode 100644 index 0000000..fa9a96d --- /dev/null +++ b/packages/adapters/deno/lib/index.js @@ -0,0 +1,320 @@ +// Production runtime for the Deno adapter. +// This file contains only Deno-compatible code — no Node.js APIs, no Buffer. +// The dev-mode Vite plugin (which runs on Node.js) lives in ./dev.js. + +import { createDenoDevServerPlugin } from './dev.js' + +// ─── Adapter factory ────────────────────────────────────────────────────────── + +/** + * Adapter factory — pass to adex({ adapter: deno() }) in vite.config.js + * Returns an AdapterConfig object the core framework uses at build time. + * @param {import('./index.d.ts').DenoAdapterOptions} [options] + * @returns {import('./index.d.ts').AdapterConfig} + */ +export function deno(options = {}) { + return { + name: 'adex-adapter-deno', + + devServerPlugin({ islands }) { + return createDenoDevServerPlugin({ islands }) + }, + + /** + * Extend the Rollup SSR build options for Deno compatibility: + * - preserveModules: true → Rollup emits one file per module and leaves + * import specifiers (jsr:, npm:, https://, node:) untouched, so user + * code can freely use Deno-style imports without Rollup erroring on + * unknown protocols. + * - external additions: https?:// and node: specifiers that may appear + * in user pages / API routes. + * - sanitizeFileName: replaces ':' (invalid on Windows / confusing on + * Unix) in virtual module IDs with '_'. + * The adex core will emit an index.js shim pointing at the real entry + * when the entry is not already named index.js (preserveModules case). + */ + rollupOptions(base) { + const baseExternal = Array.isArray(base.external) + ? base.external + : base.external + ? [base.external] + : [] + return { + ...base, + external: [...baseExternal, /^https?:\/\//, /^node:/], + output: { + ...(Array.isArray(base.output) ? {} : (base.output ?? {})), + preserveModules: true, + sanitizeFileName: name => name.replace(/[:<>|?*]/g, '_'), + }, + } + }, + + serverEntry({ islands }) { + return `import { createServer } from 'adex-adapter-deno' +import { join } from 'jsr:@std/path' +import { env } from 'adex/env' + +import 'virtual:adex:font.css' +import 'virtual:adex:global.css' + +// Deno 1.40+ exposes import.meta.dirname; fall back to URL parsing for older versions. +const __dirname = import.meta.dirname ?? new URL('.', import.meta.url).pathname + +const PORT = parseInt(env.get('PORT', '3000'), 10) +const HOSTNAME = env.get('HOST', 'localhost') + +function readJSON(p) { + try { return JSON.parse(Deno.readTextFileSync(p)) } catch { return {} } +} + +const adexManifest = readJSON(join(__dirname, 'adex.manifest.json')) +const serverManifest = readJSON(join(__dirname, 'manifest.json')) +const clientManifest = adexManifest?.client?.bundle + ? readJSON(join(__dirname, adexManifest.client.manifestPath)) + : {} + +const paths = { + assets: join(__dirname, './assets'), + islands: join(__dirname, './islands'), + client: join(__dirname, adexManifest?.client?.outDir ?? '../client'), +} + +const server = createServer({ + port: PORT, + hostname: HOSTNAME, + adex: { + manifests: { server: serverManifest, client: clientManifest }, + paths, + client: adexManifest?.client ?? { bundle: false, islands: false }, + }, +}) + +if ('run' in server) { server.run() } +export default server.fetch +` + }, + } +} + +// ─── Production server ──────────────────────────────────────────────────────── + +/** + * Build the injected HTML strings for CSS/JS assets from the Vite manifests. + * + * @param {object} manifest + * @param {string} filePath + * @returns {{ links: string[], scripts: string[] }} + */ +function manifestToHTML(manifest, filePath) { + let links = [] + let scripts = [] + + const rootServerFile = 'virtual:adex:server' + if (manifest[rootServerFile]) { + const graph = manifest[rootServerFile] + links = links.concat( + (graph.css || []).map(d => ``) + ) + } + + const rootClientFile = 'virtual:adex:client' + if (manifest[rootClientFile]) { + const graph = manifest[rootClientFile] + links = links.concat( + (graph.css || []).map(d => ``) + ) + } + + if (manifest[filePath]) { + const graph = manifest[filePath] + links = links.concat( + (graph.css || []).map(d => ``) + ) + + const depsHasCSS = (manifest[filePath].imports || []) + .map(d => manifest[d]) + .filter(d => d?.css?.length) + + if (depsHasCSS.length) { + links = links.concat( + depsHasCSS.map(d => + d.css.map(p => ``).join('\n') + ) + ) + } + + scripts = scripts.concat( + `` + ) + } + + return { scripts, links } +} + +/** + * Inject manifest-driven CSS/JS asset tags into an HTML string. + * + * @param {string} template + * @param {string} pageRoute + * @param {object} serverManifest + * @param {object} clientManifest + * @returns {string} + */ +function addDependencyAssets( + template, + pageRoute, + serverManifest, + clientManifest +) { + if (!pageRoute) return template + + const filePath = pageRoute.startsWith('/') ? pageRoute.slice(1) : pageRoute + + const { links: serverLinks } = manifestToHTML(serverManifest, filePath) + const { links: clientLinks, scripts: clientScripts } = manifestToHTML( + clientManifest, + filePath + ) + + const links = [...serverLinks, ...clientLinks] + const scripts = [...clientScripts] + + return template.replace( + '', + links.join('\n') + scripts.join('\n') + '' + ) +} + +/** + * Serve a static file from a directory using Deno's standard library. + * Returns a Response if found, null if not. + * + * @param {Request} request + * @param {string} dir — absolute path to the directory root + * @param {string} prefix — URL prefix to strip before resolving (e.g. '/assets') + * @returns {Promise} + */ +async function serveStatic(request, dir, prefix = '') { + const { serveDir } = await import('jsr:@std/http/file-server') + return serveDir(request, { + fsRoot: dir, + urlRoot: prefix.replace(/^\//, ''), + quiet: true, + }) +} + +/** + * Production Deno server factory. + * + * @param {import('./index.d.ts').DenoServerOptions} options + * @returns {{ run(): void; fetch: (req: Request) => Promise }} + */ +export const createServer = ({ + port = 3000, + hostname = '127.0.0.1', + adex = { + manifests: { server: {}, client: {} }, + paths: {}, + client: { bundle: true, islands: false }, + }, +} = {}) => { + /** @type {((req: Request) => Promise) | null} */ + let fetchHandler = null + + async function getFetchHandler() { + if (fetchHandler) return fetchHandler + + // @ts-expect-error injected by vite + const { handler } = await import('virtual:adex:handler') + + const { manifests, paths, client } = adex + + /** + * @param {Request} request + * @returns {Promise} + */ + async function handle(request) { + const url = new URL(request.url) + const pathname = url.pathname + + // 1. /assets/** — server-emitted static assets (fonts, etc.) + if (pathname.startsWith('/assets/') && paths.assets) { + try { + const resp = await serveStatic(request, paths.assets, '/assets') + if (resp && resp.status !== 404) return resp + } catch { + // fall through to app handler + } + } + + // 2. /islands/** — island JS bundles + if (client.islands && pathname.startsWith('/islands/') && paths.islands) { + try { + const resp = await serveStatic(request, paths.islands, '/islands') + if (resp && resp.status !== 404) return resp + } catch { + // fall through + } + } + + // 3. Client bundle — CSS, hashed JS, etc. + if (client.bundle && paths.client) { + try { + const resp = await serveStatic(request, paths.client, '') + if (resp && resp.status !== 404) return resp + } catch { + // fall through + } + } + + // 4. App handler (SSR pages + API routes) + const response = await handler(request) + const pageRoute = response.headers.get('x-adex-page-route') + + if (!pageRoute) { + // API response or 404 — strip internal headers and return + return new Response(response.body, { + status: response.status, + headers: Object.fromEntries( + [...response.headers.entries()].filter( + ([k]) => !k.startsWith('x-adex-') + ) + ), + }) + } + + // Page response — inject manifest CSS/JS asset tags + const html = await response.text() + const finalHTML = addDependencyAssets( + html, + pageRoute, + manifests.server, + manifests.client + ) + + return new Response(finalHTML, { + status: response.status, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }) + } + + fetchHandler = handle + return fetchHandler + } + + return { + async run() { + const handle = await getFetchHandler() + // @ts-expect-error Deno global — not present in Node type definitions + Deno.serve({ port, hostname }, handle) + console.log(`Listening on http://${hostname}:${port}`) + }, + // Exposed so the server entry can `export default server.fetch` for + // edge/serverless runtimes that call the handler directly. + fetch: async request => { + const handle = await getFetchHandler() + return handle(request) + }, + } +} diff --git a/packages/adapters/deno/package.json b/packages/adapters/deno/package.json new file mode 100644 index 0000000..e4d6ff9 --- /dev/null +++ b/packages/adapters/deno/package.json @@ -0,0 +1,45 @@ +{ + "name": "adex-adapter-deno", + "version": "0.0.21", + "description": "Deno adapter for Adex, enabling server-side rendering via Deno.serve.", + "keywords": [ + "adex", + "preact", + "minimal", + "server", + "deno", + "ssr", + "adapter" + ], + "author": "reaper ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/barelyhuman/adex" + }, + "bugs": { + "url": "https://github.com/barelyhuman/adex/issues" + }, + "homepage": "https://github.com/barelyhuman/adex/tree/main/packages/adapters/deno", + "type": "module", + "main": "./lib/index.js", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js" + } + }, + "files": [ + "lib" + ], + "engines": { + "deno": ">=1.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/barelyhuman" + }, + "dependencies": { + "adex": "workspace:*" + } +} diff --git a/packages/adapters/node/lib/dev.js b/packages/adapters/node/lib/dev.js new file mode 100644 index 0000000..f916ec5 --- /dev/null +++ b/packages/adapters/node/lib/dev.js @@ -0,0 +1,100 @@ +// Dev-mode Vite plugin for the Node.js adapter. +// This file is only ever loaded in a Node.js/Vite context (build tooling), +// so Node-only APIs (Buffer, node:path, adex/http) are safe to use here. + +import { nodeRequestToFetch, fetchResponseToNode } from 'adex/http' +import { resolve } from 'node:path' + +/** + * Creates the Vite dev server plugin for the Node.js adapter. + * + * @param {{ islands: boolean }} options + * @returns {import('vite').Plugin} + */ +export function createNodeDevServerPlugin({ islands = false } = {}) { + const devCSSMap = new Map() + let cfg + + return { + name: 'adex-dev-server', + apply: 'serve', + enforce: 'pre', + config() { + return { + ssr: { + noExternal: ['adex/app'], + }, + } + }, + configResolved(_cfg) { + cfg = _cfg + }, + async resolveId(id, importer, meta) { + if (id.endsWith('.css')) { + if (!importer) return + const importerFromRoot = importer.replace(resolve(cfg.root), '') + const resolvedCss = await this.resolve(id, importer, meta) + if (resolvedCss) { + devCSSMap.set( + importerFromRoot, + (devCSSMap.get(importer) ?? []).concat(resolvedCss.id) + ) + } + return + } + }, + configureServer(server) { + return () => { + server.middlewares.use(async function (req, res, next) { + const module = await server.ssrLoadModule('virtual:adex:handler') + if (!module) { + return next() + } + try { + const fetchRequest = await nodeRequestToFetch(req) + const response = await module.handler(fetchRequest) + const pageRoute = response.headers.get('x-adex-page-route') + + if (!pageRoute) { + // API response or 404 — write directly to node res + await fetchResponseToNode(response, res) + return + } + + // Page response — inject dev CSS preload links + HMR client script + const cssLinks = devCSSMap.get(pageRoute) ?? [] + let html = await response.text() + + html = html.replace( + '', + ` + + ${cssLinks + .map( + d => + `` + ) + .join('')} + ` + ) + + if (!islands) { + html = html.replace( + '', + `` + ) + } + + const finalHTML = await server.transformIndexHtml(req.url, html) + res.setHeader('content-type', 'text/html') + res.write(finalHTML) + res.end() + } catch (err) { + server.ssrFixStacktrace(err) + next(err) + } + }) + } + }, + } +} diff --git a/packages/adapters/node/lib/index.js b/packages/adapters/node/lib/index.js index 76be837..a6a9e97 100644 --- a/packages/adapters/node/lib/index.js +++ b/packages/adapters/node/lib/index.js @@ -1,66 +1,10 @@ import { existsSync } from 'node:fs' import http from 'node:http' -import { resolve } from 'node:path' +import { createNodeDevServerPlugin } from './dev.js' +import { nodeRequestToFetch, fetchResponseToNode } from 'adex/http' let islandMode = false -/** - * Convert a Node.js IncomingMessage to a Fetch API Request. - * @param {import('node:http').IncomingMessage} req - * @returns {Promise} - */ -async function nodeRequestToFetch(req) { - const protocol = req.socket?.encrypted ? 'https' : 'http' - const host = req.headers['host'] ?? 'localhost' - const url = new URL(req.url, `${protocol}://${host}`) - - const headers = new Headers() - for (const [key, value] of Object.entries(req.headers)) { - if (Array.isArray(value)) { - for (const v of value) headers.append(key, v) - } else if (value != null) { - headers.set(key, value) - } - } - - const hasBody = req.method !== 'GET' && req.method !== 'HEAD' - let body = undefined - if (hasBody) { - body = await new Promise((resolve, reject) => { - const chunks = [] - req.on('data', chunk => chunks.push(chunk)) - req.on('end', () => resolve(Buffer.concat(chunks))) - req.on('error', reject) - }) - } - - return new Request(url.href, { - method: req.method, - headers, - body: body ?? null, - }) -} - -/** - * Write a Fetch API Response to a Node.js ServerResponse. - * Skips x-adex-* internal headers. - * @param {Response} response - * @param {import('node:http').ServerResponse} res - * @returns {Promise} - */ -async function fetchResponseToNode(response, res) { - res.statusCode = response.status - for (const [key, value] of response.headers.entries()) { - if (key.startsWith('x-adex-')) continue - res.setHeader(key, value) - } - if (response.body) { - const buf = Buffer.from(await response.arrayBuffer()) - res.write(buf) - } - res.end() -} - /** * Adapter factory — pass to adex({ adapter: node() }) in vite.config.js * Returns an AdapterConfig object the core framework uses at build time. @@ -121,103 +65,6 @@ export default server.fetch } } -/** - * Creates the Vite dev server plugin for the node adapter. - * This is the relocated + updated equivalent of the old adexDevServer plugin - * that used to live in adex/src/vite.js. It owns how requests are served - * in dev mode for a Node.js environment. - * - * @param {{ islands: boolean }} options - * @returns {import('vite').Plugin} - */ -function createNodeDevServerPlugin({ islands = false } = {}) { - const devCSSMap = new Map() - let cfg - - return { - name: 'adex-dev-server', - apply: 'serve', - enforce: 'pre', - config() { - return { - ssr: { - noExternal: ['adex/app'], - }, - } - }, - configResolved(_cfg) { - cfg = _cfg - }, - async resolveId(id, importer, meta) { - if (id.endsWith('.css')) { - if (!importer) return - const importerFromRoot = importer.replace(resolve(cfg.root), '') - const resolvedCss = await this.resolve(id, importer, meta) - if (resolvedCss) { - devCSSMap.set( - importerFromRoot, - (devCSSMap.get(importer) ?? []).concat(resolvedCss.id) - ) - } - return - } - }, - configureServer(server) { - return () => { - server.middlewares.use(async function (req, res, next) { - const module = await server.ssrLoadModule('virtual:adex:handler') - if (!module) { - return next() - } - try { - const fetchRequest = await nodeRequestToFetch(req) - const response = await module.handler(fetchRequest) - const pageRoute = response.headers.get('x-adex-page-route') - - if (!pageRoute) { - // API response or 404 — write directly to node res - await fetchResponseToNode(response, res) - return - } - - // Page response — inject dev CSS preload links + HMR client script - const cssLinks = devCSSMap.get(pageRoute) ?? [] - let html = await response.text() - - html = html.replace( - '', - ` - - ${cssLinks - .map( - d => - `` - ) - .join('')} - ` - ) - - if (!islands) { - html = html.replace( - '', - `` - ) - } - - const finalHTML = await server.transformIndexHtml(req.url, html) - res.setHeader('content-type', 'text/html') - res.write(finalHTML) - res.end() - } catch (err) { - server.ssrFixStacktrace(err) - next(err) - } - }) - } - }, - } -} - /** * @param {{ manifests: object, paths: object, client: import('./index.d.ts').AdapterClientInfo }} adexConfig */ diff --git a/packages/adapters/node/package.json b/packages/adapters/node/package.json index 97bf7e2..28b99aa 100644 --- a/packages/adapters/node/package.json +++ b/packages/adapters/node/package.json @@ -38,5 +38,8 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/barelyhuman" + }, + "dependencies": { + "adex": "workspace:*" } } diff --git a/playground/deno.lock b/playground/deno.lock new file mode 100644 index 0000000..588197e --- /dev/null +++ b/playground/deno.lock @@ -0,0 +1,70 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/cli@^1.0.12": "1.0.12", + "jsr:@std/encoding@^1.0.7": "1.0.7", + "jsr:@std/fmt@^1.0.5": "1.0.5", + "jsr:@std/html@^1.0.3": "1.0.3", + "jsr:@std/http@*": "1.0.13", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/net@^1.0.4": "1.0.4", + "jsr:@std/path@*": "1.0.8", + "jsr:@std/path@^1.0.8": "1.0.8", + "jsr:@std/streams@^1.0.9": "1.0.9" + }, + "jsr": { + "@std/cli@1.0.12": { + "integrity": "e5cfb7814d189da174ecd7a34fbbd63f3513e24a1b307feb2fcd5da47a070d90" + }, + "@std/encoding@1.0.7": { + "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" + }, + "@std/fmt@1.0.5": { + "integrity": "0cfab43364bc36650d83c425cd6d99910fc20c4576631149f0f987eddede1a4d" + }, + "@std/html@1.0.3": { + "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" + }, + "@std/http@1.0.13": { + "integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e", + "dependencies": [ + "jsr:@std/cli", + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/html", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@^1.0.8", + "jsr:@std/streams" + ] + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@1.0.4": { + "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/streams@1.0.9": { + "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@barelyhuman/prettier-config@^1.1.0", + "npm:@preact/signals@^1.3.0", + "npm:@tailwindcss/forms@~0.5.11", + "npm:@types/node@^20.19.39", + "npm:autoprefixer@^10.4.27", + "npm:postcss@^8.5.9", + "npm:preact@^10.24.2", + "npm:prettier@^3.8.1", + "npm:tailwindcss@^3.4.19", + "npm:vite@^6.4.2" + ] + } + } +} diff --git a/playground/package.json b/playground/package.json index 8a81e53..b4c6991 100644 --- a/playground/package.json +++ b/playground/package.json @@ -14,6 +14,7 @@ "dependencies": { "@preact/signals": "^1.3.0", "adex-adapter-node": "workspace:*", + "adex-adapter-deno": "workspace:*", "preact": "^10.24.2" }, "devDependencies": { diff --git a/playground/vite.config.js b/playground/vite.config.js index 418da50..4a561aa 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import preact from '@preact/preset-vite' import { adex } from 'adex' -import { node } from 'adex-adapter-node' +// import { node } from 'adex-adapter-node' +import { deno } from 'adex-adapter-deno' import { providers } from 'adex/fonts' // https://vitejs.dev/config/ @@ -9,7 +10,7 @@ export default defineConfig({ plugins: [ adex({ islands: false, - adapter: node(), + adapter: deno(), fonts: { providers: [providers.google()], families: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8d7b5d..476f52b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,13 +214,26 @@ importers: specifier: ^1.15.0 version: 1.15.0 - packages/adapters/node: {} + packages/adapters/deno: + dependencies: + adex: + specifier: workspace:* + version: link:../../../adex + + packages/adapters/node: + dependencies: + adex: + specifier: workspace:* + version: link:../../../adex playground: dependencies: '@preact/signals': specifier: ^1.3.0 version: 1.3.4(preact@10.29.1) + adex-adapter-deno: + specifier: workspace:* + version: link:../packages/adapters/deno adex-adapter-node: specifier: workspace:* version: link:../packages/adapters/node From 4f4b7174548f4d6a55ffe07ceb5787a1a481a14b Mon Sep 17 00:00:00 2001 From: reaper Date: Sun, 12 Apr 2026 11:19:22 +0530 Subject: [PATCH 6/7] chore: remove lock during execution test --- playground/deno.lock | 70 -------------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 playground/deno.lock diff --git a/playground/deno.lock b/playground/deno.lock deleted file mode 100644 index 588197e..0000000 --- a/playground/deno.lock +++ /dev/null @@ -1,70 +0,0 @@ -{ - "version": "4", - "specifiers": { - "jsr:@std/cli@^1.0.12": "1.0.12", - "jsr:@std/encoding@^1.0.7": "1.0.7", - "jsr:@std/fmt@^1.0.5": "1.0.5", - "jsr:@std/html@^1.0.3": "1.0.3", - "jsr:@std/http@*": "1.0.13", - "jsr:@std/media-types@^1.1.0": "1.1.0", - "jsr:@std/net@^1.0.4": "1.0.4", - "jsr:@std/path@*": "1.0.8", - "jsr:@std/path@^1.0.8": "1.0.8", - "jsr:@std/streams@^1.0.9": "1.0.9" - }, - "jsr": { - "@std/cli@1.0.12": { - "integrity": "e5cfb7814d189da174ecd7a34fbbd63f3513e24a1b307feb2fcd5da47a070d90" - }, - "@std/encoding@1.0.7": { - "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" - }, - "@std/fmt@1.0.5": { - "integrity": "0cfab43364bc36650d83c425cd6d99910fc20c4576631149f0f987eddede1a4d" - }, - "@std/html@1.0.3": { - "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" - }, - "@std/http@1.0.13": { - "integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e", - "dependencies": [ - "jsr:@std/cli", - "jsr:@std/encoding", - "jsr:@std/fmt", - "jsr:@std/html", - "jsr:@std/media-types", - "jsr:@std/net", - "jsr:@std/path@^1.0.8", - "jsr:@std/streams" - ] - }, - "@std/media-types@1.1.0": { - "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" - }, - "@std/net@1.0.4": { - "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" - }, - "@std/streams@1.0.9": { - "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" - } - }, - "workspace": { - "packageJson": { - "dependencies": [ - "npm:@barelyhuman/prettier-config@^1.1.0", - "npm:@preact/signals@^1.3.0", - "npm:@tailwindcss/forms@~0.5.11", - "npm:@types/node@^20.19.39", - "npm:autoprefixer@^10.4.27", - "npm:postcss@^8.5.9", - "npm:preact@^10.24.2", - "npm:prettier@^3.8.1", - "npm:tailwindcss@^3.4.19", - "npm:vite@^6.4.2" - ] - } - } -} From 4d9c6acd49911a7a9386eae045700fe7ee03f8b6 Mon Sep 17 00:00:00 2001 From: reaper Date: Sun, 12 Apr 2026 11:25:59 +0530 Subject: [PATCH 7/7] chore: separate deno playground --- playground-deno/.gitignore | 29 +++++++++++ playground-deno/package.json | 32 ++++++++++++ playground-deno/postcss.config.js | 6 +++ playground-deno/public/vite.svg | 1 + playground-deno/src/_app.jsx | 19 +++++++ playground-deno/src/api/$id/hello.js | 11 ++++ playground-deno/src/api/$id/json.js | 9 ++++ playground-deno/src/api/hello.js | 11 ++++ playground-deno/src/api/html.js | 14 ++++++ playground-deno/src/api/random-data.js | 10 ++++ playground-deno/src/api/status-helpers.js | 50 +++++++++++++++++++ playground-deno/src/assets/preact.svg | 1 + playground-deno/src/components/FormIsland.tsx | 19 +++++++ playground-deno/src/components/ListIsland.tsx | 14 ++++++ .../src/components/SharedSignal.tsx | 34 +++++++++++++ playground-deno/src/components/counter.tsx | 6 +++ playground-deno/src/global.css | 3 ++ playground-deno/src/index.html | 11 ++++ playground-deno/src/lib/test-env.js | 3 ++ playground-deno/src/pages/$id/hello.tsx | 3 ++ playground-deno/src/pages/about.tsx | 6 +++ playground-deno/src/pages/index.jsx | 42 ++++++++++++++++ playground-deno/src/pages/islands-test.tsx | 11 ++++ playground-deno/src/pages/local-index.css | 3 ++ playground-deno/src/pages/shared-signal.tsx | 10 ++++ playground-deno/tailwind.config.js | 12 +++++ playground-deno/tsconfig.json | 13 +++++ playground-deno/vite.config.js | 26 ++++++++++ playground/package.json | 1 - playground/vite.config.js | 5 +- pnpm-lock.yaml | 3 -- pnpm-workspace.yaml | 1 + 32 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 playground-deno/.gitignore create mode 100644 playground-deno/package.json create mode 100644 playground-deno/postcss.config.js create mode 100644 playground-deno/public/vite.svg create mode 100644 playground-deno/src/_app.jsx create mode 100644 playground-deno/src/api/$id/hello.js create mode 100644 playground-deno/src/api/$id/json.js create mode 100644 playground-deno/src/api/hello.js create mode 100644 playground-deno/src/api/html.js create mode 100644 playground-deno/src/api/random-data.js create mode 100644 playground-deno/src/api/status-helpers.js create mode 100644 playground-deno/src/assets/preact.svg create mode 100644 playground-deno/src/components/FormIsland.tsx create mode 100644 playground-deno/src/components/ListIsland.tsx create mode 100644 playground-deno/src/components/SharedSignal.tsx create mode 100644 playground-deno/src/components/counter.tsx create mode 100644 playground-deno/src/global.css create mode 100644 playground-deno/src/index.html create mode 100644 playground-deno/src/lib/test-env.js create mode 100644 playground-deno/src/pages/$id/hello.tsx create mode 100644 playground-deno/src/pages/about.tsx create mode 100644 playground-deno/src/pages/index.jsx create mode 100644 playground-deno/src/pages/islands-test.tsx create mode 100644 playground-deno/src/pages/local-index.css create mode 100644 playground-deno/src/pages/shared-signal.tsx create mode 100644 playground-deno/tailwind.config.js create mode 100644 playground-deno/tsconfig.json create mode 100644 playground-deno/vite.config.js diff --git a/playground-deno/.gitignore b/playground-deno/.gitignore new file mode 100644 index 0000000..c4beedd --- /dev/null +++ b/playground-deno/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env* +!.env.example + +/.islands \ No newline at end of file diff --git a/playground-deno/package.json b/playground-deno/package.json new file mode 100644 index 0000000..10b6eff --- /dev/null +++ b/playground-deno/package.json @@ -0,0 +1,32 @@ +{ + "name": "playground-deno", + "private": true, + "version": "0.0.21", + "engines": { + "node": ">=18.0.0" + }, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@preact/signals": "^1.3.0", + "adex-adapter-deno": "workspace:*", + "preact": "^10.24.2" + }, + "devDependencies": { + "@barelyhuman/prettier-config": "^1.1.0", + "@preact/preset-vite": "catalog:", + "@tailwindcss/forms": "^0.5.11", + "@types/node": "^20.19.39", + "adex": "workspace:*", + "autoprefixer": "^10.4.27", + "postcss": "^8.5.9", + "prettier": "^3.8.1", + "tailwindcss": "^3.4.19", + "vite": "^6.4.2" + }, + "prettier": "@barelyhuman/prettier-config" +} diff --git a/playground-deno/postcss.config.js b/playground-deno/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/playground-deno/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/playground-deno/public/vite.svg b/playground-deno/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/playground-deno/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground-deno/src/_app.jsx b/playground-deno/src/_app.jsx new file mode 100644 index 0000000..3968381 --- /dev/null +++ b/playground-deno/src/_app.jsx @@ -0,0 +1,19 @@ +import { App as AdexApp } from 'adex/app' +import { h } from 'preact' +import { hydrate as preactHydrate } from 'adex/router' + +export { prerender } from 'adex/app' + +import 'virtual:adex:global.css' + +export const App = ({ url }) => { + return +} + +async function hydrate() { + preactHydrate(h(App, null), document.getElementById('app')) +} + +if (typeof window !== 'undefined') { + hydrate() +} diff --git a/playground-deno/src/api/$id/hello.js b/playground-deno/src/api/$id/hello.js new file mode 100644 index 0000000..fd515b3 --- /dev/null +++ b/playground-deno/src/api/$id/hello.js @@ -0,0 +1,11 @@ +/** + * @param {Request} request + */ +export default request => { + const { pathname } = new URL(request.url) + // route: /api/:id/hello — id is the second path segment + const id = pathname.split('/')[2] + return new Response(`Hello from ${id}`, { + headers: { 'content-type': 'text/plain' }, + }) +} diff --git a/playground-deno/src/api/$id/json.js b/playground-deno/src/api/$id/json.js new file mode 100644 index 0000000..34f36d4 --- /dev/null +++ b/playground-deno/src/api/$id/json.js @@ -0,0 +1,9 @@ +/** + * @param {Request} request + */ +export default request => { + const { pathname } = new URL(request.url) + // route: /api/:id/json — id is the second path segment + const id = pathname.split('/')[2] + return Response.json({ message: `Hello in ${id}` }) +} diff --git a/playground-deno/src/api/hello.js b/playground-deno/src/api/hello.js new file mode 100644 index 0000000..5d2f9e4 --- /dev/null +++ b/playground-deno/src/api/hello.js @@ -0,0 +1,11 @@ +import { env } from 'adex/env' + +/** + * @param {Request} request + */ +export default request => { + return Response.json({ + pong: true, + appUrl: env.get('APP_URL'), + }) +} diff --git a/playground-deno/src/api/html.js b/playground-deno/src/api/html.js new file mode 100644 index 0000000..bf77345 --- /dev/null +++ b/playground-deno/src/api/html.js @@ -0,0 +1,14 @@ +import { afterAPICall } from 'adex/hook' + +afterAPICall(ctx => { + console.log('called after api') +}) + +/** + * @param {Request} request + */ +export default request => { + return new Response(`

Html Response

`, { + headers: { 'content-type': 'text/html' }, + }) +} diff --git a/playground-deno/src/api/random-data.js b/playground-deno/src/api/random-data.js new file mode 100644 index 0000000..0cb2f18 --- /dev/null +++ b/playground-deno/src/api/random-data.js @@ -0,0 +1,10 @@ +/** + * @param {Request} request + */ +export default function (request) { + return Response.json( + Array.from({ length: 3 }) + .fill(0) + .map((d, i) => (i + 1) * Math.random()) + ) +} diff --git a/playground-deno/src/api/status-helpers.js b/playground-deno/src/api/status-helpers.js new file mode 100644 index 0000000..5ac563b --- /dev/null +++ b/playground-deno/src/api/status-helpers.js @@ -0,0 +1,50 @@ +/** + * @param {Request} request + */ +export default request => { + const { searchParams } = new URL(request.url) + const type = searchParams.get('type') + const message = searchParams.get('message') + const errorBody = msg => (msg ? JSON.stringify({ error: msg }) : null) + const jsonHeaders = { 'content-type': 'application/json' } + + switch (type) { + case 'badRequest': + return new Response(errorBody(message), { + status: 400, + headers: jsonHeaders, + }) + case 'unauthorized': + return new Response(errorBody(message), { + status: 401, + headers: jsonHeaders, + }) + case 'forbidden': + return new Response(errorBody(message), { + status: 403, + headers: jsonHeaders, + }) + case 'notFound': + return new Response(errorBody(message), { + status: 404, + headers: jsonHeaders, + }) + case 'internalServerError': + return new Response(errorBody(message), { + status: 500, + headers: jsonHeaders, + }) + default: + return Response.json({ + usage: + 'Add ?type=badRequest&message=Custom%20message to test status helpers', + available: [ + 'badRequest', + 'unauthorized', + 'forbidden', + 'notFound', + 'internalServerError', + ], + }) + } +} diff --git a/playground-deno/src/assets/preact.svg b/playground-deno/src/assets/preact.svg new file mode 100644 index 0000000..908f17d --- /dev/null +++ b/playground-deno/src/assets/preact.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/playground-deno/src/components/FormIsland.tsx b/playground-deno/src/components/FormIsland.tsx new file mode 100644 index 0000000..fdb7e12 --- /dev/null +++ b/playground-deno/src/components/FormIsland.tsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'preact/hooks' + +export function ListIsland() { + const [data, setData] = useState([]) + + useEffect(() => { + setData([1, 2, 3]) + }, []) + + return ( +
+
    + {data.map(d => ( +
  • {d}
  • + ))} +
+
+ ) +} diff --git a/playground-deno/src/components/ListIsland.tsx b/playground-deno/src/components/ListIsland.tsx new file mode 100644 index 0000000..0f1a762 --- /dev/null +++ b/playground-deno/src/components/ListIsland.tsx @@ -0,0 +1,14 @@ +export function FormIsland() { + const onSubmit = e => { + e.preventDefault() + alert('sumbitted') + } + return ( +
+
+ + +
+
+ ) +} diff --git a/playground-deno/src/components/SharedSignal.tsx b/playground-deno/src/components/SharedSignal.tsx new file mode 100644 index 0000000..115dc14 --- /dev/null +++ b/playground-deno/src/components/SharedSignal.tsx @@ -0,0 +1,34 @@ +import { signal } from '@preact/signals' +import { useEffect } from 'preact/hooks' + +const data$ = signal([]) + +async function fetchData() { + const data = await fetch('/api/random-data').then(d => d.json()) + data$.value = data +} + +export function Triggerer() { + useEffect(() => { + fetchData() + }, []) + return ( +
+

Triggerer Island

+ +
+ ) +} + +export function Renderer() { + return ( +
+

Renderer Island

+
    + {data$.value.map(d => ( +
  • {d}
  • + ))} +
+
+ ) +} diff --git a/playground-deno/src/components/counter.tsx b/playground-deno/src/components/counter.tsx new file mode 100644 index 0000000..5bb30ab --- /dev/null +++ b/playground-deno/src/components/counter.tsx @@ -0,0 +1,6 @@ +import { useState } from 'preact/hooks' + +export function Counter() { + const [count, setCount] = useState(0) + return +} diff --git a/playground-deno/src/global.css b/playground-deno/src/global.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/playground-deno/src/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/playground-deno/src/index.html b/playground-deno/src/index.html new file mode 100644 index 0000000..8ee2bb9 --- /dev/null +++ b/playground-deno/src/index.html @@ -0,0 +1,11 @@ + + + + + + Document + + +
+ + \ No newline at end of file diff --git a/playground-deno/src/lib/test-env.js b/playground-deno/src/lib/test-env.js new file mode 100644 index 0000000..0fbed76 --- /dev/null +++ b/playground-deno/src/lib/test-env.js @@ -0,0 +1,3 @@ +import { env } from 'adex/env' + +export const APP_URL = env.get('APP_URL') diff --git a/playground-deno/src/pages/$id/hello.tsx b/playground-deno/src/pages/$id/hello.tsx new file mode 100644 index 0000000..8cc9032 --- /dev/null +++ b/playground-deno/src/pages/$id/hello.tsx @@ -0,0 +1,3 @@ +export default ({ routeParams }) => { + return

Hello from {routeParams.id}

+} diff --git a/playground-deno/src/pages/about.tsx b/playground-deno/src/pages/about.tsx new file mode 100644 index 0000000..6bf8fbc --- /dev/null +++ b/playground-deno/src/pages/about.tsx @@ -0,0 +1,6 @@ +import { useTitle } from 'adex/head' + +export default () => { + useTitle('About Page') + return

About

+} diff --git a/playground-deno/src/pages/index.jsx b/playground-deno/src/pages/index.jsx new file mode 100644 index 0000000..47aa423 --- /dev/null +++ b/playground-deno/src/pages/index.jsx @@ -0,0 +1,42 @@ +import { useState } from 'preact/hooks' +import './local-index.css' +import { Counter } from '../components/counter.tsx' + +export default function Page() { + const [count, setCount] = useState(0) + + return ( +
+ +

Vite + Preact

+
+ +

+ Edit src/app.jsx and save to test HMR +

+
+

+ Click on the Vite and Preact logos to learn more +

+

+ Here's an island{' '} + + + +

+
+ ) +} diff --git a/playground-deno/src/pages/islands-test.tsx b/playground-deno/src/pages/islands-test.tsx new file mode 100644 index 0000000..b29fc50 --- /dev/null +++ b/playground-deno/src/pages/islands-test.tsx @@ -0,0 +1,11 @@ +import { ListIsland } from '../components/FormIsland' +import { FormIsland } from '../components/ListIsland' + +export default function IslandsTest() { + return ( + <> + + + + ) +} diff --git a/playground-deno/src/pages/local-index.css b/playground-deno/src/pages/local-index.css new file mode 100644 index 0000000..a9cc5ca --- /dev/null +++ b/playground-deno/src/pages/local-index.css @@ -0,0 +1,3 @@ +body{ + background:red; +} \ No newline at end of file diff --git a/playground-deno/src/pages/shared-signal.tsx b/playground-deno/src/pages/shared-signal.tsx new file mode 100644 index 0000000..9f63c7b --- /dev/null +++ b/playground-deno/src/pages/shared-signal.tsx @@ -0,0 +1,10 @@ +import { Renderer, Triggerer } from '../components/SharedSignal.js' + +export default function SharedSignal() { + return ( +
+ + +
+ ) +} diff --git a/playground-deno/tailwind.config.js b/playground-deno/tailwind.config.js new file mode 100644 index 0000000..0e2ccbb --- /dev/null +++ b/playground-deno/tailwind.config.js @@ -0,0 +1,12 @@ +import forms from '@tailwindcss/forms' +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,jsx,ts,tsx}'], + theme: { + extend: {}, + fontFamily: { + sans: 'Inter, sans-serif', + }, + }, + plugins: [forms], +} diff --git a/playground-deno/tsconfig.json b/playground-deno/tsconfig.json new file mode 100644 index 0000000..9db2d98 --- /dev/null +++ b/playground-deno/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "moduleResolution": "Node", + "paths": { + "adex": ["../adex/vite.js"], + "adex/*": ["../adex/src/*"] + } + } +} diff --git a/playground-deno/vite.config.js b/playground-deno/vite.config.js new file mode 100644 index 0000000..28581d1 --- /dev/null +++ b/playground-deno/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { adex } from 'adex' +import { deno } from 'adex-adapter-deno' +import { providers } from 'adex/fonts' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + adex({ + islands: false, + adapter: deno(), + fonts: { + providers: [providers.google()], + families: [ + { + name: 'Inter', + weights: ['400', '600'], + styles: ['normal'], + }, + ], + }, + }), + preact(), + ], +}) diff --git a/playground/package.json b/playground/package.json index b4c6991..8a81e53 100644 --- a/playground/package.json +++ b/playground/package.json @@ -14,7 +14,6 @@ "dependencies": { "@preact/signals": "^1.3.0", "adex-adapter-node": "workspace:*", - "adex-adapter-deno": "workspace:*", "preact": "^10.24.2" }, "devDependencies": { diff --git a/playground/vite.config.js b/playground/vite.config.js index 4a561aa..418da50 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -1,8 +1,7 @@ import { defineConfig } from 'vite' import preact from '@preact/preset-vite' import { adex } from 'adex' -// import { node } from 'adex-adapter-node' -import { deno } from 'adex-adapter-deno' +import { node } from 'adex-adapter-node' import { providers } from 'adex/fonts' // https://vitejs.dev/config/ @@ -10,7 +9,7 @@ export default defineConfig({ plugins: [ adex({ islands: false, - adapter: deno(), + adapter: node(), fonts: { providers: [providers.google()], families: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 476f52b..0a6d438 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,9 +231,6 @@ importers: '@preact/signals': specifier: ^1.3.0 version: 1.3.4(preact@10.29.1) - adex-adapter-deno: - specifier: workspace:* - version: link:../packages/adapters/deno adex-adapter-node: specifier: workspace:* version: link:../packages/adapters/node diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6541f37..d24bcc7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,6 +6,7 @@ packages: - adex - create-adex - playground + - playground-* - packages/**/* - adex/tests/**/*