@davaux/multisite 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md ADDED
@@ -0,0 +1,133 @@
1
+ <!-- pka-generated -->
2
+ # @davaux/multisite
3
+
4
+ > Generated by Project Knowledge Analyzer on 2026-06-06T21:53:10.370Z
5
+
6
+ ## Overview
7
+
8
+ Multi-site hosting for Davaux — run multiple sites from a single codebase
9
+
10
+ **Version**: 0.8.0
11
+ **Author**: David L Dyess II
12
+ **License**: MIT
13
+ **Repository**: https://codeberg.org/davaux/davaux#readme
14
+
15
+ ## Tech Stack
16
+
17
+ - **Language**: TypeScript
18
+ - **Module System**: ESM (`type: module`)
19
+ - **Build Tool**: esbuild
20
+
21
+ ## Commands
22
+
23
+ - `npm run build` — tsc
24
+ - `npm run typecheck` — tsc --noEmit
25
+ - `npm run test` — node --import tsx/esm --test 'src/test/**/*.test.ts'
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ ├── README.md
31
+ ├── package.json
32
+ ├── tsconfig.json
33
+ └── src/
34
+ ├── test/
35
+ │ ├── fixtures/
36
+ │ │ ├── base/
37
+ │ │ │ └── routes/
38
+ │ │ │ ├── _layout.ts
39
+ │ │ │ ├── about.page.ts
40
+ │ │ │ └── index.page.ts
41
+ │ │ ├── site-a/
42
+ │ │ │ └── routes/
43
+ │ │ │ ├── _layout.ts
44
+ │ │ │ ├── _middleware.ts
45
+ │ │ │ ├── config.page.ts
46
+ │ │ │ ├── index.page.ts
47
+ │ │ │ ├── shop.page.ts
48
+ │ │ │ └── state.page.ts
49
+ │ │ └── site-b/
50
+ │ │ └── routes/
51
+ │ │ ├── _error.ts
52
+ │ │ └── about.page.ts
53
+ │ └── multisite.test.ts
54
+ └── index.ts
55
+
56
+ ```
57
+
58
+ ## Entry Points
59
+
60
+ - `src/index.ts`
61
+
62
+ ## Files by Type
63
+
64
+ ### Documentation (1)
65
+ - `README.md`
66
+
67
+ ### Config (2)
68
+ - `package.json`
69
+ - `tsconfig.json`
70
+
71
+ ### Module (1)
72
+ - `src/index.ts`
73
+
74
+
75
+ ## Git
76
+ - **Branch**: main
77
+ - **Last Commit**: chore: Add alpha status note to README
78
+ - **Author**: David Dyess II
79
+ - **Date**: 2026-06-06 15:52:27 -0600
80
+ - **Remote**: https://codeberg.org/davaux/davaux.git
81
+
82
+ ### Recent Commits
83
+ ```
84
+ f527031 chore: Add alpha status note to README
85
+ 90c819e chore: Add repo info to package.json files
86
+ b200d9d feat(davaux)!: Add OmlCacheConfig - opt-in with includes option or opt-out with excludes option; Fix OML implementation to follow OML spec - use output instead of return
87
+ 3bce0c2 chore: Update and add READMEs to packages; update ROADMAP
88
+ fc27c20 fix(davaux): Remove old dist folder on new builds; fix server port per DavauxConfig
89
+ c760659 feat(davaux): Add minify CSS in production builds
90
+ c899ab8 fix(davaux): Added method JS and JSX extenstion to scanner
91
+ 7d99f04 feat(davaux): Add support for declarative partial updates
92
+ 3b47f37 chore: Bump package versions to 0.8.0
93
+ aa0460f chore: Add CHANGELOG.md
94
+ ```
95
+
96
+ ## Dependencies
97
+
98
+
99
+ ### Development
100
+ @types/node, davaux, esbuild, tsx, typescript
101
+
102
+
103
+
104
+
105
+
106
+ ## Tests (12)
107
+
108
+ ### Test Suites (1)
109
+ - `src/test/multisite.test.ts`
110
+
111
+ ### Fixtures (11)
112
+ - `src/test/fixtures/base/routes/_layout.ts`
113
+ - `src/test/fixtures/base/routes/about.page.ts`
114
+ - `src/test/fixtures/base/routes/index.page.ts`
115
+ - `src/test/fixtures/site-a/routes/_layout.ts`
116
+ - `src/test/fixtures/site-a/routes/_middleware.ts`
117
+ - `src/test/fixtures/site-a/routes/config.page.ts`
118
+ - `src/test/fixtures/site-a/routes/index.page.ts`
119
+ - `src/test/fixtures/site-a/routes/shop.page.ts`
120
+ - `src/test/fixtures/site-a/routes/state.page.ts`
121
+ - `src/test/fixtures/site-b/routes/_error.ts`
122
+ - `src/test/fixtures/site-b/routes/about.page.ts`
123
+
124
+
125
+
126
+ ## Import Graph
127
+
128
+ ## Exported Symbols
129
+
130
+ **`src/index.ts`**
131
+ `mergeScanResults`, `defineSites`, `buildMultisiteApps`, `startMultisiteServer`, `dispatchToSite`, `startMultisiteDev`, `startMultisite`, `getSite`, `SiteDefinition`, `MultisiteConfig`, `BuildOptions`, `ServerOptions`, `StartMultisiteOptions`
132
+
133
+
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @davaux/multisite
2
+
3
+ Multi-site hosting for Davaux — run multiple sites from a single process, dispatched by `Host` header.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @davaux/multisite
9
+ ```
10
+
11
+ ## Basic setup
12
+
13
+ ```ts
14
+ // server.ts
15
+ import { startMultisite } from '@davaux/multisite'
16
+
17
+ startMultisite({
18
+ sites: [
19
+ {
20
+ name: 'main',
21
+ hostname: 'example.com',
22
+ routesDir: './src/routes/main',
23
+ },
24
+ {
25
+ name: 'blog',
26
+ hostname: 'blog.example.com',
27
+ routesDir: './src/routes/blog',
28
+ },
29
+ ],
30
+ }, { port: 3000, cwd: import.meta.dirname })
31
+ ```
32
+
33
+ `startMultisite` auto-detects `NODE_ENV` — dev mode with file watching and live reload when `NODE_ENV !== 'production'`, production dispatch otherwise. Pass `cwd: import.meta.dirname` from your `server.ts` for reliable path resolution.
34
+
35
+ ## Shared base + per-site overlay
36
+
37
+ Use `baseDir` for routes shared across all sites. Each site's `routesDir` overlays on top — when both define the same URL pattern and type, the site-specific route wins:
38
+
39
+ ```ts
40
+ startMultisite({
41
+ baseDir: './src/routes/base',
42
+ islandsDir: './src/islands/base',
43
+ publicDir: './public',
44
+ sites: [
45
+ { name: 'tenant-a', hostname: 'a.example.com', routesDir: './src/routes/tenant-a' },
46
+ { name: 'tenant-b', hostname: 'b.example.com', routesDir: './src/routes/tenant-b' },
47
+ { name: 'fallback', hostname: '*' },
48
+ ],
49
+ }, { cwd: import.meta.dirname })
50
+ ```
51
+
52
+ `'*'` as `hostname` is a catch-all for any host not explicitly registered.
53
+
54
+ ## Per-site config
55
+
56
+ Attach arbitrary data to each site via `SiteDefinition.config`. Access it in any handler, layout, or middleware via `getSite<T>(ctx)`:
57
+
58
+ ```ts
59
+ import { getSite } from '@davaux/multisite'
60
+
61
+ // In server.ts:
62
+ startMultisite({
63
+ sites: [
64
+ { name: 'acme', hostname: 'acme.example.com', config: { theme: 'blue', name: 'Acme' } },
65
+ { name: 'globex', hostname: 'globex.example.com', config: { theme: 'red', name: 'Globex' } },
66
+ ],
67
+ })
68
+
69
+ // In any route file:
70
+ export default definePage((ctx) => {
71
+ const site = getSite<{ theme: string; name: string }>(ctx)
72
+ return <h1>Welcome to {site?.name}</h1>
73
+ })
74
+ ```
75
+
76
+ `getSite` returns `undefined` when called outside a multisite server (e.g. in tests).
77
+
78
+ ## `SiteDefinition` options
79
+
80
+ | Option | Type | Description |
81
+ |---|---|---|
82
+ | `name` | `string` | Unique site identifier used in logging and asset paths |
83
+ | `hostname` | `string \| string[]` | `Host` header value(s) that route to this site. `'*'` is a catch-all |
84
+ | `routesDir` | `string?` | Site-specific routes directory, overlaid on `baseDir` |
85
+ | `islandsDir` | `string?` | Site-specific islands directory, merged with shared `islandsDir` |
86
+ | `publicDir` | `string?` | Site-specific static files directory (takes priority over shared `publicDir`) |
87
+ | `clientEntry` | `string?` | Site-specific client bundle entry. Overrides shared `clientEntry` |
88
+ | `config` | `T?` | Arbitrary per-site data, accessible via `getSite<T>(ctx)` |
89
+
90
+ ## `MultisiteConfig` options
91
+
92
+ | Option | Type | Description |
93
+ |---|---|---|
94
+ | `sites` | `SiteDefinition[]` | Site definitions (required) |
95
+ | `baseDir` | `string?` | Shared base routes directory |
96
+ | `islandsDir` | `string?` | Shared islands directory — included in every site's client bundle |
97
+ | `publicDir` | `string?` | Shared static files directory |
98
+ | `clientEntry` | `string?` | Shared client bundle entry compiled to `/_davaux/client.js` |
99
+
100
+ ## Production build
101
+
102
+ ```ts
103
+ // build.ts
104
+ import { buildMultisite } from '@davaux/multisite/build'
105
+
106
+ await buildMultisite({
107
+ sites: [
108
+ { name: 'main', hostname: 'example.com', routesDir: './src/routes/main' },
109
+ ],
110
+ }, { cwd: import.meta.dirname })
111
+ ```
112
+
113
+ Compiles all route files, per-site island bundles, and `server.ts` / `multisite.config.ts` to `dist/`. Run with `node dist/server.js`.
114
+
115
+ ## Advanced: embedding in a custom server
116
+
117
+ ```ts
118
+ import { buildMultisiteApps, dispatchToSite } from '@davaux/multisite'
119
+ import { createServer } from 'node:http'
120
+
121
+ const apps = await buildMultisiteApps(config, { cwd: import.meta.dirname })
122
+
123
+ const server = createServer(async (req, res) => {
124
+ const handled = await dispatchToSite(apps, req, res)
125
+ if (!handled) { res.writeHead(404); res.end('No site') }
126
+ })
127
+ ```
128
+
129
+ ## TypeScript
130
+
131
+ Use `defineSites<T>` to get type inference on `SiteDefinition.config` without a type annotation at every call site:
132
+
133
+ ```ts
134
+ import { defineSites } from '@davaux/multisite'
135
+
136
+ interface SiteConfig { theme: string }
137
+
138
+ export const sites = defineSites<SiteConfig>({
139
+ sites: [
140
+ { name: 'acme', hostname: 'acme.example.com', config: { theme: 'blue' } },
141
+ ],
142
+ })
143
+ ```
144
+
145
+ ## Documentation
146
+
147
+ [davaux.codeberg.page/docs/multisite](https://davaux.codeberg.page/docs/multisite)
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@davaux/multisite",
3
+ "version": "0.8.0",
4
+ "description": "Multi-site hosting for Davaux — run multiple sites from a single codebase",
5
+ "type": "module",
6
+ "author": "David L Dyess II",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://codeberg.org/davaux/davaux"
11
+ },
12
+ "bugs": {
13
+ "url": "https://codeberg.org/davaux/davaux/issues"
14
+ },
15
+ "homepage": "https://codeberg.org/davaux/davaux#readme",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./build": {
22
+ "import": "./dist/build.js",
23
+ "types": "./dist/build.d.ts"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "node --import tsx/esm --test 'src/test/**/*.test.ts'"
30
+ },
31
+ "peerDependencies": {
32
+ "davaux": ">=0.8.0",
33
+ "esbuild": ">=0.17.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.0.0",
37
+ "davaux": "*",
38
+ "esbuild": "^0.28.0",
39
+ "tsx": "^4.22.3",
40
+ "typescript": "^6.0.3"
41
+ }
42
+ }
package/src/build.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import { collectCss, generateIslandsEntry, islandServerPlugin } from 'davaux/build'
4
+ import {
5
+ collectEsbuildPlugins,
6
+ collectScannerSuffixes,
7
+ type DavauxPlugin,
8
+ pathsToAlias,
9
+ } from 'davaux/config'
10
+ import { scanIslands, scanRoutes } from 'davaux/scanner'
11
+ import type { MultisiteConfig } from './index.js'
12
+
13
+ export interface BuildMultisiteOptions {
14
+ /** Project root directory. Defaults to `process.cwd()`. */
15
+ cwd?: string
16
+ /** Output directory. Defaults to `{cwd}/dist`. */
17
+ outDir?: string
18
+ /** Extra packages to mark as external in addition to `node:*`, `davaux`, and `@davaux/multisite`. */
19
+ external?: string[]
20
+ /**
21
+ * Davaux plugins to apply during the build — contributes esbuild transforms and scanner
22
+ * suffix extensions. Use the same plugins here as in your `davaux.config.ts`.
23
+ *
24
+ * @example
25
+ * import { mdx } from '@davaux/mdx'
26
+ * await buildMultisite(sites, { cwd: import.meta.dirname, plugins: [mdx()] })
27
+ */
28
+ plugins?: DavauxPlugin[]
29
+ /**
30
+ * tsconfig-style path aliases forwarded to esbuild `alias`. Same format as
31
+ * `compilerOptions.paths` — e.g. `{ '~/*': ['./src/*'] }`.
32
+ */
33
+ paths?: Record<string, string>
34
+ }
35
+
36
+ /**
37
+ * Compile a multisite project to JavaScript.
38
+ *
39
+ * Reads route and island directories directly from the `MultisiteConfig` so the
40
+ * config is the single source of truth. Discovers and compiles all route, layout,
41
+ * middleware, and error-page files, per-site client island bundles, plus
42
+ * `server.ts` / `multisite.config.ts` if they exist at the project root.
43
+ *
44
+ * Requires `esbuild` to be installed (it is present in any project that
45
+ * depends on `davaux`).
46
+ *
47
+ * @example
48
+ * // build.ts
49
+ * import { buildMultisite } from '@davaux/multisite/build'
50
+ * import { sites } from './multisite.config.js'
51
+ *
52
+ * await buildMultisite(sites, { cwd: import.meta.dirname })
53
+ */
54
+ export async function buildMultisite<T>(
55
+ config: MultisiteConfig<T>,
56
+ options: BuildMultisiteOptions = {},
57
+ ): Promise<void> {
58
+ const { build } = await import('esbuild')
59
+ const cwd = options.cwd ?? process.cwd()
60
+ const outDir = options.outDir ?? resolve(cwd, 'dist')
61
+ const davauxPlugins = options.plugins ?? []
62
+ const extraSuffixes = [...(config.extraSuffixes ?? []), ...collectScannerSuffixes(davauxPlugins)]
63
+ const userAlias = pathsToAlias(options.paths ?? {})
64
+
65
+ // All island directories (for the server-side island plugin)
66
+ const allIslandDirs: string[] = [
67
+ ...(config.islandsDir ? [config.islandsDir] : []),
68
+ ...config.sites.flatMap((s) => (s.islandsDir ? [s.islandsDir] : [])),
69
+ ]
70
+
71
+ // Collect route directories directly from config — no re-discovery needed
72
+ const routeDirs: string[] = []
73
+ if (config.baseDir) routeDirs.push(config.baseDir)
74
+ for (const site of config.sites) {
75
+ if (site.routesDir) routeDirs.push(site.routesDir)
76
+ }
77
+
78
+ // Scan for compilable files in each routes directory
79
+ const routeFiles: string[] = []
80
+ for (const dir of routeDirs) {
81
+ if (!existsSync(dir)) continue
82
+ const { routes, layouts, middlewares, errorPage } = await scanRoutes(dir, extraSuffixes)
83
+ routeFiles.push(
84
+ ...routes.map((r) => r.filePath),
85
+ ...layouts.map((l) => l.filePath),
86
+ ...middlewares.map((m) => m.filePath),
87
+ ...(errorPage ? [errorPage] : []),
88
+ )
89
+ }
90
+
91
+ // Include server.ts and multisite.config.ts at the project root if present
92
+ const entryPoints: string[] = [...routeFiles]
93
+ for (const name of ['server.ts', 'multisite.config.ts']) {
94
+ const p = resolve(cwd, name)
95
+ if (existsSync(p)) entryPoints.push(p)
96
+ }
97
+
98
+ const baseExternal = ['node:*', 'davaux', '@davaux/multisite', ...(options.external ?? [])]
99
+
100
+ // Include src/middleware.ts if present (app-level middleware runs before route matching)
101
+ const middlewareSrc = resolve(cwd, 'src', 'middleware.ts')
102
+ if (existsSync(middlewareSrc)) entryPoints.push(middlewareSrc)
103
+
104
+ // Compile routes (with island server wrapping + plugin transforms)
105
+ await build({
106
+ entryPoints,
107
+ outdir: outDir,
108
+ outbase: cwd,
109
+ format: 'esm',
110
+ platform: 'node',
111
+ target: 'node22',
112
+ bundle: true,
113
+ jsx: 'automatic',
114
+ jsxImportSource: 'davaux',
115
+ external: baseExternal,
116
+ sourcemap: true,
117
+ alias: { ...userAlias, 'davaux/client': 'davaux/signal' },
118
+ plugins: [
119
+ ...(allIslandDirs.length > 0 ? [islandServerPlugin(allIslandDirs)] : []),
120
+ ...collectEsbuildPlugins(davauxPlugins),
121
+ ],
122
+ })
123
+
124
+ // Collect CSS side-effect outputs into dist/_davaux/styles.css
125
+ const stylesOutFile = join(outDir, '_davaux', 'styles.css')
126
+ await collectCss(outDir, stylesOutFile)
127
+
128
+ // Compile per-site client island bundles to dist/_davaux/<name>/islands.js
129
+ const baseIslands = config.islandsDir ? await scanIslands(config.islandsDir) : []
130
+ let islandBundles = 0
131
+
132
+ for (const site of config.sites) {
133
+ const siteIslands = site.islandsDir ? await scanIslands(site.islandsDir) : []
134
+ const allIslands = [...baseIslands, ...siteIslands]
135
+ if (allIslands.length === 0) continue
136
+
137
+ await build({
138
+ stdin: {
139
+ contents: generateIslandsEntry(allIslands),
140
+ loader: 'ts',
141
+ resolveDir: cwd,
142
+ },
143
+ outfile: join(outDir, '_davaux', site.name, 'islands.js'),
144
+ format: 'esm',
145
+ platform: 'browser',
146
+ target: 'es2022',
147
+ bundle: true,
148
+ jsx: 'automatic',
149
+ jsxImportSource: 'davaux/client',
150
+ sourcemap: true,
151
+ alias: userAlias,
152
+ plugins: collectEsbuildPlugins(davauxPlugins),
153
+ })
154
+ islandBundles++
155
+ }
156
+
157
+ // Compile per-site client bundles to dist/_davaux/<name>/client.js
158
+ let clientBundles = 0
159
+
160
+ for (const site of config.sites) {
161
+ const clientEntry = site.clientEntry ?? config.clientEntry
162
+ if (!clientEntry || !existsSync(clientEntry)) continue
163
+
164
+ await build({
165
+ entryPoints: [clientEntry],
166
+ outfile: join(outDir, '_davaux', site.name, 'client.js'),
167
+ format: 'esm',
168
+ platform: 'browser',
169
+ target: 'es2022',
170
+ bundle: true,
171
+ jsx: 'automatic',
172
+ jsxImportSource: 'davaux/client',
173
+ sourcemap: true,
174
+ alias: userAlias,
175
+ plugins: collectEsbuildPlugins(davauxPlugins),
176
+ })
177
+ clientBundles++
178
+ }
179
+
180
+ console.log(
181
+ `[davaux/multisite] Built ${routeFiles.length} route file(s), ${islandBundles} island bundle(s), ${clientBundles} client bundle(s)`,
182
+ )
183
+ }