@typed/app 1.0.0-beta.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 (116) hide show
  1. package/README.md +166 -0
  2. package/dist/HttpApiVirtualModulePlugin.d.ts +26 -0
  3. package/dist/HttpApiVirtualModulePlugin.d.ts.map +1 -0
  4. package/dist/HttpApiVirtualModulePlugin.js +301 -0
  5. package/dist/RouterVirtualModulePlugin.d.ts +23 -0
  6. package/dist/RouterVirtualModulePlugin.d.ts.map +1 -0
  7. package/dist/RouterVirtualModulePlugin.js +176 -0
  8. package/dist/createTypeInfoApiSessionForApp.d.ts +29 -0
  9. package/dist/createTypeInfoApiSessionForApp.d.ts.map +1 -0
  10. package/dist/createTypeInfoApiSessionForApp.js +46 -0
  11. package/dist/httpapi/defineApiHandler.d.ts +70 -0
  12. package/dist/httpapi/defineApiHandler.d.ts.map +1 -0
  13. package/dist/httpapi/defineApiHandler.js +23 -0
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/internal/appConfigTypes.d.ts +11 -0
  18. package/dist/internal/appConfigTypes.d.ts.map +1 -0
  19. package/dist/internal/appConfigTypes.js +1 -0
  20. package/dist/internal/appLayerTypes.d.ts +24 -0
  21. package/dist/internal/appLayerTypes.d.ts.map +1 -0
  22. package/dist/internal/appLayerTypes.js +28 -0
  23. package/dist/internal/buildRouteDescriptors.d.ts +48 -0
  24. package/dist/internal/buildRouteDescriptors.d.ts.map +1 -0
  25. package/dist/internal/buildRouteDescriptors.js +371 -0
  26. package/dist/internal/emitHttpApiSource.d.ts +18 -0
  27. package/dist/internal/emitHttpApiSource.d.ts.map +1 -0
  28. package/dist/internal/emitHttpApiSource.js +404 -0
  29. package/dist/internal/emitRouterHelpers.d.ts +17 -0
  30. package/dist/internal/emitRouterHelpers.d.ts.map +1 -0
  31. package/dist/internal/emitRouterHelpers.js +74 -0
  32. package/dist/internal/emitRouterSource.d.ts +8 -0
  33. package/dist/internal/emitRouterSource.d.ts.map +1 -0
  34. package/dist/internal/emitRouterSource.js +139 -0
  35. package/dist/internal/extractHttpApiLiterals.d.ts +17 -0
  36. package/dist/internal/extractHttpApiLiterals.d.ts.map +1 -0
  37. package/dist/internal/extractHttpApiLiterals.js +45 -0
  38. package/dist/internal/httpapiDescriptorTree.d.ts +75 -0
  39. package/dist/internal/httpapiDescriptorTree.d.ts.map +1 -0
  40. package/dist/internal/httpapiDescriptorTree.js +182 -0
  41. package/dist/internal/httpapiEndpointContract.d.ts +32 -0
  42. package/dist/internal/httpapiEndpointContract.d.ts.map +1 -0
  43. package/dist/internal/httpapiEndpointContract.js +79 -0
  44. package/dist/internal/httpapiFileRoles.d.ts +67 -0
  45. package/dist/internal/httpapiFileRoles.d.ts.map +1 -0
  46. package/dist/internal/httpapiFileRoles.js +145 -0
  47. package/dist/internal/httpapiId.d.ts +30 -0
  48. package/dist/internal/httpapiId.d.ts.map +1 -0
  49. package/dist/internal/httpapiId.js +57 -0
  50. package/dist/internal/httpapiOpenApiConfig.d.ts +87 -0
  51. package/dist/internal/httpapiOpenApiConfig.d.ts.map +1 -0
  52. package/dist/internal/httpapiOpenApiConfig.js +144 -0
  53. package/dist/internal/httpapiSort.d.ts +16 -0
  54. package/dist/internal/httpapiSort.d.ts.map +1 -0
  55. package/dist/internal/httpapiSort.js +29 -0
  56. package/dist/internal/path.d.ts +16 -0
  57. package/dist/internal/path.d.ts.map +1 -0
  58. package/dist/internal/path.js +38 -0
  59. package/dist/internal/resolveConfig.d.ts +8 -0
  60. package/dist/internal/resolveConfig.d.ts.map +1 -0
  61. package/dist/internal/resolveConfig.js +13 -0
  62. package/dist/internal/routeIdentifiers.d.ts +18 -0
  63. package/dist/internal/routeIdentifiers.d.ts.map +1 -0
  64. package/dist/internal/routeIdentifiers.js +90 -0
  65. package/dist/internal/routeTypeNode.d.ts +45 -0
  66. package/dist/internal/routeTypeNode.d.ts.map +1 -0
  67. package/dist/internal/routeTypeNode.js +93 -0
  68. package/dist/internal/routerDescriptorTree.d.ts +110 -0
  69. package/dist/internal/routerDescriptorTree.d.ts.map +1 -0
  70. package/dist/internal/routerDescriptorTree.js +230 -0
  71. package/dist/internal/typeTargetBootstrap.d.ts +2 -0
  72. package/dist/internal/typeTargetBootstrap.d.ts.map +1 -0
  73. package/dist/internal/typeTargetBootstrap.js +23 -0
  74. package/dist/internal/typeTargetBootstrapHttpApi.d.ts +2 -0
  75. package/dist/internal/typeTargetBootstrapHttpApi.d.ts.map +1 -0
  76. package/dist/internal/typeTargetBootstrapHttpApi.js +21 -0
  77. package/dist/internal/typeTargetSpecs.d.ts +15 -0
  78. package/dist/internal/typeTargetSpecs.d.ts.map +1 -0
  79. package/dist/internal/typeTargetSpecs.js +32 -0
  80. package/dist/internal/validation.d.ts +12 -0
  81. package/dist/internal/validation.d.ts.map +1 -0
  82. package/dist/internal/validation.js +32 -0
  83. package/package.json +45 -0
  84. package/src/HttpApiVirtualModulePlugin.test.ts +1062 -0
  85. package/src/HttpApiVirtualModulePlugin.ts +376 -0
  86. package/src/RouterVirtualModulePlugin.test.ts +1254 -0
  87. package/src/RouterVirtualModulePlugin.ts +242 -0
  88. package/src/createTypeInfoApiSessionForApp.ts +57 -0
  89. package/src/defineApiHandler.test.ts +100 -0
  90. package/src/httpapi/defineApiHandler.ts +141 -0
  91. package/src/httpapiDescriptorTree.test.ts +124 -0
  92. package/src/httpapiEndpointContract.test.ts +160 -0
  93. package/src/httpapiFileRoles.test.ts +105 -0
  94. package/src/index.ts +40 -0
  95. package/src/internal/appConfigTypes.ts +12 -0
  96. package/src/internal/appLayerTypes.ts +79 -0
  97. package/src/internal/buildRouteDescriptors.ts +489 -0
  98. package/src/internal/emitHttpApiSource.ts +563 -0
  99. package/src/internal/emitRouterHelpers.ts +89 -0
  100. package/src/internal/emitRouterSource.ts +191 -0
  101. package/src/internal/extractHttpApiLiterals.ts +67 -0
  102. package/src/internal/httpapiDescriptorTree.ts +283 -0
  103. package/src/internal/httpapiEndpointContract.ts +110 -0
  104. package/src/internal/httpapiFileRoles.ts +204 -0
  105. package/src/internal/httpapiId.ts +78 -0
  106. package/src/internal/httpapiOpenApiConfig.ts +228 -0
  107. package/src/internal/httpapiSort.ts +39 -0
  108. package/src/internal/path.ts +46 -0
  109. package/src/internal/resolveConfig.ts +15 -0
  110. package/src/internal/routeIdentifiers.ts +93 -0
  111. package/src/internal/routeTypeNode.ts +120 -0
  112. package/src/internal/routerDescriptorTree.ts +366 -0
  113. package/src/internal/typeTargetBootstrap.ts +24 -0
  114. package/src/internal/typeTargetBootstrapHttpApi.ts +22 -0
  115. package/src/internal/typeTargetSpecs.ts +35 -0
  116. package/src/internal/validation.ts +46 -0
@@ -0,0 +1,93 @@
1
+ import { stripScriptExtension, toPosixPath } from "./path.js";
2
+
3
+ /**
4
+ * Sanitize a path segment for use in a JS identifier: strip _ and brackets, keep only [a-zA-Z0-9], capitalize.
5
+ * Empty after sanitization is skipped (returns "").
6
+ */
7
+ function segmentToIdentifierPart(seg: string): string {
8
+ let name = seg.trim().startsWith("_") ? seg.trim().slice(1) : seg.trim();
9
+ name = name.replace(/[[\]]/g, "").replace(/[^a-zA-Z0-9]/g, "");
10
+ if (!name) return "";
11
+ return name.charAt(0).toUpperCase() + name.slice(1);
12
+ }
13
+
14
+ /**
15
+ * Path relative to baseDir → valid JS identifier.
16
+ * Safe for special chars in filenames; never emits invalid identifiers.
17
+ */
18
+ export function pathToIdentifier(relativeFilePath: string): string {
19
+ const posix = toPosixPath(relativeFilePath);
20
+ const withoutExt = stripScriptExtension(posix);
21
+ const raw =
22
+ withoutExt.split("/").filter(Boolean).map(segmentToIdentifierPart).filter(Boolean).join("") ||
23
+ "Module";
24
+ const safe = raw.replace(/[^a-zA-Z0-9_$]/g, "");
25
+ if (!safe) return "Module";
26
+ if (/^\d/.test(safe)) return `M${safe}`;
27
+ return safe;
28
+ }
29
+
30
+ /** Final guard: ensure a string is a valid JS identifier (no spaces, brackets, or leading digit). */
31
+ function toSafeIdentifier(s: string): string {
32
+ const cleaned = s.replace(/[\s[\]]/g, "").replace(/[^a-zA-Z0-9_$]/g, "");
33
+ if (!cleaned) return "Module";
34
+ if (/^\d/.test(cleaned)) return `M${cleaned}`;
35
+ return cleaned;
36
+ }
37
+
38
+ const RESERVED_NAMES = new Set(["Router", "Fx", "Effect", "Stream"]);
39
+
40
+ /**
41
+ * Route module identifier: prefix with M to avoid clashing with Router/Fx/Effect/Stream.
42
+ */
43
+ export function routeModuleIdentifier(relativeFilePath: string): string {
44
+ const base = pathToIdentifier(relativeFilePath);
45
+ return RESERVED_NAMES.has(base) ? `M${base}` : base;
46
+ }
47
+
48
+ /** True if the path has a dynamic segment (bracket). */
49
+ function pathHasParamSegment(relativePath: string): boolean {
50
+ return /\[[^\]]*\]/.test(relativePath);
51
+ }
52
+
53
+ /**
54
+ * Assign unique var names when proposed names collide (e.g. users/[id].ts and users/id.ts both → UsersId).
55
+ * First occurrence keeps the base name; others get base + "Param" (if path has [x]), "Literal", or numeric suffix.
56
+ */
57
+ export function makeUniqueVarNames(
58
+ entries: readonly { path: string; proposedName: string }[],
59
+ ): Map<string, string> {
60
+ const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path));
61
+ const nameToPaths = new Map<string, string[]>();
62
+ for (const { path, proposedName } of sorted) {
63
+ const base = toSafeIdentifier(proposedName);
64
+ const list = nameToPaths.get(base) ?? [];
65
+ list.push(path);
66
+ nameToPaths.set(base, list);
67
+ }
68
+ const pathToUnique = new Map<string, string>();
69
+ const used = new Set<string>();
70
+ for (const [base, paths] of nameToPaths) {
71
+ for (let i = 0; i < paths.length; i++) {
72
+ const path = paths[i]!;
73
+ let name: string;
74
+ if (paths.length === 1) {
75
+ name = base;
76
+ } else if (i === 0) {
77
+ name = base;
78
+ } else {
79
+ const suffix = pathHasParamSegment(path) ? "Param" : "Literal";
80
+ let candidate = base + suffix;
81
+ let n = 0;
82
+ while (used.has(candidate)) {
83
+ n += 1;
84
+ candidate = base + String(n);
85
+ }
86
+ name = candidate;
87
+ }
88
+ used.add(name);
89
+ pathToUnique.set(path, name);
90
+ }
91
+ }
92
+ return pathToUnique;
93
+ }
@@ -0,0 +1,120 @@
1
+ import type {
2
+ ConstructorTypeNode,
3
+ FunctionTypeNode,
4
+ OverloadSetTypeNode,
5
+ TypeInfoApi,
6
+ TypeNode,
7
+ } from "@typed/virtual-modules";
8
+
9
+ /** True when the node represents a callable (function, overload set, or constructor). */
10
+ export function isCallableNode(
11
+ node: TypeNode,
12
+ ): node is FunctionTypeNode | OverloadSetTypeNode | ConstructorTypeNode {
13
+ return node.kind === "function" || node.kind === "overloadSet" || node.kind === "constructor";
14
+ }
15
+
16
+ /** First signature's parameters for function-like nodes; undefined otherwise. */
17
+ function getCallableParameters(
18
+ node: TypeNode,
19
+ ): readonly { name: string; optional: boolean; type: unknown }[] | undefined {
20
+ if (node.kind === "function") return (node as FunctionTypeNode).parameters;
21
+ if (node.kind === "overloadSet") {
22
+ const sigs = (node as OverloadSetTypeNode).signatures;
23
+ return sigs[0]?.parameters;
24
+ }
25
+ if (node.kind === "constructor") return (node as ConstructorTypeNode).parameters;
26
+ return undefined;
27
+ }
28
+
29
+ /** First signature's return type for function-like nodes; undefined otherwise. */
30
+ export function getCallableReturnType(node: TypeNode): TypeNode | undefined {
31
+ if (node.kind === "function") return (node as FunctionTypeNode).returnType;
32
+ if (node.kind === "overloadSet") {
33
+ const sigs = (node as OverloadSetTypeNode).signatures;
34
+ return sigs[0]?.returnType;
35
+ }
36
+ if (node.kind === "constructor") return (node as ConstructorTypeNode).returnType;
37
+ return undefined;
38
+ }
39
+
40
+ /**
41
+ * True iff the type is structurally assignable to Route.
42
+ */
43
+ export function typeNodeIsRouteCompatible(node: TypeNode, api: TypeInfoApi): boolean {
44
+ return api.isAssignableTo(node, "Route");
45
+ }
46
+
47
+ export type RuntimeKind = "fx" | "effect" | "stream" | "plain" | "unknown";
48
+
49
+ /**
50
+ * Structurally determines runtime kind via api.isAssignableTo. No fallbacks.
51
+ * Never returns "unknown"; use callers' failWhenNoTargetsResolved for that.
52
+ */
53
+ export function typeNodeToRuntimeKind(node: TypeNode, api: TypeInfoApi): RuntimeKind {
54
+ if (api.isAssignableTo(node, "Fx")) return "fx";
55
+ if (api.isAssignableTo(node, "Effect")) return "effect";
56
+ if (api.isAssignableTo(node, "Stream")) return "stream";
57
+ return "plain";
58
+ }
59
+
60
+ /** Dependency export form for targeted .provide() lifts. */
61
+ export type DepsExportKind = "layer" | "servicemap" | "array";
62
+
63
+ /** Result of classifyDepsExport; "unknown" means validation must fail. */
64
+ export type DepsExportClassification = DepsExportKind | "unknown";
65
+
66
+ /** Classify dependency default export for optimal provide lift. Uses api; node.kind "array" for T[]. */
67
+ export function classifyDepsExport(
68
+ node: TypeNode,
69
+ api: TypeInfoApi,
70
+ ): DepsExportClassification {
71
+ if (api.isAssignableTo(node, "Layer")) return "layer";
72
+ if (api.isAssignableTo(node, "ServiceMap")) return "servicemap";
73
+ if (node.kind === "array") return "array";
74
+ return "unknown";
75
+ }
76
+
77
+ /** True iff the type node is a function whose first parameter expects RefSubject. */
78
+ export function typeNodeExpectsRefSubjectParam(node: TypeNode, api: TypeInfoApi): boolean {
79
+ if (!isCallableNode(node)) return false;
80
+ const params = getCallableParameters(node);
81
+ if (!params || params.length === 0) return false;
82
+ return api.isAssignableTo(node, "RefSubject", [{ kind: "param", index: 0 }]);
83
+ }
84
+
85
+ /**
86
+ * True iff the function's return type is Effect and Effect's success type (first type arg) is assignable to Option.
87
+ */
88
+ export function typeNodeIsEffectOptionReturn(node: TypeNode, api: TypeInfoApi): boolean {
89
+ return api.isAssignableTo(node, "Option", [
90
+ { kind: "returnType" },
91
+ { kind: "ensure", targetId: "Effect" },
92
+ { kind: "typeArg", index: 0 },
93
+ ]);
94
+ }
95
+
96
+ /**
97
+ * Classify catch handler form: native (RefSubject=>Fx), fn-cause ((Cause)=>...), fn-error ((E)=>...), or value.
98
+ */
99
+ export type CatchForm =
100
+ | { form: "native"; returnKind: RuntimeKind }
101
+ | { form: "value"; returnKind: RuntimeKind }
102
+ | { form: "fn-cause"; returnKind: RuntimeKind }
103
+ | { form: "fn-error"; returnKind: RuntimeKind };
104
+
105
+ export function classifyCatchForm(node: TypeNode, api: TypeInfoApi): CatchForm {
106
+ const returnType = getCallableReturnType(node);
107
+ const returnKind = returnType
108
+ ? typeNodeToRuntimeKind(returnType, api)
109
+ : typeNodeToRuntimeKind(node, api);
110
+ if (!isCallableNode(node)) {
111
+ return { form: "value", returnKind };
112
+ }
113
+ const params = getCallableParameters(node);
114
+ if (!params || params.length === 0) return { form: "fn-error", returnKind };
115
+ if (api.isAssignableTo(node, "RefSubject", [{ kind: "param", index: 0 }]))
116
+ return { form: "native", returnKind };
117
+ if (api.isAssignableTo(node, "Cause", [{ kind: "param", index: 0 }]))
118
+ return { form: "fn-cause", returnKind };
119
+ return { form: "fn-error", returnKind };
120
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Declarative descriptor tree for router virtual module emission.
3
+ * Describes ALL route structure and metadata; rendering is a separate phase.
4
+ *
5
+ * Optimizations in renderer:
6
+ * - Router.merge only when 2+ siblings
7
+ * - No Router.merge() for single child
8
+ * - Parens only when chaining on multi-arg call expressions
9
+ * - Positional match when no extra opts
10
+ */
11
+
12
+ import type { CatchForm, DepsExportKind, RuntimeKind } from "./routeTypeNode.js";
13
+
14
+ /** Resolved companion paths per concern (guard, deps, layout, catch). */
15
+ export type ComposedConcernsShape = {
16
+ readonly guard: readonly string[];
17
+ readonly dependencies: readonly string[];
18
+ readonly layout: readonly string[];
19
+ readonly catch: readonly string[];
20
+ };
21
+
22
+ /** Path-based refs (relative to baseDir); var names resolved at render time. */
23
+ export type PathRef = string;
24
+
25
+ /** Per-route match configuration (declarative, path-based). */
26
+ export type RouteMatchDescriptor = {
27
+ readonly routePath: PathRef;
28
+ readonly entrypointExport: "handler" | "template" | "default";
29
+ readonly runtimeKind: RuntimeKind;
30
+ readonly entrypointIsFunction: boolean;
31
+ readonly entrypointExpectsRefSubject: boolean;
32
+ readonly inFileConcerns: {
33
+ readonly layout?: boolean;
34
+ readonly dependencies?: boolean;
35
+ readonly catch?: boolean;
36
+ readonly guard?: boolean;
37
+ };
38
+ readonly composedConcerns: ComposedConcernsShape;
39
+ /** Guard from in-file, sibling, or directory (path). */
40
+ readonly guardPath?: PathRef;
41
+ /** Catch from in-file or sibling (path). */
42
+ readonly catchPath?: PathRef;
43
+ readonly catchExport?: "catch" | "catchFn";
44
+ readonly catchForm?: CatchForm;
45
+ /** Layout from in-file or sibling (path). */
46
+ readonly layoutPath?: PathRef;
47
+ /** Dependencies from in-file or sibling (path). */
48
+ readonly depsPath?: PathRef;
49
+ };
50
+
51
+ /** Directory companions (path-based). */
52
+ export type DirectoryCompanions = {
53
+ readonly layout?: PathRef;
54
+ readonly catch?: PathRef;
55
+ readonly dependencies?: PathRef;
56
+ };
57
+
58
+ /** Declarative tree node: route leaf or directory with children. */
59
+ export type RouterDescriptorNode =
60
+ | {
61
+ readonly type: "route";
62
+ readonly match: RouteMatchDescriptor;
63
+ }
64
+ | {
65
+ readonly type: "directory";
66
+ readonly dirPath: string;
67
+ readonly children: readonly RouterDescriptorNode[];
68
+ readonly companions?: DirectoryCompanions;
69
+ };
70
+
71
+ /** Root descriptor tree. */
72
+ export type RouterDescriptorTree = {
73
+ readonly root: RouterDescriptorNode;
74
+ readonly rootCompanions?: Pick<DirectoryCompanions, "dependencies">;
75
+ };
76
+
77
+ /** Input for building the descriptor tree from route descriptors. */
78
+ export type BuildDescriptorTreeInput = {
79
+ readonly descriptors: readonly {
80
+ readonly filePath: string;
81
+ readonly entrypointExport: "handler" | "template" | "default";
82
+ readonly runtimeKind: RuntimeKind;
83
+ readonly entrypointIsFunction: boolean;
84
+ readonly entrypointExpectsRefSubject: boolean;
85
+ readonly composedConcerns: ComposedConcernsShape;
86
+ readonly inFileConcerns: {
87
+ layout?: boolean;
88
+ dependencies?: boolean;
89
+ catch?: boolean;
90
+ guard?: boolean;
91
+ };
92
+ }[];
93
+ readonly dirToCompanions: ReadonlyMap<
94
+ string,
95
+ { layout?: string; dependencies?: string; catch?: string }
96
+ >;
97
+ readonly guardExportByPath: Readonly<Record<string, "default" | "guard">>;
98
+ readonly catchExportByPath: Readonly<Record<string, "catch" | "catchFn">>;
99
+ readonly catchFormByPath: Readonly<Record<string, CatchForm>>;
100
+ readonly normalizeDir: (dir: string) => string;
101
+ readonly isDirectoryCompanion: (path: string) => boolean;
102
+ readonly siblingCompanionPath: (
103
+ leafPath: string,
104
+ kind: "guard" | "dependencies" | "layout" | "catch",
105
+ ) => string;
106
+ };
107
+
108
+ /** Build declarative tree from route descriptors. */
109
+ export function buildRouterDescriptorTree(input: BuildDescriptorTreeInput): RouterDescriptorTree {
110
+ const {
111
+ descriptors,
112
+ dirToCompanions,
113
+ catchExportByPath,
114
+ catchFormByPath,
115
+ normalizeDir,
116
+ isDirectoryCompanion,
117
+ siblingCompanionPath,
118
+ } = input;
119
+
120
+ const toMatchDescriptor = (
121
+ d: BuildDescriptorTreeInput["descriptors"][number],
122
+ ): RouteMatchDescriptor => {
123
+ const sibling = (k: "guard" | "dependencies" | "layout" | "catch") =>
124
+ siblingCompanionPath(d.filePath, k);
125
+ const hasSibling = (k: "guard" | "dependencies" | "layout" | "catch") =>
126
+ d.composedConcerns[k].includes(sibling(k));
127
+ const dirGuardPath = d.composedConcerns.guard.find(isDirectoryCompanion);
128
+
129
+ const guardPath = d.inFileConcerns.guard
130
+ ? d.filePath
131
+ : hasSibling("guard")
132
+ ? sibling("guard")
133
+ : dirGuardPath;
134
+ const catchPath = d.inFileConcerns.catch
135
+ ? d.filePath
136
+ : hasSibling("catch")
137
+ ? sibling("catch")
138
+ : undefined;
139
+ return {
140
+ routePath: d.filePath,
141
+ entrypointExport: d.entrypointExport,
142
+ runtimeKind: d.runtimeKind,
143
+ entrypointIsFunction: d.entrypointIsFunction,
144
+ entrypointExpectsRefSubject: d.entrypointExpectsRefSubject,
145
+ inFileConcerns: d.inFileConcerns,
146
+ composedConcerns: d.composedConcerns,
147
+ guardPath,
148
+ catchPath,
149
+ catchExport: catchPath
150
+ ? (() => {
151
+ const exp = catchExportByPath[catchPath];
152
+ if (!exp) throw new Error(`RVM: catch export missing for ${catchPath}`);
153
+ return exp;
154
+ })()
155
+ : undefined,
156
+ catchForm: catchPath
157
+ ? (() => {
158
+ const form = catchFormByPath[catchPath];
159
+ if (!form) throw new Error(`RVM: catch form missing for ${catchPath}`);
160
+ return form;
161
+ })()
162
+ : undefined,
163
+ layoutPath: d.inFileConcerns.layout
164
+ ? d.filePath
165
+ : hasSibling("layout")
166
+ ? sibling("layout")
167
+ : undefined,
168
+ depsPath: d.inFileConcerns.dependencies
169
+ ? d.filePath
170
+ : hasSibling("dependencies")
171
+ ? sibling("dependencies")
172
+ : undefined,
173
+ };
174
+ };
175
+
176
+ const allDirs = new Set(descriptors.map((d) => normalizeDir(dirname(d.filePath))));
177
+ for (const [dir] of dirToCompanions) allDirs.add(dir);
178
+ if (allDirs.size > 0) allDirs.add("");
179
+ const sortedDirs = [...allDirs].sort(
180
+ (a, b) => b.split("/").filter(Boolean).length - a.split("/").filter(Boolean).length,
181
+ );
182
+
183
+ const dirNodeByPath = new Map<string, RouterDescriptorNode>();
184
+
185
+ for (const dir of sortedDirs) {
186
+ const directRoutes = descriptors.filter((d) => normalizeDir(dirname(d.filePath)) === dir);
187
+ const isImmediateChild = (s: string) =>
188
+ s !== dir &&
189
+ (dir === ""
190
+ ? s.indexOf("/") === -1
191
+ : s.startsWith(dir + "/") && s.slice((dir + "/").length).indexOf("/") === -1);
192
+ const childDirs = sortedDirs.filter(isImmediateChild);
193
+ const routeChildren: RouterDescriptorNode[] = directRoutes.map((d) => ({
194
+ type: "route",
195
+ match: toMatchDescriptor(d),
196
+ }));
197
+ const dirChildren: RouterDescriptorNode[] = childDirs.map((s) => dirNodeByPath.get(s)!);
198
+ const children: RouterDescriptorNode[] = [...routeChildren, ...dirChildren];
199
+
200
+ const companions = dirToCompanions.get(dir);
201
+ const hasCompanions =
202
+ companions &&
203
+ (companions.layout || companions.catch || (dir !== "" && companions.dependencies));
204
+ const node: RouterDescriptorNode =
205
+ children.length === 0
206
+ ? {
207
+ type: "directory",
208
+ dirPath: dir,
209
+ children: [],
210
+ companions: hasCompanions
211
+ ? {
212
+ layout: companions!.layout,
213
+ catch: companions!.catch,
214
+ dependencies: dir !== "" ? companions!.dependencies : undefined,
215
+ }
216
+ : undefined,
217
+ }
218
+ : children.length === 1 && !hasCompanions
219
+ ? children[0]!
220
+ : {
221
+ type: "directory",
222
+ dirPath: dir,
223
+ children,
224
+ companions: hasCompanions
225
+ ? {
226
+ layout: companions!.layout,
227
+ catch: companions!.catch,
228
+ dependencies: dir !== "" ? companions!.dependencies : undefined,
229
+ }
230
+ : undefined,
231
+ };
232
+ dirNodeByPath.set(dir, node);
233
+ }
234
+
235
+ let root = dirNodeByPath.get("");
236
+ if (!root || (root.type === "directory" && root.children.length === 0)) {
237
+ const flatChildren: RouterDescriptorNode[] = descriptors.map((d) => ({
238
+ type: "route",
239
+ match: toMatchDescriptor(d),
240
+ }));
241
+ root =
242
+ flatChildren.length === 0
243
+ ? { type: "directory", dirPath: "", children: [] }
244
+ : flatChildren.length === 1
245
+ ? flatChildren[0]!
246
+ : { type: "directory", dirPath: "", children: flatChildren };
247
+ }
248
+ const rootCompanions = dirToCompanions.get("");
249
+ return {
250
+ root,
251
+ rootCompanions: rootCompanions?.dependencies
252
+ ? { dependencies: rootCompanions.dependencies }
253
+ : undefined,
254
+ };
255
+ }
256
+
257
+ function dirname(p: string): string {
258
+ const i = p.lastIndexOf("/");
259
+ return i < 0 ? "" : p.slice(0, i);
260
+ }
261
+
262
+ /** Context for rendering: var names and expr generators. */
263
+ export type RenderContext = {
264
+ readonly varNameByPath: ReadonlyMap<string, string>;
265
+ readonly guardExportByPath: Readonly<Record<string, "default" | "guard">>;
266
+ readonly catchExportByPath: Readonly<Record<string, "catch" | "catchFn">>;
267
+ readonly catchFormByPath: Readonly<Record<string, CatchForm>>;
268
+ readonly depsFormByPath: Readonly<Record<string, DepsExportKind>>;
269
+ readonly handlerExprFor: (match: RouteMatchDescriptor, varName: string) => string;
270
+ readonly catchExprFor: (form: CatchForm, varName: string, exportName: string) => string;
271
+ readonly depsExprFor: (kind: DepsExportKind, varName: string) => string;
272
+ };
273
+
274
+ /** Render tree to source. Optimizes: no merge for single child, minimal parens. */
275
+ export function renderRouterDescriptorTree(tree: RouterDescriptorTree, ctx: RenderContext): string {
276
+ const emit = (node: RouterDescriptorNode): string => {
277
+ if (node.type === "route") {
278
+ return emitRoute(node.match, ctx);
279
+ }
280
+ const { children, companions } = node;
281
+ let inner: string;
282
+ if (children.length === 0) {
283
+ throw new Error("RVM: directory node with no children should not be rendered");
284
+ }
285
+ if (children.length === 1) {
286
+ inner = emit(children[0]!);
287
+ } else {
288
+ inner = `Router.merge(\n ${children.map((c) => emit(c)).join(",\n ")}\n)`;
289
+ }
290
+ if (companions?.layout) {
291
+ const v = ctx.varNameByPath.get(companions.layout)!;
292
+ inner = `${inner}.layout(${v}.layout)`;
293
+ }
294
+ if (companions?.catch) {
295
+ const v = ctx.varNameByPath.get(companions.catch)!;
296
+ const exp = ctx.catchExportByPath[companions.catch];
297
+ if (!exp) throw new Error(`RVM: catch export missing for ${companions.catch}`);
298
+ const form = ctx.catchFormByPath[companions.catch];
299
+ if (!form) throw new Error(`RVM: catch form missing for ${companions.catch}`);
300
+ const catchExpr = ctx.catchExprFor(form, v, exp);
301
+ inner = `${inner}.catchCause(${catchExpr})`;
302
+ }
303
+ if (companions?.dependencies) {
304
+ const v = ctx.varNameByPath.get(companions.dependencies)!;
305
+ const depsKind = ctx.depsFormByPath[companions.dependencies];
306
+ if (depsKind === undefined)
307
+ throw new Error(
308
+ `RVM: dependency form unknown for ${companions.dependencies}; validation should have failed (RVM-DEPS-001)`,
309
+ );
310
+ const depsExpr = ctx.depsExprFor(depsKind, v);
311
+ inner = `${inner}.provide(${depsExpr})`;
312
+ }
313
+ return inner;
314
+ };
315
+
316
+ let result = emit(tree.root);
317
+ if (tree.rootCompanions?.dependencies) {
318
+ const v = ctx.varNameByPath.get(tree.rootCompanions.dependencies)!;
319
+ const depsKind = ctx.depsFormByPath[tree.rootCompanions.dependencies];
320
+ if (depsKind === undefined)
321
+ throw new Error(
322
+ `RVM: dependency form unknown for ${tree.rootCompanions.dependencies}; validation should have failed (RVM-DEPS-001)`,
323
+ );
324
+ const depsExpr = ctx.depsExprFor(depsKind, v);
325
+ result = `${result}.provide(${depsExpr})`;
326
+ }
327
+ return result;
328
+ }
329
+
330
+ /** Emit Router.match(...) for a route. Favors positional when no extra opts. */
331
+ function emitRoute(match: RouteMatchDescriptor, ctx: RenderContext): string {
332
+ const routeVar = ctx.varNameByPath.get(match.routePath)!;
333
+ const routeRef = `${routeVar}.route`;
334
+ const handlerExpr = ctx.handlerExprFor(match, routeVar);
335
+ const hasExtraOpts = match.layoutPath || match.depsPath || match.catchPath;
336
+ const guardPath = match.guardPath;
337
+ const guardExport = guardPath ? ctx.guardExportByPath[guardPath] : undefined;
338
+ const guardExpr =
339
+ guardPath && guardExport ? `${ctx.varNameByPath.get(guardPath)!}.${guardExport}` : undefined;
340
+
341
+ const opts: string[] = [`handler: ${handlerExpr}`];
342
+ if (match.depsPath) {
343
+ opts.push(`dependencies: ${ctx.varNameByPath.get(match.depsPath)!}.dependencies`);
344
+ }
345
+ if (match.layoutPath) {
346
+ opts.push(`layout: ${ctx.varNameByPath.get(match.layoutPath)!}.layout`);
347
+ }
348
+ if (match.catchPath && match.catchExport) {
349
+ const catchVar = ctx.varNameByPath.get(match.catchPath)!;
350
+ const form = match.catchForm;
351
+ if (!form) throw new Error(`RVM: catch form missing for ${match.catchPath}`);
352
+ opts.push(`catch: ${ctx.catchExprFor(form, catchVar, match.catchExport)}`);
353
+ }
354
+
355
+ if (guardExpr && !hasExtraOpts) {
356
+ return `Router.match(${routeRef}, ${guardExpr}, ${handlerExpr})`;
357
+ }
358
+ if (guardExpr && hasExtraOpts) {
359
+ opts.unshift(`guard: ${guardExpr}`);
360
+ return `Router.match(${routeRef}, ${guardExpr}, { ${opts.join(", ")} })`;
361
+ }
362
+ if (!hasExtraOpts) {
363
+ return `Router.match(${routeRef}, ${handlerExpr})`;
364
+ }
365
+ return `Router.match(${routeRef}, { ${opts.join(", ")} })`;
366
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bootstrap file for TypeInfoApi type target resolution in tests.
3
+ * Exists so resolveTypeTargetsFromSpecs can find canonical Effect/Fx/RefSubject/Stream/Route types
4
+ * when the program includes this file. Do not use; for test harness only.
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Fx from "@typed/fx/Fx";
8
+ import * as RefSubject from "@typed/fx/RefSubject";
9
+ import * as Stream from "effect/Stream";
10
+ import * as Route from "@typed/router";
11
+ import * as Option from "effect/Option";
12
+ import * as Cause from "effect/Cause";
13
+ import * as Layer from "effect/Layer";
14
+ import * as ServiceMap from "effect/ServiceMap";
15
+ void Effect;
16
+ void Fx;
17
+ void RefSubject;
18
+ void Stream;
19
+ void Route;
20
+ void Option;
21
+ void Cause;
22
+ void Layer;
23
+ void ServiceMap;
24
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bootstrap file for TypeInfoApi type target resolution in HttpApi tests.
3
+ * Exists so resolveTypeTargetsFromSpecs can find canonical HttpApi/Route/Effect/Schema types
4
+ * when the program includes this file. Do not use; for test harness only.
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Schema from "effect/Schema";
8
+ import * as Route from "@typed/router";
9
+ import * as HttpApiModule from "effect/unstable/httpapi/HttpApi";
10
+ import * as HttpApiGroupModule from "effect/unstable/httpapi/HttpApiGroup";
11
+ import * as HttpApiEndpointModule from "effect/unstable/httpapi/HttpApiEndpoint";
12
+ import * as HttpApiBuilderModule from "effect/unstable/httpapi/HttpApiBuilder";
13
+ import * as HttpServerResponseModule from "effect/unstable/http/HttpServerResponse";
14
+ void Effect;
15
+ void Schema;
16
+ void Route;
17
+ void HttpApiModule;
18
+ void HttpApiGroupModule;
19
+ void HttpApiEndpointModule;
20
+ void HttpApiBuilderModule;
21
+ void HttpServerResponseModule;
22
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { TypeTargetSpec } from "@typed/virtual-modules";
2
+
3
+ /**
4
+ * Type target specs for router virtual module structural validation.
5
+ * Pass to createTypeInfoApiSession typeTargetSpecs option.
6
+ *
7
+ * IMPORTANT: Each spec's `module` must exactly match the import string in user source.
8
+ * Specs are canonical-only (no fallbacks or alternate import paths).
9
+ */
10
+ export const ROUTER_TYPE_TARGET_SPECS: readonly TypeTargetSpec[] = [
11
+ { id: "Fx", module: "@typed/fx/Fx", exportName: "Fx" },
12
+ { id: "Effect", module: "effect/Effect", exportName: "Effect" },
13
+ { id: "Stream", module: "effect/Stream", exportName: "Stream" },
14
+ { id: "RefSubject", module: "@typed/fx/RefSubject", exportName: "RefSubject" },
15
+ { id: "Cause", module: "effect/Cause", exportName: "Cause" },
16
+ { id: "Option", module: "effect/Option", exportName: "Option" },
17
+ { id: "Layer", module: "effect/Layer", exportName: "Layer" },
18
+ { id: "ServiceMap", module: "effect/ServiceMap", exportName: "ServiceMap" },
19
+ { id: "Route", module: "@typed/router", exportName: "Route" },
20
+ ];
21
+
22
+ /**
23
+ * Type target specs for HttpApi virtual module structural validation.
24
+ * Pass to createTypeInfoApiSession typeTargetSpecs option.
25
+ */
26
+ export const HTTPAPI_TYPE_TARGET_SPECS: readonly TypeTargetSpec[] = [
27
+ { id: "HttpApi", module: "effect/unstable/httpapi/HttpApi", exportName: "HttpApi" },
28
+ { id: "HttpApiGroup", module: "effect/unstable/httpapi/HttpApiGroup", exportName: "HttpApiGroup" },
29
+ { id: "HttpApiEndpoint", module: "effect/unstable/httpapi/HttpApiEndpoint", exportName: "HttpApiEndpoint" },
30
+ { id: "HttpApiBuilder", module: "effect/unstable/httpapi/HttpApiBuilder", exportName: "HttpApiBuilder" },
31
+ { id: "Schema", module: "effect/Schema", exportName: "Top" },
32
+ { id: "Effect", module: "effect/Effect", exportName: "Effect" },
33
+ { id: "Route", module: "@typed/router", exportName: "Route" },
34
+ { id: "HttpServerResponse", module: "effect/unstable/http/HttpServerResponse", exportName: "HttpServerResponse" },
35
+ ];