@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,45 @@
1
+ function getLiteralText(node) {
2
+ if (!node || node.kind !== "literal")
3
+ return null;
4
+ return node.text || null;
5
+ }
6
+ function getPathFromRouteType(type) {
7
+ if (type.kind === "literal") {
8
+ const raw = type.text;
9
+ return raw ? (raw.startsWith("/") ? raw : `/${raw}`) : null;
10
+ }
11
+ if (type.kind === "object") {
12
+ const pathProp = type.properties.find((p) => p.name === "path");
13
+ if (!pathProp)
14
+ return null;
15
+ const raw = getLiteralText(pathProp.type);
16
+ return raw ? (raw.startsWith("/") ? raw : `/${raw}`) : null;
17
+ }
18
+ if (type.kind === "reference") {
19
+ const typeArgs = type.typeArguments;
20
+ if (typeArgs?.length)
21
+ return getPathFromRouteType(typeArgs[0]);
22
+ }
23
+ return null;
24
+ }
25
+ /**
26
+ * Extracts route path and method from TypeInfo snapshot exports.
27
+ * Reads from route and method exports; path from route type (literal or object.path).
28
+ */
29
+ export function extractEndpointLiterals(snapshot, stem) {
30
+ let path = `/${stem}`;
31
+ let method = "GET";
32
+ const routeExport = snapshot.exports.find((e) => e.name === "route");
33
+ if (routeExport) {
34
+ const p = getPathFromRouteType(routeExport.type);
35
+ if (p)
36
+ path = p;
37
+ }
38
+ const methodExport = snapshot.exports.find((e) => e.name === "method");
39
+ if (methodExport) {
40
+ const m = getLiteralText(methodExport.type);
41
+ if (m)
42
+ method = m.toUpperCase();
43
+ }
44
+ return { path, method, name: stem };
45
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Filesystem tree AST for HttpApi virtual module discovery.
3
+ * Converts role-classified artifacts into a deterministic tree (ApiRoot, Group, PathlessDirectory, Endpoint, Convention metadata).
4
+ * Parenthesized pathless directories do not create a named group but preserve composition order.
5
+ */
6
+ import type { HttpApiDirectoryCompanionKind, HttpApiEndpointCompanionKind, HttpApiFileRole } from "./httpapiFileRoles.js";
7
+ export declare function isPathlessDirectorySegment(segment: string): boolean;
8
+ /** Reference to a directory-level convention file (e.g. _dependencies.ts). */
9
+ export type DirectoryConventionRef = {
10
+ readonly path: string;
11
+ readonly kind: HttpApiDirectoryCompanionKind;
12
+ };
13
+ /** Reference to API root or group override convention. */
14
+ export type RootOrGroupConventionRef = {
15
+ readonly path: string;
16
+ readonly kind: "api_root" | "group_override";
17
+ };
18
+ /** Reference to an endpoint companion file (e.g. list.openapi.ts). */
19
+ export type EndpointCompanionRef = {
20
+ readonly path: string;
21
+ readonly kind: HttpApiEndpointCompanionKind;
22
+ };
23
+ /** Leaf node: endpoint primary with optional companion refs. */
24
+ export type HttpApiEndpointNode = {
25
+ readonly type: "endpoint";
26
+ readonly path: string;
27
+ /** Filename stem without extension (e.g. "list"). */
28
+ readonly stem: string;
29
+ readonly companions: readonly EndpointCompanionRef[];
30
+ };
31
+ /** Directory that does not create a named group (e.g. (internal)). */
32
+ export type HttpApiPathlessDirectoryNode = {
33
+ readonly type: "pathless_directory";
34
+ /** Full relative dir path (e.g. "users/(internal)"). */
35
+ readonly dirPath: string;
36
+ readonly conventions: readonly (DirectoryConventionRef | RootOrGroupConventionRef)[];
37
+ readonly children: readonly HttpApiTreeNode[];
38
+ };
39
+ /** Named group directory (creates HttpApiGroup). */
40
+ export type HttpApiGroupNode = {
41
+ readonly type: "group";
42
+ /** Group name (default from directory segment, override from _group.ts). */
43
+ readonly groupName: string;
44
+ /** Full relative dir path (e.g. "users"). */
45
+ readonly dirPath: string;
46
+ readonly children: readonly HttpApiTreeNode[];
47
+ readonly conventions: readonly (DirectoryConventionRef | RootOrGroupConventionRef)[];
48
+ };
49
+ export type HttpApiTreeNode = HttpApiEndpointNode | HttpApiGroupNode | HttpApiPathlessDirectoryNode;
50
+ /** Root of the descriptor tree. */
51
+ export type HttpApiDescriptorTree = {
52
+ readonly type: "api_root";
53
+ readonly dirPath: "";
54
+ readonly children: readonly HttpApiTreeNode[];
55
+ readonly conventions: readonly (DirectoryConventionRef | RootOrGroupConventionRef)[];
56
+ /** Diagnostics-ready: unsupported roles collected during parse. */
57
+ readonly diagnostics: readonly HttpApiTreeDiagnostic[];
58
+ };
59
+ export type HttpApiTreeDiagnostic = {
60
+ readonly code: string;
61
+ readonly message: string;
62
+ readonly path: string;
63
+ };
64
+ export type BuildHttpApiDescriptorTreeInput = {
65
+ /** Classified file roles (supported and unsupported_reserved). */
66
+ readonly roles: readonly HttpApiFileRole[];
67
+ /** Optional: only include paths under this base (relative); not used for filtering if empty. */
68
+ readonly baseDir?: string;
69
+ };
70
+ /**
71
+ * Builds a deterministic tree AST from classified file roles.
72
+ * Pathless directory segments (e.g. "(internal)") are represented as PathlessDirectoryNode and do not create a group name.
73
+ */
74
+ export declare function buildHttpApiDescriptorTree(input: BuildHttpApiDescriptorTreeInput): HttpApiDescriptorTree;
75
+ //# sourceMappingURL=httpapiDescriptorTree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpapiDescriptorTree.d.ts","sourceRoot":"","sources":["../../src/internal/httpapiDescriptorTree.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAS/B,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;CAC9C,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,gBAAgB,CAAC;CAC9C,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;CAC7C,CAAC;AAEF,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACtD,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,sBAAsB,GAAG,wBAAwB,CAAC,EAAE,CAAC;IACrF,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C,CAAC;AAEF,oDAAoD;AACpD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,sBAAsB,GAAG,wBAAwB,CAAC,EAAE,CAAC;CACtF,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB,mBAAmB,GACnB,gBAAgB,GAChB,4BAA4B,CAAC;AAEjC,mCAAmC;AACnC,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,sBAAsB,GAAG,wBAAwB,CAAC,EAAE,CAAC;IACrF,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,gGAAgG;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,+BAA+B,GACrC,qBAAqB,CAsJvB"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Filesystem tree AST for HttpApi virtual module discovery.
3
+ * Converts role-classified artifacts into a deterministic tree (ApiRoot, Group, PathlessDirectory, Endpoint, Convention metadata).
4
+ * Parenthesized pathless directories do not create a named group but preserve composition order.
5
+ */
6
+ import { compareHttpApiPathOrder, sortHttpApiPaths, } from "./httpapiFileRoles.js";
7
+ /** Path segment is pathless when it matches (name) and does not create an HttpApiGroup. */
8
+ const PATHLESS_DIRECTORY_PATTERN = /^\([^)]*\)$/;
9
+ export function isPathlessDirectorySegment(segment) {
10
+ return PATHLESS_DIRECTORY_PATTERN.test(segment);
11
+ }
12
+ /**
13
+ * Builds a deterministic tree AST from classified file roles.
14
+ * Pathless directory segments (e.g. "(internal)") are represented as PathlessDirectoryNode and do not create a group name.
15
+ */
16
+ export function buildHttpApiDescriptorTree(input) {
17
+ const { roles } = input;
18
+ const diagnostics = [];
19
+ const supported = [];
20
+ for (const r of roles) {
21
+ if (r.role === "unsupported_reserved") {
22
+ diagnostics.push({
23
+ code: r.diagnosticCode,
24
+ message: r.diagnosticMessage,
25
+ path: r.path,
26
+ });
27
+ }
28
+ else {
29
+ supported.push(r);
30
+ }
31
+ }
32
+ const apiRootConventions = [];
33
+ const directoryConventionsByDir = new Map();
34
+ const endpointCompanionsByKey = new Map();
35
+ for (const r of supported) {
36
+ if (r.role === "api_root") {
37
+ apiRootConventions.push({ path: r.path, kind: "api_root" });
38
+ continue;
39
+ }
40
+ if (r.role === "group_override") {
41
+ const dir = dirname(r.path);
42
+ directoryConventionsByDir.set(dir, [
43
+ ...(directoryConventionsByDir.get(dir) ?? []),
44
+ { path: r.path, kind: "group_override" },
45
+ ]);
46
+ continue;
47
+ }
48
+ if (r.role === "directory_companion") {
49
+ const dir = dirname(r.path);
50
+ const list = directoryConventionsByDir.get(dir) ?? [];
51
+ list.push({ path: r.path, kind: r.kind });
52
+ directoryConventionsByDir.set(dir, list);
53
+ continue;
54
+ }
55
+ if (r.role === "endpoint_primary") {
56
+ continue;
57
+ }
58
+ if (r.role === "endpoint_companion") {
59
+ const dir = dirname(r.path);
60
+ const key = dir ? `${dir}/${r.endpointStem}` : r.endpointStem;
61
+ const list = endpointCompanionsByKey.get(key) ?? [];
62
+ list.push({ path: r.path, kind: r.kind });
63
+ endpointCompanionsByKey.set(key, list);
64
+ }
65
+ }
66
+ const allDirs = new Set();
67
+ function addDirAndAncestors(d) {
68
+ allDirs.add(d);
69
+ const idx = d.lastIndexOf("/");
70
+ if (idx > 0)
71
+ addDirAndAncestors(d.slice(0, idx));
72
+ }
73
+ for (const r of supported) {
74
+ const p = "path" in r ? r.path : "";
75
+ addDirAndAncestors(dirname(p));
76
+ }
77
+ for (const [dir] of directoryConventionsByDir) {
78
+ addDirAndAncestors(dir);
79
+ }
80
+ const sortedDirs = sortHttpApiPaths([...allDirs]);
81
+ const dirSegments = (dirPath) => dirPath ? dirPath.split("/").filter(Boolean) : [];
82
+ const childrenByParentDir = new Map();
83
+ for (const d of sortedDirs) {
84
+ const segments = dirSegments(d);
85
+ if (segments.length === 0)
86
+ continue;
87
+ const parent = segments.length === 1 ? "" : segments.slice(0, -1).join("/");
88
+ const list = childrenByParentDir.get(parent) ?? [];
89
+ if (!list.includes(d))
90
+ list.push(d);
91
+ childrenByParentDir.set(parent, list);
92
+ }
93
+ function buildNode(dirPath) {
94
+ const directFiles = supported
95
+ .filter((r) => {
96
+ if (r.role !== "endpoint_primary")
97
+ return false;
98
+ const p = r.path;
99
+ const d = dirname(p);
100
+ return d === dirPath;
101
+ })
102
+ .map((r) => r)
103
+ .sort((a, b) => compareHttpApiPathOrder(a.path, b.path));
104
+ const childDirs = (childrenByParentDir.get(dirPath) ?? []).slice().sort(compareHttpApiPathOrder);
105
+ const nodes = [];
106
+ for (const f of directFiles) {
107
+ const stem = stemFromPath(f.path);
108
+ const companionKey = dirPath ? `${dirPath}/${stem}` : stem;
109
+ const companions = endpointCompanionsByKey.get(companionKey) ?? [];
110
+ nodes.push({
111
+ type: "endpoint",
112
+ path: f.path,
113
+ stem,
114
+ companions: [...companions].sort((a, b) => compareHttpApiPathOrder(a.path, b.path)),
115
+ });
116
+ }
117
+ for (const childDir of childDirs) {
118
+ const segments = dirSegments(childDir);
119
+ const segmentName = segments[segments.length - 1] ?? "";
120
+ if (isPathlessDirectorySegment(segmentName)) {
121
+ const childNodes = buildNode(childDir);
122
+ const conventions = directoryConventionsByDir.get(childDir) ?? [];
123
+ nodes.push({
124
+ type: "pathless_directory",
125
+ dirPath: childDir,
126
+ conventions: [...conventions].sort((a, b) => compareHttpApiPathOrder(a.path, b.path)),
127
+ children: childNodes.length === 0 ? [] : childNodes.slice().sort(compareTreeNodes),
128
+ });
129
+ }
130
+ else {
131
+ const groupName = segmentName;
132
+ const childNodes = buildNode(childDir);
133
+ const conventions = directoryConventionsByDir.get(childDir) ?? [];
134
+ nodes.push({
135
+ type: "group",
136
+ groupName,
137
+ dirPath: childDir,
138
+ children: childNodes.length === 0 ? [] : childNodes.slice().sort(compareTreeNodes),
139
+ conventions: [...conventions].sort((a, b) => compareHttpApiPathOrder(a.path, b.path)),
140
+ });
141
+ }
142
+ }
143
+ return nodes.slice().sort(compareTreeNodes);
144
+ }
145
+ const rootChildren = buildNode("");
146
+ const rootConventions = [...apiRootConventions];
147
+ const rootDirConventions = directoryConventionsByDir.get("") ?? [];
148
+ for (const c of rootDirConventions) {
149
+ rootConventions.push(c);
150
+ }
151
+ rootConventions.sort((a, b) => compareHttpApiPathOrder(a.path, b.path));
152
+ return {
153
+ type: "api_root",
154
+ dirPath: "",
155
+ children: rootChildren,
156
+ conventions: rootConventions,
157
+ diagnostics,
158
+ };
159
+ }
160
+ function dirname(p) {
161
+ const i = p.lastIndexOf("/");
162
+ return i < 0 ? "" : p.slice(0, i);
163
+ }
164
+ function stemFromPath(path) {
165
+ const base = path.slice(path.lastIndexOf("/") + 1);
166
+ const extIdx = base.lastIndexOf(".");
167
+ return extIdx <= 0 ? base : base.slice(0, extIdx);
168
+ }
169
+ function nodeSortKey(n) {
170
+ switch (n.type) {
171
+ case "endpoint":
172
+ return n.path;
173
+ case "group":
174
+ case "pathless_directory":
175
+ return n.dirPath;
176
+ default:
177
+ return "";
178
+ }
179
+ }
180
+ function compareTreeNodes(a, b) {
181
+ return compareHttpApiPathOrder(nodeSortKey(a), nodeSortKey(b));
182
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Endpoint contract validation for HttpApi virtual module.
3
+ * Validates required fields (route, method, handler). Optional: headers, body, error, success.
4
+ * Route covers path + query; headers/body mirror decoders from HttpServerRequest;
5
+ * error/success encode response payloads with annotated status codes.
6
+ * Handler returns require structural type checking (assignable to success/error schemas)
7
+ * and coercion into HttpServerResponse via Effect's HttpApi layer.
8
+ *
9
+ * @see .docs/specs/httpapi-virtual-module-plugin/spec.md (Endpoint contract, Endpoint Contract Validator)
10
+ */
11
+ import type { ValidationResult } from "./validation.js";
12
+ /** Minimal endpoint contract shape (runtime check) */
13
+ export interface EndpointContractInput {
14
+ readonly route?: unknown;
15
+ readonly method?: unknown;
16
+ readonly handler?: unknown;
17
+ readonly headers?: unknown;
18
+ readonly body?: unknown;
19
+ readonly error?: unknown;
20
+ readonly success?: unknown;
21
+ }
22
+ /** Validated endpoint contract */
23
+ export interface ValidatedEndpointContract {
24
+ readonly route: unknown;
25
+ readonly method: string;
26
+ readonly handler: unknown;
27
+ }
28
+ /**
29
+ * Validates endpoint contract: required route, method, handler; optional headers, body, error, success.
30
+ */
31
+ export declare function validateEndpointContract(input: unknown): ValidationResult<ValidatedEndpointContract>;
32
+ //# sourceMappingURL=httpapiEndpointContract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpapiEndpointContract.d.ts","sourceRoot":"","sources":["../../src/internal/httpapiEndpointContract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAcxD,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,kCAAkC;AAClC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AASD;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,gBAAgB,CAAC,yBAAyB,CAAC,CAsD7C"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Endpoint contract validation for HttpApi virtual module.
3
+ * Validates required fields (route, method, handler). Optional: headers, body, error, success.
4
+ * Route covers path + query; headers/body mirror decoders from HttpServerRequest;
5
+ * error/success encode response payloads with annotated status codes.
6
+ * Handler returns require structural type checking (assignable to success/error schemas)
7
+ * and coercion into HttpServerResponse via Effect's HttpApi layer.
8
+ *
9
+ * @see .docs/specs/httpapi-virtual-module-plugin/spec.md (Endpoint contract, Endpoint Contract Validator)
10
+ */
11
+ import { validateNonEmptyString } from "./validation.js";
12
+ /** Supported HTTP method literals for endpoint contract */
13
+ const HTTP_METHODS = new Set([
14
+ "GET",
15
+ "POST",
16
+ "PUT",
17
+ "PATCH",
18
+ "DELETE",
19
+ "HEAD",
20
+ "OPTIONS",
21
+ ]);
22
+ /**
23
+ * Checks that value is a non-null object (route/schema-like values are objects).
24
+ */
25
+ function isObject(value) {
26
+ return typeof value === "object" && value !== null;
27
+ }
28
+ /**
29
+ * Validates endpoint contract: required route, method, handler; optional headers, body, error, success.
30
+ */
31
+ export function validateEndpointContract(input) {
32
+ if (!isObject(input)) {
33
+ return { ok: false, reason: "Endpoint contract must be an object" };
34
+ }
35
+ const contract = input;
36
+ if (contract.route === undefined) {
37
+ return { ok: false, reason: "Endpoint contract missing required field: route" };
38
+ }
39
+ if (!isObject(contract.route)) {
40
+ return { ok: false, reason: "Endpoint contract route must be an object (path + pathSchema + querySchema)" };
41
+ }
42
+ const route = contract.route;
43
+ if (route.pathSchema === undefined) {
44
+ return { ok: false, reason: "Endpoint contract route must include pathSchema" };
45
+ }
46
+ if (route.querySchema === undefined) {
47
+ return { ok: false, reason: "Endpoint contract route must include querySchema" };
48
+ }
49
+ const methodResult = validateNonEmptyString(contract.method, "method");
50
+ if (!methodResult.ok) {
51
+ return {
52
+ ok: false,
53
+ reason: `Endpoint contract method: ${methodResult.reason}`,
54
+ };
55
+ }
56
+ if (!HTTP_METHODS.has(methodResult.value)) {
57
+ return {
58
+ ok: false,
59
+ reason: `Endpoint contract method must be one of: ${[...HTTP_METHODS].join(", ")}`,
60
+ };
61
+ }
62
+ if (contract.handler === undefined) {
63
+ return { ok: false, reason: "Endpoint contract missing required field: handler" };
64
+ }
65
+ if (typeof contract.handler !== "function") {
66
+ return {
67
+ ok: false,
68
+ reason: "Endpoint contract handler must be a function",
69
+ };
70
+ }
71
+ return {
72
+ ok: true,
73
+ value: {
74
+ route: contract.route,
75
+ method: methodResult.value,
76
+ handler: contract.handler,
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * File-role classification for HttpApi virtual module discovery.
3
+ * Implements the supported file-role matrix: API root, group, endpoint primary, endpoint companions, directory companions.
4
+ * Diagnostics-ready metadata for unsupported reserved names.
5
+ */
6
+ /** Script extensions that count as API source candidates (aligned with router plugin). */
7
+ export declare const HTTPAPI_SCRIPT_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
8
+ export declare const HTTPAPI_SCRIPT_EXTENSION_SET: Set<string>;
9
+ /** Directory companion filenames (exact match). */
10
+ export declare const HTTPAPI_DIRECTORY_COMPANION_FILES: readonly ["_dependencies.ts", "_middlewares.ts", "_prefix.ts", "_openapi.ts"];
11
+ /** Endpoint companion suffixes (stem ends with these before extension). */
12
+ export declare const HTTPAPI_ENDPOINT_COMPANION_SUFFIXES: readonly [".name", ".dependencies", ".middlewares", ".prefix", ".openapi"];
13
+ export type HttpApiDirectoryCompanionKind = (typeof HTTPAPI_DIRECTORY_COMPANION_FILES)[number];
14
+ export type HttpApiEndpointCompanionKind = (typeof HTTPAPI_ENDPOINT_COMPANION_SUFFIXES)[number];
15
+ /** Role of a discovered file relative to the API root. */
16
+ export type HttpApiFileRole = {
17
+ readonly role: "api_root";
18
+ readonly path: string;
19
+ } | {
20
+ readonly role: "group_override";
21
+ readonly path: string;
22
+ } | {
23
+ readonly role: "endpoint_primary";
24
+ readonly path: string;
25
+ } | {
26
+ readonly role: "endpoint_companion";
27
+ readonly path: string;
28
+ readonly kind: HttpApiEndpointCompanionKind;
29
+ readonly endpointStem: string;
30
+ } | {
31
+ readonly role: "directory_companion";
32
+ readonly path: string;
33
+ readonly kind: HttpApiDirectoryCompanionKind;
34
+ } | {
35
+ readonly role: "unsupported_reserved";
36
+ readonly path: string;
37
+ readonly diagnosticCode: string;
38
+ readonly diagnosticMessage: string;
39
+ };
40
+ /** Diagnostic-ready metadata for classification issues (e.g. unsupported companion name). */
41
+ export type HttpApiFileRoleDiagnostic = {
42
+ readonly code: string;
43
+ readonly message: string;
44
+ readonly path: string;
45
+ };
46
+ /** Normalized path (posix) relative to API base; used for stable ordering. */
47
+ export declare function normalizeHttpApiRelativePath(relativePath: string): string;
48
+ /**
49
+ * Returns true if the file has a supported script extension for API discovery.
50
+ */
51
+ export declare function isHttpApiScriptExtension(ext: string): boolean;
52
+ /**
53
+ * Classifies a single file path (relative to API root, posix) into an HttpApi file role.
54
+ * Does not read the filesystem; only interprets path and filename.
55
+ * Returns diagnostic-ready metadata for unsupported reserved names.
56
+ */
57
+ export declare function classifyHttpApiFileRole(relativePath: string): HttpApiFileRole;
58
+ /**
59
+ * Stable sort key for discovered files: normalized relative path (posix).
60
+ * Use with Array.sort(compareHttpApiPathOrder) for deterministic ordering.
61
+ */
62
+ export declare function compareHttpApiPathOrder(a: string, b: string): number;
63
+ /**
64
+ * Sorts an array of relative paths in deterministic API discovery order.
65
+ */
66
+ export declare function sortHttpApiPaths(paths: readonly string[]): string[];
67
+ //# sourceMappingURL=httpapiFileRoles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpapiFileRoles.d.ts","sourceRoot":"","sources":["../../src/internal/httpapiFileRoles.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,0FAA0F;AAC1F,eAAO,MAAM,yBAAyB,yEAS5B,CAAC;AAEX,eAAO,MAAM,4BAA4B,aAExC,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,iCAAiC,+EAKpC,CAAC;AAEX,2EAA2E;AAC3E,eAAO,MAAM,mCAAmC,4EAMtC,CAAC;AAEX,MAAM,MAAM,6BAA6B,GACvC,CAAC,OAAO,iCAAiC,CAAC,CAAC,MAAM,CAAC,CAAC;AACrD,MAAM,MAAM,4BAA4B,GACtC,CAAC,OAAO,mCAAmC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,0DAA0D;AAC1D,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5D;IACE,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,6BAA6B,CAAC;CAC9C,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC,CAAC;AAEN,6FAA6F;AAC7F,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAMF,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,eAAe,CAwF7E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAEnE"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * File-role classification for HttpApi virtual module discovery.
3
+ * Implements the supported file-role matrix: API root, group, endpoint primary, endpoint companions, directory companions.
4
+ * Diagnostics-ready metadata for unsupported reserved names.
5
+ */
6
+ import { toPosixPath } from "./path.js";
7
+ /** Script extensions that count as API source candidates (aligned with router plugin). */
8
+ export const HTTPAPI_SCRIPT_EXTENSIONS = [
9
+ ".ts",
10
+ ".tsx",
11
+ ".js",
12
+ ".jsx",
13
+ ".mts",
14
+ ".cts",
15
+ ".mjs",
16
+ ".cjs",
17
+ ];
18
+ export const HTTPAPI_SCRIPT_EXTENSION_SET = new Set(HTTPAPI_SCRIPT_EXTENSIONS.map((e) => e.toLowerCase()));
19
+ /** Directory companion filenames (exact match). */
20
+ export const HTTPAPI_DIRECTORY_COMPANION_FILES = [
21
+ "_dependencies.ts",
22
+ "_middlewares.ts",
23
+ "_prefix.ts",
24
+ "_openapi.ts",
25
+ ];
26
+ /** Endpoint companion suffixes (stem ends with these before extension). */
27
+ export const HTTPAPI_ENDPOINT_COMPANION_SUFFIXES = [
28
+ ".name",
29
+ ".dependencies",
30
+ ".middlewares",
31
+ ".prefix",
32
+ ".openapi",
33
+ ];
34
+ const DIRECTORY_COMPANION_SET = new Set(HTTPAPI_DIRECTORY_COMPANION_FILES.map((f) => f.toLowerCase()));
35
+ /** Normalized path (posix) relative to API base; used for stable ordering. */
36
+ export function normalizeHttpApiRelativePath(relativePath) {
37
+ const posix = toPosixPath(relativePath);
38
+ return posix.replace(/^\.\//, "").replace(/\/+/g, "/");
39
+ }
40
+ /**
41
+ * Returns true if the file has a supported script extension for API discovery.
42
+ */
43
+ export function isHttpApiScriptExtension(ext) {
44
+ return HTTPAPI_SCRIPT_EXTENSION_SET.has(ext.toLowerCase());
45
+ }
46
+ /**
47
+ * Classifies a single file path (relative to API root, posix) into an HttpApi file role.
48
+ * Does not read the filesystem; only interprets path and filename.
49
+ * Returns diagnostic-ready metadata for unsupported reserved names.
50
+ */
51
+ export function classifyHttpApiFileRole(relativePath) {
52
+ const path = normalizeHttpApiRelativePath(relativePath);
53
+ const lastSlash = path.lastIndexOf("/");
54
+ const dir = lastSlash < 0 ? "" : path.slice(0, lastSlash);
55
+ const fileName = lastSlash < 0 ? path : path.slice(lastSlash + 1);
56
+ const lower = fileName.toLowerCase();
57
+ if (lower.endsWith(".d.ts")) {
58
+ return {
59
+ role: "unsupported_reserved",
60
+ path,
61
+ diagnosticCode: "HTTPAPI-ROLE-001",
62
+ diagnosticMessage: `declaration files are not API sources: ${fileName}`,
63
+ };
64
+ }
65
+ const extIdx = fileName.lastIndexOf(".");
66
+ if (extIdx <= 0) {
67
+ return {
68
+ role: "unsupported_reserved",
69
+ path,
70
+ diagnosticCode: "HTTPAPI-ROLE-002",
71
+ diagnosticMessage: `filename must have a script extension: ${fileName}`,
72
+ };
73
+ }
74
+ const ext = fileName.slice(extIdx);
75
+ if (!isHttpApiScriptExtension(ext)) {
76
+ return {
77
+ role: "unsupported_reserved",
78
+ path,
79
+ diagnosticCode: "HTTPAPI-ROLE-003",
80
+ diagnosticMessage: `unsupported extension for API source: ${fileName}`,
81
+ };
82
+ }
83
+ const stem = fileName.slice(0, extIdx);
84
+ if (stem === "_api" && dir === "") {
85
+ return { role: "api_root", path };
86
+ }
87
+ if (stem === "_group") {
88
+ return { role: "group_override", path };
89
+ }
90
+ if (DIRECTORY_COMPANION_SET.has(fileName)) {
91
+ const kind = HTTPAPI_DIRECTORY_COMPANION_FILES.find((k) => k.toLowerCase() === lower);
92
+ return { role: "directory_companion", path, kind };
93
+ }
94
+ if (stem === "_api" || stem === "_group") {
95
+ return {
96
+ role: "unsupported_reserved",
97
+ path,
98
+ diagnosticCode: "HTTPAPI-ROLE-004",
99
+ diagnosticMessage: `convention file "${fileName}" must be at API root (_api.ts) or directory root (_group.ts)`,
100
+ };
101
+ }
102
+ for (const suffix of HTTPAPI_ENDPOINT_COMPANION_SUFFIXES) {
103
+ if (stem.endsWith(suffix)) {
104
+ const endpointStem = stem.slice(0, -suffix.length);
105
+ if (endpointStem === "") {
106
+ return {
107
+ role: "unsupported_reserved",
108
+ path,
109
+ diagnosticCode: "HTTPAPI-ROLE-005",
110
+ diagnosticMessage: `endpoint companion must have a base name: ${fileName}`,
111
+ };
112
+ }
113
+ return {
114
+ role: "endpoint_companion",
115
+ path,
116
+ kind: suffix,
117
+ endpointStem,
118
+ };
119
+ }
120
+ }
121
+ if (stem.startsWith("_")) {
122
+ return {
123
+ role: "unsupported_reserved",
124
+ path,
125
+ diagnosticCode: "HTTPAPI-ROLE-006",
126
+ diagnosticMessage: `reserved underscore-prefixed filename not in supported matrix: ${fileName}`,
127
+ };
128
+ }
129
+ return { role: "endpoint_primary", path };
130
+ }
131
+ /**
132
+ * Stable sort key for discovered files: normalized relative path (posix).
133
+ * Use with Array.sort(compareHttpApiPathOrder) for deterministic ordering.
134
+ */
135
+ export function compareHttpApiPathOrder(a, b) {
136
+ const na = normalizeHttpApiRelativePath(a);
137
+ const nb = normalizeHttpApiRelativePath(b);
138
+ return na.localeCompare(nb, "en");
139
+ }
140
+ /**
141
+ * Sorts an array of relative paths in deterministic API discovery order.
142
+ */
143
+ export function sortHttpApiPaths(paths) {
144
+ return [...paths].sort(compareHttpApiPathOrder);
145
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Parse and resolve `api:` virtual module IDs (router-plugin semantics).
3
+ * Used by HttpApi virtual module plugin for target-directory resolution.
4
+ */
5
+ export declare const HTTPAPI_VIRTUAL_MODULE_PREFIX = "api:";
6
+ export type ParseHttpApiVirtualModuleIdResult = {
7
+ readonly ok: true;
8
+ readonly relativeDirectory: string;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly reason: string;
12
+ };
13
+ /**
14
+ * Parses an `api:./<directory>` (or `api:<directory>`) ID.
15
+ * Normalizes bare directory to `./<directory>`.
16
+ */
17
+ export declare function parseHttpApiVirtualModuleId(id: string, prefix?: string): ParseHttpApiVirtualModuleIdResult;
18
+ export type ResolveHttpApiTargetDirectoryResult = {
19
+ readonly ok: true;
20
+ readonly targetDirectory: string;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly reason: string;
24
+ };
25
+ /**
26
+ * Resolves the target directory for an `api:` ID relative to the importer.
27
+ * Rejects paths that escape the importer base directory.
28
+ */
29
+ export declare function resolveHttpApiTargetDirectory(id: string, importer: string, prefix?: string): ResolveHttpApiTargetDirectoryResult;
30
+ //# sourceMappingURL=httpapiId.d.ts.map