├── .husky
└── pre-commit
├── adex
├── .prettierignore
├── src
│ ├── app.js
│ ├── env.d.ts
│ ├── utils
│ │ └── isomorphic.js
│ ├── head.js
│ ├── head.d.ts
│ ├── ssr.d.ts
│ ├── app.d.ts
│ ├── ssr.js
│ ├── router.d.ts
│ ├── env.js
│ ├── router.js
│ ├── vite.d.ts
│ ├── fonts.d.ts
│ ├── http.d.ts
│ ├── hook.d.ts
│ ├── hook.js
│ ├── fonts.js
│ ├── http.js
│ └── vite.js
├── tests
│ ├── fixtures
│ │ ├── minimal-tailwind
│ │ │ ├── src
│ │ │ │ ├── global.css
│ │ │ │ └── pages
│ │ │ │ │ ├── about.jsx
│ │ │ │ │ └── index.jsx
│ │ │ ├── postcss.config.js
│ │ │ ├── jsconfig.json
│ │ │ ├── tailwind.config.js
│ │ │ ├── vite.config.js
│ │ │ └── package.json
│ │ ├── minimal
│ │ │ ├── src
│ │ │ │ └── pages
│ │ │ │ │ ├── about.jsx
│ │ │ │ │ └── index.jsx
│ │ │ ├── jsconfig.json
│ │ │ ├── vite.config.js
│ │ │ └── package.json
│ │ └── minimal-no-ssr
│ │ │ ├── src
│ │ │ └── pages
│ │ │ │ ├── about.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── jsconfig.json
│ │ │ ├── vite.config.js
│ │ │ └── package.json
│ ├── minimal-tailwind.spec.js
│ ├── minimal.spec.js
│ ├── minimal-no-ssr.spec.js
│ ├── utils.js
│ └── response-helpers.spec.js
├── vitest.config.ts
├── CHANGELOG.md
├── .gitignore
├── runtime
│ ├── client.js
│ ├── app.js
│ ├── pages.js
│ └── handler.js
├── tsconfig.json
├── snapshots
│ └── tests
│ │ ├── minimal-tailwind.spec.snap.cjs
│ │ ├── minimal-no-ssr.spec.snap.cjs
│ │ └── minimal.spec.snap.cjs
├── LICENSE
├── readme.md
└── package.json
├── playground
├── src
│ ├── pages
│ │ ├── local-index.css
│ │ ├── $id
│ │ │ └── hello.tsx
│ │ ├── about.tsx
│ │ ├── shared-signal.tsx
│ │ ├── islands-test.tsx
│ │ └── index.jsx
│ ├── global.css
│ ├── lib
│ │ └── test-env.js
│ ├── api
│ │ ├── random-data.js
│ │ ├── $id
│ │ │ ├── hello.js
│ │ │ └── json.js
│ │ ├── hello.js
│ │ ├── html.js
│ │ └── status-helpers.js
│ ├── components
│ │ ├── counter.tsx
│ │ ├── ListIsland.tsx
│ │ ├── FormIsland.tsx
│ │ └── SharedSignal.tsx
│ ├── index.html
│ ├── _app.jsx
│ └── assets
│ │ └── preact.svg
├── postcss.config.js
├── tsconfig.json
├── tailwind.config.js
├── .gitignore
├── vite.config.js
├── package.json
└── public
│ └── vite.svg
├── readme
└── title.png
├── .gitignore
├── .vscode
└── settings.json
├── readme.md
├── packages
└── adapters
│ └── node
│ ├── lib
│ ├── index.d.ts
│ └── index.js
│ └── package.json
├── pnpm-workspace.yaml
├── tsconfig.json
├── lerna.json
├── .github
└── workflows
│ ├── test.yml
│ ├── release.yml
│ └── release-beta.yml
├── LICENSE
├── create-adex
├── package.json
└── cli.mjs
└── package.json
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | pnpm nano-staged
2 |
--------------------------------------------------------------------------------
/adex/.prettierignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /snapshots
3 |
--------------------------------------------------------------------------------
/playground/src/pages/local-index.css:
--------------------------------------------------------------------------------
1 | body{
2 | background:red;
3 | }
--------------------------------------------------------------------------------
/readme/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barelyhuman/adex/HEAD/readme/title.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .DS_Store
4 | package-lock.json
5 |
6 | coverage
7 |
--------------------------------------------------------------------------------
/adex/src/app.js:
--------------------------------------------------------------------------------
1 | export const App = () => null
2 | export const prerender = () => ({})
3 |
--------------------------------------------------------------------------------
/playground/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "prettier.enable": true
4 | }
5 |
--------------------------------------------------------------------------------
/playground/src/lib/test-env.js:
--------------------------------------------------------------------------------
1 | import { env } from 'adex/env'
2 |
3 | export const APP_URL = env.get('APP_URL')
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal/src/pages/about.jsx:
--------------------------------------------------------------------------------
1 | export default function AboutPage() {
2 | return
About
3 | }
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return Hello World
3 | }
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-no-ssr/src/pages/about.jsx:
--------------------------------------------------------------------------------
1 | export default function AboutPage() {
2 | return About
3 | }
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-no-ssr/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return Hello World
3 | }
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/src/pages/about.jsx:
--------------------------------------------------------------------------------
1 | export default function AboutPage() {
2 | return About
3 | }
4 |
--------------------------------------------------------------------------------
/playground/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/playground/src/pages/$id/hello.tsx:
--------------------------------------------------------------------------------
1 | export default ({ routeParams }) => {
2 | return Hello from {routeParams.id}
3 | }
4 |
--------------------------------------------------------------------------------
/adex/src/env.d.ts:
--------------------------------------------------------------------------------
1 | export const env: {
2 | get(key: any, defaultValue?: string): string
3 | set(key: any, value: any): any
4 | }
5 |
--------------------------------------------------------------------------------
/adex/src/utils/isomorphic.js:
--------------------------------------------------------------------------------
1 | export { parse as pathToRegex } from 'regexparam'
2 | export { renderToStringAsync } from 'preact-render-to-string'
3 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "jsxImportSource": "preact"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return Hello World
3 | }
4 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-no-ssr/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "jsxImportSource": "preact"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "jsxImportSource": "preact"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/adex/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | export default defineConfig({
4 | test: {
5 | hookTimeout: 30_000,
6 | },
7 | })
8 |
--------------------------------------------------------------------------------
/playground/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import { useTitle } from 'adex/head'
2 |
3 | export default () => {
4 | useTitle('About Page')
5 | return About
6 | }
7 |
--------------------------------------------------------------------------------
/adex/src/head.js:
--------------------------------------------------------------------------------
1 | export {
2 | useHead,
3 | useLang,
4 | useLink,
5 | useMeta,
6 | useScript,
7 | useTitle,
8 | useTitleTemplate,
9 | } from 'hoofd/preact'
10 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [Get Started →](https://github.com/barelyhuman/adex-default-template)\
4 | [Docs →](https://barelyhuman.github.io/adex-docs)
5 |
--------------------------------------------------------------------------------
/adex/src/head.d.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useHead,
3 | useLang,
4 | useLink,
5 | useMeta,
6 | useScript,
7 | useTitle,
8 | useTitleTemplate,
9 | } from 'hoofd/preact'
10 |
--------------------------------------------------------------------------------
/packages/adapters/node/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | type ServerOut = {
2 | run: () => any
3 | fetch: undefined
4 | }
5 |
6 | export const createServer: ({ port: number, host: string }) => ServerOut
7 |
--------------------------------------------------------------------------------
/adex/src/ssr.d.ts:
--------------------------------------------------------------------------------
1 | export { toStatic } from 'hoofd/preact'
2 | export { use as useMiddleware } from '@barelyhuman/tiny-use'
3 | export { default as sirv } from 'sirv'
4 | export { default as mri } from 'mri'
5 |
--------------------------------------------------------------------------------
/playground/src/api/random-data.js:
--------------------------------------------------------------------------------
1 | export default function (req, res) {
2 | return res.json(
3 | Array.from({ length: 3 })
4 | .fill(0)
5 | .map((d, i) => (i + 1) * Math.random())
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - adex
3 | - create-adex
4 | - playground
5 | - packages/**/*
6 | - adex/tests/**/*
7 |
8 | catalog:
9 | "preact": 10.24.2
10 | "@preact/preset-vite": 2.9.1
11 |
--------------------------------------------------------------------------------
/adex/src/app.d.ts:
--------------------------------------------------------------------------------
1 | export declare const App: ({ url }: { url?: string | undefined }) => any
2 | export declare const prerender: ({ url }: { url: any }) => Promise<{
3 | html: any
4 | links: Set
5 | }>
6 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./src/pages/**/*.{tsx,jsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/playground/src/components/counter.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'preact/hooks'
2 |
3 | export function Counter() {
4 | const [count, setCount] = useState(0)
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": true,
5 | "jsxImportSource": "preact",
6 | "module": "ESNext",
7 | "target": "ESNext",
8 | "noEmit": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/playground/src/api/$id/hello.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {import("adex/http").IncomingMessage} req
3 | * @param {import("adex/http").ServerResponse} res
4 | */
5 | export default (req, res) => {
6 | return res.text(`Hello from ${req.params.id}`)
7 | }
8 |
--------------------------------------------------------------------------------
/adex/src/ssr.js:
--------------------------------------------------------------------------------
1 | export { prerender as renderToString } from 'preact-iso'
2 | export { default as sirv } from 'sirv'
3 | export { default as mri } from 'mri'
4 | export { toStatic } from 'hoofd/preact'
5 | export { use as useMiddleware } from '@barelyhuman/tiny-use'
6 |
--------------------------------------------------------------------------------
/adex/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # adex
2 |
3 | ## 0.0.5
4 |
5 | ### Patch Changes
6 |
7 | - c8a7177: Port and host management in the server
8 |
9 | ## 0.0.3
10 |
11 | ### Patch Changes
12 |
13 | - Move back to simple hydration instead of island hydration for the time being
14 |
--------------------------------------------------------------------------------
/adex/src/router.d.ts:
--------------------------------------------------------------------------------
1 | export {
2 | hydrate,
3 | Router,
4 | Route,
5 | lazy,
6 | LocationProvider,
7 | ErrorBoundary,
8 | useLocation,
9 | useRoute,
10 | prerender,
11 | } from 'preact-iso'
12 |
13 | export declare const join: (...args: string[]) => string
14 |
--------------------------------------------------------------------------------
/playground/src/api/$id/json.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {import("adex/http").IncomingMessage} req
3 | * @param {import("adex/http").ServerResponse} res
4 | */
5 | export default (req, res) => {
6 | return res.json({
7 | message: `Hello in ${req.params.id}`,
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/playground/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json",
3 | "changelogPreset": "angular",
4 | "npmClient": "pnpm",
5 | "version": "fixed",
6 | "packages": [
7 | "adex",
8 | "create-adex",
9 | "playground",
10 | "packages/**/*"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/playground/src/pages/shared-signal.tsx:
--------------------------------------------------------------------------------
1 | import { Renderer, Triggerer } from '../components/SharedSignal.js'
2 |
3 | export default function SharedSignal() {
4 | return (
5 |
6 |
7 |
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/playground/src/pages/islands-test.tsx:
--------------------------------------------------------------------------------
1 | import { ListIsland } from '../components/FormIsland'
2 | import { FormIsland } from '../components/ListIsland'
3 |
4 | export default function IslandsTest() {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/playground/src/api/hello.js:
--------------------------------------------------------------------------------
1 | import { env } from 'adex/env'
2 | /**
3 | * @param {import("adex/http").IncomingMessage} req
4 | * @param {import("adex/http").ServerResponse} res
5 | */
6 | export default (req, res) => {
7 | return res.json({
8 | pong: true,
9 | appUrl: env.get('APP_URL'),
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { adex } from 'adex'
3 | import preact from '@preact/preset-vite'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | adex({
8 | islands: false,
9 | ssr: true,
10 | }),
11 | preact(),
12 | ],
13 | })
14 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "noEmit": true,
5 | "jsx": "react-jsx",
6 | "jsxImportSource": "preact",
7 | "moduleResolution": "Node",
8 | "paths": {
9 | "adex": ["../adex/vite.js"],
10 | "adex/*": ["../adex/src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-no-ssr/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { adex } from 'adex'
3 | import preact from '@preact/preset-vite'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | adex({
8 | islands: false,
9 | ssr: false,
10 | }),
11 | preact(),
12 | ],
13 | })
14 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { adex } from 'adex'
3 | import preact from '@preact/preset-vite'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | adex({
8 | islands: false,
9 | ssr: true,
10 | }),
11 | preact(),
12 | ],
13 | })
14 |
--------------------------------------------------------------------------------
/playground/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import forms from '@tailwindcss/forms'
2 | /** @type {import('tailwindcss').Config} */
3 | export default {
4 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
5 | theme: {
6 | extend: {},
7 | fontFamily: {
8 | sans: 'Inter, sans-serif',
9 | },
10 | },
11 | plugins: [forms],
12 | }
13 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minimal",
3 | "type": "module",
4 | "private": true,
5 | "version": "0.0.0",
6 | "dependencies": {
7 | "adex": "workspace:*",
8 | "preact": "catalog:",
9 | "@preact/preset-vite": "catalog:"
10 | },
11 | "devDependencies": {
12 | "vite": "^5.4.8"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-no-ssr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minimal",
3 | "type": "module",
4 | "private": true,
5 | "version": "0.0.0",
6 | "dependencies": {
7 | "adex": "workspace:*",
8 | "preact": "catalog:",
9 | "@preact/preset-vite": "catalog:"
10 | },
11 | "devDependencies": {
12 | "vite": "^5.4.8"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/playground/src/api/html.js:
--------------------------------------------------------------------------------
1 | import { afterAPICall } from 'adex/hook'
2 |
3 | afterAPICall(ctx => {
4 | console.log('called after api')
5 | })
6 |
7 | /**
8 | * @param {import("adex/http").IncomingMessage} req
9 | * @param {import("adex/http").ServerResponse} res
10 | */
11 | export default (req, res) => {
12 | return res.html(`Html Response
`)
13 | }
14 |
--------------------------------------------------------------------------------
/adex/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/playground/src/components/ListIsland.tsx:
--------------------------------------------------------------------------------
1 | export function FormIsland() {
2 | const onSubmit = e => {
3 | e.preventDefault()
4 | alert('sumbitted')
5 | }
6 | return (
7 |
8 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/adex/runtime/client.js:
--------------------------------------------------------------------------------
1 | import { App } from 'adex/app'
2 | import { h } from 'preact'
3 | import { hydrate as preactHydrate } from 'adex/router'
4 |
5 | export { App, prerender } from 'adex/app'
6 |
7 | import 'virtual:adex:global.css'
8 |
9 | async function hydrate() {
10 | preactHydrate(h(App, null), document.getElementById('app'))
11 | }
12 | if (typeof window !== 'undefined') {
13 | hydrate()
14 | }
15 |
--------------------------------------------------------------------------------
/adex/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "module": "Node16",
5 | "target": "esnext",
6 | "checkJs": true,
7 | "moduleResolution": "Node16",
8 | "allowSyntheticDefaultImports": true,
9 | "noEmit": true,
10 | "types": ["vite/client"],
11 | "paths": {
12 | "adex": ["./src/vite.js"],
13 | "adex/*": ["./src/*"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/playground/src/components/FormIsland.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'preact/hooks'
2 |
3 | export function ListIsland() {
4 | const [data, setData] = useState([])
5 |
6 | useEffect(() => {
7 | setData([1, 2, 3])
8 | }, [])
9 |
10 | return (
11 |
12 |
13 | {data.map(d => (
14 | - {d}
15 | ))}
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/adex/tests/fixtures/minimal-tailwind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minimal-tailwind",
3 | "type": "module",
4 | "private": true,
5 | "version": "0.0.0",
6 | "dependencies": {
7 | "@preact/preset-vite": "catalog:",
8 | "adex": "workspace:*",
9 | "preact": "catalog:"
10 | },
11 | "devDependencies": {
12 | "autoprefixer": "^10.4.20",
13 | "tailwindcss": "^3",
14 | "vite": "^5.4.8"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env*
27 | !.env.example
28 |
29 | /.islands
30 |
--------------------------------------------------------------------------------
/adex/src/env.js:
--------------------------------------------------------------------------------
1 | const isClient = typeof window !== 'undefined'
2 |
3 | if (isClient) {
4 | throw new Error('[adex] Cannot use/import `adex/env` on the client side')
5 | }
6 |
7 | export const env = {
8 | get(key, defaultValue = '') {
9 | if (isClient) return ''
10 | return process.env[key] ?? defaultValue
11 | },
12 | set(key, value) {
13 | if (isClient) return ''
14 | return (process.env[key] = value)
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/adex/src/router.js:
--------------------------------------------------------------------------------
1 | export {
2 | hydrate,
3 | Router,
4 | ErrorBoundary,
5 | Route,
6 | lazy,
7 | LocationProvider,
8 | useLocation,
9 | useRoute,
10 | prerender,
11 | } from 'preact-iso'
12 |
13 | export const join = (...parts) => {
14 | if (parts.some(part => part == null)) {
15 | throw new Error(
16 | 'Expected join to get valid paths, but received undefined or null'
17 | )
18 | }
19 | return parts.join('/').replace(/\/{2,}/g, '/')
20 | }
21 |
--------------------------------------------------------------------------------
/adex/src/vite.d.ts:
--------------------------------------------------------------------------------
1 | import { UserConfig, Plugin } from 'vite'
2 | import type { Options as FontOptions } from './fonts.js'
3 |
4 | export type Adapters = 'node'
5 |
6 | export interface AdexOptions {
7 | fonts?: FontOptions
8 | islands?: boolean
9 | adapter?: Adapters
10 | ssr?: boolean
11 | __clientConfig?: UserConfig
12 | }
13 |
14 | export function adex(options: AdexOptions): Plugin[]
15 |
16 | declare module 'vite' {
17 | interface Plugin {
18 | adexServer?: boolean
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/playground/src/_app.jsx:
--------------------------------------------------------------------------------
1 | import { App as AdexApp } from 'adex/app'
2 | import { h } from 'preact'
3 | import { hydrate as preactHydrate } from 'adex/router'
4 |
5 | export { prerender } from 'adex/app'
6 |
7 | import 'virtual:adex:global.css'
8 |
9 | export const App = ({ url }) => {
10 | return
11 | }
12 |
13 | async function hydrate() {
14 | preactHydrate(h(App, null), document.getElementById('app'))
15 | }
16 |
17 | if (typeof window !== 'undefined') {
18 | hydrate()
19 | }
20 |
--------------------------------------------------------------------------------
/adex/src/fonts.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | providers,
3 | // ResolveFontFacesOptions
4 | } from 'unifont'
5 | import { Plugin } from 'vite'
6 | export { providers } from 'unifont'
7 |
8 | export type FontFamilies = {
9 | name: string
10 | weights: string[]
11 | styles: Array<'normal' | 'italic' | 'oblique'> // ResolveFontFacesOptions['styles']
12 | }
13 |
14 | export type Options = {
15 | providers: (typeof providers)[]
16 | families: FontFamilies[]
17 | }
18 |
19 | export function fonts(options: Options): Plugin
20 |
--------------------------------------------------------------------------------
/playground/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import preact from '@preact/preset-vite'
3 | import { adex } from 'adex'
4 | import { providers } from 'adex/fonts'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | adex({
10 | islands: false,
11 | fonts: {
12 | providers: [providers.google()],
13 | families: [
14 | {
15 | name: 'Inter',
16 | weights: ['400', '600'],
17 | styles: ['normal'],
18 | },
19 | ],
20 | },
21 | }),
22 | preact(),
23 | ],
24 | })
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | with:
13 | ref: ${{ github.head_ref }}
14 |
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: '20'
18 | corepack-enable: true
19 | registry-url: 'https://registry.npmjs.org'
20 |
21 | - name: Deps
22 | run: |
23 | npm i -g corepack@latest
24 | corepack enable
25 | pnpm i --frozen-lockfile
26 |
27 | - name: Test
28 | run: pnpm test:ci
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags-ignore:
6 | - '*-alpha.*'
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 |
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: '20'
18 | corepack-enable: true
19 | registry-url: 'https://registry.npmjs.org'
20 |
21 | - name: Deps
22 | run: |
23 | npm i -g corepack@latest
24 | corepack enable
25 | pnpm i --frozen-lockfile
26 |
27 | - name: Test
28 | run: pnpm test
29 |
30 | - name: Publish
31 | run: |
32 | pnpm publish:ci
33 | env:
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.github/workflows/release-beta.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*-alpha.*'
7 |
8 | jobs:
9 | publish_beta:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 |
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: '20'
18 | corepack-enable: true
19 | registry-url: 'https://registry.npmjs.org'
20 |
21 | - name: Deps
22 | run: |
23 | npm i -g corepack@latest
24 | corepack enable
25 | pnpm i --frozen-lockfile
26 |
27 | - name: Test
28 | run: pnpm test
29 |
30 | - name: Publish
31 | run: |
32 | pnpm publish:ci --dist-tag beta
33 | env:
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/adex/src/http.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IncomingMessage as HTTPIncomingMessage,
3 | ServerResponse as HTTPServerResponse,
4 | } from 'http'
5 |
6 | export type IncomingMessage = HTTPIncomingMessage & {
7 | parseBodyJSON: () => Promise>
8 | }
9 |
10 | export type ServerResponse = HTTPServerResponse & {
11 | html: (data: string) => void
12 | json: (data: any) => void
13 | text: (data: string) => void
14 | redirect: (url: string, statusCode: number) => void
15 | badRequest: (message?: string) => void
16 | unauthorized: (message?: string) => void
17 | forbidden: (message?: string) => void
18 | notFound: (message?: string) => void
19 | internalServerError: (message?: string) => void
20 | }
21 |
22 | export function prepareRequest(req: IncomingMessage): void
23 | export function prepareResponse(res: ServerResponse): void
24 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "private": true,
4 | "version": "0.0.19",
5 | "engines": {
6 | "node": ">=18.0.0"
7 | },
8 | "type": "module",
9 | "scripts": {
10 | "dev": "vite",
11 | "build": "vite build",
12 | "preview": "vite preview"
13 | },
14 | "dependencies": {
15 | "@preact/signals": "^1.3.0",
16 | "adex-adapter-node": "workspace:*",
17 | "preact": "^10.24.2"
18 | },
19 | "devDependencies": {
20 | "@barelyhuman/prettier-config": "^1.1.0",
21 | "@preact/preset-vite": "^2.9.1",
22 | "@tailwindcss/forms": "^0.5.9",
23 | "@types/node": "^20.16.10",
24 | "adex": "workspace:*",
25 | "autoprefixer": "^10.4.20",
26 | "postcss": "^8.4.47",
27 | "prettier": "^3.5.3",
28 | "tailwindcss": "^3.4.13",
29 | "vite": "^5.4.8"
30 | },
31 | "prettier": "@barelyhuman/prettier-config"
32 | }
33 |
--------------------------------------------------------------------------------
/playground/src/components/SharedSignal.tsx:
--------------------------------------------------------------------------------
1 | import { signal } from '@preact/signals'
2 | import { useEffect } from 'preact/hooks'
3 |
4 | const data$ = signal([])
5 |
6 | async function fetchData() {
7 | const data = await fetch('/api/random-data').then(d => d.json())
8 | data$.value = data
9 | }
10 |
11 | export function Triggerer() {
12 | useEffect(() => {
13 | fetchData()
14 | }, [])
15 | return (
16 |
17 |
Triggerer Island
18 |
19 |
20 | )
21 | }
22 |
23 | export function Renderer() {
24 | return (
25 |
26 |
Renderer Island
27 |
28 | {data$.value.map(d => (
29 | - {d}
30 | ))}
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/playground/src/api/status-helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {import("adex/http").IncomingMessage} req
3 | * @param {import("adex/http").ServerResponse} res
4 | */
5 | export default (req, res) => {
6 | const { pathname, searchParams } = new URL(req.url, 'http://localhost')
7 | const type = searchParams.get('type')
8 | const message = searchParams.get('message')
9 |
10 | switch (type) {
11 | case 'badRequest':
12 | return res.badRequest(message)
13 | case 'unauthorized':
14 | return res.unauthorized(message)
15 | case 'forbidden':
16 | return res.forbidden(message)
17 | case 'notFound':
18 | return res.notFound(message)
19 | case 'internalServerError':
20 | return res.internalServerError(message)
21 | default:
22 | return res.json({
23 | usage: 'Add ?type=badRequest&message=Custom%20message to test status helpers',
24 | available: ['badRequest', 'unauthorized', 'forbidden', 'notFound', 'internalServerError']
25 | })
26 | }
27 | }
--------------------------------------------------------------------------------
/adex/src/hook.d.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage } from 'node:http'
2 |
3 | export type Context = {
4 | req: IncomingMessage
5 | html: string
6 | }
7 |
8 | export type APIContext = {
9 | req: IncomingMessage
10 | }
11 |
12 | export declare const CONSTANTS: {
13 | beforePageRender: symbol
14 | afterPageRender: symbol
15 | beforeApiCall: symbol
16 | afterApiCall: symbol
17 | }
18 |
19 | export declare function hook(
20 | eventName: string | symbol,
21 | handler: (data: any) => void | Promise
22 | ): void
23 |
24 | export declare function beforePageRender(
25 | fn: (ctx: Omit) => void
26 | ): Promise
27 |
28 | export declare function afterPageRender(
29 | fn: (ctx: Context) => void
30 | ): Promise
31 |
32 | export declare function beforeAPICall(
33 | fn: (ctx: APIContext) => void
34 | ): Promise
35 |
36 | export declare function afterAPICall(
37 | fn: (ctx: APIContext) => void
38 | ): Promise
39 |
40 | export declare function emitToHooked(eventName: any, data: any): Promise
41 |
--------------------------------------------------------------------------------
/adex/snapshots/tests/minimal-tailwind.spec.snap.cjs:
--------------------------------------------------------------------------------
1 | exports["devMode ssr minimal with styles > gives a non-static ssr response 1"] = `"
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Hello World
17 |
18 |
19 | "`
20 |
21 | exports["devMode ssr minimal with styles > gives a static SSR response 1"] = `"
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | About
37 |
38 |
39 | "`
40 |
41 |
--------------------------------------------------------------------------------
/packages/adapters/node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adex-adapter-node",
3 | "version": "0.0.19",
4 | "description": "Node.js adapter for Adex, enabling minimal server-side rendering and integration with Preact.",
5 | "keywords": [
6 | "adex",
7 | "preact",
8 | "minimal",
9 | "server",
10 | "node",
11 | "ssr",
12 | "adapter"
13 | ],
14 | "author": "reaper ",
15 | "license": "MIT",
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/barelyhuman/adex"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/barelyhuman/adex/issues"
22 | },
23 | "homepage": "https://github.com/barelyhuman/adex/tree/main/packages/adapters/node",
24 | "type": "module",
25 | "main": "./lib/index.js",
26 | "exports": {
27 | ".": {
28 | "types": "./lib/index.d.ts",
29 | "import": "./lib/index.js"
30 | }
31 | },
32 | "files": [
33 | "lib"
34 | ],
35 | "engines": {
36 | "node": ">=18.0.0"
37 | },
38 | "funding": {
39 | "type": "github",
40 | "url": "https://github.com/sponsors/barelyhuman"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 reaper
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/adex/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 reaper
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/create-adex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-adex",
3 | "version": "0.0.19",
4 | "description": "A CLI tool to quickly scaffold new Adex projects with minimal setup.",
5 | "keywords": [
6 | "adex",
7 | "preact",
8 | "minimal",
9 | "server",
10 | "node",
11 | "cli",
12 | "scaffold",
13 | "starter",
14 | "project"
15 | ],
16 | "homepage": "https://github.com/barelyhuman/adex#create-adex",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/barelyhuman/adex.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/barelyhuman/adex/issues"
23 | },
24 | "author": "reaper ",
25 | "license": "MIT",
26 | "funding": {
27 | "type": "github",
28 | "url": "https://github.com/sponsors/barelyhuman"
29 | },
30 | "main": "./cli.mjs",
31 | "exports": {
32 | ".": "./cli.mjs"
33 | },
34 | "type": "module",
35 | "files": [
36 | "cli.mjs"
37 | ],
38 | "bin": {
39 | "create-adex": "./cli.mjs"
40 | },
41 | "engines": {
42 | "node": ">=18.0.0"
43 | },
44 | "dependencies": {
45 | "kleur": "^4.1.5",
46 | "node-stream-zip": "^1.15.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/adex/tests/minimal-tailwind.spec.js:
--------------------------------------------------------------------------------
1 | import { after, before, describe, it } from 'node:test'
2 | import assert, { deepEqual } from 'node:assert'
3 |
4 | import { devServerURL, launchDemoDevServer } from './utils.js'
5 | import { snapshot } from '@barelyhuman/node-snapshot'
6 |
7 | describe('devMode ssr minimal with styles', async () => {
8 | let devServerProc
9 | before(async () => {
10 | devServerProc = await launchDemoDevServer('tests/fixtures/minimal-tailwind')
11 | })
12 | after(async () => {
13 | devServerProc.kill()
14 | })
15 |
16 | await it('gives a non-static ssr response', async ctx => {
17 | const response = await fetch(devServerURL).then(d => d.text())
18 | snapshot(ctx, response)
19 | })
20 |
21 | await it('gives a static SSR response', async ctx => {
22 | const response2 = await fetch(new URL('/about', devServerURL)).then(d =>
23 | d.text()
24 | )
25 | snapshot(ctx, response2)
26 | })
27 |
28 | await it('has styles', async ctx => {
29 | const response = await fetch(
30 | new URL('/virtual:adex:global.css', devServerURL)
31 | ).then(d => d.text())
32 | assert.ok(response.includes('.text-red-500'))
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adex-root",
3 | "type": "module",
4 | "private": true,
5 | "description": "",
6 | "keywords": [],
7 | "packageManager": "pnpm@9.5.0",
8 | "author": "reaper",
9 | "scripts": {
10 | "play": "pnpm --filter='playground' -r dev",
11 | "test": "pnpm -r test",
12 | "test:ci": "pnpm -r test:ci",
13 | "publish:ci": "lerna publish from-git --registry 'https://registry.npmjs.org' --yes",
14 | "next": "bumpp -r",
15 | "nuke": "pnpm -r exec rm -rvf node_modules",
16 | "fix": "pnpm -r run --if-present fix",
17 | "prepare": "husky"
18 | },
19 | "license": "MIT",
20 | "prettier": "@barelyhuman/prettier-config",
21 | "devDependencies": {
22 | "@barelyhuman/prettier-config": "^1.1.0",
23 | "@lerna-lite/cli": "^4.0.0",
24 | "@lerna-lite/publish": "^4.0.0",
25 | "bumpp": "^9.4.1",
26 | "husky": "^9.1.7",
27 | "nano-staged": "^0.8.0",
28 | "prettier": "^3.5.3"
29 | },
30 | "nano-staged": {
31 | "*.{js,md,ts,json}": [
32 | "prettier --write"
33 | ]
34 | },
35 | "pnpm": {
36 | "overrides": {
37 | "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5"
38 | }
39 | },
40 | "version": "0.0.19"
41 | }
42 |
--------------------------------------------------------------------------------
/adex/tests/minimal.spec.js:
--------------------------------------------------------------------------------
1 | import { after, before, describe, it } from 'node:test'
2 |
3 | import { devServerURL, launchDemoDevServer } from './utils.js'
4 | import { snapshot } from '@barelyhuman/node-snapshot'
5 |
6 | describe('devMode ssr minimal', async () => {
7 | let devServerProc
8 | before(async () => {
9 | devServerProc = await launchDemoDevServer('tests/fixtures/minimal')
10 | })
11 | after(async () => {
12 | devServerProc.kill()
13 | })
14 |
15 | await it('gives a non-static ssr response', async ctx => {
16 | const response = await fetch(devServerURL).then(d => d.text())
17 | snapshot(ctx, response)
18 | })
19 |
20 | await it('gives a static SSR response', async ctx => {
21 | const response2 = await fetch(new URL('/about', devServerURL)).then(d =>
22 | d.text()
23 | )
24 | snapshot(ctx, response2)
25 | })
26 |
27 | await it('blank styles', async ctx => {
28 | const response = await fetch(
29 | new URL('/virtual:adex:global.css', devServerURL)
30 | ).then(d => d.text())
31 | snapshot(
32 | ctx,
33 | response.replaceAll(
34 | '\\u0000virtual:adex:global.css',
35 | 'virtual:adex:global.css'
36 | )
37 | )
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/adex/tests/minimal-no-ssr.spec.js:
--------------------------------------------------------------------------------
1 | import { snapshot } from '@barelyhuman/node-snapshot'
2 | import { after, before, describe, it } from 'node:test'
3 |
4 | import { devServerURL, launchDemoDevServer } from './utils.js'
5 |
6 | describe('devMode ssr minimal', async () => {
7 | let devServerProc
8 | before(async () => {
9 | devServerProc = await launchDemoDevServer('tests/fixtures/minimal-no-ssr')
10 | })
11 | after(async () => {
12 | devServerProc.kill()
13 | })
14 |
15 | await it('gives a static response', async ctx => {
16 | const response2 = await fetch(new URL('/', devServerURL)).then(d =>
17 | d.text()
18 | )
19 | snapshot(ctx, response2)
20 | })
21 |
22 | await it('gives a static response', async ctx => {
23 | const response2 = await fetch(new URL('/about', devServerURL)).then(d =>
24 | d.text()
25 | )
26 | snapshot(ctx, response2)
27 | })
28 |
29 | await it('blank styles', async ctx => {
30 | const response = await fetch(
31 | new URL('/virtual:adex:global.css', devServerURL)
32 | ).then(d => d.text())
33 | snapshot(
34 | ctx,
35 | response.replaceAll(
36 | '\\u0000virtual:adex:global.css',
37 | 'virtual:adex:global.css'
38 | )
39 | )
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/playground/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'preact/hooks'
2 | import './local-index.css'
3 | import { Counter } from '../components/counter.tsx'
4 |
5 | export default function Page() {
6 | const [count, setCount] = useState(0)
7 |
8 | return (
9 |
10 |
19 |
Vite + Preact
20 |
21 |
27 |
28 | Edit src/app.jsx and save to test HMR
29 |
30 |
31 |
32 | Click on the Vite and Preact logos to learn more
33 |
34 |
35 | Here's an island{' '}
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/playground/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/assets/preact.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/adex/snapshots/tests/minimal-no-ssr.spec.snap.cjs:
--------------------------------------------------------------------------------
1 | exports["devMode ssr minimal > gives a static response 1"] = `"
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Hello World
17 |
18 |
19 | "`
20 |
21 | exports["devMode ssr minimal > gives a static response 2"] = `"
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | About
37 |
38 |
39 | "`
40 |
41 | exports["devMode ssr minimal > blank styles 1"] = `"import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/@id/__x00__virtual:adex:global.css");import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from "/@vite/client"
42 | const __vite__id = "virtual:adex:global.css"
43 | const __vite__css = ""
44 | __vite__updateStyle(__vite__id, __vite__css)
45 | import.meta.hot.accept()
46 | import.meta.hot.prune(() => __vite__removeStyle(__vite__id))"`
47 |
48 |
--------------------------------------------------------------------------------
/adex/snapshots/tests/minimal.spec.snap.cjs:
--------------------------------------------------------------------------------
1 | exports["devMode ssr minimal > gives a non-static ssr response 1"] = `"
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Hello World
17 |
18 |
19 | "`
20 |
21 | exports["devMode ssr minimal > blank styles 1"] = `"import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/@id/__x00__virtual:adex:global.css");import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from "/@vite/client"
22 | const __vite__id = "virtual:adex:global.css"
23 | const __vite__css = ""
24 | __vite__updateStyle(__vite__id, __vite__css)
25 | import.meta.hot.accept()
26 | import.meta.hot.prune(() => __vite__removeStyle(__vite__id))"`
27 |
28 | exports["devMode ssr minimal > gives a static SSR response 1"] = `"
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | About
44 |
45 |
46 | "`
47 |
48 |
--------------------------------------------------------------------------------
/adex/readme.md:
--------------------------------------------------------------------------------
1 | # adex
2 |
3 | > An easier way to build apps with [Vite](http://vitejs.dev) and
4 | > [Preact](http://preactjs.com)
5 |
6 | - [adex](#adex)
7 | - [About](#about)
8 | - [Highlights](#highlights)
9 | - [Setup](#setup)
10 | - [Docs](#docs)
11 | - [License](#license)
12 |
13 | ## About
14 |
15 | `adex` is just a vite plugin that adds in the required functionality to be able
16 | to build server rendered preact apps.
17 |
18 | ### Highlights
19 |
20 | - Unified routing for both backend and frontend
21 | - Tiny and Simple
22 | - Builds on existing tooling instead of adding yet another way to do things
23 |
24 | ## Setup
25 |
26 | As there are a few steps needed to get it running, it's not recommended to do
27 | this manually, instead use the
28 | [existing template to get started](https://github.com/barelyhuman/adex-default-template)
29 |
30 | If you still wish to set it up manually.
31 |
32 | **Create a preact based vite app**
33 |
34 | ```sh
35 | npm create vite@latest -- -t preact
36 | ```
37 |
38 | **Add in the required deps**
39 |
40 | ```sh
41 | npm add -D adex
42 | ```
43 |
44 | **Modify config to use adex**
45 |
46 | ```diff
47 | // vite.config.js
48 | import { defineConfig } from 'vite'
49 | import preact from '@preact/preset-vite'
50 | + import { adex } from 'adex'
51 |
52 | // https://vitejs.dev/config/
53 | export default defineConfig({
54 | plugins: [
55 | + adex(),
56 | preact()],
57 | })
58 | ```
59 |
60 | **Remove the default preact files and add in basic structure for adex**
61 |
62 | ```sh
63 | rm -rf ./src/* index.html
64 | mkdir -p src/pages src/api
65 | touch src/global.css
66 | ```
67 |
68 | And you are done.
69 |
70 | ## Docs
71 |
72 | [Docs →](https://barelyhuman.github.io/adex-docs/)
73 |
74 | ## License
75 |
76 | [MIT](/LICENSE)
77 |
--------------------------------------------------------------------------------
/adex/src/hook.js:
--------------------------------------------------------------------------------
1 | const hookListeners = new Map()
2 |
3 | export const CONSTANTS = {
4 | beforePageRender: Symbol('before-page-render'),
5 | afterPageRender: Symbol('after-page-render'),
6 | beforeApiCall: Symbol('before-api-call'),
7 | afterApiCall: Symbol('after-api-call'),
8 | }
9 |
10 | /**
11 | * Register a hook handler for a given event
12 | * @param {symbol} eventName
13 | * @param {(context: any) => any|Promise} handler
14 | */
15 | export function hook(eventName, handler) {
16 | const handlers = hookListeners.get(eventName) || []
17 | handlers.push(handler)
18 | hookListeners.set(eventName, handlers)
19 | }
20 |
21 | /**
22 | * Register a hook to run before page render
23 | * @param {(context: any) => any|Promise} fn
24 | */
25 | export function beforePageRender(fn) {
26 | hook(CONSTANTS.beforePageRender, fn)
27 | }
28 |
29 | /**
30 | * Register a hook to run after page render
31 | * @param {(context: any) => any|Promise} fn
32 | */
33 | export function afterPageRender(fn) {
34 | hook(CONSTANTS.afterPageRender, fn)
35 | }
36 |
37 | /**
38 | * Register a hook to run before API call
39 | * @param {(context: any) => any|Promise} fn
40 | */
41 | export function beforeAPICall(fn) {
42 | hook(CONSTANTS.beforeApiCall, fn)
43 | }
44 |
45 | /**
46 | * Register a hook to run after API call
47 | * @param {(context: any) => any|Promise} fn
48 | */
49 | export function afterAPICall(fn) {
50 | hook(CONSTANTS.afterApiCall, fn)
51 | }
52 |
53 | /**
54 | * Emit an event to all registered hooks
55 | * @param {symbol} eventName
56 | * @param {any} data
57 | */
58 | export async function emitToHooked(eventName, data) {
59 | const handlers = hookListeners.get(eventName) || []
60 | for (let handler of handlers) {
61 | await handler(data)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/adex/runtime/app.js:
--------------------------------------------------------------------------------
1 | import {
2 | ErrorBoundary,
3 | LocationProvider,
4 | Route,
5 | Router,
6 | join,
7 | lazy,
8 | prerender as ssr,
9 | } from 'adex/router'
10 | import { h } from 'preact'
11 |
12 | const baseURL = import.meta.env.BASE_URL ?? '/'
13 |
14 | const normalizeURLPath = url => (url ? join(baseURL, url) : undefined)
15 |
16 | const removeBaseURL = url => {
17 | if (typeof url !== 'string') {
18 | return undefined
19 | }
20 | if (url.startsWith(baseURL)) {
21 | const result = url.slice(baseURL.length)
22 | return result === '' ? '/' : result
23 | }
24 | const baseURLWithoutSlash = baseURL.endsWith('/')
25 | ? baseURL.slice(0, -1)
26 | : baseURL
27 | if (url.startsWith(baseURLWithoutSlash)) {
28 | const result = url.slice(baseURLWithoutSlash.length)
29 | return result === '' ? '/' : result
30 | }
31 | return undefined
32 | }
33 |
34 | // @ts-expect-error injected by vite
35 | import { routes } from '~routes'
36 |
37 | function ComponentWrapper({ url = '' }) {
38 | return h(
39 | LocationProvider,
40 | {
41 | // @ts-expect-error LocationProvider doesn't expose this
42 | url: normalizeURLPath(url),
43 | },
44 | h(
45 | ErrorBoundary,
46 | null,
47 | h(
48 | Router,
49 | null,
50 | routes.map(d => {
51 | const routePath = d.routePath.replace(/\/\*(\w+)/, '/:$1*')
52 |
53 | return h(Route, {
54 | path: normalizeURLPath(routePath),
55 | component: lazy(d.module),
56 | })
57 | })
58 | )
59 | )
60 | )
61 | }
62 |
63 | export const App = ({ url = '' }) => {
64 | return h(ComponentWrapper, {
65 | url: url,
66 | })
67 | }
68 | export const prerender = async ({ url }) => {
69 | const { html, links: discoveredLinks } = await ssr(
70 | h(ComponentWrapper, {
71 | url: url,
72 | })
73 | )
74 | return {
75 | html,
76 | links: new Set(
77 | [...discoveredLinks].map(d => removeBaseURL(d)).filter(Boolean)
78 | ),
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/adex/src/fonts.js:
--------------------------------------------------------------------------------
1 | import { createUnifont } from 'unifont'
2 | export { providers } from 'unifont'
3 |
4 | const fontVirtualId = 'virtual:adex:font.css'
5 |
6 | /**
7 | * @returns {import("vite").Plugin}
8 | */
9 | export function fonts({ providers = [], families = [] } = {}) {
10 | return {
11 | name: 'adex-fonts',
12 | enforce: 'pre',
13 | resolveId(requestId) {
14 | if (requestId === fontVirtualId || requestId === '/' + fontVirtualId) {
15 | return `\0${fontVirtualId}`
16 | }
17 | },
18 | async load(id) {
19 | if (id === `\0${fontVirtualId}`) {
20 | const unifont = await createUnifont([...providers])
21 | const fontsToResolve = families.map(userFamily => {
22 | return unifont
23 | .resolveFontFace(userFamily.name, {
24 | weights: userFamily.weights ?? ['600'],
25 | styles: userFamily.styles ?? ['normal'],
26 | subsets: [],
27 | })
28 | .then(resolvedFont => {
29 | const toUse = resolvedFont.fonts.filter(
30 | d =>
31 | []
32 | .concat(d.weight)
33 | .map(String)
34 | .find(d => userFamily.weights.includes(d)) ?? false
35 | )
36 |
37 | return { fonts: toUse, name: userFamily.name }
38 | })
39 | })
40 | const fontImports = []
41 | for await (let resolvedFont of fontsToResolve) {
42 | const fontFace = fontsToFontFace(resolvedFont)
43 | fontImports.push(fontFace.join('\n'))
44 | }
45 | return {
46 | code: fontImports.join('\n'),
47 | }
48 | }
49 | },
50 | async transform(code, id) {
51 | const resolvedData = await this.resolve('virtual:adex:client')
52 | if (resolvedData?.id == id) {
53 | return {
54 | code: `import "${fontVirtualId}";\n` + code,
55 | }
56 | }
57 | },
58 | }
59 | }
60 |
61 | function fontsToFontFace(resolvedFont) {
62 | return resolvedFont.fonts.map(fontDetails => {
63 | return fontDetails.src
64 | .map(x => {
65 | return `@font-face {
66 | font-family: '${resolvedFont.name}';
67 | font-weight: ${[].concat(fontDetails.weight).join(',')};
68 | font-style: ${[].concat(fontDetails.style).join(',')};
69 | src: url(${x.url}) format('woff2');
70 | }`
71 | })
72 | .join('\n')
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/adex/runtime/pages.js:
--------------------------------------------------------------------------------
1 | import { pathToRegex } from 'adex/utils/isomorphic'
2 |
3 | const pages = import.meta.glob('#{__PLUGIN_PAGES_ROOT}')
4 |
5 | export const routes = normalizeRouteImports(pages, [
6 | new RegExp('#{__PLUGIN_PAGES_ROOT_REGEX}'),
7 | '#{__PLUGIN_PAGES_ROOT_REGEX_REPLACER}',
8 | ])
9 |
10 | // major bits taken from
11 | // https://github.com/cyco130/smf/blob/c4b601f48cd3b3b71bea6d76b52b9a85800813e4/smf/shared.ts#L22
12 | // as it's decently tested and aligns to what we want for our routing
13 | function compareRoutePatterns(a, b) {
14 | // Non-catch-all routes first: /foo before /$$rest
15 | const catchAll = Number(a.match(/\$\$(\w+)$/)) - Number(b.match(/\$\$(\w+)$/))
16 | if (catchAll) return catchAll
17 |
18 | // Split into segments
19 | const aSegments = a.split('/')
20 | const bSegments = b.split('/')
21 |
22 | // Routes with fewer dynamic segments first: /foo/bar before /foo/$bar
23 | const dynamicSegments =
24 | aSegments.filter(segment => segment.includes('$')).length -
25 | bSegments.filter(segment => segment.includes('$')).length
26 | if (dynamicSegments) return dynamicSegments
27 |
28 | // Routes with fewer segments first: /foo/bar before /foo/bar
29 | const segments = aSegments.length - bSegments.length
30 | if (segments) return segments
31 |
32 | // Routes with earlier dynamic segments first: /foo/$bar before /$foo/bar
33 | for (let i = 0; i < aSegments.length; i++) {
34 | const aSegment = aSegments[i]
35 | const bSegment = bSegments[i]
36 | const dynamic =
37 | Number(aSegment.includes('$')) - Number(bSegment.includes('$'))
38 | if (dynamic) return dynamic
39 |
40 | // Routes with more dynamic subsegments at this position first: /foo/$a-$b before /foo/$a
41 | const subsegments = aSegment.split('$').length - bSegment.split('$').length
42 | if (subsegments) return subsegments
43 | }
44 |
45 | // Equal as far as we can tell
46 | return 0
47 | }
48 |
49 | function normalizeRouteImports(imports, baseKeyMatcher) {
50 | return Object.keys(imports)
51 | .sort(compareRoutePatterns)
52 | .map(route => {
53 | const routePath = simplifyPath(route).replace(
54 | baseKeyMatcher[0],
55 | baseKeyMatcher[1]
56 | )
57 |
58 | const regex = pathToRegex(routePath)
59 |
60 | return {
61 | route,
62 | regex,
63 | routePath,
64 | module: imports[route],
65 | }
66 | })
67 | }
68 |
69 | function simplifyPath(path) {
70 | return path
71 | .replace(/(\.(js|ts)x?)/, '')
72 | .replace(/index/, '/')
73 | .replace('//', '/')
74 | .replace(/\$\$/g, '*')
75 | .replace(/\$/g, ':')
76 | }
77 |
--------------------------------------------------------------------------------
/adex/tests/utils.js:
--------------------------------------------------------------------------------
1 | import { spawn } from 'node:child_process'
2 | import path from 'node:path'
3 | import { fileURLToPath } from 'node:url'
4 | import { stripColors } from 'kolorist'
5 |
6 | const __dirname = path.dirname(fileURLToPath(import.meta.url))
7 | export const dir = (...args) => path.join(__dirname, '..', ...args)
8 |
9 | export const devServerURL = new URL('http://localhost:5173/')
10 |
11 | /**
12 | * Wait for vite dev server to start
13 | * @param {import('node:child_process').ChildProcess} devServerProc
14 | * @returns {Promise}
15 | */
16 | function waitForServerStart(devServerProc) {
17 | return new Promise((resolve, reject) => {
18 | function onError(err) {
19 | cleanup()
20 | reject(err)
21 | }
22 |
23 | /** @param {number | null} code */
24 | function onClose(code) {
25 | cleanup()
26 | reject(new Error(`Dev server closed unexpectedly with code "${code}"`))
27 | }
28 |
29 | let serverReady = false
30 | let stdout = ''
31 | /** @param {Buffer | string} chunk */
32 | function onData(chunk) {
33 | try {
34 | /** @type {string} */
35 | const data = Buffer.isBuffer(chunk)
36 | ? chunk.toString('utf-8')
37 | : chunk.toString()
38 |
39 | stdout += data
40 |
41 | const matchableStdout = stripColors(stdout)
42 |
43 | if (matchableStdout.match(/ready\sin\s[0-9]+\sms/g) != null) {
44 | serverReady = true
45 | }
46 |
47 | if (matchableStdout.match(/localhost:(\d+)/) != null) {
48 | const matchedPort = matchableStdout.match(/localhost:(\d+)/)
49 | devServerURL.port = matchedPort[1]
50 | if (serverReady) {
51 | cleanup()
52 | resolve()
53 | }
54 | }
55 | } catch (e) {
56 | reject(e)
57 | }
58 | }
59 |
60 | function cleanup() {
61 | try {
62 | devServerProc.stdout?.off('data', onData)
63 | devServerProc.off('error', onError)
64 | devServerProc.off('close', onClose)
65 | } catch (e) {
66 | reject(e)
67 | }
68 | }
69 |
70 | devServerProc.stdout?.on('data', onData)
71 | devServerProc.on('error', onError)
72 | devServerProc.on('close', onClose)
73 | })
74 | }
75 |
76 | /**
77 | * @param {string} fixturePath
78 | */
79 | export async function launchDemoDevServer(fixturePath) {
80 | console.log(`launching on ${dir(fixturePath)}`)
81 | /** @type {import('node:child_process').ChildProcess} */
82 | const devServerProc = spawn(
83 | process.execPath,
84 | [dir('node_modules/vite/bin/vite.js')],
85 | { cwd: dir(fixturePath), stdio: 'pipe' }
86 | )
87 |
88 | await waitForServerStart(devServerProc)
89 |
90 | // Ensure the server remains active until the test completes.
91 | process.once('exit', () => {
92 | devServerProc.kill()
93 | })
94 |
95 | return devServerProc
96 | }
97 |
--------------------------------------------------------------------------------
/adex/src/http.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {import("./http.js").IncomingMessage} req
3 | */
4 | export function prepareRequest(req) {
5 | req.parseBodyJSON = async function () {
6 | return new Promise((resolve, reject) => {
7 | let jsonChunk = ''
8 | req.on('data', chunk => {
9 | jsonChunk += chunk
10 | })
11 | req.on('error', err => {
12 | const oldStack = err.stack
13 | const newError = new Error(
14 | `failed to parse json body with error: ${err.message}`
15 | )
16 | newError.stack = oldStack + newError.stack
17 | reject(newError)
18 | })
19 | req.on('end', () => {
20 | try {
21 | const parsedJSON = JSON.parse(Buffer.from(jsonChunk).toString('utf8'))
22 | resolve(parsedJSON)
23 | } catch (err) {
24 | reject(err)
25 | }
26 | })
27 | })
28 | }
29 | }
30 |
31 | /**
32 | * @param {import("./http.js").ServerResponse} res
33 | */
34 | export function prepareResponse(res) {
35 | res.html = data => {
36 | if (typeof data !== 'string') {
37 | throw new Error('[res.html] only accepts html string')
38 | }
39 | res.setHeader('content-type', 'text/html')
40 | res.write(data)
41 | res.end()
42 | }
43 | res.text = data => {
44 | let _data = data
45 | if (typeof data !== 'string') {
46 | _data = JSON.stringify(data)
47 | }
48 | res.setHeader('content-type', 'text/plain')
49 | res.write(_data)
50 | res.end()
51 | }
52 | res.json = data => {
53 | const str = JSON.stringify(data)
54 | res.setHeader('content-type', 'application/json')
55 | res.write(str)
56 | res.end()
57 | }
58 | res.redirect = (url, statusCode = 302) => {
59 | res.statusCode = statusCode
60 | res.setHeader('Location', url)
61 | }
62 |
63 | // HTTP Status helpers
64 | res.badRequest = message => {
65 | res.statusCode = 400
66 | if (message) {
67 | res.setHeader('content-type', 'application/json')
68 | res.write(JSON.stringify({ error: message }))
69 | }
70 | res.end()
71 | }
72 |
73 | res.unauthorized = message => {
74 | res.statusCode = 401
75 | if (message) {
76 | res.setHeader('content-type', 'application/json')
77 | res.write(JSON.stringify({ error: message }))
78 | }
79 | res.end()
80 | }
81 |
82 | res.forbidden = message => {
83 | res.statusCode = 403
84 | if (message) {
85 | res.setHeader('content-type', 'application/json')
86 | res.write(JSON.stringify({ error: message }))
87 | }
88 | res.end()
89 | }
90 |
91 | res.notFound = message => {
92 | res.statusCode = 404
93 | if (message) {
94 | res.setHeader('content-type', 'application/json')
95 | res.write(JSON.stringify({ error: message }))
96 | }
97 | res.end()
98 | }
99 |
100 | res.internalServerError = message => {
101 | res.statusCode = 500
102 | if (message) {
103 | res.setHeader('content-type', 'application/json')
104 | res.write(JSON.stringify({ error: message }))
105 | }
106 | res.end()
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/adex/tests/response-helpers.spec.js:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'node:test'
2 | import assert from 'node:assert'
3 | import { prepareResponse } from '../src/http.js'
4 |
5 | // Mock ServerResponse class
6 | class MockServerResponse {
7 | constructor() {
8 | this.statusCode = 200
9 | this.headers = {}
10 | this.writtenData = []
11 | this.ended = false
12 | }
13 |
14 | setHeader(name, value) {
15 | this.headers[name] = value
16 | }
17 |
18 | write(data) {
19 | this.writtenData.push(data)
20 | }
21 |
22 | end() {
23 | this.ended = true
24 | }
25 | }
26 |
27 | describe('HTTP Response Helpers', () => {
28 | it('badRequest sets 400 status and ends response', () => {
29 | const res = new MockServerResponse()
30 | prepareResponse(res)
31 |
32 | res.badRequest()
33 |
34 | assert.strictEqual(res.statusCode, 400)
35 | assert.strictEqual(res.ended, true)
36 | assert.strictEqual(res.writtenData.length, 0)
37 | })
38 |
39 | it('badRequest with message sets 400 status and sends JSON error', () => {
40 | const res = new MockServerResponse()
41 | prepareResponse(res)
42 |
43 | res.badRequest('Invalid input')
44 |
45 | assert.strictEqual(res.statusCode, 400)
46 | assert.strictEqual(res.ended, true)
47 | assert.strictEqual(res.headers['content-type'], 'application/json')
48 | assert.strictEqual(
49 | res.writtenData[0],
50 | JSON.stringify({ error: 'Invalid input' })
51 | )
52 | })
53 |
54 | it('unauthorized sets 401 status', () => {
55 | const res = new MockServerResponse()
56 | prepareResponse(res)
57 |
58 | res.unauthorized('Authentication required')
59 |
60 | assert.strictEqual(res.statusCode, 401)
61 | assert.strictEqual(res.ended, true)
62 | assert.strictEqual(res.headers['content-type'], 'application/json')
63 | assert.strictEqual(
64 | res.writtenData[0],
65 | JSON.stringify({ error: 'Authentication required' })
66 | )
67 | })
68 |
69 | it('forbidden sets 403 status', () => {
70 | const res = new MockServerResponse()
71 | prepareResponse(res)
72 |
73 | res.forbidden('Access denied')
74 |
75 | assert.strictEqual(res.statusCode, 403)
76 | assert.strictEqual(res.ended, true)
77 | assert.strictEqual(res.headers['content-type'], 'application/json')
78 | assert.strictEqual(
79 | res.writtenData[0],
80 | JSON.stringify({ error: 'Access denied' })
81 | )
82 | })
83 |
84 | it('notFound sets 404 status', () => {
85 | const res = new MockServerResponse()
86 | prepareResponse(res)
87 |
88 | res.notFound('Resource not found')
89 |
90 | assert.strictEqual(res.statusCode, 404)
91 | assert.strictEqual(res.ended, true)
92 | assert.strictEqual(res.headers['content-type'], 'application/json')
93 | assert.strictEqual(
94 | res.writtenData[0],
95 | JSON.stringify({ error: 'Resource not found' })
96 | )
97 | })
98 |
99 | it('internalServerError sets 500 status', () => {
100 | const res = new MockServerResponse()
101 | prepareResponse(res)
102 |
103 | res.internalServerError('Something went wrong')
104 |
105 | assert.strictEqual(res.statusCode, 500)
106 | assert.strictEqual(res.ended, true)
107 | assert.strictEqual(res.headers['content-type'], 'application/json')
108 | assert.strictEqual(
109 | res.writtenData[0],
110 | JSON.stringify({ error: 'Something went wrong' })
111 | )
112 | })
113 |
114 | it('status helpers without message only set status and end', () => {
115 | const res = new MockServerResponse()
116 | prepareResponse(res)
117 |
118 | res.notFound()
119 |
120 | assert.strictEqual(res.statusCode, 404)
121 | assert.strictEqual(res.ended, true)
122 | assert.strictEqual(res.writtenData.length, 0)
123 | assert.strictEqual(res.headers['content-type'], undefined)
124 | })
125 | })
126 |
--------------------------------------------------------------------------------
/adex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adex",
3 | "version": "0.0.19",
4 | "description": "A minimal, fast framework for building apps with Vite and Preact. Simple SSR, routing, and utilities for modern web projects.",
5 | "keywords": [
6 | "adex",
7 | "preact",
8 | "vite",
9 | "ssr",
10 | "framework",
11 | "minimal",
12 | "server",
13 | "node",
14 | "web",
15 | "routing"
16 | ],
17 | "homepage": "https://github.com/barelyhuman/adex#readme",
18 | "bugs": {
19 | "url": "https://github.com/barelyhuman/adex/issues",
20 | "email": "ahoy@barelyhuman.dev"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/barelyhuman/adex.git"
25 | },
26 | "author": {
27 | "name": "barelyhuman",
28 | "email": "ahoy@barelyhuman.dev",
29 | "url": "https://reaper.is"
30 | },
31 | "contributors": [
32 | {
33 | "name": "barelyhuman",
34 | "email": "ahoy@barelyhuman.dev",
35 | "url": "https://reaper.is"
36 | }
37 | ],
38 | "license": "MIT",
39 | "engines": {
40 | "node": ">=18.0.0"
41 | },
42 | "funding": {
43 | "type": "github",
44 | "url": "https://github.com/sponsors/barelyhuman"
45 | },
46 | "type": "module",
47 | "main": "./src/vite.js",
48 | "module": "./src/vite.js",
49 | "exports": {
50 | "./package.json": "./package.json",
51 | "./router": {
52 | "types": "./src/router.d.ts",
53 | "import": "./src/router.js"
54 | },
55 | "./app": {
56 | "types": "./src/app.d.ts",
57 | "import": "./src/app.js"
58 | },
59 | "./utils/isomorphic": {
60 | "types": "./src/utils/isomorphic.d.ts",
61 | "import": "./src/utils/isomorphic.js"
62 | },
63 | "./ssr": {
64 | "types": "./src/ssr.d.ts",
65 | "import": "./src/ssr.js"
66 | },
67 | "./head": {
68 | "types": "./src/head.d.ts",
69 | "import": "./src/head.js"
70 | },
71 | "./hook": {
72 | "types": "./src/hook.d.ts",
73 | "import": "./src/hook.js"
74 | },
75 | "./http": {
76 | "types": "./src/http.d.ts",
77 | "import": "./src/http.js"
78 | },
79 | "./env": {
80 | "types": "./src/env.d.ts",
81 | "import": "./src/env.js"
82 | },
83 | "./fonts": {
84 | "types": "./src/fonts.d.ts",
85 | "import": "./src/fonts.js"
86 | },
87 | ".": {
88 | "types": "./src/vite.d.ts",
89 | "import": "./src/vite.js"
90 | }
91 | },
92 | "sideEffects": false,
93 | "publishConfig": {
94 | "access": "public",
95 | "registry": "https://registry.npmjs.org/"
96 | },
97 | "files": [
98 | "src",
99 | "runtime"
100 | ],
101 | "scripts": {
102 | "next": "bumpp",
103 | "test": "glob -c 'node --test --test-reporter=spec' tests/**/*.spec.js",
104 | "test:ci": "c8 pnpm test",
105 | "fix": "prettier --write ."
106 | },
107 | "dependencies": {
108 | "@barelyhuman/tiny-use": "^0.0.2",
109 | "@dumbjs/preland": "^0.0.2",
110 | "bumpp": "^9.4.1",
111 | "dotenv": "^16.4.5",
112 | "hoofd": "^1.7.1",
113 | "mri": "^1.2.0",
114 | "node-stream-zip": "^1.15.0",
115 | "preact-iso": "^2.9.0",
116 | "preact-render-to-string": "^6.5.5",
117 | "regexparam": "^3.0.0",
118 | "sirv": "^2.0.4",
119 | "trouter": "^4.0.0",
120 | "unifont": "^0.0.2"
121 | },
122 | "devDependencies": {
123 | "@barelyhuman/node-snapshot": "^1.0.2",
124 | "@barelyhuman/prettier-config": "^1.1.0",
125 | "@preact/preset-vite": "^2.8.2",
126 | "@types/node": "^20.14.10",
127 | "adex-adapter-node": "^0.0.17",
128 | "autoprefixer": "^10.4.19",
129 | "c8": "^10.1.3",
130 | "glob": "^11.0.1",
131 | "kolorist": "^1.8.0",
132 | "nano-staged": "^0.8.0",
133 | "prettier": "^3.5.3",
134 | "tailwindcss": "^3.4.4",
135 | "vite": "^5.3.1"
136 | },
137 | "peerDependenciesMeta": {
138 | "adex-adapter-node": {
139 | "optional": true
140 | }
141 | },
142 | "peerDependencies": {
143 | "@preact/preset-vite": ">=2.8.2",
144 | "adex-adapter-node": ">=0.0.15",
145 | "preact": "^10.22.0"
146 | },
147 | "prettier": "@barelyhuman/prettier-config"
148 | }
149 |
--------------------------------------------------------------------------------
/create-adex/cli.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import StreamZip from 'node-stream-zip'
4 | import { createWriteStream, existsSync } from 'node:fs'
5 | import { copyFile, mkdir, readdir, rm } from 'node:fs/promises'
6 | import { dirname, join, resolve } from 'node:path'
7 | import { Readable } from 'node:stream'
8 | import { parseArgs } from 'node:util'
9 | import { finished } from 'stream/promises'
10 | import k from 'kleur'
11 |
12 | const TMP_FOLDER_PREFIX = '.create-adex'
13 | const info = k.cyan
14 | const success = k.green
15 | const failure = k.red
16 |
17 | const TEMPLATES = {
18 | default: {
19 | name: 'adex-default-template',
20 | link: 'https://github.com/barelyhuman/adex-default-template/archive/refs/heads/main.zip',
21 | branch: 'main',
22 | },
23 | }
24 |
25 | const flags = parseArgs({
26 | allowPositionals: true,
27 | options: {
28 | help: {
29 | short: 'h',
30 | type: 'boolean',
31 | default: false,
32 | },
33 | },
34 | })
35 |
36 | await main()
37 |
38 | async function main() {
39 | const { help } = flags.values
40 | const targetDir = flags.positionals[0] ?? '.'
41 |
42 | if (help) {
43 | showHelp()
44 | return
45 | }
46 |
47 | if (await isDirectoryNotEmpty(targetDir)) {
48 | console.log(
49 | `${failure(`[FAIL]`)} ${k.bold(targetDir)} is not empty, aborting initialization`
50 | )
51 | return
52 | }
53 |
54 | console.log(info(`Initializing in ${targetDir}`))
55 | const selectedTemplate = TEMPLATES.default
56 | const targetFilePath = await downloadFile(
57 | selectedTemplate.link,
58 | 'adex-template.zip'
59 | )
60 |
61 | const unzipStream = new StreamZip.async({ file: targetFilePath })
62 | const entries = await unzipStream.entries()
63 |
64 | const files = await extractFiles(entries, unzipStream)
65 | await copyFiles(files, targetDir, selectedTemplate)
66 |
67 | await rm(TMP_FOLDER_PREFIX, { recursive: true })
68 |
69 | console.log(`\nNext Steps\n$ cd ${targetDir}\n$ npm i`)
70 | console.log(success('\nDone!\n'))
71 | }
72 |
73 | function showHelp() {
74 | console.log(`
75 | ${k.gray('[USAGE]')}
76 |
77 | $ adex [flags] [args]
78 |
79 | ${k.gray('[FLAGS]')}
80 |
81 | --help,-h Show this help
82 | --init PATH Initialize a new adex project at PATH (${k.gray('default: ./')})
83 | `)
84 | }
85 |
86 | async function isDirectoryNotEmpty(targetDir) {
87 | if (existsSync(targetDir)) {
88 | const entries = await readdir(targetDir)
89 | return entries.filter(d => d !== '.tmp').length > 0
90 | }
91 | return false
92 | }
93 |
94 | async function downloadFile(url, fileName) {
95 | try {
96 | const res = await fetch(url)
97 | if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.statusText}`)
98 |
99 | if (!existsSync(TMP_FOLDER_PREFIX)) await mkdir(TMP_FOLDER_PREFIX)
100 | const destination = resolve(TMP_FOLDER_PREFIX, fileName)
101 | if (existsSync(destination)) {
102 | await rm(destination, { recursive: true })
103 | }
104 |
105 | const fileStream = createWriteStream(destination, { flags: 'wx' })
106 | await finished(Readable.fromWeb(res.body).pipe(fileStream))
107 | return destination
108 | } catch (err) {
109 | console.error(failure(`[FAIL] Download: ${err.message}`))
110 | process.exit(1)
111 | }
112 | }
113 |
114 | async function extractFiles(entries, unzipStream) {
115 | const files = await Promise.all(
116 | Object.values(entries).map(async entry => {
117 | const outFile = join(TMP_FOLDER_PREFIX, 'out', entry.name)
118 | await mkdir(dirname(outFile), { recursive: true })
119 | await unzipStream.extract(entry, outFile)
120 | if (entry.isFile) {
121 | return outFile
122 | }
123 | })
124 | )
125 | return files.filter(Boolean)
126 | }
127 |
128 | async function copyFiles(files, targetDir, selectedTemplate) {
129 | const copyPromises = files.map(d => {
130 | const absolutePath = resolve(d)
131 | const dest = absolutePath.replace(
132 | join(
133 | process.cwd(),
134 | TMP_FOLDER_PREFIX,
135 | 'out',
136 | `${selectedTemplate.name}-${selectedTemplate.branch}`
137 | ),
138 | resolve(targetDir)
139 | )
140 | return copyFileWithLogging(absolutePath, dest)
141 | })
142 |
143 | await Promise.allSettled(copyPromises)
144 | }
145 |
146 | async function copyFileWithLogging(source, dest) {
147 | try {
148 | await mkdir(dirname(dest), { recursive: true })
149 | await copyFile(source, dest)
150 | console.log(`${k.gray('[Created]')} ${k.white(dest)}`)
151 | } catch (err) {
152 | console.log(failure(`[FAIL] Creation: ${dest}, ${err.message}`))
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/adex/runtime/handler.js:
--------------------------------------------------------------------------------
1 | import { CONSTANTS, emitToHooked } from 'adex/hook'
2 | import { prepareRequest, prepareResponse } from 'adex/http'
3 | import { toStatic } from 'adex/ssr'
4 | import { renderToStringAsync } from 'adex/utils/isomorphic'
5 | import { h } from 'preact'
6 |
7 | // @ts-expect-error injected by vite
8 | import { App } from 'virtual:adex:client'
9 |
10 | // @ts-expect-error injected by vite
11 | import { routes as apiRoutes } from '~apiRoutes'
12 | // @ts-expect-error injected by vite
13 | import { routes as pageRoutes } from '~routes'
14 |
15 | const html = String.raw
16 |
17 | export async function handler(req, res) {
18 | res.statusCode = 200
19 |
20 | prepareRequest(req)
21 | prepareResponse(res)
22 |
23 | const [url, search] = req.url.split('?')
24 | const baseURL = normalizeRequestUrl(url)
25 |
26 | const { metas, links, title, lang } = toStatic()
27 |
28 | if (baseURL.startsWith('/api') || baseURL.startsWith('api')) {
29 | const matchedInAPI = apiRoutes.find(d => {
30 | return d.regex.pattern.test(baseURL)
31 | })
32 | if (matchedInAPI) {
33 | const module = await matchedInAPI.module()
34 | const routeParams = getRouteParams(baseURL, matchedInAPI)
35 | req.params = routeParams
36 | const modifiableContext = {
37 | req: req,
38 | res: res,
39 | }
40 | await emitToHooked(CONSTANTS.beforeApiCall, modifiableContext)
41 | const handlerFn =
42 | 'default' in module ? module.default : (_, res) => res.end()
43 | const serverHandler = async (req, res) => {
44 | await handlerFn(req, res)
45 | await emitToHooked(CONSTANTS.afterApiCall, { req, res })
46 | }
47 | return {
48 | serverHandler,
49 | }
50 | }
51 | return {
52 | serverHandler: async (_, res) => {
53 | res.statusCode = 404
54 | res.end('Not found')
55 | await emitToHooked(CONSTANTS.afterApiCall, { req, res })
56 | },
57 | }
58 | }
59 |
60 | const matchedInPages = pageRoutes.find(d => {
61 | return d.regex.pattern.test(baseURL)
62 | })
63 |
64 | if (matchedInPages) {
65 | const routeParams = getRouteParams(baseURL, matchedInPages)
66 |
67 | // @ts-expect-error
68 | global.location = new URL(req.url, 'http://localhost')
69 |
70 | const modifiableContext = {
71 | req: req,
72 | }
73 | await emitToHooked(CONSTANTS.beforePageRender, modifiableContext)
74 |
75 | const rendered = await renderToStringAsync(
76 | h(App, { url: modifiableContext.req.url }),
77 | {}
78 | )
79 |
80 | let htmlString = HTMLTemplate({
81 | metas,
82 | links,
83 | title,
84 | lang,
85 | entryPage: matchedInPages.route,
86 | routeParams: Buffer.from(JSON.stringify(routeParams), 'utf8').toString(
87 | 'base64'
88 | ),
89 | body: rendered,
90 | })
91 |
92 | modifiableContext.html = htmlString
93 | await emitToHooked(CONSTANTS.afterPageRender, modifiableContext)
94 | htmlString = modifiableContext.html
95 | return {
96 | html: htmlString,
97 | pageRoute: matchedInPages.route,
98 | }
99 | }
100 |
101 | return {
102 | html: HTMLTemplate({
103 | metas,
104 | links,
105 | title,
106 | lang,
107 | body: '404 | Not Found',
108 | }),
109 | }
110 | }
111 |
112 | function HTMLTemplate({
113 | metas = [],
114 | links = [],
115 | title = '',
116 | lang = '',
117 | entryPage = '',
118 | routeParams = {},
119 | body = '',
120 | }) {
121 | const headString = stringify(title, metas, links)
122 | return html`
123 |
124 |
125 |
126 |
127 | ${headString}
128 |
129 |
130 | ${body}
131 |
132 |
133 | `
134 | }
135 |
136 | function getRouteParams(baseURL, matchedRoute) {
137 | const matchedParams = baseURL.match(matchedRoute.regex.pattern)
138 | const routeParams = regexToParams(matchedRoute, matchedParams)
139 | return routeParams
140 | }
141 |
142 | function regexToParams(matchedRoute, regexMatchGroup) {
143 | return matchedRoute.regex.keys.reduce((acc, key, index) => {
144 | acc[key] = regexMatchGroup[index + 1]
145 | return acc
146 | }, {})
147 | }
148 |
149 | const stringify = (title, metas, links) => {
150 | const stringifyTag = (tagName, tags) =>
151 | tags.reduce((acc, tag) => {
152 | return `${acc}<${tagName}${Object.keys(tag).reduce(
153 | (properties, key) => `${properties} ${key}="${tag[key]}"`,
154 | ''
155 | )}>`
156 | }, '')
157 |
158 | return `
159 | ${title}
160 |
161 | ${stringifyTag('meta', metas)}
162 | ${stringifyTag('link', links)}
163 | `
164 | }
165 |
166 | function normalizeRequestUrl(url) {
167 | return url.replace(/\/(index\.html)$/, '/')
168 | }
169 |
--------------------------------------------------------------------------------
/packages/adapters/node/lib/index.js:
--------------------------------------------------------------------------------
1 | import { existsSync } from 'node:fs'
2 | import http from 'node:http'
3 |
4 | import { sirv, useMiddleware } from 'adex/ssr'
5 |
6 | import { handler } from 'virtual:adex:handler'
7 |
8 | let islandMode = false
9 |
10 | function createHandler({ manifests, paths }) {
11 | const serverAssets = sirv(paths.assets, {
12 | maxAge: 31536000,
13 | immutable: true,
14 | onNoMatch: defaultHandler,
15 | })
16 |
17 | let islandsWereGenerated = existsSync(paths.islands)
18 |
19 | // @ts-ignore
20 | let islandAssets = (req, res, next) => {
21 | next()
22 | }
23 |
24 | if (islandsWereGenerated) {
25 | islandMode = true
26 | islandAssets = sirv(paths.islands, {
27 | maxAge: 31536000,
28 | immutable: true,
29 | onNoMatch: defaultHandler,
30 | })
31 | }
32 |
33 | let clientWasGenerated = existsSync(paths.client)
34 |
35 | // @ts-ignore
36 | let clientAssets = (req, res, next) => {
37 | next()
38 | }
39 |
40 | if (clientWasGenerated) {
41 | clientAssets = sirv(paths.client, {
42 | maxAge: 31536000,
43 | immutable: true,
44 | onNoMatch: defaultHandler,
45 | })
46 | }
47 |
48 | async function defaultHandler(req, res) {
49 | const { html: template, pageRoute, serverHandler } = await handler(req, res)
50 | if (serverHandler) {
51 | return serverHandler(req, res)
52 | }
53 |
54 | const templateWithDeps = addDependencyAssets(
55 | template,
56 | pageRoute,
57 | manifests.server,
58 | manifests.client
59 | )
60 |
61 | const finalHTML = templateWithDeps
62 | res.setHeader('content-type', 'text/html')
63 | res.write(finalHTML)
64 | res.end()
65 | }
66 |
67 | return useMiddleware(
68 | async (req, res, next) => {
69 | // @ts-expect-error shared-state between the middlewares
70 | req.__originalUrl = req.url
71 | // @ts-expect-error shared-state between the middlewares
72 | req.url = req.__originalUrl.replace(/(\/?assets\/?)/, '/')
73 | return serverAssets(req, res, next)
74 | },
75 | async (req, res, next) => {
76 | // @ts-expect-error shared-state between the middlewares
77 | req.url = req.__originalUrl.replace(/(\/?islands\/?)/, '/')
78 | return islandAssets(req, res, next)
79 | },
80 | async (req, res, next) => {
81 | return clientAssets(req, res, next)
82 | },
83 | async (req, res) => {
84 | // @ts-expect-error shared-state between the middlewares
85 | req.url = req.__originalUrl
86 | return defaultHandler(req, res)
87 | }
88 | )
89 | }
90 |
91 | // function parseManifest(manifestString) {
92 | // try {
93 | // const manifestJSON = JSON.parse(manifestString)
94 | // return manifestJSON
95 | // } catch (err) {
96 | // return {}
97 | // }
98 | // }
99 |
100 | // function getServerManifest() {
101 | // const manifestPath = join(__dirname, 'manifest.json')
102 | // if (existsSync(manifestPath)) {
103 | // const manifestFile = readFileSync(manifestPath, 'utf8')
104 | // return parseManifest(manifestFile)
105 | // }
106 | // return {}
107 | // }
108 |
109 | // function getClientManifest() {
110 | // const manifestPath = join(__dirname, '../client/manifest.json')
111 | // if (existsSync(manifestPath)) {
112 | // const manifestFile = readFileSync(manifestPath, 'utf8')
113 | // return parseManifest(manifestFile)
114 | // }
115 | // return {}
116 | // }
117 |
118 | function manifestToHTML(manifest, filePath) {
119 | let links = []
120 | let scripts = []
121 |
122 | // TODO: move it up the chain
123 | const rootServerFile = 'virtual:adex:server'
124 | // if root manifest, also add it's css imports in
125 | if (manifest[rootServerFile]) {
126 | const graph = manifest[rootServerFile]
127 | links = links.concat(
128 | (graph.css || []).map(
129 | d =>
130 | ``
134 | )
135 | )
136 | }
137 |
138 | // TODO: move it up the chain
139 | const rootClientFile = 'virtual:adex:client'
140 | // if root manifest, also add it's css imports in
141 | if (!islandMode && manifest[rootClientFile]) {
142 | const graph = manifest[rootClientFile]
143 | links = links.concat(
144 | (graph.css || []).map(
145 | d =>
146 | ``
150 | )
151 | )
152 | }
153 |
154 | if (manifest[filePath]) {
155 | const graph = manifest[filePath]
156 | links = links.concat(
157 | (graph.css || []).map(
158 | d =>
159 | ``
163 | )
164 | )
165 | const depsHasCSS = (manifest[filePath].imports || [])
166 | .map(d => manifest[d])
167 | .filter(d => d.css?.length)
168 |
169 | if (depsHasCSS.length) {
170 | links = links.concat(
171 | depsHasCSS.map(d =>
172 | d.css
173 | .map(
174 | p =>
175 | ``
179 | )
180 | .join('\n')
181 | )
182 | )
183 | }
184 |
185 | scripts = scripts.concat(
186 | ``
187 | )
188 | }
189 | return {
190 | scripts,
191 | links,
192 | }
193 | }
194 |
195 | function addDependencyAssets(
196 | template,
197 | pageRoute,
198 | serverManifest,
199 | clientManifest
200 | ) {
201 | if (!pageRoute) {
202 | return template
203 | }
204 |
205 | const filePath = pageRoute.startsWith('/') ? pageRoute.slice(1) : pageRoute
206 |
207 | const { links: serverLinks } = manifestToHTML(serverManifest, filePath)
208 |
209 | const { links: clientLinks, scripts: clientScripts } = manifestToHTML(
210 | clientManifest,
211 | filePath
212 | )
213 |
214 | const links = [...serverLinks, ...clientLinks]
215 | const scripts = [...clientScripts]
216 |
217 | return template.replace(
218 | '',
219 | links.join('\n') + scripts.join('\n') + ''
220 | )
221 | }
222 |
223 | export const createServer = ({
224 | port = '3000',
225 | host = '127.0.0.1',
226 | adex = {
227 | manifests: {
228 | server: {},
229 | client: {},
230 | },
231 | paths: {},
232 | },
233 | } = {}) => {
234 | const handler = createHandler(adex)
235 | const server = http.createServer(handler)
236 |
237 | return {
238 | run() {
239 | return server.listen(port, host, () => {
240 | console.log(`Listening on ${host}:${port}`)
241 | })
242 | },
243 | fetch: undefined,
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/adex/src/vite.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_TRANSPILED_IDENTIFIERS,
3 | IMPORT_PATH_PLACEHOLDER,
4 | findIslands,
5 | generateClientTemplate,
6 | getIslandName,
7 | getServerTemplatePlaceholder,
8 | injectIslandAST,
9 | isFunctionIsland,
10 | readSourceFile,
11 | } from '@dumbjs/preland'
12 | import { addImportToAST, codeFromAST } from '@dumbjs/preland/ast'
13 | import preact from '@preact/preset-vite'
14 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
15 | import { readFile, rm } from 'fs/promises'
16 | import { dirname, join, resolve } from 'path'
17 | import { fileURLToPath } from 'url'
18 | import { build, mergeConfig } from 'vite'
19 | import { fonts as addFontsPlugin } from './fonts.js'
20 |
21 | const __dirname = dirname(fileURLToPath(import.meta.url))
22 | const cwd = process.cwd()
23 | const islandsDir = join(cwd, '.islands')
24 | let runningIslandBuild = false
25 |
26 | const adapterMap = {
27 | node: 'adex-adapter-node',
28 | }
29 |
30 | /**
31 | * @param {import("./vite.js").AdexOptions} [options]
32 | * @returns {(import("vite").Plugin)[]}
33 | */
34 | export function adex({
35 | fonts,
36 | islands = false,
37 | ssr = true,
38 | adapter: adapter = 'node',
39 | } = {}) {
40 | // @ts-expect-error probably because of the `.filter`
41 | return [
42 | preactPages({
43 | root: '/src/pages',
44 | id: '~routes',
45 | }),
46 | preactPages({
47 | root: '/src/api',
48 | id: '~apiRoutes',
49 | replacer: '/api',
50 | }),
51 | createUserDefaultVirtualModule(
52 | 'virtual:adex:global.css',
53 | '',
54 | 'src/global.css'
55 | ),
56 | createUserDefaultVirtualModule(
57 | 'virtual:adex:client',
58 | readFileSync(join(__dirname, '../runtime/client.js'), 'utf8'),
59 | '/src/_app'
60 | ),
61 | createVirtualModule(
62 | 'adex/app',
63 | readFileSync(join(__dirname, '../runtime/app.js'), 'utf8')
64 | ),
65 | createVirtualModule(
66 | 'virtual:adex:handler',
67 | readFileSync(join(__dirname, '../runtime/handler.js'), 'utf8')
68 | ),
69 | createVirtualModule(
70 | 'virtual:adex:server',
71 | `import { createServer } from '${adapterMap[adapter]}'
72 | import { dirname, join } from 'node:path'
73 | import { fileURLToPath } from 'node:url'
74 | import { existsSync, readFileSync } from 'node:fs'
75 | import { env } from 'adex/env'
76 |
77 | import 'virtual:adex:font.css'
78 | import 'virtual:adex:global.css'
79 |
80 | const __dirname = dirname(fileURLToPath(import.meta.url))
81 |
82 | const PORT = parseInt(env.get('PORT', '3000'), 10)
83 | const HOST = env.get('HOST', 'localhost')
84 |
85 | const paths = {
86 | assets: join(__dirname, './assets'),
87 | islands: join(__dirname, './islands'),
88 | client: join(__dirname, '../client'),
89 | }
90 |
91 | function getServerManifest() {
92 | const manifestPath = join(__dirname, 'manifest.json')
93 | if (existsSync(manifestPath)) {
94 | const manifestFile = readFileSync(manifestPath, 'utf8')
95 | return parseManifest(manifestFile)
96 | }
97 | return {}
98 | }
99 |
100 | function getClientManifest() {
101 | const manifestPath = join(__dirname, '../client/manifest.json')
102 | if (existsSync(manifestPath)) {
103 | const manifestFile = readFileSync(manifestPath, 'utf8')
104 | return parseManifest(manifestFile)
105 | }
106 | return {}
107 | }
108 |
109 | function parseManifest(manifestString) {
110 | try {
111 | const manifestJSON = JSON.parse(manifestString)
112 | return manifestJSON
113 | } catch (err) {
114 | return {}
115 | }
116 | }
117 |
118 | const server = createServer({
119 | port: PORT,
120 | host: HOST,
121 | adex:{
122 | manifests:{server:getServerManifest(),client:getClientManifest()},
123 | paths,
124 | }
125 | })
126 |
127 | if ('run' in server) {
128 | server.run()
129 | }
130 |
131 | export default server.fetch
132 | `
133 | ),
134 | addFontsPlugin(fonts),
135 | adexDevServer({ islands }),
136 | adexBuildPrep({ islands }),
137 | adexClientBuilder({ ssr, islands }),
138 | islands && adexIslandsBuilder(),
139 |
140 | // SSR/Render Server Specific plugins
141 | ssr && adexServerBuilder({ fonts, adapter, islands }),
142 | ].filter(Boolean)
143 | }
144 |
145 | /**
146 | * @returns {import("vite").Plugin}
147 | */
148 | function adexClientBuilder({ ssr = true, islands = false } = {}) {
149 | let baseUrl = '/'
150 | return {
151 | name: 'adex-client-builder',
152 | config(cfg) {
153 | const out = cfg.build.outDir ?? 'dist'
154 | return {
155 | appType: 'custom',
156 | build: {
157 | write: !islands,
158 | manifest: 'manifest.json',
159 | outDir: join(out, 'client'),
160 | rollupOptions: {
161 | input: 'virtual:adex:client',
162 | },
163 | output: {
164 | entryFileNames: '[name]-[hash].js',
165 | format: 'esm',
166 | },
167 | },
168 | }
169 | },
170 | configResolved(cfg) {
171 | baseUrl = cfg.base
172 | return
173 | },
174 | generateBundle(opts, bundle) {
175 | let clientEntryPath
176 | for (const key in bundle) {
177 | if (
178 | ['_virtual_adex_client', '_app'].includes(bundle[key].name) &&
179 | 'isEntry' in bundle[key] &&
180 | bundle[key].isEntry
181 | ) {
182 | clientEntryPath = key
183 | }
184 | }
185 |
186 | const links = [
187 | // @ts-expect-error invalid types by vite? figure this out
188 | ...(bundle[clientEntryPath]?.viteMetadata?.importedCss ?? new Set()),
189 | ].map(d => {
190 | return ``
191 | })
192 |
193 | if (!ssr) {
194 | this.emitFile({
195 | type: 'asset',
196 | fileName: 'index.html',
197 | source: `
198 |
199 | ${links.join('\n')}
200 |
201 |
202 |
203 |
204 |
205 | `,
206 | })
207 | }
208 | },
209 | }
210 | }
211 |
212 | /**
213 | * @returns {import("vite").Plugin}
214 | */
215 | function adexBuildPrep({ islands = false }) {
216 | return {
217 | name: 'adex-build-prep',
218 | apply: 'build',
219 | async configResolved(config) {
220 | if (!islands) return
221 |
222 | // Making it order safe
223 | const outDirNormalized = config.build.outDir.endsWith('/server')
224 | ? dirname(config.build.outDir)
225 | : config.build.outDir
226 |
227 | // remove the `client` dir if islands are on,
228 | // we don't generate the client assets and
229 | // their existence adds the client entry into the bundle
230 | const clientDir = join(outDirNormalized, 'client')
231 | if (!existsSync(clientDir)) return
232 | await rm(clientDir, {
233 | recursive: true,
234 | force: true,
235 | })
236 | },
237 | }
238 | }
239 |
240 | /**
241 | * @returns {import("vite").Plugin[]}
242 | */
243 | function adexIslandsBuilder() {
244 | const clientVirtuals = {}
245 | let isBuild = false
246 | let outDir
247 | return [
248 | {
249 | name: 'adex-islands',
250 | enforce: 'pre',
251 | config(d, e) {
252 | outDir = d.build?.outDir ?? 'dist'
253 | isBuild = e.command === 'build'
254 | },
255 | transform(code, id, viteEnv) {
256 | if (!/\.(js|ts)x$/.test(id)) return
257 |
258 | // if being imported by the client, don't send
259 | // back the transformed server code, send the
260 | // original component
261 | if (!viteEnv?.ssr) return
262 |
263 | const islands = findIslands(readSourceFile(id), {
264 | isFunctionIsland: node =>
265 | isFunctionIsland(node, {
266 | transpiledIdentifiers:
267 | DEFAULT_TRANSPILED_IDENTIFIERS.concat('_jsxDEV'),
268 | }),
269 | })
270 | if (!islands.length) return
271 |
272 | islands.forEach(node => {
273 | //@ts-expect-error FIX: in preland
274 | injectIslandAST(node.ast, node)
275 | const clientCode = generateClientTemplate(node.id).replace(
276 | IMPORT_PATH_PLACEHOLDER,
277 | id
278 | )
279 |
280 | mkdirSync(islandsDir, { recursive: true })
281 | writeFileSync(
282 | join(islandsDir, getIslandName(node.id) + '.js'),
283 | clientCode,
284 | 'utf8'
285 | )
286 |
287 | clientVirtuals[node.id] = clientCode
288 | })
289 |
290 | const addImport = addImportToAST(islands[0].ast)
291 | addImport('h', 'preact', { named: true })
292 | addImport('Fragment', 'preact', { named: true })
293 |
294 | let serverTemplateCode = codeFromAST(islands[0].ast)
295 | islands.forEach(island => {
296 | serverTemplateCode = serverTemplateCode.replace(
297 | getServerTemplatePlaceholder(island.id),
298 | !isBuild
299 | ? `/virtual:adex:island-${island.id}`
300 | : `/islands/${getIslandName(island.id) + '.js'}`
301 | )
302 | })
303 |
304 | return {
305 | code: serverTemplateCode,
306 | }
307 | },
308 | },
309 | {
310 | name: 'adex-island-builds',
311 | enforce: 'post',
312 | writeBundle: {
313 | sequential: true,
314 | async handler() {
315 | if (Object.keys(clientVirtuals).length === 0) return
316 | if (runningIslandBuild) return
317 |
318 | await build(
319 | mergeConfig(
320 | {},
321 | {
322 | configFile: false,
323 | plugins: [preact()],
324 | build: {
325 | ssr: false,
326 | outDir: join(outDir, 'islands'),
327 | emptyOutDir: true,
328 | rollupOptions: {
329 | output: {
330 | format: 'esm',
331 | entryFileNames: '[name].js',
332 | },
333 | input: Object.fromEntries(
334 | Object.entries(clientVirtuals).map(([k, v]) => {
335 | const key = getIslandName(k)
336 | return [key, join(islandsDir, key + '.js')]
337 | })
338 | ),
339 | },
340 | },
341 | }
342 | )
343 | )
344 | },
345 | },
346 | },
347 | {
348 | name: 'adex-island-virtuals',
349 | resolveId(id) {
350 | if (
351 | id.startsWith('virtual:adex:island') ||
352 | id.startsWith('/virtual:adex:island')
353 | ) {
354 | return `\0${id}`
355 | }
356 | },
357 | load(id) {
358 | if (
359 | (id.startsWith('\0') && id.startsWith('\0virtual:adex:island')) ||
360 | id.startsWith('\0/virtual:adex:island')
361 | ) {
362 | const compName = id
363 | .replace('\0', '')
364 | .replace(/\/?(virtual\:adex\:island\-)/, '')
365 |
366 | if (clientVirtuals[compName]) {
367 | return {
368 | code: clientVirtuals[compName],
369 | }
370 | }
371 | }
372 | },
373 | },
374 | ]
375 | }
376 |
377 | /**
378 | * @returns {import("vite").Plugin}
379 | */
380 | export function createVirtualModule(id, content) {
381 | return {
382 | name: `adex-virtual-${id}`,
383 | enforce: 'pre',
384 | resolveId(requestId) {
385 | if (requestId === id || requestId === '/' + id) {
386 | return `\0${id}`
387 | }
388 | },
389 | load(requestId) {
390 | if (requestId === `\0${id}`) {
391 | return content
392 | }
393 | },
394 | }
395 | }
396 |
397 | /**
398 | * @returns {import("vite").Plugin}
399 | */
400 | function createUserDefaultVirtualModule(id, content, userPath) {
401 | return {
402 | name: `adex-virtual-user-default-${id}`,
403 | enforce: 'pre',
404 | async resolveId(requestId) {
405 | if (
406 | requestId === id ||
407 | requestId === '/' + id ||
408 | requestId === userPath
409 | ) {
410 | const userPathResolved = await this.resolve(userPath)
411 | return userPathResolved ?? `\0${id}`
412 | }
413 | },
414 | load(requestId) {
415 | if (requestId === `\0${id}`) {
416 | return content
417 | }
418 | },
419 | }
420 | }
421 |
422 | /**
423 | * @param {object} options
424 | * @param {import('vite').UserConfig} options.config
425 | * @returns {import("vite").Plugin}
426 | */
427 | function adexClientSSRBuilder(opts) {
428 | let options
429 | return {
430 | name: 'adex-client',
431 | enforce: 'post',
432 | config(conf) {
433 | return {
434 | appType: 'custom',
435 | build: {
436 | outDir: join(conf.build.outDir ?? 'dist', 'client'),
437 | emptyOutDir: true,
438 | ssr: false,
439 | manifest: 'manifest.json',
440 | rollupOptions: {
441 | input: {
442 | index: 'virtual:adex:client',
443 | },
444 | output: {
445 | entryFileNames: '[name]-[hash].js',
446 | format: 'esm',
447 | },
448 | },
449 | },
450 | }
451 | },
452 | configResolved(config) {
453 | options = config
454 | },
455 | closeBundle() {
456 | // process.nextTick(async () => {
457 | // const usablePlugins = options.plugins
458 | // .filter(d => !d.name.startsWith('vite:'))
459 | // .filter(d => !d.name.startsWith('adex-') || d.name === 'adex-fonts')
460 | // await build(
461 | // mergeConfig(opts, {
462 | // plugins: [
463 | // ...usablePlugins,
464 | // createVirtualModule(
465 | // 'virtual:adex:client',
466 | // readFileSync(join(__dirname, '../runtime/client.js'), 'utf8')
467 | // ),
468 | // createUserDefaultVirtualModule(
469 | // 'virtual:adex:index.html',
470 | // '',
471 | // 'src/index.html'
472 | // ),
473 | // preact({ prefreshEnabled: false }),
474 | // ],
475 | // build: {
476 | // outDir: 'dist/client',
477 | // emptyOutDir: true,
478 | // ssr: false,
479 | // manifest: 'manifest.json',
480 | // rollupOptions: {
481 | // input: {
482 | // index: 'virtual:adex:client',
483 | // },
484 | // output: {
485 | // entryFileNames: '[name]-[hash].js',
486 | // format: 'esm',
487 | // },
488 | // },
489 | // },
490 | // })
491 | // )
492 | // })
493 | },
494 | }
495 | }
496 |
497 | /**
498 | * @returns {import("vite").Plugin}
499 | */
500 | function adexDevServer({ islands = false } = {}) {
501 | const devCSSMap = new Map()
502 | let cfg
503 | return {
504 | name: 'adex-dev-server',
505 | apply: 'serve',
506 | enforce: 'pre',
507 | config() {
508 | return {
509 | ssr: {
510 | noExternal: ['adex/app'],
511 | },
512 | }
513 | },
514 | configResolved(_cfg) {
515 | cfg = _cfg
516 | },
517 | async resolveId(id, importer, meta) {
518 | if (id.endsWith('.css')) {
519 | if (!importer) return
520 | const importerFromRoot = importer.replace(resolve(cfg.root), '')
521 | const resolvedCss = await this.resolve(id, importer, meta)
522 | if (resolvedCss) {
523 | devCSSMap.set(
524 | importerFromRoot,
525 | (devCSSMap.get(importer) ?? []).concat(resolvedCss.id)
526 | )
527 | }
528 | return
529 | }
530 | },
531 | configureServer(server) {
532 | return () => {
533 | server.middlewares.use(async function (req, res, next) {
534 | const module = await server.ssrLoadModule('virtual:adex:handler')
535 | if (!module) {
536 | return next()
537 | }
538 | try {
539 | const { html, serverHandler, pageRoute } = await module.handler(
540 | req,
541 | res
542 | )
543 | if (serverHandler) {
544 | return serverHandler(req, res)
545 | }
546 | const cssLinks = devCSSMap.get(pageRoute) ?? []
547 | let renderedHTML = html.replace(
548 | '',
549 | `
550 |
551 | ${cssLinks.map(d => {
552 | return ``
553 | })}
554 |
555 | `
556 | )
557 | if (!islands) {
558 | renderedHTML = html.replace(
559 | '