tina4-nodejs 3.13.114 → 3.13.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
13
13
 
14
14
  The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
15
15
 
16
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.114)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.116)
17
17
 
18
18
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
19
19
 
20
20
  ## What This Project Is
21
21
 
22
- Tina4 for Node.js/TypeScript v3.13.114 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
22
+ Tina4 for Node.js/TypeScript v3.13.116 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
23
23
 
24
24
  The philosophy: zero ceremony, batteries included, file system as source of truth.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.114",
3
+ "version": "3.13.116",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -36727,18 +36727,24 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
36727
36727
  if (!res.raw.writableEnded) res.raw.end();
36728
36728
  return true;
36729
36729
  }
36730
- async function invokeRouteHandler(match, req2, res) {
36731
- const routeParams = req2.params || {};
36732
- const fnStr = match.handler.toString();
36730
+ function resolveHandlerArgs(handler, req2, res, routeParams) {
36731
+ const fnStr = handler.toString();
36733
36732
  const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
36734
36733
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
36735
36734
  const filteredArgs = argNames.filter((n) => n.length > 0);
36736
- if (filteredArgs.length === 0) return await match.handler();
36737
- const args = filteredArgs.map((name) => {
36735
+ if (filteredArgs.length === 0) return [];
36736
+ let unmatchedPos = 0;
36737
+ return filteredArgs.map((name) => {
36738
36738
  if (name in routeParams) return routeParams[name];
36739
36739
  if (name === "request" || name === "req") return req2;
36740
- return res;
36740
+ if (name === "response" || name === "res") return res;
36741
+ return unmatchedPos++ === 0 ? req2 : res;
36741
36742
  });
36743
+ }
36744
+ async function invokeRouteHandler(match, req2, res) {
36745
+ const routeParams = req2.params || {};
36746
+ const args = resolveHandlerArgs(match.handler, req2, res, routeParams);
36747
+ if (args.length === 0) return await match.handler();
36742
36748
  return await match.handler(...args);
36743
36749
  }
36744
36750
  async function renderIfTemplateRoute(match, res, result) {
@@ -38493,6 +38499,7 @@ var registry, watchedFiles, Tina4Service, ServiceRunner;
38493
38499
  var init_service = __esm({
38494
38500
  "../core/src/service.ts"() {
38495
38501
  "use strict";
38502
+ init_logger();
38496
38503
  registry = /* @__PURE__ */ new Map();
38497
38504
  watchedFiles = /* @__PURE__ */ new Set();
38498
38505
  Tina4Service = class {
@@ -38636,6 +38643,17 @@ var init_service = __esm({
38636
38643
  static stop(name) {
38637
38644
  const targets = name ? [registry.get(name)].filter(Boolean) : Array.from(registry.values());
38638
38645
  for (const svc of targets) {
38646
+ const instance = svc.instance;
38647
+ if (instance && typeof instance.stop === "function") {
38648
+ try {
38649
+ instance.stop();
38650
+ } catch (err) {
38651
+ Log.error("Error stopping service instance", {
38652
+ name: svc.name,
38653
+ error: err instanceof Error ? err.message : String(err)
38654
+ });
38655
+ }
38656
+ }
38639
38657
  svc.context.running = false;
38640
38658
  if (svc.timerId) {
38641
38659
  clearInterval(svc.timerId);
@@ -36706,18 +36706,24 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
36706
36706
  if (!res.raw.writableEnded) res.raw.end();
36707
36707
  return true;
36708
36708
  }
36709
- async function invokeRouteHandler(match, req2, res) {
36710
- const routeParams = req2.params || {};
36711
- const fnStr = match.handler.toString();
36709
+ function resolveHandlerArgs(handler, req2, res, routeParams) {
36710
+ const fnStr = handler.toString();
36712
36711
  const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
36713
36712
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
36714
36713
  const filteredArgs = argNames.filter((n) => n.length > 0);
36715
- if (filteredArgs.length === 0) return await match.handler();
36716
- const args = filteredArgs.map((name) => {
36714
+ if (filteredArgs.length === 0) return [];
36715
+ let unmatchedPos = 0;
36716
+ return filteredArgs.map((name) => {
36717
36717
  if (name in routeParams) return routeParams[name];
36718
36718
  if (name === "request" || name === "req") return req2;
36719
- return res;
36719
+ if (name === "response" || name === "res") return res;
36720
+ return unmatchedPos++ === 0 ? req2 : res;
36720
36721
  });
36722
+ }
36723
+ async function invokeRouteHandler(match, req2, res) {
36724
+ const routeParams = req2.params || {};
36725
+ const args = resolveHandlerArgs(match.handler, req2, res, routeParams);
36726
+ if (args.length === 0) return await match.handler();
36721
36727
  return await match.handler(...args);
36722
36728
  }
36723
36729
  async function renderIfTemplateRoute(match, res, result) {
@@ -38472,6 +38478,7 @@ var registry, watchedFiles, Tina4Service, ServiceRunner;
38472
38478
  var init_service = __esm({
38473
38479
  "src/service.ts"() {
38474
38480
  "use strict";
38481
+ init_logger();
38475
38482
  registry = /* @__PURE__ */ new Map();
38476
38483
  watchedFiles = /* @__PURE__ */ new Set();
38477
38484
  Tina4Service = class {
@@ -38615,6 +38622,17 @@ var init_service = __esm({
38615
38622
  static stop(name) {
38616
38623
  const targets = name ? [registry.get(name)].filter(Boolean) : Array.from(registry.values());
38617
38624
  for (const svc of targets) {
38625
+ const instance = svc.instance;
38626
+ if (instance && typeof instance.stop === "function") {
38627
+ try {
38628
+ instance.stop();
38629
+ } catch (err) {
38630
+ Log.error("Error stopping service instance", {
38631
+ name: svc.name,
38632
+ error: err instanceof Error ? err.message : String(err)
38633
+ });
38634
+ }
38635
+ }
38618
38636
  svc.context.running = false;
38619
38637
  if (svc.timerId) {
38620
38638
  clearInterval(svc.timerId);
@@ -999,24 +999,49 @@ async function runGlobalMiddlewarePass(
999
999
  * or nothing - and each parameter is resolved by its name: a path param wins,
1000
1000
  * then `request`/`req`, then the response.
1001
1001
  */
1002
- async function invokeRouteHandler(
1003
- match: { handler: unknown },
1002
+ /**
1003
+ * Resolve a handler's argument list to `[reqOrParam, resOrParam, ...]` values.
1004
+ *
1005
+ * By-name path preserved (route-param names, `req`/`request`, `res`/`response`)
1006
+ * so existing code keeps its DX. Any remaining unmatched name falls back to
1007
+ * POSITIONAL binding: first unmatched -> request, rest -> response. That makes
1008
+ * dispatch bundler-safe by construction: Bun `--compile` and terser/esbuild
1009
+ * identifier-mangling rename `(req, res)` to `(req2, r$0)` — the by-name path
1010
+ * misses, the positional fallback catches, and the handler still receives the
1011
+ * request as its first argument. Fixes #56.
1012
+ *
1013
+ * Exported so the regression test can pin the behaviour directly, without a
1014
+ * live HTTP server.
1015
+ */
1016
+ export function resolveHandlerArgs(
1017
+ handler: unknown,
1004
1018
  req: Tina4Request,
1005
1019
  res: Tina4Response,
1006
- ): Promise<unknown> {
1007
- const routeParams = req.params || {};
1008
- const fnStr = (match.handler as { toString(): string }).toString();
1020
+ routeParams: Record<string, unknown>,
1021
+ ): unknown[] {
1022
+ const fnStr = (handler as { toString(): string }).toString();
1009
1023
  const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
1010
1024
  const argNames = argMatch?.[1]?.split(",").map((a: string) => a.trim().replace(/[:=].*/, "")) ?? [];
1011
1025
  const filteredArgs = argNames.filter((n: string) => n.length > 0);
1026
+ if (filteredArgs.length === 0) return [];
1012
1027
 
1013
- if (filteredArgs.length === 0) return await (match.handler as any)();
1014
-
1015
- const args = filteredArgs.map((name: string) => {
1028
+ let unmatchedPos = 0;
1029
+ return filteredArgs.map((name: string) => {
1016
1030
  if (name in routeParams) return routeParams[name];
1017
1031
  if (name === "request" || name === "req") return req;
1018
- return res;
1032
+ if (name === "response" || name === "res") return res;
1033
+ return (unmatchedPos++ === 0) ? req : res;
1019
1034
  });
1035
+ }
1036
+
1037
+ async function invokeRouteHandler(
1038
+ match: { handler: unknown },
1039
+ req: Tina4Request,
1040
+ res: Tina4Response,
1041
+ ): Promise<unknown> {
1042
+ const routeParams = req.params || {};
1043
+ const args = resolveHandlerArgs(match.handler, req, res, routeParams);
1044
+ if (args.length === 0) return await (match.handler as any)();
1020
1045
  return await (match.handler as any)(...args);
1021
1046
  }
1022
1047
 
@@ -5,6 +5,7 @@
5
5
  import { readdirSync, statSync, watchFile, unwatchFile } from "node:fs";
6
6
  import { join, extname } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
+ import { Log } from "./logger.js";
8
9
 
9
10
  // ─── Types ───────────────────────────────────────────────────────────────────
10
11
 
@@ -40,6 +41,7 @@ interface RegisteredService {
40
41
  context: ServiceContext;
41
42
  timerId: ReturnType<typeof setInterval> | null;
42
43
  retries: number;
44
+ instance?: Tina4Service;
43
45
  }
44
46
 
45
47
  const registry = new Map<string, RegisteredService>();
@@ -266,11 +268,10 @@ export class ServiceRunner {
266
268
  ): void {
267
269
  const merged: ServiceOptions = { daemon: true, ...options };
268
270
  this.register(name, service.asHandler(), merged);
269
- // Stash the instance on the registry entry so future stop() calls
270
- // can route to service.stop().
271
+ // Stash the instance so stop() can route to service.stop().
271
272
  const entry = registry.get(name);
272
273
  if (entry) {
273
- (entry as unknown as Record<string, unknown>).instance = service;
274
+ entry.instance = service;
274
275
  }
275
276
  }
276
277
 
@@ -362,6 +363,17 @@ export class ServiceRunner {
362
363
  : Array.from(registry.values());
363
364
 
364
365
  for (const svc of targets) {
366
+ const instance = svc.instance;
367
+ if (instance && typeof instance.stop === "function") {
368
+ try {
369
+ instance.stop();
370
+ } catch (err) {
371
+ Log.error("Error stopping service instance", {
372
+ name: svc.name,
373
+ error: err instanceof Error ? err.message : String(err),
374
+ });
375
+ }
376
+ }
365
377
  svc.context.running = false;
366
378
  if (svc.timerId) {
367
379
  clearInterval(svc.timerId);
@@ -25065,18 +25065,24 @@ async function runGlobalMiddlewarePass(middleware, req2, res) {
25065
25065
  if (!res.raw.writableEnded) res.raw.end();
25066
25066
  return true;
25067
25067
  }
25068
- async function invokeRouteHandler(match, req2, res) {
25069
- const routeParams = req2.params || {};
25070
- const fnStr = match.handler.toString();
25068
+ function resolveHandlerArgs(handler, req2, res, routeParams) {
25069
+ const fnStr = handler.toString();
25071
25070
  const argMatch = fnStr.match(/^(?:async\s*)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
25072
25071
  const argNames = argMatch?.[1]?.split(",").map((a) => a.trim().replace(/[:=].*/, "")) ?? [];
25073
25072
  const filteredArgs = argNames.filter((n) => n.length > 0);
25074
- if (filteredArgs.length === 0) return await match.handler();
25075
- const args = filteredArgs.map((name) => {
25073
+ if (filteredArgs.length === 0) return [];
25074
+ let unmatchedPos = 0;
25075
+ return filteredArgs.map((name) => {
25076
25076
  if (name in routeParams) return routeParams[name];
25077
25077
  if (name === "request" || name === "req") return req2;
25078
- return res;
25078
+ if (name === "response" || name === "res") return res;
25079
+ return unmatchedPos++ === 0 ? req2 : res;
25079
25080
  });
25081
+ }
25082
+ async function invokeRouteHandler(match, req2, res) {
25083
+ const routeParams = req2.params || {};
25084
+ const args = resolveHandlerArgs(match.handler, req2, res, routeParams);
25085
+ if (args.length === 0) return await match.handler();
25080
25086
  return await match.handler(...args);
25081
25087
  }
25082
25088
  async function renderIfTemplateRoute(match, res, result) {
@@ -27299,6 +27305,7 @@ var registry, watchedFiles, Tina4Service, ServiceRunner;
27299
27305
  var init_service = __esm({
27300
27306
  "../core/src/service.ts"() {
27301
27307
  "use strict";
27308
+ init_logger();
27302
27309
  registry = /* @__PURE__ */ new Map();
27303
27310
  watchedFiles = /* @__PURE__ */ new Set();
27304
27311
  Tina4Service = class {
@@ -27442,6 +27449,17 @@ var init_service = __esm({
27442
27449
  static stop(name) {
27443
27450
  const targets = name ? [registry.get(name)].filter(Boolean) : Array.from(registry.values());
27444
27451
  for (const svc of targets) {
27452
+ const instance = svc.instance;
27453
+ if (instance && typeof instance.stop === "function") {
27454
+ try {
27455
+ instance.stop();
27456
+ } catch (err) {
27457
+ Log.error("Error stopping service instance", {
27458
+ name: svc.name,
27459
+ error: err instanceof Error ? err.message : String(err)
27460
+ });
27461
+ }
27462
+ }
27445
27463
  svc.context.running = false;
27446
27464
  if (svc.timerId) {
27447
27465
  clearInterval(svc.timerId);
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
- import type { Tina4Config } from "./types.js";
2
+ import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
3
3
  import { Router } from "./router.js";
4
4
  import { MiddlewareChain } from "./middleware.js";
5
5
  /** How long a graceful shutdown waits for in-flight requests, in seconds. */
@@ -167,6 +167,28 @@ export declare function stop(): void;
167
167
  * Useful for testing and embedding.
168
168
  */
169
169
  export declare function handle(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void>;
170
+ /**
171
+ * Invoke a matched route's handler, binding path params BY NAME.
172
+ *
173
+ * A handler declares whatever it needs - `(id, request, response)`, `(req, res)`,
174
+ * or nothing - and each parameter is resolved by its name: a path param wins,
175
+ * then `request`/`req`, then the response.
176
+ */
177
+ /**
178
+ * Resolve a handler's argument list to `[reqOrParam, resOrParam, ...]` values.
179
+ *
180
+ * By-name path preserved (route-param names, `req`/`request`, `res`/`response`)
181
+ * so existing code keeps its DX. Any remaining unmatched name falls back to
182
+ * POSITIONAL binding: first unmatched -> request, rest -> response. That makes
183
+ * dispatch bundler-safe by construction: Bun `--compile` and terser/esbuild
184
+ * identifier-mangling rename `(req, res)` to `(req2, r$0)` — the by-name path
185
+ * misses, the positional fallback catches, and the handler still receives the
186
+ * request as its first argument. Fixes #56.
187
+ *
188
+ * Exported so the regression test can pin the behaviour directly, without a
189
+ * live HTTP server.
190
+ */
191
+ export declare function resolveHandlerArgs(handler: unknown, req: Tina4Request, res: Tina4Response, routeParams: Record<string, unknown>): unknown[];
170
192
  /**
171
193
  * Everything one request's dispatch needs beyond req/res: the resolved
172
194
  * router, middleware chain, filesystem roots and template engine a boot