@davaux/multisite 0.8.0 → 0.8.1
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/dist/build.d.ts +44 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +136 -0
- package/dist/build.js.map +1 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +944 -0
- package/dist/index.js.map +1 -0
- package/dist/test/fixtures/base/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/base/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/base/routes/_layout.js +6 -0
- package/dist/test/fixtures/base/routes/_layout.js.map +1 -0
- package/dist/test/fixtures/base/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/base/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/base/routes/about.page.js +3 -0
- package/dist/test/fixtures/base/routes/about.page.js.map +1 -0
- package/dist/test/fixtures/base/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/base/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/base/routes/index.page.js +3 -0
- package/dist/test/fixtures/base/routes/index.page.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/_layout.js +6 -0
- package/dist/test/fixtures/site-a/routes/_layout.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/_middleware.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/_middleware.js +6 -0
- package/dist/test/fixtures/site-a/routes/_middleware.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/config.page.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/config.page.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/config.page.js +7 -0
- package/dist/test/fixtures/site-a/routes/config.page.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/index.page.js +3 -0
- package/dist/test/fixtures/site-a/routes/index.page.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/shop.page.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/shop.page.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/shop.page.js +3 -0
- package/dist/test/fixtures/site-a/routes/shop.page.js.map +1 -0
- package/dist/test/fixtures/site-a/routes/state.page.d.ts +3 -0
- package/dist/test/fixtures/site-a/routes/state.page.d.ts.map +1 -0
- package/dist/test/fixtures/site-a/routes/state.page.js +3 -0
- package/dist/test/fixtures/site-a/routes/state.page.js.map +1 -0
- package/dist/test/fixtures/site-b/routes/_error.d.ts +3 -0
- package/dist/test/fixtures/site-b/routes/_error.d.ts.map +1 -0
- package/dist/test/fixtures/site-b/routes/_error.js +3 -0
- package/dist/test/fixtures/site-b/routes/_error.js.map +1 -0
- package/dist/test/fixtures/site-b/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/site-b/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/site-b/routes/about.page.js +3 -0
- package/dist/test/fixtures/site-b/routes/about.page.js.map +1 -0
- package/dist/test/multisite.test.d.ts +2 -0
- package/dist/test/multisite.test.d.ts.map +1 -0
- package/dist/test/multisite.test.js +492 -0
- package/dist/test/multisite.test.js.map +1 -0
- package/package.json +6 -3
- package/CLAUDE.md +0 -133
- package/src/build.ts +0 -183
- package/src/index.ts +0 -1219
- package/src/test/fixtures/base/routes/_layout.ts +0 -6
- package/src/test/fixtures/base/routes/about.page.ts +0 -3
- package/src/test/fixtures/base/routes/index.page.ts +0 -3
- package/src/test/fixtures/site-a/routes/_layout.ts +0 -6
- package/src/test/fixtures/site-a/routes/_middleware.ts +0 -6
- package/src/test/fixtures/site-a/routes/config.page.ts +0 -7
- package/src/test/fixtures/site-a/routes/index.page.ts +0 -3
- package/src/test/fixtures/site-a/routes/shop.page.ts +0 -3
- package/src/test/fixtures/site-a/routes/state.page.ts +0 -3
- package/src/test/fixtures/site-b/routes/_error.ts +0 -3
- package/src/test/fixtures/site-b/routes/about.page.ts +0 -3
- package/src/test/multisite.test.ts +0 -650
- package/tsconfig.json +0 -17
package/src/index.ts
DELETED
|
@@ -1,1219 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { createReadStream, existsSync, watch as fsWatch, statSync } from 'node:fs'
|
|
3
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
-
import { createServer } from 'node:http'
|
|
5
|
-
import { join, relative } from 'node:path'
|
|
6
|
-
import type {
|
|
7
|
-
IslandFile,
|
|
8
|
-
LayoutFile,
|
|
9
|
-
MiddlewareFile,
|
|
10
|
-
RequestContext,
|
|
11
|
-
RouteFile,
|
|
12
|
-
RouteType,
|
|
13
|
-
ScanResult,
|
|
14
|
-
} from 'davaux'
|
|
15
|
-
import {
|
|
16
|
-
collectCss,
|
|
17
|
-
cssCollectorPlugin,
|
|
18
|
-
generateIslandsEntry,
|
|
19
|
-
islandServerPlugin,
|
|
20
|
-
} from 'davaux/build'
|
|
21
|
-
import {
|
|
22
|
-
collectEsbuildPlugins,
|
|
23
|
-
collectScannerSuffixes,
|
|
24
|
-
type DavauxPlugin,
|
|
25
|
-
pathsToAlias,
|
|
26
|
-
} from 'davaux/config'
|
|
27
|
-
import type { CompiledApp } from 'davaux/handler'
|
|
28
|
-
import { buildApp, dispatch } from 'davaux/handler'
|
|
29
|
-
import { scanIslands, scanRoutes } from 'davaux/scanner'
|
|
30
|
-
import { context, type Plugin } from 'esbuild'
|
|
31
|
-
|
|
32
|
-
// Keyed on the Node IncomingMessage object — auto-cleaned when the request is GC'd.
|
|
33
|
-
const siteConfigMap = new WeakMap<IncomingMessage, unknown>()
|
|
34
|
-
|
|
35
|
-
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
export interface SiteDefinition<T = Record<string, unknown>> {
|
|
38
|
-
/** Unique name for this site. Used in logging. */
|
|
39
|
-
name: string
|
|
40
|
-
/**
|
|
41
|
-
* Hostname(s) that route to this site. Supply `'*'` as a catch-all fallback.
|
|
42
|
-
* Port is stripped before matching, so `'example.com'` matches both
|
|
43
|
-
* `example.com` and `example.com:3000`.
|
|
44
|
-
*/
|
|
45
|
-
hostname: string | string[]
|
|
46
|
-
/**
|
|
47
|
-
* Absolute path to the site-specific routes directory. Routes here are
|
|
48
|
-
* overlaid on top of `baseDir`: same URL pattern + type in the overlay wins.
|
|
49
|
-
* Omit to run the base routes without any site-specific overrides.
|
|
50
|
-
*/
|
|
51
|
-
routesDir?: string
|
|
52
|
-
/**
|
|
53
|
-
* Absolute path to the site-specific islands directory. Islands here are
|
|
54
|
-
* merged with those in `MultisiteConfig.islandsDir` for this site's client bundle.
|
|
55
|
-
*/
|
|
56
|
-
islandsDir?: string
|
|
57
|
-
/**
|
|
58
|
-
* Absolute path to the site-specific public directory. Static files here are
|
|
59
|
-
* served at their URL path before route dispatch, taking priority over the
|
|
60
|
-
* shared `publicDir` when both contain a file at the same path.
|
|
61
|
-
*/
|
|
62
|
-
publicDir?: string
|
|
63
|
-
/**
|
|
64
|
-
* Absolute path to a site-specific client-side entry file. Overrides
|
|
65
|
-
* `MultisiteConfig.clientEntry` for this site when set.
|
|
66
|
-
*/
|
|
67
|
-
clientEntry?: string
|
|
68
|
-
/** Arbitrary per-site config — accessible via `getSite<T>(ctx)` in any handler, layout, or middleware. */
|
|
69
|
-
config?: T
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface MultisiteConfig<T = Record<string, unknown>> {
|
|
73
|
-
/** Absolute path to the shared base routes directory. */
|
|
74
|
-
baseDir?: string
|
|
75
|
-
/** Absolute path to the shared islands directory. Islands here are included in every site's client bundle. */
|
|
76
|
-
islandsDir?: string
|
|
77
|
-
/** Absolute path to the shared public directory. Static files are served at their URL path before route dispatch. */
|
|
78
|
-
publicDir?: string
|
|
79
|
-
/**
|
|
80
|
-
* Absolute path to a shared client-side entry file compiled to `/_davaux/client.js` and
|
|
81
|
-
* injected into every page. Per-site `clientEntry` overrides this when set.
|
|
82
|
-
*/
|
|
83
|
-
clientEntry?: string
|
|
84
|
-
/** Site definitions — one entry per hostname (or hostname group). */
|
|
85
|
-
sites: SiteDefinition<T>[]
|
|
86
|
-
/** Extra route suffixes forwarded from plugins (e.g. `[['.page.md', 'page']]`). */
|
|
87
|
-
extraSuffixes?: [string, RouteType][]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface BuildOptions {
|
|
91
|
-
isDev?: boolean
|
|
92
|
-
clientScripts?: string[]
|
|
93
|
-
clientStylesheets?: string[]
|
|
94
|
-
basePath?: string
|
|
95
|
-
/** Absolute path to a compiled app-level middleware file (runs before route matching on every request). */
|
|
96
|
-
appMiddlewarePath?: string
|
|
97
|
-
/**
|
|
98
|
-
* Absolute path to the `src/middleware.ts` source file. In dev mode it is
|
|
99
|
-
* compiled alongside route files and used as `appMiddlewarePath`.
|
|
100
|
-
* `startMultisite` auto-detects `<cwd>/src/middleware.ts` when `cwd` is set.
|
|
101
|
-
*/
|
|
102
|
-
middlewareSrc?: string
|
|
103
|
-
/**
|
|
104
|
-
* Absolute path to the project root — used in dev mode to locate the esbuild
|
|
105
|
-
* temp directory (`.davaux-multisite/`). Defaults to `process.cwd()`.
|
|
106
|
-
* Pass `import.meta.dirname` from your `server.ts` for reliable resolution.
|
|
107
|
-
*/
|
|
108
|
-
cwd?: string
|
|
109
|
-
/**
|
|
110
|
-
* Davaux plugins to apply — contributes esbuild transforms and scanner suffix
|
|
111
|
-
* extensions. Use the same list here as in `buildMultisite`.
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* import { mdx } from '@davaux/mdx'
|
|
115
|
-
* startMultisite(sites, { plugins: [mdx()] })
|
|
116
|
-
*/
|
|
117
|
-
plugins?: DavauxPlugin[]
|
|
118
|
-
/**
|
|
119
|
-
* tsconfig-style path aliases forwarded to esbuild `alias` in every build context.
|
|
120
|
-
* Same format as `compilerOptions.paths` — use `{ '~/*': ['./src/*'] }`.
|
|
121
|
-
*/
|
|
122
|
-
paths?: Record<string, string>
|
|
123
|
-
/**
|
|
124
|
-
* Extra packages to mark as external beyond the defaults (`node:*`, `davaux`,
|
|
125
|
-
* `@davaux/multisite`). Useful for native or CJS-only packages like `canvas` or
|
|
126
|
-
* `better-sqlite3`.
|
|
127
|
-
*/
|
|
128
|
-
external?: string[]
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export interface ServerOptions {
|
|
132
|
-
port?: number
|
|
133
|
-
hostname?: string
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface StartMultisiteOptions extends BuildOptions, ServerOptions {}
|
|
137
|
-
|
|
138
|
-
// ─── Internal types ───────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
interface SiteEntry<T> {
|
|
141
|
-
app: CompiledApp
|
|
142
|
-
config: T | undefined
|
|
143
|
-
/** Absolute path to the compiled client islands bundle, if any. */
|
|
144
|
-
islandsPath?: string
|
|
145
|
-
/** Absolute path to the compiled CSS stylesheet, if any. */
|
|
146
|
-
stylesPath?: string
|
|
147
|
-
/** Absolute path to the compiled client entry bundle, if any. */
|
|
148
|
-
clientPath?: string
|
|
149
|
-
/** Ordered list of public directories to serve static files from (per-site first, then shared). */
|
|
150
|
-
publicDirs: string[]
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function contentTypeFor(filePath: string): string {
|
|
154
|
-
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
|
|
155
|
-
const types: Record<string, string> = {
|
|
156
|
-
css: 'text/css',
|
|
157
|
-
gif: 'image/gif',
|
|
158
|
-
html: 'text/html',
|
|
159
|
-
ico: 'image/x-icon',
|
|
160
|
-
jpeg: 'image/jpeg',
|
|
161
|
-
jpg: 'image/jpeg',
|
|
162
|
-
js: 'application/javascript',
|
|
163
|
-
json: 'application/json',
|
|
164
|
-
mjs: 'application/javascript',
|
|
165
|
-
pdf: 'application/pdf',
|
|
166
|
-
png: 'image/png',
|
|
167
|
-
svg: 'image/svg+xml',
|
|
168
|
-
txt: 'text/plain',
|
|
169
|
-
webmanifest: 'application/manifest+json',
|
|
170
|
-
webp: 'image/webp',
|
|
171
|
-
woff: 'font/woff',
|
|
172
|
-
woff2: 'font/woff2',
|
|
173
|
-
xml: 'text/xml',
|
|
174
|
-
}
|
|
175
|
-
return types[ext] ?? 'application/octet-stream'
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Route sort helpers (mirrors scanRoutes internals) ────────────────────────
|
|
179
|
-
|
|
180
|
-
const TYPE_PRIORITY: Record<string, number> = { page: 1 }
|
|
181
|
-
|
|
182
|
-
function dynamicWeight(pattern: string): number {
|
|
183
|
-
const segs = pattern.split('/')
|
|
184
|
-
if (segs.some((s) => s.startsWith('*'))) return 1000
|
|
185
|
-
return segs.filter((s) => s.startsWith(':')).length
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function sortRoutes(routes: RouteFile[]): RouteFile[] {
|
|
189
|
-
return [...routes].sort((a, b) => {
|
|
190
|
-
const aw = dynamicWeight(a.urlPattern)
|
|
191
|
-
const bw = dynamicWeight(b.urlPattern)
|
|
192
|
-
if (aw !== bw) return aw - bw
|
|
193
|
-
if (a.urlPattern.length !== b.urlPattern.length)
|
|
194
|
-
return a.urlPattern.length - b.urlPattern.length
|
|
195
|
-
return (TYPE_PRIORITY[a.type] ?? 0) - (TYPE_PRIORITY[b.type] ?? 0)
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Merge logic ──────────────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Merge a site-specific `ScanResult` on top of a base `ScanResult`.
|
|
203
|
-
*
|
|
204
|
-
* - **Routes**: overlay wins at the same `urlPattern + type`. The merged list
|
|
205
|
-
* is re-sorted so static routes still precede dynamic ones.
|
|
206
|
-
* - **Layouts**: overlay wins at the same `dirPath` (directory-level override).
|
|
207
|
-
* Layouts in directories unique to either tree are kept as-is.
|
|
208
|
-
* - **Middlewares**: overlay wins at the same `dirPath`. Middlewares unique to
|
|
209
|
-
* the base still run; overlapping ones are replaced by the site version.
|
|
210
|
-
* - **Error page**: overlay wins if present, otherwise falls back to base.
|
|
211
|
-
*/
|
|
212
|
-
export function mergeScanResults(base: ScanResult, overlay: ScanResult): ScanResult {
|
|
213
|
-
// Routes — overlay wins by URL pattern + type
|
|
214
|
-
const overlayRouteKeys = new Set(overlay.routes.map((r) => `${r.type}:${r.urlPattern}`))
|
|
215
|
-
const routes: RouteFile[] = sortRoutes([
|
|
216
|
-
...overlay.routes,
|
|
217
|
-
...base.routes.filter((r) => !overlayRouteKeys.has(`${r.type}:${r.urlPattern}`)),
|
|
218
|
-
])
|
|
219
|
-
|
|
220
|
-
// Layouts — overlay wins by dirPath
|
|
221
|
-
const overlayLayoutDirs = new Set(overlay.layouts.map((l) => l.dirPath))
|
|
222
|
-
const layouts: LayoutFile[] = [
|
|
223
|
-
...overlay.layouts,
|
|
224
|
-
...base.layouts.filter((l) => !overlayLayoutDirs.has(l.dirPath)),
|
|
225
|
-
]
|
|
226
|
-
|
|
227
|
-
// Middlewares — overlay wins by dirPath; base middlewares for non-overlapping
|
|
228
|
-
// dirs still run (they're naturally ordered outermost-first by the handler)
|
|
229
|
-
const overlayMiddlewareDirs = new Set(overlay.middlewares.map((m) => m.dirPath))
|
|
230
|
-
const middlewares: MiddlewareFile[] = [
|
|
231
|
-
...base.middlewares.filter((m) => !overlayMiddlewareDirs.has(m.dirPath)),
|
|
232
|
-
...overlay.middlewares,
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
routes,
|
|
237
|
-
layouts,
|
|
238
|
-
middlewares,
|
|
239
|
-
errorPage: overlay.errorPage ?? base.errorPage,
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ─── Config helper ────────────────────────────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
/** Identity function — returns config unchanged. Provides TypeScript inference for the site config type `T`. */
|
|
246
|
-
export function defineSites<T = Record<string, unknown>>(
|
|
247
|
-
config: MultisiteConfig<T>,
|
|
248
|
-
): MultisiteConfig<T> {
|
|
249
|
-
return config
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ─── Build ────────────────────────────────────────────────────────────────────
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Scan all site route directories, merge each with the shared base, and return
|
|
256
|
-
* a per-hostname map of compiled apps ready for dispatch.
|
|
257
|
-
*
|
|
258
|
-
* In dev mode (`isDev: true`), all route files are compiled with esbuild
|
|
259
|
-
* (applying plugin transforms and wrapping island imports server-side) and
|
|
260
|
-
* per-site client island bundles are built before the server starts.
|
|
261
|
-
*
|
|
262
|
-
* @example
|
|
263
|
-
* const apps = await buildMultisiteApps(sites, { isDev: true, cwd: import.meta.dirname })
|
|
264
|
-
* startMultisiteServer(apps, { port: 3000 })
|
|
265
|
-
*/
|
|
266
|
-
export async function buildMultisiteApps<T = Record<string, unknown>>(
|
|
267
|
-
config: MultisiteConfig<T>,
|
|
268
|
-
options: BuildOptions = {},
|
|
269
|
-
): Promise<Map<string, SiteEntry<T>>> {
|
|
270
|
-
const { baseDir, islandsDir: baseIslandsDir, sites, extraSuffixes: configSuffixes = [] } = config
|
|
271
|
-
const {
|
|
272
|
-
isDev = false,
|
|
273
|
-
clientScripts = [],
|
|
274
|
-
clientStylesheets = [],
|
|
275
|
-
basePath = '',
|
|
276
|
-
appMiddlewarePath,
|
|
277
|
-
middlewareSrc,
|
|
278
|
-
cwd: cwdOpt,
|
|
279
|
-
plugins: davauxPlugins = [],
|
|
280
|
-
paths,
|
|
281
|
-
external: userExternal = [],
|
|
282
|
-
} = options
|
|
283
|
-
|
|
284
|
-
const cwd = cwdOpt ?? process.cwd()
|
|
285
|
-
const allSuffixes = [...configSuffixes, ...collectScannerSuffixes(davauxPlugins)]
|
|
286
|
-
const userAlias = pathsToAlias(paths ?? {})
|
|
287
|
-
const devExternal = ['node:*', 'davaux', '@davaux/multisite', ...userExternal]
|
|
288
|
-
|
|
289
|
-
// All island directories across base + all sites (for the server-side island plugin)
|
|
290
|
-
const allIslandDirs: string[] = [
|
|
291
|
-
...(baseIslandsDir ? [baseIslandsDir] : []),
|
|
292
|
-
...sites.flatMap((s) => (s.islandsDir ? [s.islandsDir] : [])),
|
|
293
|
-
]
|
|
294
|
-
|
|
295
|
-
// Scan all route directories upfront
|
|
296
|
-
const baseScanRaw: ScanResult = baseDir
|
|
297
|
-
? await scanRoutes(baseDir, allSuffixes)
|
|
298
|
-
: { routes: [], layouts: [], middlewares: [] }
|
|
299
|
-
|
|
300
|
-
const siteScanMap = new Map<string, ScanResult>()
|
|
301
|
-
for (const site of sites) {
|
|
302
|
-
siteScanMap.set(
|
|
303
|
-
site.name,
|
|
304
|
-
site.routesDir
|
|
305
|
-
? await scanRoutes(site.routesDir, allSuffixes)
|
|
306
|
-
: { routes: [], layouts: [], middlewares: [] },
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Populated during the blocks below; consumed when building per-site SiteEntry
|
|
311
|
-
const siteIslandsPaths = new Map<string, string>()
|
|
312
|
-
const siteClientPaths = new Map<string, string>()
|
|
313
|
-
let sharedStylesPath: string | undefined
|
|
314
|
-
|
|
315
|
-
// Path-mapping functions — identity in production, remapped in dev
|
|
316
|
-
let toCompiledPath = (p: string) => p
|
|
317
|
-
let toCompiledDir = (d: string) => d
|
|
318
|
-
|
|
319
|
-
if (isDev) {
|
|
320
|
-
const { build } = await import('esbuild')
|
|
321
|
-
const dauxDir = join(cwd, '.davaux-multisite')
|
|
322
|
-
const routesOutDir = join(dauxDir, 'routes')
|
|
323
|
-
sharedStylesPath = join(dauxDir, 'styles.css')
|
|
324
|
-
|
|
325
|
-
// Compile all route files — applies plugin transforms and server-side island wrapping
|
|
326
|
-
function collectRouteFiles(scan: ScanResult): string[] {
|
|
327
|
-
return [
|
|
328
|
-
...scan.routes.map((r) => r.filePath),
|
|
329
|
-
...scan.layouts.map((l) => l.filePath),
|
|
330
|
-
...scan.middlewares.map((m) => m.filePath),
|
|
331
|
-
...(scan.errorPage ? [scan.errorPage] : []),
|
|
332
|
-
]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const allRouteFiles = [
|
|
336
|
-
...collectRouteFiles(baseScanRaw),
|
|
337
|
-
...[...siteScanMap.values()].flatMap(collectRouteFiles),
|
|
338
|
-
...(middlewareSrc && existsSync(middlewareSrc) ? [middlewareSrc] : []),
|
|
339
|
-
]
|
|
340
|
-
|
|
341
|
-
if (allRouteFiles.length > 0) {
|
|
342
|
-
await build({
|
|
343
|
-
entryPoints: allRouteFiles,
|
|
344
|
-
outdir: routesOutDir,
|
|
345
|
-
outbase: cwd,
|
|
346
|
-
format: 'esm',
|
|
347
|
-
platform: 'node',
|
|
348
|
-
target: 'node22',
|
|
349
|
-
bundle: true,
|
|
350
|
-
external: devExternal,
|
|
351
|
-
jsx: 'automatic',
|
|
352
|
-
jsxImportSource: 'davaux',
|
|
353
|
-
sourcemap: 'inline',
|
|
354
|
-
alias: { ...userAlias, 'davaux/client': 'davaux/signal' },
|
|
355
|
-
plugins: [
|
|
356
|
-
...(allIslandDirs.length > 0 ? [islandServerPlugin(allIslandDirs)] : []),
|
|
357
|
-
...collectEsbuildPlugins(davauxPlugins),
|
|
358
|
-
],
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Compile per-site client island bundles
|
|
363
|
-
const baseIslands: IslandFile[] = baseIslandsDir ? await scanIslands(baseIslandsDir) : []
|
|
364
|
-
|
|
365
|
-
for (const site of sites) {
|
|
366
|
-
const siteIslands: IslandFile[] = site.islandsDir ? await scanIslands(site.islandsDir) : []
|
|
367
|
-
const allIslands = [...baseIslands, ...siteIslands]
|
|
368
|
-
if (allIslands.length === 0) continue
|
|
369
|
-
|
|
370
|
-
const islandsPath = join(dauxDir, site.name, 'islands.js')
|
|
371
|
-
await build({
|
|
372
|
-
stdin: {
|
|
373
|
-
contents: generateIslandsEntry(allIslands),
|
|
374
|
-
loader: 'ts',
|
|
375
|
-
resolveDir: cwd,
|
|
376
|
-
},
|
|
377
|
-
outfile: islandsPath,
|
|
378
|
-
format: 'esm',
|
|
379
|
-
platform: 'browser',
|
|
380
|
-
target: 'es2022',
|
|
381
|
-
bundle: true,
|
|
382
|
-
jsx: 'automatic',
|
|
383
|
-
jsxImportSource: 'davaux/client',
|
|
384
|
-
sourcemap: 'inline',
|
|
385
|
-
plugins: collectEsbuildPlugins(davauxPlugins),
|
|
386
|
-
})
|
|
387
|
-
siteIslandsPaths.set(site.name, islandsPath)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Compile per-site client bundles
|
|
391
|
-
for (const site of sites) {
|
|
392
|
-
const clientEntry = site.clientEntry ?? config.clientEntry
|
|
393
|
-
if (!clientEntry || !existsSync(clientEntry)) continue
|
|
394
|
-
const clientPath = join(dauxDir, site.name, 'client.js')
|
|
395
|
-
await build({
|
|
396
|
-
entryPoints: [clientEntry],
|
|
397
|
-
outfile: clientPath,
|
|
398
|
-
format: 'esm',
|
|
399
|
-
platform: 'browser',
|
|
400
|
-
target: 'es2022',
|
|
401
|
-
bundle: true,
|
|
402
|
-
jsx: 'automatic',
|
|
403
|
-
jsxImportSource: 'davaux/client',
|
|
404
|
-
sourcemap: 'inline',
|
|
405
|
-
plugins: collectEsbuildPlugins(davauxPlugins),
|
|
406
|
-
})
|
|
407
|
-
siteClientPaths.set(site.name, clientPath)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Collect all CSS emitted by the builds into one shared stylesheet
|
|
411
|
-
await collectCss(dauxDir, sharedStylesPath)
|
|
412
|
-
|
|
413
|
-
toCompiledPath = (src: string) =>
|
|
414
|
-
join(routesOutDir, relative(cwd, src).replace(/\.(tsx?|jsx?|mdx?)$/, '.js'))
|
|
415
|
-
toCompiledDir = (srcDir: string) => join(routesOutDir, relative(cwd, srcDir))
|
|
416
|
-
} else {
|
|
417
|
-
// Production: pick up pre-compiled bundles written by buildMultisite
|
|
418
|
-
for (const site of sites) {
|
|
419
|
-
const islands = join(cwd, '_davaux', site.name, 'islands.js')
|
|
420
|
-
if (existsSync(islands)) siteIslandsPaths.set(site.name, islands)
|
|
421
|
-
const client = join(cwd, '_davaux', site.name, 'client.js')
|
|
422
|
-
if (existsSync(client)) siteClientPaths.set(site.name, client)
|
|
423
|
-
}
|
|
424
|
-
const prodStyles = join(cwd, '_davaux', 'styles.css')
|
|
425
|
-
if (existsSync(prodStyles)) sharedStylesPath = prodStyles
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function remapScan(scan: ScanResult): ScanResult {
|
|
429
|
-
return {
|
|
430
|
-
routes: scan.routes.map((r) => ({ ...r, filePath: toCompiledPath(r.filePath) })),
|
|
431
|
-
layouts: scan.layouts.map((l) => ({
|
|
432
|
-
filePath: toCompiledPath(l.filePath),
|
|
433
|
-
dirPath: toCompiledDir(l.dirPath),
|
|
434
|
-
})),
|
|
435
|
-
middlewares: scan.middlewares.map((m) => ({
|
|
436
|
-
filePath: toCompiledPath(m.filePath),
|
|
437
|
-
dirPath: toCompiledDir(m.dirPath),
|
|
438
|
-
})),
|
|
439
|
-
errorPage: scan.errorPage ? toCompiledPath(scan.errorPage) : undefined,
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const effectiveMiddlewarePath =
|
|
444
|
-
isDev && middlewareSrc && existsSync(middlewareSrc)
|
|
445
|
-
? toCompiledPath(middlewareSrc)
|
|
446
|
-
: appMiddlewarePath
|
|
447
|
-
|
|
448
|
-
const baseScan = isDev ? remapScan(baseScanRaw) : baseScanRaw
|
|
449
|
-
const result = new Map<string, SiteEntry<T>>()
|
|
450
|
-
|
|
451
|
-
for (const site of sites) {
|
|
452
|
-
const rawSiteScan = siteScanMap.get(site.name) ?? { routes: [], layouts: [], middlewares: [] }
|
|
453
|
-
const siteScan = isDev ? remapScan(rawSiteScan) : rawSiteScan
|
|
454
|
-
const merged = mergeScanResults(baseScan, siteScan)
|
|
455
|
-
|
|
456
|
-
const islandsPath = siteIslandsPaths.get(site.name)
|
|
457
|
-
const clientPath = siteClientPaths.get(site.name)
|
|
458
|
-
const siteClientScripts = [
|
|
459
|
-
...(islandsPath ? ['/_davaux/islands.js'] : []),
|
|
460
|
-
...(clientPath ? ['/_davaux/client.js'] : []),
|
|
461
|
-
...clientScripts,
|
|
462
|
-
]
|
|
463
|
-
const siteClientStylesheets = [
|
|
464
|
-
...(sharedStylesPath ? ['/_davaux/styles.css'] : []),
|
|
465
|
-
...clientStylesheets,
|
|
466
|
-
]
|
|
467
|
-
|
|
468
|
-
const publicDirs: string[] = [
|
|
469
|
-
...(site.publicDir ? [site.publicDir] : []),
|
|
470
|
-
...(config.publicDir ? [config.publicDir] : []),
|
|
471
|
-
]
|
|
472
|
-
|
|
473
|
-
const app = buildApp(
|
|
474
|
-
merged,
|
|
475
|
-
isDev,
|
|
476
|
-
siteClientScripts,
|
|
477
|
-
siteClientStylesheets,
|
|
478
|
-
effectiveMiddlewarePath,
|
|
479
|
-
basePath,
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
const entry: SiteEntry<T> = {
|
|
483
|
-
app,
|
|
484
|
-
config: site.config,
|
|
485
|
-
islandsPath,
|
|
486
|
-
stylesPath: sharedStylesPath,
|
|
487
|
-
clientPath,
|
|
488
|
-
publicDirs,
|
|
489
|
-
}
|
|
490
|
-
const hostnames = Array.isArray(site.hostname) ? site.hostname : [site.hostname]
|
|
491
|
-
for (const h of hostnames) result.set(h, entry)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return result
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// ─── Server ───────────────────────────────────────────────────────────────────
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Start an HTTP server that dispatches each request to the matching site by
|
|
501
|
-
* hostname. Falls back to the `'*'` entry if the hostname is not explicitly
|
|
502
|
-
* registered. Responds 404 if neither matches.
|
|
503
|
-
*
|
|
504
|
-
* `sites` may be a `() => Map` factory so the caller can hot-swap compiled apps
|
|
505
|
-
* in dev mode without restarting the server.
|
|
506
|
-
*/
|
|
507
|
-
export function startMultisiteServer<T = Record<string, unknown>>(
|
|
508
|
-
sites: Map<string, SiteEntry<T>> | (() => Map<string, SiteEntry<T>>),
|
|
509
|
-
options: ServerOptions = {},
|
|
510
|
-
): ReturnType<typeof createServer> {
|
|
511
|
-
const { port = 3000, hostname = 'localhost' } = options
|
|
512
|
-
const getSites = typeof sites === 'function' ? sites : () => sites
|
|
513
|
-
|
|
514
|
-
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
515
|
-
const host = (req.headers.host ?? '').split(':')[0]
|
|
516
|
-
const currentSites = getSites()
|
|
517
|
-
const entry = currentSites.get(host) ?? currentSites.get('*')
|
|
518
|
-
|
|
519
|
-
if (!entry) {
|
|
520
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
521
|
-
res.end(`[davaux/multisite] No site registered for host: ${host}`)
|
|
522
|
-
return
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (entry.config !== undefined) siteConfigMap.set(req, entry.config)
|
|
526
|
-
|
|
527
|
-
// Serve davaux static assets — per-site for islands, shared for CSS
|
|
528
|
-
const urlPath = req.url?.split('?')[0]
|
|
529
|
-
|
|
530
|
-
if (urlPath === '/_davaux/islands.js') {
|
|
531
|
-
if (entry.islandsPath && existsSync(entry.islandsPath)) {
|
|
532
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
533
|
-
createReadStream(entry.islandsPath).pipe(res)
|
|
534
|
-
} else {
|
|
535
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
536
|
-
res.end('Not Found')
|
|
537
|
-
}
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (urlPath === '/_davaux/client.js') {
|
|
542
|
-
if (entry.clientPath && existsSync(entry.clientPath)) {
|
|
543
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
544
|
-
createReadStream(entry.clientPath).pipe(res)
|
|
545
|
-
} else {
|
|
546
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
547
|
-
res.end('Not Found')
|
|
548
|
-
}
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (urlPath === '/_davaux/styles.css') {
|
|
553
|
-
res.writeHead(200, { 'Content-Type': 'text/css' })
|
|
554
|
-
if (entry.stylesPath && existsSync(entry.stylesPath)) {
|
|
555
|
-
createReadStream(entry.stylesPath).pipe(res)
|
|
556
|
-
} else {
|
|
557
|
-
res.end('')
|
|
558
|
-
}
|
|
559
|
-
return
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Stub livereload endpoints — live reload is not yet supported in multisite
|
|
563
|
-
if (urlPath === '/_davaux/livereload.js' || urlPath === '/_davaux/livereload-worker.js') {
|
|
564
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
565
|
-
res.end('')
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Serve static files from public directories (per-site dir takes priority over shared)
|
|
570
|
-
if (urlPath) {
|
|
571
|
-
const relPath = urlPath.replace(/^\/+/, '')
|
|
572
|
-
if (relPath) {
|
|
573
|
-
for (const dir of entry.publicDirs) {
|
|
574
|
-
const filePath = join(dir, relPath)
|
|
575
|
-
const normalizedDir = dir.endsWith('/') ? dir : `${dir}/`
|
|
576
|
-
if (!filePath.startsWith(normalizedDir)) continue // path traversal guard
|
|
577
|
-
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
578
|
-
res.writeHead(200, { 'Content-Type': contentTypeFor(filePath) })
|
|
579
|
-
createReadStream(filePath).pipe(res)
|
|
580
|
-
return
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
dispatch(req, res, entry.app).catch((err) => {
|
|
587
|
-
console.error('[davaux/multisite] Unhandled dispatch error:', err)
|
|
588
|
-
if (!res.headersSent) {
|
|
589
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
590
|
-
res.end('Internal Server Error')
|
|
591
|
-
}
|
|
592
|
-
})
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
server.listen(port, hostname, () => {
|
|
596
|
-
console.log(`\n davaux/multisite http://${hostname}:${port}\n`)
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
return server
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// ─── Low-level dispatch ───────────────────────────────────────────────────────
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Look up the correct site by the request's `host` header, inject the site
|
|
606
|
-
* config into the WeakMap, and dispatch the request. Returns `false` if no
|
|
607
|
-
* site is registered for the host (including the `'*'` fallback).
|
|
608
|
-
*
|
|
609
|
-
* Useful for embedding multisite dispatch into a custom server or for testing
|
|
610
|
-
* without spinning up a real HTTP server.
|
|
611
|
-
*
|
|
612
|
-
* @example
|
|
613
|
-
* const server = createServer(async (req, res) => {
|
|
614
|
-
* const handled = await dispatchToSite(sites, req, res)
|
|
615
|
-
* if (!handled) { res.writeHead(404); res.end('No site') }
|
|
616
|
-
* })
|
|
617
|
-
*/
|
|
618
|
-
export async function dispatchToSite<T = Record<string, unknown>>(
|
|
619
|
-
sites: Map<string, SiteEntry<T>>,
|
|
620
|
-
req: IncomingMessage,
|
|
621
|
-
res: ServerResponse,
|
|
622
|
-
): Promise<boolean> {
|
|
623
|
-
const host = (req.headers.host ?? '').split(':')[0]
|
|
624
|
-
const entry = sites.get(host) ?? sites.get('*')
|
|
625
|
-
if (!entry) return false
|
|
626
|
-
if (entry.config !== undefined) siteConfigMap.set(req, entry.config)
|
|
627
|
-
await dispatch(req, res, entry.app)
|
|
628
|
-
return true
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// ─── Dev mode ────────────────────────────────────────────────────────────────
|
|
632
|
-
|
|
633
|
-
// SharedWorker fans one SSE connection per origin to all tabs; falls back to per-tab EventSource.
|
|
634
|
-
const SHARED_WORKER_SCRIPT = `var ports=[];var source=null;
|
|
635
|
-
self.onconnect=function(ev){
|
|
636
|
-
var port=ev.ports[0];port.start();ports.push(port);
|
|
637
|
-
if(!source){
|
|
638
|
-
source=new EventSource('/_davaux/livereload');
|
|
639
|
-
source.onmessage=function(e){
|
|
640
|
-
ports=ports.filter(function(p){try{p.postMessage(e.data);return true}catch(_){return false}});
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
};`
|
|
644
|
-
|
|
645
|
-
const LIVERELOAD_SCRIPT = `;(function(){
|
|
646
|
-
var overlay=null
|
|
647
|
-
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
648
|
-
function show(errors){
|
|
649
|
-
if(!overlay){
|
|
650
|
-
overlay=document.createElement('div')
|
|
651
|
-
overlay.style.cssText='position:fixed;inset:0;z-index:99999;overflow:auto;background:#0d0d0d;color:#e8e8e8;font:13px/1.6 monospace;padding:2rem;box-sizing:border-box'
|
|
652
|
-
document.body.appendChild(overlay)
|
|
653
|
-
}
|
|
654
|
-
overlay.innerHTML='<p style="color:#ff5555;font-size:1.1em;margin:0 0 1.5rem 0"><b>[davaux] Build Error</b></p>'+
|
|
655
|
-
errors.map(function(e){
|
|
656
|
-
var loc=e.file?(e.file+(e.line?':'+e.line+':'+e.column:'')):'unknown'
|
|
657
|
-
return '<div style="margin-bottom:1.25rem;border:1px solid #ff3333;border-radius:6px;padding:1rem">'+
|
|
658
|
-
'<div style="color:#888;font-size:0.9em;margin-bottom:0.4rem">'+esc(loc)+'</div>'+
|
|
659
|
-
'<div style="color:#ff6b6b">'+esc(e.text)+'</div>'+
|
|
660
|
-
(e.lineText?'<pre style="margin:0.75rem 0 0;padding:0.5rem;background:#1a1a1a;border-radius:4px;overflow:auto;color:#aaa">'+esc(e.lineText)+'</pre>':'')+
|
|
661
|
-
'</div>'
|
|
662
|
-
}).join('')
|
|
663
|
-
}
|
|
664
|
-
function handle(data){
|
|
665
|
-
if(data==='reload'){location.reload()}
|
|
666
|
-
else if(data.slice(0,6)==='error:'){show(JSON.parse(data.slice(6)))}
|
|
667
|
-
}
|
|
668
|
-
if(typeof SharedWorker!=='undefined'){
|
|
669
|
-
var w=new SharedWorker('/_davaux/livereload-worker.js')
|
|
670
|
-
w.port.onmessage=function(ev){handle(ev.data)}
|
|
671
|
-
w.port.start()
|
|
672
|
-
} else {
|
|
673
|
-
new EventSource('/_davaux/livereload').onmessage=function(ev){handle(ev.data)}
|
|
674
|
-
}
|
|
675
|
-
})()`
|
|
676
|
-
|
|
677
|
-
function startTypeChecker(cwd: string): void {
|
|
678
|
-
const tscBin = join(cwd, 'node_modules', '.bin', 'tsc')
|
|
679
|
-
if (!existsSync(tscBin) || !existsSync(join(cwd, 'tsconfig.json'))) return
|
|
680
|
-
const proc = spawn(tscBin, ['--noEmit', '--watch', '--preserveWatchOutput'], {
|
|
681
|
-
cwd,
|
|
682
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
683
|
-
})
|
|
684
|
-
proc.on('error', () => {})
|
|
685
|
-
process.on('exit', () => proc.kill())
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Start a multisite dev server with file watching, live reload, and TypeScript type checking.
|
|
690
|
-
* Called automatically by `startMultisite` when `NODE_ENV !== 'production'`.
|
|
691
|
-
*/
|
|
692
|
-
export async function startMultisiteDev<T = Record<string, unknown>>(
|
|
693
|
-
config: MultisiteConfig<T>,
|
|
694
|
-
options: StartMultisiteOptions = {},
|
|
695
|
-
): Promise<ReturnType<typeof createServer>> {
|
|
696
|
-
const {
|
|
697
|
-
port = 3000,
|
|
698
|
-
hostname = 'localhost',
|
|
699
|
-
clientScripts: extraClientScripts = [],
|
|
700
|
-
clientStylesheets: extraClientStylesheets = [],
|
|
701
|
-
basePath = '',
|
|
702
|
-
appMiddlewarePath: explicitMiddlewarePath,
|
|
703
|
-
middlewareSrc,
|
|
704
|
-
cwd: cwdOpt,
|
|
705
|
-
plugins: davauxPlugins = [],
|
|
706
|
-
paths,
|
|
707
|
-
external: userExternal = [],
|
|
708
|
-
} = options
|
|
709
|
-
|
|
710
|
-
const cwd = cwdOpt ?? process.cwd()
|
|
711
|
-
const { baseDir, islandsDir: baseIslandsDir, sites } = config
|
|
712
|
-
const allSuffixes = [...(config.extraSuffixes ?? []), ...collectScannerSuffixes(davauxPlugins)]
|
|
713
|
-
const extraPlugins = collectEsbuildPlugins(davauxPlugins)
|
|
714
|
-
const userAlias = pathsToAlias(paths ?? {})
|
|
715
|
-
const serverExternal = ['node:*', 'davaux', '@davaux/multisite', ...userExternal]
|
|
716
|
-
|
|
717
|
-
const dauxDir = join(cwd, '.davaux-multisite')
|
|
718
|
-
const routesOutDir = join(dauxDir, 'routes')
|
|
719
|
-
const sharedStylesPath = join(dauxDir, 'styles.css')
|
|
720
|
-
|
|
721
|
-
const allIslandDirs: string[] = [
|
|
722
|
-
...(baseIslandsDir ? [baseIslandsDir] : []),
|
|
723
|
-
...sites.flatMap((s) => (s.islandsDir ? [s.islandsDir] : [])),
|
|
724
|
-
]
|
|
725
|
-
|
|
726
|
-
function toCompiledPath(src: string): string {
|
|
727
|
-
return join(routesOutDir, relative(cwd, src).replace(/\.(tsx?|jsx?|mdx?)$/, '.js'))
|
|
728
|
-
}
|
|
729
|
-
function toCompiledDir(srcDir: string): string {
|
|
730
|
-
return join(routesOutDir, relative(cwd, srcDir))
|
|
731
|
-
}
|
|
732
|
-
function remapScan(scan: ScanResult): ScanResult {
|
|
733
|
-
return {
|
|
734
|
-
routes: scan.routes.map((r) => ({ ...r, filePath: toCompiledPath(r.filePath) })),
|
|
735
|
-
layouts: scan.layouts.map((l) => ({
|
|
736
|
-
filePath: toCompiledPath(l.filePath),
|
|
737
|
-
dirPath: toCompiledDir(l.dirPath),
|
|
738
|
-
})),
|
|
739
|
-
middlewares: scan.middlewares.map((m) => ({
|
|
740
|
-
filePath: toCompiledPath(m.filePath),
|
|
741
|
-
dirPath: toCompiledDir(m.dirPath),
|
|
742
|
-
})),
|
|
743
|
-
errorPage: scan.errorPage ? toCompiledPath(scan.errorPage) : undefined,
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
function collectRouteFiles(scan: ScanResult): string[] {
|
|
747
|
-
return [
|
|
748
|
-
...scan.routes.map((r) => r.filePath),
|
|
749
|
-
...scan.layouts.map((l) => l.filePath),
|
|
750
|
-
...scan.middlewares.map((m) => m.filePath),
|
|
751
|
-
...(scan.errorPage ? [scan.errorPage] : []),
|
|
752
|
-
]
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// ─── SSE live reload ─────────────────────────────────────────────────────────
|
|
756
|
-
const sseClients = new Set<ServerResponse>()
|
|
757
|
-
let serverReady = false
|
|
758
|
-
let reloadTimer: ReturnType<typeof setTimeout> | undefined
|
|
759
|
-
|
|
760
|
-
function sendToClients(data: string): void {
|
|
761
|
-
for (const client of sseClients) {
|
|
762
|
-
try {
|
|
763
|
-
client.write(`data: ${data}\n\n`)
|
|
764
|
-
} catch {
|
|
765
|
-
sseClients.delete(client)
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function scheduleReload(): void {
|
|
771
|
-
if (!serverReady) return
|
|
772
|
-
if (reloadTimer) clearTimeout(reloadTimer)
|
|
773
|
-
reloadTimer = setTimeout(() => {
|
|
774
|
-
reloadTimer = undefined
|
|
775
|
-
sendToClients('reload')
|
|
776
|
-
}, 50)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const reloadPlugin: Plugin = {
|
|
780
|
-
name: 'davaux-multisite-reload',
|
|
781
|
-
setup(build) {
|
|
782
|
-
build.onEnd((result) => {
|
|
783
|
-
if (result.errors.length > 0) {
|
|
784
|
-
console.error('\n[davaux/multisite] Build error:')
|
|
785
|
-
for (const e of result.errors) {
|
|
786
|
-
const loc = e.location
|
|
787
|
-
console.error(loc ? ` ${loc.file}:${loc.line}: ${e.text}` : ` ${e.text}`)
|
|
788
|
-
}
|
|
789
|
-
if (serverReady) {
|
|
790
|
-
const payload = result.errors.map((e) => ({
|
|
791
|
-
text: e.text,
|
|
792
|
-
file: e.location?.file,
|
|
793
|
-
line: e.location?.line,
|
|
794
|
-
column: e.location?.column,
|
|
795
|
-
lineText: e.location?.lineText?.trim(),
|
|
796
|
-
}))
|
|
797
|
-
sendToClients(`error:${JSON.stringify(payload)}`)
|
|
798
|
-
}
|
|
799
|
-
return
|
|
800
|
-
}
|
|
801
|
-
scheduleReload()
|
|
802
|
-
})
|
|
803
|
-
},
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// ─── Initial scan ────────────────────────────────────────────────────────────
|
|
807
|
-
let baseScanRaw: ScanResult = baseDir
|
|
808
|
-
? await scanRoutes(baseDir, allSuffixes)
|
|
809
|
-
: { routes: [], layouts: [], middlewares: [] }
|
|
810
|
-
|
|
811
|
-
const siteScanMap = new Map<string, ScanResult>()
|
|
812
|
-
for (const site of sites) {
|
|
813
|
-
siteScanMap.set(
|
|
814
|
-
site.name,
|
|
815
|
-
site.routesDir
|
|
816
|
-
? await scanRoutes(site.routesDir, allSuffixes)
|
|
817
|
-
: { routes: [], layouts: [], middlewares: [] },
|
|
818
|
-
)
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const siteIslandsMap = new Map<string, IslandFile[]>()
|
|
822
|
-
{
|
|
823
|
-
const baseIslands: IslandFile[] = baseIslandsDir ? await scanIslands(baseIslandsDir) : []
|
|
824
|
-
for (const site of sites) {
|
|
825
|
-
const siteIslands = site.islandsDir ? await scanIslands(site.islandsDir) : []
|
|
826
|
-
const combined = [...baseIslands, ...siteIslands]
|
|
827
|
-
if (combined.length > 0) siteIslandsMap.set(site.name, combined)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function currentRouteFiles(): string[] {
|
|
832
|
-
return [
|
|
833
|
-
...collectRouteFiles(baseScanRaw),
|
|
834
|
-
...[...siteScanMap.values()].flatMap(collectRouteFiles),
|
|
835
|
-
...(middlewareSrc && existsSync(middlewareSrc) ? [middlewareSrc] : []),
|
|
836
|
-
]
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// ─── Esbuild contexts ────────────────────────────────────────────────────────
|
|
840
|
-
type BuildCtx = Awaited<ReturnType<typeof context>>
|
|
841
|
-
|
|
842
|
-
function routesCtxOptions() {
|
|
843
|
-
return {
|
|
844
|
-
entryPoints: currentRouteFiles(),
|
|
845
|
-
outdir: routesOutDir,
|
|
846
|
-
outbase: cwd,
|
|
847
|
-
format: 'esm' as const,
|
|
848
|
-
platform: 'node' as const,
|
|
849
|
-
target: 'node22',
|
|
850
|
-
bundle: true,
|
|
851
|
-
external: serverExternal,
|
|
852
|
-
jsx: 'automatic' as const,
|
|
853
|
-
jsxImportSource: 'davaux',
|
|
854
|
-
sourcemap: 'inline' as const,
|
|
855
|
-
alias: { ...userAlias, 'davaux/client': 'davaux/signal' },
|
|
856
|
-
plugins: [
|
|
857
|
-
reloadPlugin,
|
|
858
|
-
...(allIslandDirs.length > 0 ? [islandServerPlugin(allIslandDirs)] : []),
|
|
859
|
-
cssCollectorPlugin(dauxDir, sharedStylesPath),
|
|
860
|
-
...extraPlugins,
|
|
861
|
-
],
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function islandsCtxOptions(allIslands: IslandFile[], outfile: string) {
|
|
866
|
-
return {
|
|
867
|
-
stdin: { contents: generateIslandsEntry(allIslands), loader: 'ts' as const, resolveDir: cwd },
|
|
868
|
-
outfile,
|
|
869
|
-
format: 'esm' as const,
|
|
870
|
-
platform: 'browser' as const,
|
|
871
|
-
target: 'es2022',
|
|
872
|
-
bundle: true,
|
|
873
|
-
jsx: 'automatic' as const,
|
|
874
|
-
jsxImportSource: 'davaux/client',
|
|
875
|
-
sourcemap: 'inline' as const,
|
|
876
|
-
alias: userAlias,
|
|
877
|
-
plugins: [reloadPlugin, ...extraPlugins],
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
let routesCtx: BuildCtx = await context(routesCtxOptions())
|
|
882
|
-
await routesCtx.rebuild()
|
|
883
|
-
|
|
884
|
-
const islandCtxMap = new Map<string, BuildCtx>()
|
|
885
|
-
for (const [siteName, allIslands] of siteIslandsMap) {
|
|
886
|
-
const islandsCtx = await context(
|
|
887
|
-
islandsCtxOptions(allIslands, join(dauxDir, siteName, 'islands.js')),
|
|
888
|
-
)
|
|
889
|
-
await islandsCtx.rebuild()
|
|
890
|
-
islandCtxMap.set(siteName, islandsCtx)
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const clientCtxMap = new Map<string, BuildCtx>()
|
|
894
|
-
const siteClientPathsMap = new Map<string, string>()
|
|
895
|
-
for (const site of sites) {
|
|
896
|
-
const clientEntry = site.clientEntry ?? config.clientEntry
|
|
897
|
-
if (!clientEntry || !existsSync(clientEntry)) continue
|
|
898
|
-
const clientPath = join(dauxDir, site.name, 'client.js')
|
|
899
|
-
const clientCtx = await context({
|
|
900
|
-
entryPoints: [clientEntry],
|
|
901
|
-
outfile: clientPath,
|
|
902
|
-
format: 'esm',
|
|
903
|
-
platform: 'browser',
|
|
904
|
-
target: 'es2022',
|
|
905
|
-
bundle: true,
|
|
906
|
-
jsx: 'automatic',
|
|
907
|
-
jsxImportSource: 'davaux/client',
|
|
908
|
-
sourcemap: 'inline',
|
|
909
|
-
alias: userAlias,
|
|
910
|
-
plugins: [reloadPlugin, ...extraPlugins],
|
|
911
|
-
})
|
|
912
|
-
await clientCtx.rebuild()
|
|
913
|
-
clientCtxMap.set(site.name, clientCtx)
|
|
914
|
-
siteClientPathsMap.set(site.name, clientPath)
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// ─── App building ────────────────────────────────────────────────────────────
|
|
918
|
-
const effectiveMiddlewarePath =
|
|
919
|
-
middlewareSrc && existsSync(middlewareSrc)
|
|
920
|
-
? toCompiledPath(middlewareSrc)
|
|
921
|
-
: explicitMiddlewarePath
|
|
922
|
-
|
|
923
|
-
function buildCurrentApps(): Map<string, SiteEntry<T>> {
|
|
924
|
-
const baseScan = remapScan(baseScanRaw)
|
|
925
|
-
const result = new Map<string, SiteEntry<T>>()
|
|
926
|
-
for (const site of sites) {
|
|
927
|
-
const rawSiteScan = siteScanMap.get(site.name) ?? { routes: [], layouts: [], middlewares: [] }
|
|
928
|
-
const merged = mergeScanResults(baseScan, remapScan(rawSiteScan))
|
|
929
|
-
const islandsPath = siteIslandsMap.has(site.name)
|
|
930
|
-
? join(dauxDir, site.name, 'islands.js')
|
|
931
|
-
: undefined
|
|
932
|
-
const clientPath = siteClientPathsMap.get(site.name)
|
|
933
|
-
const app = buildApp(
|
|
934
|
-
merged,
|
|
935
|
-
true,
|
|
936
|
-
[
|
|
937
|
-
...(islandsPath ? ['/_davaux/islands.js'] : []),
|
|
938
|
-
...(clientPath ? ['/_davaux/client.js'] : []),
|
|
939
|
-
...extraClientScripts,
|
|
940
|
-
],
|
|
941
|
-
['/_davaux/styles.css', ...extraClientStylesheets],
|
|
942
|
-
effectiveMiddlewarePath,
|
|
943
|
-
basePath,
|
|
944
|
-
)
|
|
945
|
-
const entry: SiteEntry<T> = {
|
|
946
|
-
app,
|
|
947
|
-
config: site.config,
|
|
948
|
-
islandsPath,
|
|
949
|
-
stylesPath: sharedStylesPath,
|
|
950
|
-
clientPath,
|
|
951
|
-
publicDirs: [
|
|
952
|
-
...(site.publicDir ? [site.publicDir] : []),
|
|
953
|
-
...(config.publicDir ? [config.publicDir] : []),
|
|
954
|
-
],
|
|
955
|
-
}
|
|
956
|
-
const hostnames = Array.isArray(site.hostname) ? site.hostname : [site.hostname]
|
|
957
|
-
for (const h of hostnames) result.set(h, entry)
|
|
958
|
-
}
|
|
959
|
-
return result
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
let currentApps = buildCurrentApps()
|
|
963
|
-
|
|
964
|
-
// ─── HTTP server ─────────────────────────────────────────────────────────────
|
|
965
|
-
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
966
|
-
const host = (req.headers.host ?? '').split(':')[0]
|
|
967
|
-
const entry = currentApps.get(host) ?? currentApps.get('*')
|
|
968
|
-
|
|
969
|
-
if (!entry) {
|
|
970
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
971
|
-
res.end(`[davaux/multisite] No site registered for host: ${host}`)
|
|
972
|
-
return
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
if (entry.config !== undefined) siteConfigMap.set(req, entry.config)
|
|
976
|
-
const urlPath = req.url?.split('?')[0]
|
|
977
|
-
|
|
978
|
-
if (urlPath === '/_davaux/livereload') {
|
|
979
|
-
res.writeHead(200, {
|
|
980
|
-
'Content-Type': 'text/event-stream',
|
|
981
|
-
'Cache-Control': 'no-cache',
|
|
982
|
-
Connection: 'keep-alive',
|
|
983
|
-
})
|
|
984
|
-
res.write('\n')
|
|
985
|
-
sseClients.add(res)
|
|
986
|
-
req.on('close', () => sseClients.delete(res))
|
|
987
|
-
return
|
|
988
|
-
}
|
|
989
|
-
if (urlPath === '/_davaux/livereload.js') {
|
|
990
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
991
|
-
res.end(LIVERELOAD_SCRIPT)
|
|
992
|
-
return
|
|
993
|
-
}
|
|
994
|
-
if (urlPath === '/_davaux/livereload-worker.js') {
|
|
995
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
996
|
-
res.end(SHARED_WORKER_SCRIPT)
|
|
997
|
-
return
|
|
998
|
-
}
|
|
999
|
-
if (urlPath === '/_davaux/islands.js') {
|
|
1000
|
-
if (entry.islandsPath && existsSync(entry.islandsPath)) {
|
|
1001
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
1002
|
-
createReadStream(entry.islandsPath).pipe(res)
|
|
1003
|
-
} else {
|
|
1004
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
1005
|
-
res.end('Not Found')
|
|
1006
|
-
}
|
|
1007
|
-
return
|
|
1008
|
-
}
|
|
1009
|
-
if (urlPath === '/_davaux/client.js') {
|
|
1010
|
-
if (entry.clientPath && existsSync(entry.clientPath)) {
|
|
1011
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
1012
|
-
createReadStream(entry.clientPath).pipe(res)
|
|
1013
|
-
} else {
|
|
1014
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' })
|
|
1015
|
-
res.end('Not Found')
|
|
1016
|
-
}
|
|
1017
|
-
return
|
|
1018
|
-
}
|
|
1019
|
-
if (urlPath === '/_davaux/styles.css') {
|
|
1020
|
-
res.writeHead(200, { 'Content-Type': 'text/css' })
|
|
1021
|
-
if (existsSync(sharedStylesPath)) {
|
|
1022
|
-
createReadStream(sharedStylesPath).pipe(res)
|
|
1023
|
-
} else {
|
|
1024
|
-
res.end('')
|
|
1025
|
-
}
|
|
1026
|
-
return
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (urlPath) {
|
|
1030
|
-
const relPath = urlPath.replace(/^\/+/, '')
|
|
1031
|
-
if (relPath) {
|
|
1032
|
-
for (const dir of entry.publicDirs) {
|
|
1033
|
-
const filePath = join(dir, relPath)
|
|
1034
|
-
const normalizedDir = dir.endsWith('/') ? dir : `${dir}/`
|
|
1035
|
-
if (!filePath.startsWith(normalizedDir)) continue
|
|
1036
|
-
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
1037
|
-
res.writeHead(200, { 'Content-Type': contentTypeFor(filePath) })
|
|
1038
|
-
createReadStream(filePath).pipe(res)
|
|
1039
|
-
return
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
dispatch(req, res, entry.app).catch((err) => {
|
|
1046
|
-
console.error('[davaux/multisite] Unhandled dispatch error:', err)
|
|
1047
|
-
if (!res.headersSent) {
|
|
1048
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
1049
|
-
res.end('Internal Server Error')
|
|
1050
|
-
}
|
|
1051
|
-
})
|
|
1052
|
-
})
|
|
1053
|
-
|
|
1054
|
-
server.listen(port, hostname, () => {
|
|
1055
|
-
console.log(`\n davaux/multisite http://${hostname}:${port}\n`)
|
|
1056
|
-
})
|
|
1057
|
-
serverReady = true
|
|
1058
|
-
|
|
1059
|
-
// ─── Start watching ──────────────────────────────────────────────────────────
|
|
1060
|
-
await routesCtx.watch()
|
|
1061
|
-
for (const islandsCtx of islandCtxMap.values()) await islandsCtx.watch()
|
|
1062
|
-
for (const clientCtx of clientCtxMap.values()) await clientCtx.watch()
|
|
1063
|
-
console.log(' Watching for changes...')
|
|
1064
|
-
|
|
1065
|
-
// ─── Structural rebuild ───────────────────────────────────────────────────────
|
|
1066
|
-
// Triggered when route or island files are added or removed. esbuild's built-in
|
|
1067
|
-
// watch handles content changes; fsWatch fires only 'rename' events (create/delete).
|
|
1068
|
-
|
|
1069
|
-
let isRebuilding = false
|
|
1070
|
-
let rebuildTimer: ReturnType<typeof setTimeout> | undefined
|
|
1071
|
-
|
|
1072
|
-
async function handleStructuralChange(): Promise<void> {
|
|
1073
|
-
// Re-scan all directories with current state
|
|
1074
|
-
baseScanRaw = baseDir
|
|
1075
|
-
? await scanRoutes(baseDir, allSuffixes)
|
|
1076
|
-
: { routes: [], layouts: [], middlewares: [] }
|
|
1077
|
-
for (const site of sites) {
|
|
1078
|
-
siteScanMap.set(
|
|
1079
|
-
site.name,
|
|
1080
|
-
site.routesDir
|
|
1081
|
-
? await scanRoutes(site.routesDir, allSuffixes)
|
|
1082
|
-
: { routes: [], layouts: [], middlewares: [] },
|
|
1083
|
-
)
|
|
1084
|
-
}
|
|
1085
|
-
const newBaseIslands: IslandFile[] = baseIslandsDir ? await scanIslands(baseIslandsDir) : []
|
|
1086
|
-
siteIslandsMap.clear()
|
|
1087
|
-
for (const site of sites) {
|
|
1088
|
-
const siteIslands = site.islandsDir ? await scanIslands(site.islandsDir) : []
|
|
1089
|
-
const combined = [...newBaseIslands, ...siteIslands]
|
|
1090
|
-
if (combined.length > 0) siteIslandsMap.set(site.name, combined)
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// Recreate routes context (all were disposed in onStructuralChange)
|
|
1094
|
-
const routeFiles = currentRouteFiles()
|
|
1095
|
-
if (routeFiles.length > 0) {
|
|
1096
|
-
routesCtx = await context(routesCtxOptions())
|
|
1097
|
-
await routesCtx.rebuild()
|
|
1098
|
-
await routesCtx.watch()
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Recreate all island contexts
|
|
1102
|
-
islandCtxMap.clear()
|
|
1103
|
-
for (const [siteName, allIslands] of siteIslandsMap) {
|
|
1104
|
-
const islandsCtx = await context(
|
|
1105
|
-
islandsCtxOptions(allIslands, join(dauxDir, siteName, 'islands.js')),
|
|
1106
|
-
)
|
|
1107
|
-
await islandsCtx.rebuild()
|
|
1108
|
-
await islandsCtx.watch()
|
|
1109
|
-
islandCtxMap.set(siteName, islandsCtx)
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
currentApps = buildCurrentApps()
|
|
1113
|
-
scheduleReload()
|
|
1114
|
-
console.log('[davaux/multisite] Routes updated')
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function onStructuralChange(): void {
|
|
1118
|
-
// Dispose immediately so esbuild doesn't try to rebuild a stale entry list
|
|
1119
|
-
// (e.g. an entry point file that was just deleted).
|
|
1120
|
-
routesCtx.dispose().catch(() => {})
|
|
1121
|
-
for (const ctx of islandCtxMap.values()) ctx.dispose().catch(() => {})
|
|
1122
|
-
|
|
1123
|
-
if (rebuildTimer) clearTimeout(rebuildTimer)
|
|
1124
|
-
rebuildTimer = setTimeout(() => {
|
|
1125
|
-
if (isRebuilding) return
|
|
1126
|
-
isRebuilding = true
|
|
1127
|
-
handleStructuralChange()
|
|
1128
|
-
.catch((err) => console.error('[davaux/multisite] Route rebuild failed:', err))
|
|
1129
|
-
.finally(() => {
|
|
1130
|
-
isRebuilding = false
|
|
1131
|
-
})
|
|
1132
|
-
}, 200)
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
const routeWatchDirs = [
|
|
1136
|
-
...(baseDir ? [baseDir] : []),
|
|
1137
|
-
...sites.flatMap((s) => (s.routesDir ? [s.routesDir] : [])),
|
|
1138
|
-
]
|
|
1139
|
-
for (const dir of routeWatchDirs) {
|
|
1140
|
-
if (!existsSync(dir)) continue
|
|
1141
|
-
fsWatch(dir, { recursive: true }, (event, filename) => {
|
|
1142
|
-
if (event !== 'rename' || !filename) return
|
|
1143
|
-
if (!/\.(tsx?|jsx?|mdx?)$/.test(filename)) return
|
|
1144
|
-
onStructuralChange()
|
|
1145
|
-
})
|
|
1146
|
-
}
|
|
1147
|
-
for (const dir of allIslandDirs) {
|
|
1148
|
-
if (!existsSync(dir)) continue
|
|
1149
|
-
fsWatch(dir, { recursive: true }, (event, filename) => {
|
|
1150
|
-
if (event !== 'rename' || !filename) return
|
|
1151
|
-
if (!/\.(tsx?|jsx?|mdx?)$/.test(filename)) return
|
|
1152
|
-
onStructuralChange()
|
|
1153
|
-
})
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
startTypeChecker(cwd)
|
|
1157
|
-
|
|
1158
|
-
return server
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// ─── Convenience wrapper ──────────────────────────────────────────────────────
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Build and start a multisite server in one call.
|
|
1165
|
-
*
|
|
1166
|
-
* Combines `buildMultisiteApps` and `startMultisiteServer`. Automatically sets
|
|
1167
|
-
* `isDev` from `NODE_ENV` unless explicitly provided — `true` in development
|
|
1168
|
-
* (cache-busting for TypeScript routes), `false` in production.
|
|
1169
|
-
*
|
|
1170
|
-
* @example
|
|
1171
|
-
* // server.ts
|
|
1172
|
-
* import { startMultisite } from '@davaux/multisite'
|
|
1173
|
-
* import { sites } from './multisite.config.js'
|
|
1174
|
-
*
|
|
1175
|
-
* startMultisite(sites, { port: 3000, hostname: 'localhost' })
|
|
1176
|
-
*/
|
|
1177
|
-
export async function startMultisite<T = Record<string, unknown>>(
|
|
1178
|
-
config: MultisiteConfig<T>,
|
|
1179
|
-
options: StartMultisiteOptions = {},
|
|
1180
|
-
): Promise<ReturnType<typeof createServer>> {
|
|
1181
|
-
const { port, hostname, isDev = process.env.NODE_ENV !== 'production', ...buildOpts } = options
|
|
1182
|
-
|
|
1183
|
-
// Auto-detect src/middleware.ts when cwd is set and no middleware path is explicitly provided
|
|
1184
|
-
const cwd = buildOpts.cwd
|
|
1185
|
-
if (cwd && !buildOpts.appMiddlewarePath && !buildOpts.middlewareSrc) {
|
|
1186
|
-
if (isDev) {
|
|
1187
|
-
const mwSrc = join(cwd, 'src', 'middleware.ts')
|
|
1188
|
-
if (existsSync(mwSrc)) buildOpts.middlewareSrc = mwSrc
|
|
1189
|
-
} else {
|
|
1190
|
-
const mwCompiled = join(cwd, 'src', 'middleware.js')
|
|
1191
|
-
if (existsSync(mwCompiled)) buildOpts.appMiddlewarePath = mwCompiled
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (isDev) return startMultisiteDev(config, { ...buildOpts, port, hostname })
|
|
1196
|
-
|
|
1197
|
-
const apps = await buildMultisiteApps(config, { ...buildOpts, isDev: false })
|
|
1198
|
-
return startMultisiteServer(apps, { port, hostname })
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// ─── Runtime helpers ──────────────────────────────────────────────────────────
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Retrieve the current site's config from a request context.
|
|
1205
|
-
*
|
|
1206
|
-
* Returns `undefined` when called outside of a multisite server (e.g. in tests
|
|
1207
|
-
* using a single-site `startServer`).
|
|
1208
|
-
*
|
|
1209
|
-
* @example
|
|
1210
|
-
* import { getSite } from '@davaux/multisite'
|
|
1211
|
-
*
|
|
1212
|
-
* export default definePage((ctx) => {
|
|
1213
|
-
* const site = getSite<MySiteConfig>(ctx)
|
|
1214
|
-
* return <Layout theme={site?.theme}>...</Layout>
|
|
1215
|
-
* })
|
|
1216
|
-
*/
|
|
1217
|
-
export function getSite<T = Record<string, unknown>>(ctx: RequestContext): T | undefined {
|
|
1218
|
-
return siteConfigMap.get(ctx.req) as T | undefined
|
|
1219
|
-
}
|