@taujs/server 0.4.2 → 0.4.3

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.
@@ -6,7 +6,6 @@ type DebugCategory = (typeof DEBUG_CATEGORIES)[number];
6
6
  type DebugConfig = boolean | DebugCategory[] | ({
7
7
  all?: boolean;
8
8
  } & Partial<Record<DebugCategory, boolean>>);
9
- type LogLevel = 'debug' | 'info' | 'warn' | 'error';
10
9
  interface BaseLogger {
11
10
  debug?(meta?: Record<string, unknown>, message?: string): void;
12
11
  info?(meta?: Record<string, unknown>, message?: string): void;
@@ -23,41 +22,8 @@ interface Logs extends BaseLogger {
23
22
  child(context: Record<string, unknown>): Logs;
24
23
  isDebugEnabled(category: DebugCategory): boolean;
25
24
  }
26
- declare class Logger implements Logs {
27
- private config;
28
- private debugEnabled;
29
- private context;
30
- constructor(config?: {
31
- custom?: BaseLogger;
32
- context?: Record<string, unknown>;
33
- minLevel?: LogLevel;
34
- includeStack?: boolean | ((level: LogLevel) => boolean);
35
- includeContext?: boolean | ((level: LogLevel) => boolean);
36
- singleLine?: boolean;
37
- });
38
- child(context: Record<string, unknown>): Logger;
39
- configure(debug?: DebugConfig): void;
40
- isDebugEnabled(category: DebugCategory): boolean;
41
- private shouldEmit;
42
- private shouldIncludeStack;
43
- private stripStacks;
44
- private formatTimestamp;
45
- private emit;
46
- info(meta?: unknown, message?: string): void;
47
- warn(meta?: unknown, message?: string): void;
48
- error(meta?: unknown, message?: string): void;
49
- debug(category: DebugCategory, meta?: unknown, message?: string): void;
50
- }
51
- declare function createLogger(opts?: {
52
- debug?: DebugConfig | string | boolean;
53
- custom?: BaseLogger;
54
- context?: Record<string, unknown>;
55
- minLevel?: LogLevel;
56
- includeStack?: boolean | ((level: LogLevel) => boolean);
57
- includeContext?: boolean | ((level: LogLevel) => boolean);
58
- singleLine?: boolean;
59
- }): Logger;
60
25
 
26
+ type RegistryCaller<R extends ServiceRegistry = ServiceRegistry> = (serviceName: keyof R & string, methodName: string, args?: JsonObject) => Promise<JsonObject>;
61
27
  type JsonPrimitive = string | number | boolean | null;
62
28
  type JsonValue = JsonPrimitive | JsonValue[] | {
63
29
  [k: string]: JsonValue;
@@ -77,7 +43,9 @@ type ServiceContext = {
77
43
  id: string;
78
44
  roles: string[];
79
45
  } | null;
46
+ call?: (service: string, method: string, args?: JsonObject) => Promise<JsonObject>;
80
47
  };
48
+ declare function withDeadline(signal: AbortSignal | undefined, ms?: number): AbortSignal | undefined;
81
49
  type ServiceMethod<P, R extends JsonObject = JsonObject> = (params: P, ctx: ServiceContext) => Promise<R>;
82
50
  type ServiceDefinition = Readonly<Record<string, ServiceMethod<any, JsonObject>>>;
83
51
  type ServiceRegistry = Readonly<Record<string, ServiceDefinition>>;
@@ -94,6 +62,7 @@ declare function defineService<T extends Record<string, ServiceMethod<any, JsonO
94
62
  handler: ServiceMethod<infer P, infer R_1>;
95
63
  } ? ServiceMethod<P, R_1> : never; };
96
64
  declare const defineServiceRegistry: <R extends ServiceRegistry>(registry: R) => R;
65
+ declare function callServiceMethod(registry: ServiceRegistry, serviceName: string, methodName: string, params: JsonObject | undefined, ctx: ServiceContext): Promise<JsonObject>;
97
66
 
98
67
  type RequestContext<L extends Logs = Logs> = {
99
68
  traceId: string;
@@ -122,7 +91,11 @@ type BaseMiddleware = {
122
91
  csp?: RouteCSPConfig | false;
123
92
  };
124
93
  type DataResult = Record<string, unknown> | ServiceDescriptor;
125
- type DataHandler<Params extends PathToRegExpParams, L extends Logs = Logs> = (params: Params, ctx: RequestContext<L> & {
94
+ type RequestServiceContext<L extends Logs = Logs> = RequestContext<L> & {
95
+ call: RegistryCaller<ServiceRegistry>;
96
+ headers?: Record<string, string>;
97
+ };
98
+ type DataHandler<Params extends PathToRegExpParams, L extends Logs = Logs> = (params: Params, ctx: RequestServiceContext<L> & {
126
99
  [key: string]: unknown;
127
100
  }) => Promise<DataResult>;
128
101
  type PathToRegExpParams = Partial<Record<string, string | string[]>>;
@@ -163,6 +136,35 @@ type CSPViolationReport = {
163
136
  disposition?: 'enforce' | 'report';
164
137
  };
165
138
 
139
+ type ErrorKind = 'infra' | 'upstream' | 'domain' | 'validation' | 'auth' | 'canceled' | 'timeout';
140
+ declare class AppError extends Error {
141
+ readonly kind: ErrorKind;
142
+ readonly httpStatus: number;
143
+ readonly details?: unknown;
144
+ readonly safeMessage: string;
145
+ readonly code?: string;
146
+ constructor(message: string, kind: ErrorKind, options?: {
147
+ httpStatus?: number;
148
+ details?: unknown;
149
+ cause?: unknown;
150
+ safeMessage?: string;
151
+ code?: string;
152
+ });
153
+ private getSafeMessage;
154
+ private serialiseValue;
155
+ toJSON(): any;
156
+ static notFound(message: string, details?: unknown, code?: string): AppError;
157
+ static forbidden(message: string, details?: unknown, code?: string): AppError;
158
+ static badRequest(message: string, details?: unknown, code?: string): AppError;
159
+ static unprocessable(message: string, details?: unknown, code?: string): AppError;
160
+ static timeout(message: string, details?: unknown, code?: string): AppError;
161
+ static canceled(message: string, details?: unknown, code?: string): AppError;
162
+ static internal(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
163
+ static upstream(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
164
+ static serviceUnavailable(message: string, cause?: unknown, details?: unknown, code?: string): AppError;
165
+ static from(err: unknown, fallback?: string): AppError;
166
+ }
167
+
166
168
  /**
167
169
  * τjs [ taujs ] Orchestration System
168
170
  * (c) 2024-present Aoede Ltd
@@ -195,15 +197,15 @@ type AppConfig = {
195
197
  routes?: AppRoute[];
196
198
  };
197
199
  type TaujsConfig = {
200
+ apps: AppConfig[];
201
+ security?: SecurityConfig;
198
202
  server?: {
199
203
  host?: string;
200
204
  port?: number;
201
205
  hmrPort?: number;
202
206
  };
203
- security?: SecurityConfig;
204
- apps: AppConfig[];
205
207
  };
206
208
 
207
209
  declare function defineConfig<T extends TaujsConfig>(config: T): T;
208
210
 
209
- export { type AppConfig as A, type BaseLogger as B, type DebugConfig as D, type InitialRouteParams as I, type ServiceRegistry as S, type TaujsConfig as T, type SecurityConfig as a, type AppRoute as b, createLogger as c, defineConfig as d, defineServiceRegistry as e, defineService as f, type ServiceContext as g };
211
+ export { type AppConfig as A, type BaseLogger as B, type DebugConfig as D, type InitialRouteParams as I, type RegistryCaller as R, type ServiceRegistry as S, type TaujsConfig as T, type SecurityConfig as a, type AppRoute as b, callServiceMethod as c, defineConfig as d, defineService as e, defineServiceRegistry as f, type ServiceContext as g, AppError as h, withDeadline as w };
package/dist/Config.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import 'fastify';
2
2
  import 'vite';
3
- export { A as AppConfig, b as AppRoute, a as SecurityConfig, g as ServiceContext, T as TaujsConfig, d as defineConfig, f as defineService, e as defineServiceRegistry } from './Config-BuUuMjmO.js';
3
+ export { A as AppConfig, h as AppError, b as AppRoute, R as RegistryCaller, a as SecurityConfig, g as ServiceContext, T as TaujsConfig, c as callServiceMethod, d as defineConfig, e as defineService, f as defineServiceRegistry, w as withDeadline } from './Config-CEhfq8Mm.js';
package/dist/Config.js CHANGED
@@ -1,9 +1,137 @@
1
1
  // src/utils/DataServices.ts
2
2
  import { performance } from "perf_hooks";
3
+
4
+ // src/logging/AppError.ts
5
+ var HTTP_STATUS = {
6
+ infra: 500,
7
+ upstream: 502,
8
+ domain: 404,
9
+ validation: 400,
10
+ auth: 403,
11
+ canceled: 499,
12
+ // Client Closed Request (nginx convention)
13
+ timeout: 504
14
+ };
15
+ var AppError = class _AppError extends Error {
16
+ kind;
17
+ httpStatus;
18
+ details;
19
+ safeMessage;
20
+ code;
21
+ constructor(message, kind, options = {}) {
22
+ super(message);
23
+ this.name = "AppError";
24
+ Object.setPrototypeOf(this, new.target.prototype);
25
+ if (options.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", {
27
+ value: options.cause,
28
+ enumerable: false,
29
+ writable: false,
30
+ configurable: true
31
+ });
32
+ }
33
+ this.kind = kind;
34
+ this.httpStatus = options.httpStatus ?? HTTP_STATUS[kind];
35
+ this.details = options.details;
36
+ this.safeMessage = options.safeMessage ?? this.getSafeMessage(kind, message);
37
+ this.code = options.code;
38
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
39
+ }
40
+ getSafeMessage(kind, message) {
41
+ return kind === "domain" || kind === "validation" || kind === "auth" ? message : "Internal Server Error";
42
+ }
43
+ serialiseValue(value, seen = /* @__PURE__ */ new WeakSet()) {
44
+ if (value === null || value === void 0) return value;
45
+ if (typeof value !== "object") return value;
46
+ if (seen.has(value)) return "[circular]";
47
+ seen.add(value);
48
+ if (value instanceof Error) {
49
+ return {
50
+ name: value.name,
51
+ message: value.message,
52
+ stack: value.stack,
53
+ ...value instanceof _AppError && {
54
+ kind: value.kind,
55
+ httpStatus: value.httpStatus,
56
+ code: value.code
57
+ }
58
+ };
59
+ }
60
+ if (Array.isArray(value)) return value.map((item) => this.serialiseValue(item, seen));
61
+ const result = {};
62
+ for (const [key, val] of Object.entries(value)) {
63
+ result[key] = this.serialiseValue(val, seen);
64
+ }
65
+ return result;
66
+ }
67
+ toJSON() {
68
+ return {
69
+ name: this.name,
70
+ kind: this.kind,
71
+ message: this.message,
72
+ safeMessage: this.safeMessage,
73
+ httpStatus: this.httpStatus,
74
+ ...this.code && { code: this.code },
75
+ details: this.serialiseValue(this.details),
76
+ stack: this.stack,
77
+ ...this.cause && {
78
+ cause: this.serialiseValue(this.cause)
79
+ }
80
+ };
81
+ }
82
+ static notFound(message, details, code) {
83
+ return new _AppError(message, "domain", { httpStatus: 404, details, code });
84
+ }
85
+ static forbidden(message, details, code) {
86
+ return new _AppError(message, "auth", { httpStatus: 403, details, code });
87
+ }
88
+ static badRequest(message, details, code) {
89
+ return new _AppError(message, "validation", { httpStatus: 400, details, code });
90
+ }
91
+ static unprocessable(message, details, code) {
92
+ return new _AppError(message, "validation", { httpStatus: 422, details, code });
93
+ }
94
+ static timeout(message, details, code) {
95
+ return new _AppError(message, "timeout", { details, code });
96
+ }
97
+ static canceled(message, details, code) {
98
+ return new _AppError(message, "canceled", { details, code });
99
+ }
100
+ static internal(message, cause, details, code) {
101
+ return new _AppError(message, "infra", { cause, details, code });
102
+ }
103
+ static upstream(message, cause, details, code) {
104
+ return new _AppError(message, "upstream", { cause, details, code });
105
+ }
106
+ static serviceUnavailable(message, cause, details, code) {
107
+ return new _AppError(message, "infra", { httpStatus: 503, cause, details, code });
108
+ }
109
+ static from(err, fallback = "Internal error") {
110
+ return err instanceof _AppError ? err : _AppError.internal(err?.message ?? fallback, err);
111
+ }
112
+ };
113
+
114
+ // src/utils/DataServices.ts
3
115
  var runSchema = (schema, input) => {
4
116
  if (!schema) return input;
5
117
  return typeof schema.parse === "function" ? schema.parse(input) : schema(input);
6
118
  };
119
+ function withDeadline(signal, ms) {
120
+ if (!ms) return signal;
121
+ const ctrl = new AbortController();
122
+ const onAbort = () => ctrl.abort(signal?.reason ?? new Error("Aborted"));
123
+ signal?.addEventListener("abort", onAbort, { once: true });
124
+ const t = setTimeout(() => ctrl.abort(new Error("DeadlineExceeded")), ms);
125
+ ctrl.signal.addEventListener(
126
+ "abort",
127
+ () => {
128
+ clearTimeout(t);
129
+ signal?.removeEventListener("abort", onAbort);
130
+ },
131
+ { once: true }
132
+ );
133
+ return ctrl.signal;
134
+ }
7
135
  function defineService(spec) {
8
136
  const out = {};
9
137
  for (const [name, v] of Object.entries(spec)) {
@@ -21,6 +149,38 @@ function defineService(spec) {
21
149
  return Object.freeze(out);
22
150
  }
23
151
  var defineServiceRegistry = (registry) => Object.freeze(Object.fromEntries(Object.entries(registry).map(([k, v]) => [k, Object.freeze(v)])));
152
+ async function callServiceMethod(registry, serviceName, methodName, params, ctx) {
153
+ if (ctx.signal?.aborted) throw AppError.timeout("Request canceled");
154
+ const service = registry[serviceName];
155
+ if (!service) throw AppError.notFound(`Unknown service: ${serviceName}`);
156
+ const method = service[methodName];
157
+ if (!method) throw AppError.notFound(`Unknown method: ${serviceName}.${methodName}`);
158
+ const logger = ctx.logger?.child?.({
159
+ component: "service-call",
160
+ service: serviceName,
161
+ method: methodName,
162
+ traceId: ctx.traceId
163
+ });
164
+ const t0 = performance.now();
165
+ try {
166
+ const result = await method(params ?? {}, ctx);
167
+ if (typeof result !== "object" || result === null) {
168
+ throw AppError.internal(`Non-object result from ${serviceName}.${methodName}`);
169
+ }
170
+ logger?.debug?.({ ms: +(performance.now() - t0).toFixed(1) }, "Service method ok");
171
+ return result;
172
+ } catch (err) {
173
+ logger?.error?.(
174
+ {
175
+ params,
176
+ error: err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : String(err),
177
+ ms: +(performance.now() - t0).toFixed(1)
178
+ },
179
+ "Service method failed"
180
+ );
181
+ throw err instanceof AppError ? err : err instanceof Error ? AppError.internal(err.message, { cause: err }) : AppError.internal("Unknown error", { details: { err } });
182
+ }
183
+ }
24
184
 
25
185
  // src/Config.ts
26
186
  function defineConfig(config) {
@@ -28,7 +188,10 @@ function defineConfig(config) {
28
188
  return config;
29
189
  }
30
190
  export {
191
+ AppError,
192
+ callServiceMethod,
31
193
  defineConfig,
32
194
  defineService,
33
- defineServiceRegistry
195
+ defineServiceRegistry,
196
+ withDeadline
34
197
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FastifyPluginCallback, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { T as TaujsConfig, S as ServiceRegistry, D as DebugConfig, B as BaseLogger, A as AppConfig } from './Config-BuUuMjmO.js';
3
- export { I as InitialRouteParams, c as createLogger } from './Config-BuUuMjmO.js';
4
- import 'vite';
2
+ import { T as TaujsConfig, S as ServiceRegistry, D as DebugConfig, B as BaseLogger, A as AppConfig } from './Config-CEhfq8Mm.js';
3
+ export { I as InitialRouteParams } from './Config-CEhfq8Mm.js';
4
+ import { InlineConfig } from 'vite';
5
5
 
6
6
  type StaticMountEntry = {
7
7
  plugin: FastifyPluginCallback<any> | FastifyPluginAsync<any>;
@@ -42,13 +42,84 @@ declare const createServer: (opts: CreateServerOptions) => Promise<CreateServerR
42
42
  * including CSR, SSR, streaming, and middleware composition.
43
43
  */
44
44
 
45
- declare function taujsBuild({ config, projectRoot, clientBaseDir, isSSRBuild, }: {
45
+ type ViteBuildContext = {
46
+ appId: string;
47
+ entryPoint: string;
48
+ isSSRBuild: boolean;
49
+ clientRoot: string;
50
+ };
51
+ /**
52
+ * User-supplied vite config override.
53
+ * Can be a static config object or a function that receives build context.
54
+ *
55
+ * **Plugin order**: Framework applies plugins in this sequence:
56
+ * 1. `appConfig.plugins` (from taujs.config.ts)
57
+ * 2. `nodePolyfills({ include: ['stream'] })` (client builds only)
58
+ * 3. `userViteConfig.plugins` (from this option)
59
+ *
60
+ * If you need plugins before nodePolyfills, add them to `appConfig.plugins` instead.
61
+ *
62
+ * **Allowed customisations:**
63
+ * - `plugins`: Appended to framework plugin list
64
+ * - `define`: Shallow-merged with framework defines
65
+ * - `css.preprocessorOptions`: Deep-merged by preprocessor engine (scss, less, etc.)
66
+ * - `build.sourcemap`, `minify`, `terserOptions`: Direct overrides
67
+ * - `build.rollupOptions.external`: Direct override
68
+ * - `build.rollupOptions.output.manualChunks`: Merged into output config
69
+ * - `resolve.*` (except `alias`): Merged with framework resolve config
70
+ * - `esbuild`, `logLevel`, `optimizeDeps`: Direct overrides
71
+ *
72
+ * **Protected fields (cannot override):**
73
+ * - `root`, `base`, `publicDir`: Framework-controlled per-app paths
74
+ * - `build.outDir`: Framework manages `dist/client` vs `dist/ssr` separation
75
+ * - `build.ssr`, `ssrManifest`, `format`, `target`: Framework-controlled for SSR integrity
76
+ * - `build.rollupOptions.input`: Framework manages entry points
77
+ * - `resolve.alias`: Use top-level `alias` option in taujsBuild() instead
78
+ * - `server.*`: Ignored in builds (dev-mode only; configure in DevServer.ts)
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * // Static config
83
+ * vite: {
84
+ * plugins: [visualizer()],
85
+ * build: { sourcemap: 'inline' }
86
+ * }
87
+ *
88
+ * // Function-based (conditional per app/mode)
89
+ * vite: ({ isSSRBuild, entryPoint }) => ({
90
+ * plugins: isSSRBuild ? [] : [visualizer()],
91
+ * logLevel: entryPoint === 'admin' ? 'info' : 'warn'
92
+ * })
93
+ * ```
94
+ */
95
+ type ViteConfigOverride = Partial<InlineConfig> | ((ctx: ViteBuildContext) => Partial<InlineConfig>);
96
+ declare function taujsBuild({ config, projectRoot, clientBaseDir, isSSRBuild, alias: userAlias, vite: userViteConfig, }: {
46
97
  config: {
47
98
  apps: AppConfig[];
48
99
  };
49
100
  projectRoot: string;
50
101
  clientBaseDir: string;
51
102
  isSSRBuild?: boolean;
103
+ /**
104
+ * Top-level alias overrides. Use this instead of `vite.resolve.alias`.
105
+ * User aliases are merged with framework defaults; user values win on conflicts.
106
+ *
107
+ * Framework provides:
108
+ * - `@client`: Resolves to current app's root
109
+ * - `@server`: Resolves to `src/server`
110
+ * - `@shared`: Resolves to `src/shared`
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * alias: {
115
+ * '@utils': './src/utils',
116
+ * '@server': './custom-server', // overrides framework default
117
+ * }
118
+ * ```
119
+ */
120
+ alias?: Record<string, string>;
121
+ /** User-supplied Vite config overrides (plugins, tuning, etc.) */
122
+ vite?: ViteConfigOverride;
52
123
  }): Promise<void>;
53
124
 
54
125
  interface MessageMetaLogger {
@@ -60,4 +131,4 @@ interface MessageMetaLogger {
60
131
  }
61
132
  declare function winstonAdapter(winston: MessageMetaLogger): BaseLogger;
62
133
 
63
- export { createServer, taujsBuild, winstonAdapter };
134
+ export { BaseLogger, createServer, taujsBuild, winstonAdapter };
package/dist/index.js CHANGED
@@ -189,7 +189,7 @@ var require_plugin = __commonJS({
189
189
 
190
190
  // src/CreateServer.ts
191
191
  var import_picocolors4 = __toESM(require_picocolors(), 1);
192
- import path5 from "path";
192
+ import path7 from "path";
193
193
  import { performance as performance3 } from "perf_hooks";
194
194
  import fastifyStatic from "@fastify/static";
195
195
  import Fastify from "fastify";
@@ -259,8 +259,8 @@ var extractRoutes = (taujsConfig) => {
259
259
  apps.push({ appId: app.appId, routeCount: appRoutes.length });
260
260
  allRoutes.push(...appRoutes);
261
261
  }
262
- for (const [path7, appIds] of pathTracker.entries()) {
263
- if (appIds.length > 1) warnings.push(`Route path "${path7}" is declared in multiple apps: ${appIds.join(", ")}`);
262
+ for (const [path9, appIds] of pathTracker.entries()) {
263
+ if (appIds.length > 1) warnings.push(`Route path "${path9}" is declared in multiple apps: ${appIds.join(", ")}`);
264
264
  }
265
265
  const sortedRoutes = allRoutes.sort((a, b) => computeScore(b.path) - computeScore(a.path));
266
266
  const durationMs = performance.now() - t0;
@@ -344,8 +344,8 @@ function printContractReport(logger, report) {
344
344
  }
345
345
  }
346
346
  }
347
- var computeScore = (path7) => {
348
- return path7.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
347
+ var computeScore = (path9) => {
348
+ return path9.split("/").filter(Boolean).reduce((score, segment) => score + (segment.startsWith(":") ? 1 : 10), 0);
349
349
  };
350
350
 
351
351
  // src/logging/AppError.ts
@@ -609,9 +609,11 @@ var Logger = class _Logger {
609
609
  if (!this.shouldEmit(level)) return;
610
610
  const timestamp = this.formatTimestamp();
611
611
  const wantCtx = this.config.includeContext === void 0 ? false : typeof this.config.includeContext === "function" ? this.config.includeContext(level) : this.config.includeContext;
612
- const customSink = this.config.custom?.[level];
612
+ const owner = this.config.custom;
613
+ const rawSink = owner && typeof owner[level] === "function" ? owner[level] : void 0;
614
+ const boundSink = rawSink ? rawSink.bind(owner) : void 0;
613
615
  const consoleFallback = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
614
- const sink = customSink ?? consoleFallback;
616
+ const hasCustom = !!boundSink;
615
617
  const merged = meta ?? {};
616
618
  const withCtx = wantCtx && Object.keys(this.context).length > 0 ? { context: this.context, ...merged } : merged;
617
619
  const finalMeta = this.shouldIncludeStack(level) ? withCtx : this.stripStacks(withCtx);
@@ -632,16 +634,20 @@ var Logger = class _Logger {
632
634
  return plainTag;
633
635
  }
634
636
  })();
635
- const tagForOutput = customSink ? plainTag : coloredTag;
637
+ const tagForOutput = hasCustom ? plainTag : coloredTag;
636
638
  const formatted = `${timestamp} ${tagForOutput} ${message}`;
637
- if (this.config.singleLine && hasMeta && !customSink) {
639
+ if (this.config.singleLine && hasMeta && !hasCustom) {
638
640
  const metaStr = JSON.stringify(finalMeta).replace(/\n/g, "\\n");
639
641
  consoleFallback(`${formatted} ${metaStr}`);
640
642
  return;
641
643
  }
642
- if (customSink) {
644
+ if (hasCustom) {
643
645
  const obj = hasMeta ? finalMeta : {};
644
- customSink(obj, formatted);
646
+ try {
647
+ const result = boundSink(obj, formatted);
648
+ } catch (err) {
649
+ hasMeta ? consoleFallback(formatted, finalMeta) : consoleFallback(formatted);
650
+ }
645
651
  } else {
646
652
  hasMeta ? consoleFallback(formatted, finalMeta) : consoleFallback(formatted);
647
653
  }
@@ -839,6 +845,7 @@ var verifyContracts = (app, routes, contracts, security) => {
839
845
 
840
846
  // src/SSRServer.ts
841
847
  var import_fastify_plugin3 = __toESM(require_plugin(), 1);
848
+ import path6 from "path";
842
849
 
843
850
  // src/logging/utils/index.ts
844
851
  var httpStatusFrom = (err, fallback = 500) => err instanceof AppError ? err.httpStatus : fallback;
@@ -879,6 +886,12 @@ import { match } from "path-to-regexp";
879
886
 
880
887
  // src/utils/DataServices.ts
881
888
  import { performance as performance2 } from "perf_hooks";
889
+ function createCaller(registry, ctx) {
890
+ return (serviceName, methodName, args) => callServiceMethod(registry, serviceName, methodName, args ?? {}, ctx);
891
+ }
892
+ function ensureServiceCaller(registry, ctx) {
893
+ if (!ctx.call) ctx.call = createCaller(registry, ctx);
894
+ }
882
895
  async function callServiceMethod(registry, serviceName, methodName, params, ctx) {
883
896
  if (ctx.signal?.aborted) throw AppError.timeout("Request canceled");
884
897
  const service = registry[serviceName];
@@ -929,15 +942,15 @@ var safeDecode = (value) => {
929
942
  return value;
930
943
  }
931
944
  };
932
- var cleanPath = (path7) => {
933
- if (!path7) return "/";
934
- const basePart = path7.split("?")[0];
945
+ var cleanPath = (path9) => {
946
+ if (!path9) return "/";
947
+ const basePart = path9.split("?")[0];
935
948
  const base = basePart ? basePart.split("#")[0] : "/";
936
949
  return base || "/";
937
950
  };
938
- var calculateSpecificity = (path7) => {
951
+ var calculateSpecificity = (path9) => {
939
952
  let score = 0;
940
- const segments = path7.split("/").filter(Boolean);
953
+ const segments = path9.split("/").filter(Boolean);
941
954
  for (const segment of segments) {
942
955
  if (segment.startsWith(":")) {
943
956
  score += 1;
@@ -962,9 +975,9 @@ var createRouteMatchers = (routes) => {
962
975
  });
963
976
  };
964
977
  var matchRoute = (url, routeMatchers) => {
965
- const path7 = cleanPath(url);
978
+ const path9 = cleanPath(url);
966
979
  for (const { route, matcher, keys } of routeMatchers) {
967
- const match2 = matcher(path7);
980
+ const match2 = matcher(path9);
968
981
  if (match2) {
969
982
  return {
970
983
  route,
@@ -978,14 +991,16 @@ var matchRoute = (url, routeMatchers) => {
978
991
  var fetchInitialData = async (attr, params, serviceRegistry, ctx, callServiceMethodImpl = callServiceMethod) => {
979
992
  const dataHandler = attr?.data;
980
993
  if (!dataHandler || typeof dataHandler !== "function") return {};
994
+ const ctxForData = {
995
+ ...ctx,
996
+ headers: ctx.headers ?? {}
997
+ };
998
+ ensureServiceCaller(serviceRegistry, ctxForData);
981
999
  try {
982
- const result = await dataHandler(params, {
983
- ...ctx,
984
- headers: ctx.headers ?? {}
985
- });
1000
+ const result = await dataHandler(params, ctxForData);
986
1001
  if (isServiceDescriptor(result)) {
987
1002
  const { serviceName, serviceMethod, args } = result;
988
- return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {}, ctx);
1003
+ return callServiceMethodImpl(serviceRegistry, serviceName, serviceMethod, args ?? {}, ctxForData);
989
1004
  }
990
1005
  if (isPlainObject(result)) return result;
991
1006
  throw AppError.badRequest("attr.data must return a plain object or a ServiceDescriptor");
@@ -1102,9 +1117,9 @@ var mergeDirectives = (base, override) => {
1102
1117
  }
1103
1118
  return merged;
1104
1119
  };
1105
- var findMatchingRoute = (routeMatchers, path7) => {
1120
+ var findMatchingRoute = (routeMatchers, path9) => {
1106
1121
  if (!routeMatchers) return null;
1107
- const match2 = matchRoute(path7, routeMatchers);
1122
+ const match2 = matchRoute(path9, routeMatchers);
1108
1123
  return match2 ? { route: match2.route, params: match2.params } : null;
1109
1124
  };
1110
1125
  var cspPlugin = (0, import_fastify_plugin.default)(
@@ -1434,8 +1449,9 @@ var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssL
1434
1449
  debug: opts.debug,
1435
1450
  includeContext: true
1436
1451
  });
1452
+ const projectRoot = opts.projectRoot ?? path2.resolve(process.cwd());
1437
1453
  for (const config of processedConfigs) {
1438
- const { clientRoot, entryClient, entryServer, htmlTemplate } = config;
1454
+ const { clientRoot, entryClient, entryServer, htmlTemplate, entryPoint } = config;
1439
1455
  try {
1440
1456
  const templateHtmlPath = path2.join(clientRoot, htmlTemplate);
1441
1457
  const templateHtml = await readFile(templateHtmlPath, "utf-8");
@@ -1444,11 +1460,13 @@ var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssL
1444
1460
  const adjustedRelativePath = relativeBasePath ? `/${relativeBasePath}` : "";
1445
1461
  if (!isDevelopment) {
1446
1462
  try {
1447
- const manifestPath = path2.join(clientRoot, ".vite/manifest.json");
1463
+ const clientDistPath = path2.resolve(projectRoot, "client", entryPoint);
1464
+ const manifestPath = path2.join(clientDistPath, ".vite/manifest.json");
1448
1465
  const manifestContent = await readFile(manifestPath, "utf-8");
1449
1466
  const manifest = JSON.parse(manifestContent);
1450
1467
  manifests.set(clientRoot, manifest);
1451
- const ssrManifestPath = path2.join(clientRoot, ".vite/ssr-manifest.json");
1468
+ const ssrDistPath = path2.resolve(projectRoot, "ssr", entryPoint);
1469
+ const ssrManifestPath = path2.join(ssrDistPath, ".vite/ssr-manifest.json");
1452
1470
  const ssrManifestContent = await readFile(ssrManifestPath, "utf-8");
1453
1471
  const ssrManifest = JSON.parse(ssrManifestContent);
1454
1472
  ssrManifests.set(clientRoot, ssrManifest);
@@ -1468,7 +1486,7 @@ var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssL
1468
1486
  preloadLinks.set(clientRoot, preloadLink);
1469
1487
  const cssLink = getCssLinks(manifest, adjustedRelativePath);
1470
1488
  cssLinks.set(clientRoot, cssLink);
1471
- const renderModulePath = path2.join(clientRoot, `${entryServer}.js`);
1489
+ const renderModulePath = path2.join(ssrDistPath, `${entryServer}.js`);
1472
1490
  const moduleUrl = pathToFileURL(renderModulePath).href;
1473
1491
  try {
1474
1492
  const importedModule = await import(moduleUrl);
@@ -1476,7 +1494,7 @@ var loadAssets = async (processedConfigs, baseClientRoot, bootstrapModules, cssL
1476
1494
  } catch (err) {
1477
1495
  throw AppError.internal(`Failed to load render module ${renderModulePath}`, {
1478
1496
  cause: err,
1479
- details: { moduleUrl, clientRoot, entryServer }
1497
+ details: { moduleUrl, clientRoot, entryServer, ssrDistPath }
1480
1498
  });
1481
1499
  }
1482
1500
  } catch (err) {
@@ -1687,10 +1705,15 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1687
1705
  if (renderType === RENDERTYPE.ssr) {
1688
1706
  const { renderSSR } = renderModule;
1689
1707
  if (!renderSSR) {
1690
- throw AppError.internal("renderSSR function not found in module", {
1691
- details: { clientRoot, availableFunctions: Object.keys(renderModule) }
1692
- });
1708
+ throw AppError.internal(
1709
+ "ssr",
1710
+ {
1711
+ details: { clientRoot, availableFunctions: Object.keys(renderModule) }
1712
+ },
1713
+ "renderSSR function not found in module"
1714
+ );
1693
1715
  }
1716
+ logger.debug?.("ssr", {}, "ssr requested");
1694
1717
  const ac = new AbortController();
1695
1718
  const onAborted = () => ac.abort("client_aborted");
1696
1719
  req.raw.on("aborted", onAborted);
@@ -1699,7 +1722,7 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1699
1722
  });
1700
1723
  reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
1701
1724
  if (ac.signal.aborted) {
1702
- logger.warn("SSR skipped; already aborted", { url: req.url });
1725
+ logger.warn({ url: req.url }, "SSR skipped; already aborted");
1703
1726
  return;
1704
1727
  }
1705
1728
  const initialDataResolved = await initialDataInput();
@@ -1709,14 +1732,31 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1709
1732
  const res = await renderSSR(initialDataResolved, req.url, attr?.meta, ac.signal, { logger: reqLogger });
1710
1733
  headContent = res.headContent;
1711
1734
  appHtml = res.appHtml;
1735
+ logger.debug?.("ssr", {}, "ssr data resolved");
1736
+ if (ac.signal.aborted) {
1737
+ logger.warn({}, "SSR completed but client disconnected");
1738
+ return;
1739
+ }
1712
1740
  } catch (err) {
1713
1741
  const msg = String(err?.message ?? err ?? "");
1714
1742
  const benign = REGEX.BENIGN_NET_ERR.test(msg);
1715
1743
  if (ac.signal.aborted || benign) {
1716
- logger.warn("SSR aborted mid-render (benign)", { url: req.url, reason: msg });
1744
+ logger.warn(
1745
+ {
1746
+ url: req.url,
1747
+ reason: msg
1748
+ },
1749
+ "SSR aborted mid-render (benign)"
1750
+ );
1717
1751
  return;
1718
1752
  }
1719
- logger.error("SSR render failed", { url: req.url, error: normaliseError(err) });
1753
+ logger.error(
1754
+ {
1755
+ url: req.url,
1756
+ error: normaliseError(err)
1757
+ },
1758
+ "SSR render failed"
1759
+ );
1720
1760
  throw err;
1721
1761
  }
1722
1762
  let aggregateHeadContent = headContent;
@@ -1728,13 +1768,14 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1728
1768
  const bootstrapScriptTag = shouldHydrate && bootstrapModule ? `<script${nonceAttr} type="module" src="${bootstrapModule}" defer></script>` : "";
1729
1769
  const safeAppHtml = appHtml.trim();
1730
1770
  const fullHtml = rebuildTemplate(templateParts, aggregateHeadContent, `${safeAppHtml}${initialDataScript}${bootstrapScriptTag}`);
1771
+ logger.debug?.("ssr", {}, "ssr template rebuilt and sending response");
1731
1772
  try {
1732
1773
  return reply.status(200).header("Content-Type", "text/html").send(fullHtml);
1733
1774
  } catch (err) {
1734
1775
  const msg = String(err?.message ?? err ?? "");
1735
1776
  const benign = REGEX.BENIGN_NET_ERR.test(msg);
1736
- if (!benign) logger.error("SSR send failed", { url: req.url, error: normaliseError(err) });
1737
- else logger.warn("SSR send aborted (benign)", { url: req.url, reason: msg });
1777
+ if (!benign) logger.error({ url: req.url, error: normaliseError(err) }, "SSR send failed");
1778
+ else logger.warn({ url: req.url, reason: msg }, "SSR send aborted (benign)");
1738
1779
  return;
1739
1780
  }
1740
1781
  } else {
@@ -1744,30 +1785,44 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1744
1785
  details: { clientRoot, availableFunctions: Object.keys(renderModule) }
1745
1786
  });
1746
1787
  }
1788
+ const headers2 = reply.getHeaders();
1789
+ headers2["Content-Type"] = "text/html; charset=utf-8";
1747
1790
  const cspHeader = reply.getHeader("Content-Security-Policy");
1748
- reply.raw.writeHead(200, {
1749
- "Content-Security-Policy": cspHeader,
1750
- "Content-Type": "text/html; charset=utf-8"
1751
- });
1791
+ if (cspHeader) headers2["Content-Security-Policy"] = cspHeader;
1792
+ reply.raw.writeHead(200, headers2);
1793
+ const abortedState = { aborted: false };
1752
1794
  const ac = new AbortController();
1753
- const onAborted = () => ac.abort();
1795
+ const onAborted = () => {
1796
+ if (!abortedState.aborted) {
1797
+ logger.warn({}, "Client disconnected before stream finished");
1798
+ abortedState.aborted = true;
1799
+ }
1800
+ ac.abort();
1801
+ };
1754
1802
  req.raw.on("aborted", onAborted);
1755
1803
  reply.raw.on("close", () => {
1756
- if (!reply.raw.writableEnded) ac.abort();
1804
+ if (!reply.raw.writableEnded) {
1805
+ if (!abortedState.aborted) {
1806
+ logger.warn({}, "Client disconnected before stream finished");
1807
+ abortedState.aborted = true;
1808
+ }
1809
+ ac.abort();
1810
+ }
1811
+ });
1812
+ reply.raw.on("finish", () => {
1813
+ req.raw.off("aborted", onAborted);
1757
1814
  });
1758
- reply.raw.on("finish", () => req.raw.off("aborted", onAborted));
1759
1815
  const shouldHydrate = attr?.hydrate !== false;
1760
- const abortedState = { aborted: false };
1761
1816
  const isBenignSocketAbort = (e) => {
1762
1817
  const msg = String(e?.message ?? e ?? "");
1763
1818
  return REGEX.BENIGN_NET_ERR.test(msg);
1764
1819
  };
1765
1820
  const writable = new PassThrough();
1766
1821
  writable.on("error", (err) => {
1767
- if (!isBenignSocketAbort(err)) logger.error("PassThrough error:", { error: err });
1822
+ if (!isBenignSocketAbort(err)) logger.error({ error: err }, "PassThrough error:");
1768
1823
  });
1769
1824
  reply.raw.on("error", (err) => {
1770
- if (!isBenignSocketAbort(err)) logger.error("HTTP socket error:", { error: err });
1825
+ if (!isBenignSocketAbort(err)) logger.error({ error: err }, "HTTP socket error:");
1771
1826
  });
1772
1827
  writable.pipe(reply.raw, { end: false });
1773
1828
  let finalData = void 0;
@@ -1787,30 +1842,33 @@ var handleRender = async (req, reply, routeMatchers, processedConfigs, serviceRe
1787
1842
  },
1788
1843
  onError: (err) => {
1789
1844
  if (abortedState.aborted || isBenignSocketAbort(err)) {
1790
- logger.warn("Client disconnected before stream finished");
1845
+ logger.warn({}, "Client disconnected before stream finished");
1791
1846
  try {
1792
1847
  if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy();
1793
1848
  } catch (e) {
1794
- logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
1849
+ logger.debug?.("ssr", { error: normaliseError(e) }, "stream teardown: destroy() failed");
1795
1850
  }
1796
1851
  return;
1797
1852
  }
1798
1853
  abortedState.aborted = true;
1799
- logger.error("Critical rendering error during stream", {
1800
- error: normaliseError(err),
1801
- clientRoot,
1802
- url: req.url
1803
- });
1854
+ logger.error(
1855
+ {
1856
+ error: normaliseError(err),
1857
+ clientRoot,
1858
+ url: req.url
1859
+ },
1860
+ "Critical rendering error during stream"
1861
+ );
1804
1862
  try {
1805
1863
  ac?.abort?.();
1806
1864
  } catch (e) {
1807
- logger.debug?.("stream teardown: abort() failed", { error: normaliseError(e) });
1865
+ logger.debug?.("ssr", { error: normaliseError(e) }, "stream teardown: abort() failed");
1808
1866
  }
1809
1867
  const reason = toReason(err);
1810
1868
  try {
1811
1869
  if (!reply.raw.writableEnded && !reply.raw.destroyed) reply.raw.destroy(reason);
1812
1870
  } catch (e) {
1813
- logger.debug?.("stream teardown: destroy() failed", { error: normaliseError(e) });
1871
+ logger.debug?.("ssr", { error: normaliseError(e) }, "stream teardown: destroy() failed");
1814
1872
  }
1815
1873
  }
1816
1874
  },
@@ -1897,8 +1955,33 @@ var handleNotFound = async (req, reply, processedConfigs, maps, opts = {}) => {
1897
1955
  }
1898
1956
  };
1899
1957
 
1958
+ // src/utils/ResolveRouteData.ts
1959
+ async function resolveRouteData(url, opts) {
1960
+ const { req, reply, routeMatchers, serviceRegistry, logger } = opts;
1961
+ const match2 = matchRoute(url, routeMatchers);
1962
+ if (!match2) {
1963
+ throw AppError.notFound("route_not_found", {
1964
+ details: { url }
1965
+ });
1966
+ }
1967
+ const { route, params } = match2;
1968
+ const attr = route.attr;
1969
+ if (!attr?.data) {
1970
+ throw AppError.notFound("no_data_handler", {
1971
+ details: {
1972
+ url,
1973
+ path: route.path,
1974
+ appId: route.appId
1975
+ }
1976
+ });
1977
+ }
1978
+ const ctx = createRequestContext(req, reply, logger);
1979
+ return fetchInitialData(attr, params, serviceRegistry, ctx);
1980
+ }
1981
+
1900
1982
  // src/utils/StaticAssets.ts
1901
- function normalizeStaticAssets(reg) {
1983
+ import path5 from "path";
1984
+ function normaliseStaticAssets(reg) {
1902
1985
  if (!reg) return [];
1903
1986
  return Array.isArray(reg) ? reg : [reg];
1904
1987
  }
@@ -1906,11 +1989,14 @@ function prefixWeight(prefix) {
1906
1989
  if (typeof prefix !== "string" || prefix === "/" || prefix.length === 0) return 0;
1907
1990
  return prefix.split("/").filter(Boolean).length;
1908
1991
  }
1909
- async function registerStaticAssets(app, baseClientRoot, reg, defaults) {
1910
- const entries = normalizeStaticAssets(reg).map(({ plugin, options }) => ({
1992
+ async function registerStaticAssets(app, baseClientRoot, reg, defaults, projectRoot) {
1993
+ const isDevelopment2 = process.env.NODE_ENV === "development";
1994
+ const effectiveProjectRoot = projectRoot ?? path5.resolve(process.cwd());
1995
+ const staticRoot = isDevelopment2 ? baseClientRoot : path5.resolve(effectiveProjectRoot, "client");
1996
+ const entries = normaliseStaticAssets(reg).map(({ plugin, options }) => ({
1911
1997
  plugin,
1912
1998
  options: {
1913
- root: baseClientRoot,
1999
+ root: staticRoot,
1914
2000
  prefix: "/",
1915
2001
  index: false,
1916
2002
  wildcard: false,
@@ -1939,6 +2025,7 @@ var SSRServer = (0, import_fastify_plugin3.default)(
1939
2025
  const processedConfigs = processConfigs(configs, baseClientRoot, TEMPLATE);
1940
2026
  const routeMatchers = createRouteMatchers(routes);
1941
2027
  let viteDevServer;
2028
+ const projectRoot = path6.resolve(baseClientRoot, "..");
1942
2029
  await loadAssets(
1943
2030
  processedConfigs,
1944
2031
  baseClientRoot,
@@ -1951,10 +2038,11 @@ var SSRServer = (0, import_fastify_plugin3.default)(
1951
2038
  maps.templates,
1952
2039
  {
1953
2040
  debug: opts.debug,
1954
- logger
2041
+ logger,
2042
+ projectRoot
1955
2043
  }
1956
2044
  );
1957
- if (opts.staticAssets) await registerStaticAssets(app, baseClientRoot, opts.staticAssets);
2045
+ if (opts.staticAssets) await registerStaticAssets(app, baseClientRoot, opts.staticAssets, void 0, projectRoot);
1958
2046
  if (security?.csp?.reporting) {
1959
2047
  app.register(cspReportPlugin, {
1960
2048
  path: security.csp.reporting.endpoint,
@@ -1971,6 +2059,23 @@ var SSRServer = (0, import_fastify_plugin3.default)(
1971
2059
  });
1972
2060
  if (isDevelopment) viteDevServer = await setupDevServer(app, baseClientRoot, alias, opts.debug, opts.devNet);
1973
2061
  app.addHook("onRequest", createAuthHook(routeMatchers, logger));
2062
+ app.get("/__taujs/data", async (req, reply) => {
2063
+ const query = req.query;
2064
+ const url = typeof query.url === "string" ? query.url : "";
2065
+ if (!url) {
2066
+ throw AppError.badRequest("url query param required", {
2067
+ details: { query }
2068
+ });
2069
+ }
2070
+ const data = await resolveRouteData(url, {
2071
+ req,
2072
+ reply,
2073
+ routeMatchers,
2074
+ serviceRegistry,
2075
+ logger
2076
+ });
2077
+ return reply.status(200).send({ data });
2078
+ });
1974
2079
  app.get("/*", async (req, reply) => {
1975
2080
  await handleRender(req, reply, routeMatchers, processedConfigs, serviceRegistry, maps, {
1976
2081
  debug: opts.debug,
@@ -2026,9 +2131,9 @@ var SSRServer = (0, import_fastify_plugin3.default)(
2026
2131
  // src/CreateServer.ts
2027
2132
  var createServer = async (opts) => {
2028
2133
  const t0 = performance3.now();
2029
- const clientRoot = opts.clientRoot ?? path5.resolve(process.cwd(), "client");
2134
+ const clientRoot = opts.clientRoot ?? path7.resolve(process.cwd(), "client");
2030
2135
  const app = opts.fastify ?? Fastify({ logger: false });
2031
- const fastifyLogger = app.log && app.log.level !== "silent" ? app.log : void 0;
2136
+ const fastifyLogger = app.log && app.log.level && app.log.level !== "silent" ? app.log : void 0;
2032
2137
  const logger = createLogger({
2033
2138
  debug: opts.debug,
2034
2139
  custom: opts.logger ?? fastifyLogger,
@@ -2095,18 +2200,149 @@ ${import_picocolors4.default.bgGreen(import_picocolors4.default.black(` ${CONTEN
2095
2200
  };
2096
2201
 
2097
2202
  // src/Build.ts
2098
- import path6 from "path";
2203
+ import { existsSync } from "fs";
2204
+ import path8 from "path";
2099
2205
  import { build } from "vite";
2100
- import { nodePolyfills } from "vite-plugin-node-polyfills";
2206
+ function resolveInputs(isSSRBuild, mainExists, paths) {
2207
+ if (isSSRBuild) return { server: paths.server };
2208
+ if (mainExists) return { client: paths.client, main: paths.main };
2209
+ return { client: paths.client };
2210
+ }
2211
+ function getFrameworkInvariants(config) {
2212
+ return {
2213
+ root: config.root || "",
2214
+ base: config.base || "/",
2215
+ publicDir: config.publicDir === void 0 ? "public" : config.publicDir,
2216
+ build: {
2217
+ outDir: config.build?.outDir || "",
2218
+ manifest: config.build?.manifest ?? false,
2219
+ ssr: config.build?.ssr ?? void 0,
2220
+ // Preserve exact type
2221
+ ssrManifest: config.build?.ssrManifest ?? false,
2222
+ format: config.build?.format,
2223
+ target: config.build?.target,
2224
+ rollupOptions: {
2225
+ input: config.build?.rollupOptions?.input || {}
2226
+ }
2227
+ }
2228
+ };
2229
+ }
2230
+ function mergeViteConfig(framework, userOverride, context) {
2231
+ if (!userOverride) return framework;
2232
+ const userConfig = typeof userOverride === "function" && context ? userOverride(context) : userOverride;
2233
+ const invariants = getFrameworkInvariants(framework);
2234
+ const merged = {
2235
+ ...framework,
2236
+ build: { ...framework.build ?? {} },
2237
+ css: { ...framework.css ?? {} },
2238
+ resolve: { ...framework.resolve ?? {} },
2239
+ plugins: [...framework.plugins ?? []],
2240
+ define: { ...framework.define ?? {} }
2241
+ };
2242
+ const ignoredKeys = [];
2243
+ if (userConfig.plugins) {
2244
+ const frameworkPlugins = merged.plugins;
2245
+ merged.plugins = [...frameworkPlugins, ...userConfig.plugins];
2246
+ }
2247
+ if (userConfig.define && typeof userConfig.define === "object") {
2248
+ merged.define = {
2249
+ ...merged.define,
2250
+ ...userConfig.define
2251
+ };
2252
+ }
2253
+ if (userConfig.css?.preprocessorOptions && typeof userConfig.css.preprocessorOptions === "object") {
2254
+ const fpp = merged.css.preprocessorOptions ?? {};
2255
+ const upp = userConfig.css.preprocessorOptions;
2256
+ merged.css.preprocessorOptions = Object.keys({ ...fpp, ...upp }).reduce((acc, engine) => {
2257
+ const fppEngine = fpp[engine];
2258
+ const uppEngine = upp[engine];
2259
+ acc[engine] = { ...fppEngine ?? {}, ...uppEngine ?? {} };
2260
+ return acc;
2261
+ }, {});
2262
+ }
2263
+ if (userConfig.build) {
2264
+ const protectedBuildFields = ["outDir", "ssr", "ssrManifest", "format", "target"];
2265
+ for (const field of protectedBuildFields) {
2266
+ if (field in userConfig.build) {
2267
+ ignoredKeys.push(`build.${field}`);
2268
+ }
2269
+ }
2270
+ if ("sourcemap" in userConfig.build) merged.build.sourcemap = userConfig.build.sourcemap;
2271
+ if ("minify" in userConfig.build) merged.build.minify = userConfig.build.minify;
2272
+ if (userConfig.build.terserOptions && typeof userConfig.build.terserOptions === "object") {
2273
+ merged.build.terserOptions = {
2274
+ ...merged.build.terserOptions ?? {},
2275
+ ...userConfig.build.terserOptions
2276
+ };
2277
+ }
2278
+ if (userConfig.build.rollupOptions) {
2279
+ if (!merged.build.rollupOptions) {
2280
+ merged.build.rollupOptions = {};
2281
+ }
2282
+ const userRollup = userConfig.build.rollupOptions;
2283
+ if ("input" in userRollup) ignoredKeys.push("build.rollupOptions.input");
2284
+ if ("external" in userRollup) merged.build.rollupOptions.external = userRollup.external;
2285
+ if (userRollup.output) {
2286
+ const mro = merged.build.rollupOptions ??= {};
2287
+ const uo = Array.isArray(userRollup.output) ? userRollup.output[0] : userRollup.output;
2288
+ const baseOut = Array.isArray(mro.output) ? mro.output[0] ?? {} : mro.output ?? {};
2289
+ mro.output = { ...baseOut, ...uo?.manualChunks ? { manualChunks: uo.manualChunks } : {} };
2290
+ }
2291
+ }
2292
+ }
2293
+ if (userConfig.resolve) {
2294
+ const userResolve = userConfig.resolve;
2295
+ const { alias: _ignore, ...resolveRest } = userResolve;
2296
+ if (_ignore) ignoredKeys.push("resolve.alias");
2297
+ merged.resolve = {
2298
+ ...merged.resolve,
2299
+ ...resolveRest
2300
+ };
2301
+ }
2302
+ if (userConfig.server) ignoredKeys.push("server (ignored in build; dev-only)");
2303
+ if ("root" in userConfig) ignoredKeys.push("root");
2304
+ if ("base" in userConfig) ignoredKeys.push("base");
2305
+ if ("publicDir" in userConfig) ignoredKeys.push("publicDir");
2306
+ const safeTopLevelKeys = /* @__PURE__ */ new Set([
2307
+ "esbuild",
2308
+ "logLevel",
2309
+ "envPrefix",
2310
+ "optimizeDeps",
2311
+ "ssr"
2312
+ // NOTE: NOT 'server' (build-time irrelevant; dev-server only)
2313
+ ]);
2314
+ for (const [key, value] of Object.entries(userConfig)) {
2315
+ if (safeTopLevelKeys.has(key)) merged[key] = value;
2316
+ }
2317
+ merged.root = invariants.root;
2318
+ merged.base = invariants.base;
2319
+ merged.publicDir = invariants.publicDir;
2320
+ merged.build.outDir = invariants.build.outDir;
2321
+ merged.build.manifest = invariants.build.manifest;
2322
+ if (invariants.build.ssr !== void 0) merged.build.ssr = invariants.build.ssr;
2323
+ merged.build.ssrManifest = invariants.build.ssrManifest;
2324
+ if (invariants.build.format) merged.build.format = invariants.build.format;
2325
+ if (invariants.build.target) merged.build.target = invariants.build.target;
2326
+ if (!merged.build.rollupOptions) merged.build.rollupOptions = {};
2327
+ merged.build.rollupOptions.input = invariants.build.rollupOptions.input;
2328
+ if (ignoredKeys.length > 0) {
2329
+ const uniqueKeys = [...new Set(ignoredKeys)];
2330
+ const prefix = context ? `[taujs:build:${context.entryPoint}]` : "[taujs:build]";
2331
+ console.warn(`${prefix} Ignored Vite config overrides: ${uniqueKeys.join(", ")}`);
2332
+ }
2333
+ return merged;
2334
+ }
2101
2335
  async function taujsBuild({
2102
2336
  config,
2103
2337
  projectRoot,
2104
2338
  clientBaseDir,
2105
- isSSRBuild = process.env.BUILD_MODE === "ssr"
2339
+ isSSRBuild = process.env.BUILD_MODE === "ssr",
2340
+ alias: userAlias,
2341
+ vite: userViteConfig
2106
2342
  }) {
2107
2343
  const deleteDist = async () => {
2108
2344
  const { rm } = await import("fs/promises");
2109
- const distPath = path6.resolve(projectRoot, "dist");
2345
+ const distPath = path8.resolve(projectRoot, "dist");
2110
2346
  try {
2111
2347
  await rm(distPath, { recursive: true, force: true });
2112
2348
  console.log("Deleted the dist directory\n");
@@ -2117,26 +2353,36 @@ async function taujsBuild({
2117
2353
  const extractedConfigs = extractBuildConfigs(config);
2118
2354
  const processedConfigs = processConfigs(extractedConfigs, clientBaseDir, TEMPLATE);
2119
2355
  if (!isSSRBuild) await deleteDist();
2120
- for (const config2 of processedConfigs) {
2121
- const { appId, entryPoint, clientRoot, entryClient, entryServer, htmlTemplate, plugins = [] } = config2;
2122
- const outDir = path6.resolve(projectRoot, `dist/client/${entryPoint}`);
2123
- const root = entryPoint ? path6.resolve(clientBaseDir, entryPoint) : clientBaseDir;
2124
- const server = path6.resolve(clientRoot, `${entryServer}.tsx`);
2125
- const client = path6.resolve(clientRoot, `${entryClient}.tsx`);
2126
- const main = path6.resolve(clientRoot, htmlTemplate);
2127
- const viteConfig = {
2356
+ for (const appConfig of processedConfigs) {
2357
+ const { appId, entryPoint, clientRoot, entryClient, entryServer, htmlTemplate, plugins = [] } = appConfig;
2358
+ const outDir = path8.resolve(projectRoot, isSSRBuild ? `dist/ssr/${entryPoint}` : `dist/client/${entryPoint}`);
2359
+ const root = entryPoint ? path8.resolve(clientBaseDir, entryPoint) : clientBaseDir;
2360
+ const defaultAlias = {
2361
+ "@client": root,
2362
+ "@server": path8.resolve(projectRoot, "src/server"),
2363
+ "@shared": path8.resolve(projectRoot, "src/shared")
2364
+ };
2365
+ const resolvedAlias = { ...defaultAlias, ...userAlias ?? {} };
2366
+ const server = path8.resolve(clientRoot, `${entryServer}.tsx`);
2367
+ const client = path8.resolve(clientRoot, `${entryClient}.tsx`);
2368
+ const main = path8.resolve(clientRoot, htmlTemplate);
2369
+ const inputs = resolveInputs(isSSRBuild, !isSSRBuild && existsSync(main), { server, client, main });
2370
+ const nodeVersion = process.versions.node.split(".")[0];
2371
+ const frameworkConfig = {
2128
2372
  base: entryPoint ? `/${entryPoint}/` : "/",
2129
2373
  build: {
2130
2374
  outDir,
2375
+ emptyOutDir: true,
2131
2376
  manifest: !isSSRBuild,
2132
2377
  rollupOptions: {
2133
- input: isSSRBuild ? { server } : { client, main }
2378
+ input: inputs
2134
2379
  },
2135
2380
  ssr: isSSRBuild ? server : void 0,
2136
2381
  ssrManifest: isSSRBuild,
2137
2382
  ...isSSRBuild && {
2138
2383
  format: "esm",
2139
- target: `node${process.versions.node.split(".").map(Number)[0]}`
2384
+ target: `node${nodeVersion}`,
2385
+ copyPublicDir: false
2140
2386
  }
2141
2387
  },
2142
2388
  css: {
@@ -2144,33 +2390,28 @@ async function taujsBuild({
2144
2390
  scss: { api: "modern-compiler" }
2145
2391
  }
2146
2392
  },
2147
- plugins: [...plugins, nodePolyfills({ include: ["fs", "stream"] })],
2148
- publicDir: "public",
2149
- resolve: {
2150
- alias: {
2151
- "@client": root,
2152
- "@server": path6.resolve(projectRoot, "src/server"),
2153
- "@shared": path6.resolve(projectRoot, "src/shared")
2154
- }
2155
- },
2156
- root,
2157
- server: {
2158
- proxy: {
2159
- "/api": {
2160
- target: "http://localhost:3000",
2161
- changeOrigin: true,
2162
- rewrite: (path7) => path7.replace(/^\/api/, "")
2163
- }
2164
- }
2165
- }
2393
+ plugins,
2394
+ publicDir: isSSRBuild ? false : "public",
2395
+ // single shared public
2396
+ // publicDir: isSSRBuild ? false : path.join(root, 'public'), // per-app. no public dir for SSR builds
2397
+ resolve: { alias: resolvedAlias },
2398
+ root
2399
+ };
2400
+ const buildContext = {
2401
+ appId,
2402
+ entryPoint,
2403
+ isSSRBuild,
2404
+ clientRoot
2166
2405
  };
2406
+ const finalConfig = mergeViteConfig(frameworkConfig, userViteConfig, buildContext);
2167
2407
  try {
2168
- console.log(`Building for entryPoint: "${entryPoint}" (${appId})`);
2169
- await build(viteConfig);
2170
- console.log(`Build complete for entryPoint: "${entryPoint}"
2408
+ const mode = isSSRBuild ? "SSR" : "Client";
2409
+ console.log(`[taujs:build:${entryPoint}] Building \u2192 ${mode}`);
2410
+ await build(finalConfig);
2411
+ console.log(`[taujs:build:${entryPoint}] \u2713 Complete
2171
2412
  `);
2172
2413
  } catch (error) {
2173
- console.error(`Error building for entryPoint: "${entryPoint}"
2414
+ console.error(`[taujs:build:${entryPoint}] \u2717 Failed
2174
2415
  `, error);
2175
2416
  process.exit(1);
2176
2417
  }
@@ -2192,7 +2433,6 @@ function winstonAdapter(winston) {
2192
2433
  return messageMetaAdapter(winston);
2193
2434
  }
2194
2435
  export {
2195
- createLogger,
2196
2436
  createServer,
2197
2437
  taujsBuild,
2198
2438
  winstonAdapter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taujs/server",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "τjs [ taujs ]",
5
5
  "author": "John Smith | Aoede <taujs@aoede.uk.net> (https://www.aoede.uk.net)",
6
6
  "license": "MIT",