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