diff --git a/adex/package.json b/adex/package.json index 5e3a19e..eb01fc6 100644 --- a/adex/package.json +++ b/adex/package.json @@ -71,11 +71,17 @@ "@preact/preset-vite": "^2.8.2", "@types/node": "^20.14.10", "autoprefixer": "^10.4.19", - "postcss": "^8.4.39", "tailwindcss": "^3.4.4", - "vite": "^5.3.1" + "vite": "^5.3.1", + "adex-adapter-node": "workspace:*" + }, + "peerDependenciesMeta": { + "adex-adapter-node": { + "optional": true + } }, "peerDependencies": { + "adex-adapter-node": ">=0.0.15", "@preact/preset-vite": ">=2.8.2", "preact": "^10.22.0" } diff --git a/adex/runtime/server.js b/adex/runtime/server.js deleted file mode 100644 index 61b177a..0000000 --- a/adex/runtime/server.js +++ /dev/null @@ -1,214 +0,0 @@ -import { env } from 'adex/env' -import { existsSync, readFileSync } from 'node:fs' -import http from 'node:http' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -//@ts-expect-error internal requires -import { mri, sirv, useMiddleware } from 'adex/ssr' - -//@ts-expect-error vite virtual import -import { handler } from 'virtual:adex:handler' - -import 'virtual:adex:global.css' -import 'virtual:adex:font.css' - -const flags = mri(process.argv.slice(2)) - -const PORT = flags.port || parseInt(env.get('PORT', '3000'), 10) -const HOST = flags.host || env.get('HOST', 'localhost') - -const __dirname = dirname(fileURLToPath(import.meta.url)) - -const serverAssets = sirv(join(__dirname, './assets'), { - maxAge: 31536000, - immutable: true, - onNoMatch: defaultHandler, -}) - -let islandsWereGenerated = existsSync(join(__dirname, './islands')) - -// @ts-ignore -let islandAssets = (req, res, next) => { - next() -} - -if (islandsWereGenerated) { - islandAssets = sirv(join(__dirname, './islands'), { - maxAge: 31536000, - immutable: true, - onNoMatch: defaultHandler, - }) -} - -let clientWasGenerated = existsSync(join(__dirname, '../client')) - -// @ts-ignore -let clientAssets = (req, res, next) => { - next() -} - -if (clientWasGenerated) { - clientAssets = sirv(join(__dirname, '../client'), { - maxAge: 31536000, - immutable: true, - onNoMatch: defaultHandler, - }) -} - -async function defaultHandler(req, res) { - const { html: template, pageRoute, serverHandler } = await handler(req, res) - if (serverHandler) { - return serverHandler(req, res) - } - - const templateWithDeps = addDependencyAssets(template, pageRoute) - - const finalHTML = templateWithDeps - res.setHeader('content-type', 'text/html') - res.write(finalHTML) - res.end() -} - -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( - (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, - } -} - -function addDependencyAssets(template, pageRoute) { - if (!pageRoute) { - return template - } - const serverManifest = getServerManifest() - const manifest = getClientManifest() - const filePath = pageRoute.startsWith('/') ? pageRoute.slice(1) : pageRoute - - const { links: serverLinks } = manifestToHTML(serverManifest, filePath) - - const { links: clientLinks, scripts: clientScripts } = manifestToHTML( - manifest, - filePath - ) - - const links = [...serverLinks, ...clientLinks] - const scripts = [...clientScripts] - - return template.replace( - '', - links.join('\n') + scripts.join('\n') + '' - ) -} - -http - .createServer( - useMiddleware( - async (req, res, next) => { - // @ts-expect-error shared-state between the middlewares - req.__originalUrl = req.url - // @ts-expect-error shared-state between the middlewares - req.url = req.__originalUrl.replace(/(\/?assets\/?)/, '/') - return serverAssets(req, res, next) - }, - async (req, res, next) => { - // @ts-expect-error shared-state between the middlewares - req.url = req.__originalUrl.replace(/(\/?islands\/?)/, '/') - return islandAssets(req, res, next) - }, - async (req, res, next) => { - // @ts-expect-error shared-state between the middlewares - req.url = req.__originalUrl.replace(/(\/?client\/?)/, '/') - return clientAssets(req, res, next) - }, - async (req, res) => { - // @ts-expect-error shared-state between the middlewares - req.url = req.__originalUrl - return defaultHandler(req, res) - } - ) - ) - .listen(PORT, HOST, () => { - console.log(`Listening on ${HOST}:${PORT}`) - }) diff --git a/adex/src/ssr.d.ts b/adex/src/ssr.d.ts index 08bd139..7cff11b 100644 --- a/adex/src/ssr.d.ts +++ b/adex/src/ssr.d.ts @@ -2,3 +2,5 @@ export { toStatic } from 'hoofd/preact' export { renderToString } from 'preact-render-to-string' export { parse as pathToRegex } from 'regexparam' export { use as useMiddleware } from '@barelyhuman/tiny-use' +export { default as sirv } from 'sirv' +export { default as mri } from 'mri' \ No newline at end of file diff --git a/adex/src/vite.d.ts b/adex/src/vite.d.ts index 2263706..0fef6c2 100644 --- a/adex/src/vite.d.ts +++ b/adex/src/vite.d.ts @@ -1,9 +1,12 @@ import { Plugin } from 'vite' -import type { Options as FontOptions } from './fonts' +import type { Options as FontOptions } from './fonts.js' + +export type Adapters = 'node' export interface AdexOptions { fonts?: FontOptions islands?: boolean + adapter?: Adapters } export function adex(options: AdexOptions): Plugin[] diff --git a/adex/src/vite.js b/adex/src/vite.js index 3ba674d..9eb6205 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -23,11 +23,19 @@ const cwd = process.cwd() const islandsDir = join(cwd, '.islands') let runningIslandBuild = false +const adapterMap = { + node: 'adex-adapter-node', +} + /** * @param {import("./vite.js").AdexOptions} [options] * @returns */ -export function adex({ fonts, islands = false } = {}) { +export function adex({ + fonts, + islands = false, + adapter: adapter = 'node', +} = {}) { return [ preactPages({ root: '/src/pages', @@ -49,7 +57,68 @@ export function adex({ fonts, islands = false } = {}) { ), createVirtualModule( 'virtual:adex:server', - readFileSync(join(__dirname, '../runtime/server.js'), 'utf8') + `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 + ` ), createVirtualModule( 'virtual:adex:client', @@ -333,6 +402,7 @@ function adexServerBuilder({ islands = false } = {}) { input: { index: input, }, + external: ['adex/ssr'], }, }, } diff --git a/lerna.json b/lerna.json index 6217053..4b8689e 100644 --- a/lerna.json +++ b/lerna.json @@ -3,9 +3,5 @@ "version": "0.0.15", "changelogPreset": "angular", "npmClient": "pnpm", - "packages": [ - "adex", - "create-adex", - "playground" - ] + "packages": ["adex", "create-adex", "playground", "adex-adapter-node"] } diff --git a/package.json b/package.json index 1030c7c..c6da7fc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "play": "pnpm --filter='playground' -r dev", "test": "echo 'true'", "publish:ci": "lerna publish from-git --registry 'https://registry.npmjs.org' --yes", - "next": "lerna version --sync-workspace-lock" + "next": "lerna version --sync-workspace-lock", + "nuke": "pnpm -r exec rm -rvf node_modules" }, "license": "MIT", "prettier": "@barelyhuman/prettier-config", @@ -23,8 +24,7 @@ }, "pnpm": { "overrides": { - "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5", - "nanoid@<3.3.8": ">=3.3.8" + "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5" } } } diff --git a/packages/adaptors/node/lib/index.d.ts b/packages/adaptors/node/lib/index.d.ts new file mode 100644 index 0000000..dd955a7 --- /dev/null +++ b/packages/adaptors/node/lib/index.d.ts @@ -0,0 +1,6 @@ +type ServerOut = { + run: () => any + fetch: undefined +} + +export const createServer: ({ port: number, host: string }) => ServerOut diff --git a/packages/adaptors/node/lib/index.js b/packages/adaptors/node/lib/index.js new file mode 100644 index 0000000..61f51ae --- /dev/null +++ b/packages/adaptors/node/lib/index.js @@ -0,0 +1,228 @@ +import { existsSync } from 'node:fs' +import http from 'node:http' + +import { sirv, useMiddleware } from 'adex/ssr' + +import { handler } from 'virtual:adex:handler' + +function createHandler({ manifests, paths }) { + const serverAssets = sirv(paths.assets, { + maxAge: 31536000, + immutable: true, + onNoMatch: defaultHandler, + }) + + let islandsWereGenerated = existsSync(paths.islands) + + // @ts-ignore + let islandAssets = (req, res, next) => { + next() + } + + if (islandsWereGenerated) { + islandAssets = sirv(paths.islands, { + maxAge: 31536000, + immutable: true, + onNoMatch: defaultHandler, + }) + } + + let clientWasGenerated = existsSync(paths.client) + + // @ts-ignore + let clientAssets = (req, res, next) => { + next() + } + + if (clientWasGenerated) { + clientAssets = sirv(paths.client, { + maxAge: 31536000, + immutable: true, + onNoMatch: defaultHandler, + }) + } + + async function defaultHandler(req, res) { + const { html: template, pageRoute, serverHandler } = await handler(req, res) + if (serverHandler) { + return serverHandler(req, res) + } + + const templateWithDeps = addDependencyAssets( + template, + pageRoute, + manifests.server, + manifests.client + ) + + const finalHTML = templateWithDeps + res.setHeader('content-type', 'text/html') + res.write(finalHTML) + res.end() + } + + return useMiddleware( + async (req, res, next) => { + // @ts-expect-error shared-state between the middlewares + req.__originalUrl = req.url + // @ts-expect-error shared-state between the middlewares + req.url = req.__originalUrl.replace(/(\/?assets\/?)/, '/') + return serverAssets(req, res, next) + }, + async (req, res, next) => { + // @ts-expect-error shared-state between the middlewares + req.url = req.__originalUrl.replace(/(\/?islands\/?)/, '/') + return islandAssets(req, res, next) + }, + async (req, res, next) => { + // @ts-expect-error shared-state between the middlewares + req.url = req.__originalUrl.replace(/(\/?client\/?)/, '/') + return clientAssets(req, res, next) + }, + async (req, res) => { + // @ts-expect-error shared-state between the middlewares + req.url = req.__originalUrl + return defaultHandler(req, res) + } + ) +} + +// 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( + (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, + } +} + +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') + '' + ) +} + +export const createServer = ({ + port = '3000', + host = '127.0.0.1', + adex = { + manifests: { + server: {}, + client: {}, + }, + paths: {}, + }, +} = {}) => { + const handler = createHandler(adex) + const server = http.createServer(handler) + + return { + run() { + return server.listen(port, host, () => { + console.log(`Listening on ${host}:${port}`) + }) + }, + fetch: undefined, + } +} diff --git a/packages/adaptors/node/package.json b/packages/adaptors/node/package.json new file mode 100644 index 0000000..464736e --- /dev/null +++ b/packages/adaptors/node/package.json @@ -0,0 +1,26 @@ +{ + "name": "adex-adapter-node", + "version": "0.0.0", + "keywords": [ + "adex", + "preact", + "minimal", + "server", + "node" + ], + "bugs": "github.com/barelyhuman/adex/issues", + "repository": { + "type": "git", + "url": "github.com/barelyhuman/adex" + }, + "type": "module", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js" + } + }, + "files": [ + "lib" + ] +} diff --git a/playground/package.json b/playground/package.json index 5c56e7e..cd91db3 100644 --- a/playground/package.json +++ b/playground/package.json @@ -10,7 +10,8 @@ }, "dependencies": { "@preact/signals": "^1.3.0", - "preact": "^10.24.2" + "preact": "^10.24.2", + "adex-adapter-node": "workspace:*" }, "devDependencies": { "@preact/preset-vite": "^2.9.1", diff --git a/playground/vite.config.js b/playground/vite.config.js index 71e71b5..2ddcaeb 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -7,7 +7,7 @@ import { providers } from 'adex/fonts' export default defineConfig({ plugins: [ adex({ - islands: true, + islands: false, fonts: { providers: [providers.google()], families: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8a6331..f5e6f81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ settings: overrides: cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5' - nanoid@<3.3.8: '>=3.3.8' importers: @@ -76,12 +75,12 @@ importers: '@types/node': specifier: ^20.14.10 version: 20.16.10 + adex-adapter-node: + specifier: workspace:* + version: link:../packages/adaptors/node autoprefixer: specifier: ^10.4.19 version: 10.4.20(postcss@8.4.47) - postcss: - specifier: ^8.4.39 - version: 8.4.47 tailwindcss: specifier: ^3.4.4 version: 3.4.13 @@ -98,11 +97,16 @@ importers: specifier: ^1.15.0 version: 1.15.0 + packages/adaptors/node: {} + playground: dependencies: '@preact/signals': specifier: ^1.3.0 version: 1.3.0(preact@10.24.2) + adex-adapter-node: + specifier: workspace:* + version: link:../packages/adaptors/node preact: specifier: ^10.24.2 version: 10.24.2 @@ -1900,9 +1904,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==, tarball: https://registry.npmjs.org/mz/-/mz-2.7.0.tgz} - nanoid@5.0.9: - resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz} - engines: {node: ^18 || >=20} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true negotiator@0.6.3: @@ -4677,7 +4681,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@5.0.9: {} + nanoid@3.3.8: {} negotiator@0.6.3: {} @@ -4982,7 +4986,7 @@ snapshots: postcss@8.4.47: dependencies: - nanoid: 5.0.9 + nanoid: 3.3.8 picocolors: 1.1.0 source-map-js: 1.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8fa2310..ac5f7db 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - adex - create-adex - playground + - packages/**/*