@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
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @typed/app
2
+
3
+ > **Beta:** This package is in beta; APIs may change.
4
+
5
+ `@typed/app` provides **virtual module plugins** for the router and HttpApi stacks: `router:./path` and `api:./path` imports that generate typed route matchers and API clients from source. It also exports `createTypeInfoApiSessionForApp` for TypeInfo-backed type-checking and `defineApiHandler` for typed HttpApi endpoint contracts.
6
+
7
+ ## Purpose
8
+
9
+ Typed-smol apps get typed routes and APIs from convention-based source without manual wiring. You place route and endpoint files in directories, follow the file conventions, and import `router:./routes` or `api:./endpoints` to receive generated Matcher and Client modules. The plugins are configured in `vmc.config.ts` and consumed by `typedVitePlugin`, vmc (virtual-modules-compiler), or the TS plugin.
10
+
11
+ ## How to use
12
+
13
+ 1. Add the plugins to `vmc.config.ts` (see Configuration below).
14
+ 2. Import `router:./routes` and `api:./endpoints` in your app code to use the generated Matcher and Client.
15
+ 3. (Optional) Provide `createTypeInfoApiSessionForApp` to `typedVitePlugin` for structural type-checking of route and endpoint contracts.
16
+
17
+ ## Architecture
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ subgraph config [vmc.config.ts]
22
+ VMC[plugins array]
23
+ end
24
+
25
+ subgraph consumers [Consumers]
26
+ Vite[typedVitePlugin]
27
+ VMC2[vmc]
28
+ TS[TS plugin]
29
+ end
30
+
31
+ subgraph plugins [@typed/app plugins]
32
+ Router[Router VM plugin]
33
+ HttpApi[HttpApi VM plugin]
34
+ end
35
+
36
+ subgraph imports [App imports]
37
+ RImport["router:./routes"]
38
+ ApiImport["api:./endpoints"]
39
+ end
40
+
41
+ subgraph output [Generated output]
42
+ Matcher[Matcher source]
43
+ Client[Client + OpenAPI]
44
+ end
45
+
46
+ VMC --> Router
47
+ VMC --> HttpApi
48
+ Router --> Vite
49
+ HttpApi --> Vite
50
+ Vite --> RImport
51
+ Vite --> ApiImport
52
+ RImport --> Matcher
53
+ ApiImport --> Client
54
+ ```
55
+
56
+ ## Companion file conventions
57
+
58
+ ### Router plugin
59
+
60
+ | Pattern | Role | Behavior |
61
+ | ------- | ---- | -------- |
62
+ | `*.guard.ts` | Sibling | Guard for this route |
63
+ | `*.dependencies.ts` | Sibling | Dependencies for this route |
64
+ | `*.layout.ts` | Sibling | Layout wrapper |
65
+ | `*.catch.ts` | Sibling | Error catch handler |
66
+ | `_guard.ts` | Directory | Guard inherited by children |
67
+ | `_dependencies.ts` | Directory | Dependencies inherited by children |
68
+ | `_layout.ts` | Directory | Layout inherited by children |
69
+ | `_catch.ts` | Directory | Catch inherited by children |
70
+
71
+ Each route file must export `route` and exactly one of `handler`, `template`, or `default`.
72
+
73
+ ### HttpApi plugin
74
+
75
+ | Pattern | Role | Behavior |
76
+ | ------- | ---- | -------- |
77
+ | `_api.ts` | API root | Top-level API defaults (name, prefix, openapi) |
78
+ | `_group.ts` | Group override | Group name, prefix, dependencies, middlewares |
79
+ | `(pathless)/` | Pathless dir | Organizational only, no path segment |
80
+ | `*.name.ts` | Endpoint companion | Override endpoint name |
81
+ | `*.dependencies.ts` | Endpoint companion | Endpoint dependencies |
82
+ | `*.middlewares.ts` | Endpoint companion | Endpoint middlewares |
83
+ | `_dependencies.ts` | Directory | Inherited dependencies |
84
+ | `_middlewares.ts` | Directory | Inherited middlewares |
85
+
86
+ Each endpoint file must export `route`, `method`, and `handler`. See [router-virtual-module-plugin spec](../../.docs/specs/router-virtual-module-plugin/spec.md) and [httpapi-virtual-module-plugin spec](../../.docs/specs/httpapi-virtual-module-plugin/spec.md) for full details.
87
+
88
+ ## Dependencies
89
+
90
+ - `effect`
91
+ - `@typed/router`
92
+ - `@typed/virtual-modules`
93
+
94
+ Peer: `typescript`.
95
+
96
+ ## Installation
97
+
98
+ ```bash
99
+ pnpm add @typed/app @typed/router @typed/virtual-modules
100
+ ```
101
+
102
+ ## Configuration
103
+
104
+ Configure plugins in `vmc.config.ts` (used by vmc, the TS plugin, and typedVitePlugin):
105
+
106
+ ```ts
107
+ // vmc.config.ts
108
+ import { createRouterVirtualModulePlugin, createHttpApiVirtualModulePlugin } from "@typed/app";
109
+
110
+ export default {
111
+ plugins: [
112
+ createRouterVirtualModulePlugin(),
113
+ createHttpApiVirtualModulePlugin(),
114
+ ],
115
+ };
116
+ ```
117
+
118
+ ## Example
119
+
120
+ ```ts
121
+ // Import generated router matcher
122
+ import { Matcher } from "router:./routes";
123
+
124
+ // Import generated API client
125
+ import { Client } from "api:./endpoints";
126
+
127
+ // Define typed handler
128
+ import { defineApiHandler } from "@typed/app";
129
+ import { Route } from "@typed/router";
130
+ import * as Schema from "effect/Schema";
131
+
132
+ const handler = defineApiHandler(
133
+ Route.Parse("/todos/:id"),
134
+ "GET",
135
+ { success: Schema.Struct({ id: Schema.String }) }
136
+ )(({ path }) => Effect.succeed({ id: path.id }));
137
+ ```
138
+
139
+ ## API overview
140
+
141
+ - **Router VM plugin** — `createRouterVirtualModulePlugin(options)` — virtual `router:./routes` imports; scans route files, emits typed Matcher source.
142
+ - **HttpApi VM plugin** — `createHttpApiVirtualModulePlugin(options)` — virtual `api:./endpoints` imports; scans API files, emits typed Api + Client + OpenAPI.
143
+ - **TypeInfo session** — `createTypeInfoApiSessionForApp({ ts, program })` — session with router + HttpApi type targets; use with `typedVitePlugin` or vmc.
144
+ - **API handler helper** — `defineApiHandler(route, method, schemas?)(handler)` — typed handler with path/query/headers/body; success/error schemas for responses.
145
+ - **Parsing helpers** — `parseRouterVirtualModuleId`, `parseHttpApiVirtualModuleId`; `resolveRouterTargetDirectory`, `resolveHttpApiTargetDirectory`.
146
+ - **Type target specs** — `ROUTER_TYPE_TARGET_SPECS`, `HTTPAPI_TYPE_TARGET_SPECS`; `APP_TYPE_TARGET_BOOTSTRAP_CONTENT`.
147
+
148
+ ## API reference
149
+
150
+ ### RouterVirtualModulePluginOptions
151
+
152
+ | Property | Type | Default | Description |
153
+ | -------- | ---- | ------- | ----------- |
154
+ | `prefix` | `string` | `"router:"` | Virtual module ID prefix. |
155
+ | `name` | `string` | `"router-virtual-module"` | Plugin name for diagnostics. |
156
+
157
+ ### HttpApiVirtualModulePluginOptions
158
+
159
+ | Property | Type | Default | Description |
160
+ | -------- | ---- | ------- | ----------- |
161
+ | `prefix` | `string` | `"api:"` | Virtual module ID prefix. |
162
+ | `name` | `string` | `"httpapi-virtual-module"` | Plugin name for diagnostics. |
163
+
164
+ ### createTypeInfoApiSessionForApp
165
+
166
+ Requires a TypeScript `program` that includes imports from canonical type target modules. If the program has no such imports, write `APP_TYPE_TARGET_BOOTSTRAP_CONTENT` to a file and include it in `rootNames`.
@@ -0,0 +1,26 @@
1
+ import type { VirtualModulePlugin } from "@typed/virtual-modules";
2
+ export interface HttpApiVirtualModulePluginOptions {
3
+ readonly prefix?: string;
4
+ readonly name?: string;
5
+ }
6
+ export type ParseHttpApiVirtualModuleIdResult = {
7
+ readonly ok: true;
8
+ readonly relativeDirectory: string;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly reason: string;
12
+ };
13
+ export declare function parseHttpApiVirtualModuleId(id: string, prefix?: string): ParseHttpApiVirtualModuleIdResult;
14
+ export type ResolveHttpApiTargetDirectoryResult = {
15
+ readonly ok: true;
16
+ readonly targetDirectory: string;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly reason: string;
20
+ };
21
+ export declare function resolveHttpApiTargetDirectory(id: string, importer: string, prefix?: string): ResolveHttpApiTargetDirectoryResult;
22
+ /**
23
+ * Creates the HttpApi virtual module plugin with sync shouldResolve and build behavior.
24
+ */
25
+ export declare const createHttpApiVirtualModulePlugin: (options?: HttpApiVirtualModulePluginOptions) => VirtualModulePlugin;
26
+ //# sourceMappingURL=HttpApiVirtualModulePlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpApiVirtualModulePlugin.d.ts","sourceRoot":"","sources":["../src/HttpApiVirtualModulePlugin.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAIV,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAgChC,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,iCAAiC,GACzC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,MAAM,EACV,MAAM,GAAE,MAAuB,GAC9B,iCAAiC,CAwBnC;AAED,MAAM,MAAM,mCAAmC,GAC3C;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,wBAAgB,6BAA6B,CAC3C,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAuB,GAC9B,mCAAmC,CAiBrC;AAoHD;;GAEG;AACH,eAAO,MAAM,gCAAgC,GAC3C,UAAS,iCAAsC,KAC9C,mBAoIF,CAAC"}
@@ -0,0 +1,301 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { dirname, extname, join, relative } from "node:path";
3
+ import { pathIsUnderBase, resolvePathUnderBase, resolveRelativePath, toPosixPath, } from "./internal/path.js";
4
+ import { buildHttpApiDescriptorTree, } from "./internal/httpapiDescriptorTree.js";
5
+ import { classifyHttpApiFileRole } from "./internal/httpapiFileRoles.js";
6
+ import { emitHttpApiSource } from "./internal/emitHttpApiSource.js";
7
+ import { extractEndpointLiterals } from "./internal/extractHttpApiLiterals.js";
8
+ import { getCallableReturnType, isCallableNode, typeNodeIsRouteCompatible, } from "./internal/routeTypeNode.js";
9
+ import { validateNonEmptyString, validatePathSegment } from "./internal/validation.js";
10
+ import { HTTPAPI_TYPE_TARGET_SPECS } from "./internal/typeTargetSpecs.js";
11
+ const DEFAULT_PREFIX = "api:";
12
+ const DEFAULT_PLUGIN_NAME = "httpapi-virtual-module";
13
+ /** Extensions that count as script files when checking if a directory should resolve. */
14
+ const SCRIPT_EXTENSION_SET = new Set([
15
+ ".ts",
16
+ ".tsx",
17
+ ".js",
18
+ ".jsx",
19
+ ".mts",
20
+ ".cts",
21
+ ".mjs",
22
+ ".cjs",
23
+ ]);
24
+ /** Glob patterns for discovering API source files. */
25
+ const API_FILE_GLOBS = [
26
+ "**/*.ts",
27
+ "**/*.tsx",
28
+ "**/*.js",
29
+ "**/*.jsx",
30
+ "**/*.mts",
31
+ "**/*.cts",
32
+ "**/*.mjs",
33
+ "**/*.cjs",
34
+ ];
35
+ const REQUIRED_ENDPOINT_EXPORTS = ["route", "method", "handler"];
36
+ export function parseHttpApiVirtualModuleId(id, prefix = DEFAULT_PREFIX) {
37
+ const idResult = validateNonEmptyString(id, "id");
38
+ if (!idResult.ok)
39
+ return { ok: false, reason: idResult.reason };
40
+ const prefixResult = validateNonEmptyString(prefix, "prefix");
41
+ if (!prefixResult.ok)
42
+ return { ok: false, reason: prefixResult.reason };
43
+ if (!id.startsWith(prefix)) {
44
+ return { ok: false, reason: `id must start with "${prefix}"` };
45
+ }
46
+ let relativeDirectory = id.slice(prefix.length);
47
+ if (relativeDirectory.length > 0 &&
48
+ relativeDirectory !== "." &&
49
+ relativeDirectory !== ".." &&
50
+ !relativeDirectory.startsWith("./") &&
51
+ !relativeDirectory.startsWith("../") &&
52
+ !relativeDirectory.startsWith("/")) {
53
+ relativeDirectory = `./${relativeDirectory}`;
54
+ }
55
+ const relativeResult = validatePathSegment(relativeDirectory, "relativeDirectory");
56
+ if (!relativeResult.ok)
57
+ return { ok: false, reason: relativeResult.reason };
58
+ return { ok: true, relativeDirectory: relativeResult.value };
59
+ }
60
+ export function resolveHttpApiTargetDirectory(id, importer, prefix = DEFAULT_PREFIX) {
61
+ const parsed = parseHttpApiVirtualModuleId(id, prefix);
62
+ if (!parsed.ok)
63
+ return parsed;
64
+ const importerResult = validatePathSegment(importer, "importer");
65
+ if (!importerResult.ok)
66
+ return { ok: false, reason: importerResult.reason };
67
+ const importerDir = dirname(toPosixPath(importerResult.value));
68
+ const resolved = resolvePathUnderBase(importerDir, parsed.relativeDirectory);
69
+ if (!resolved.ok) {
70
+ return { ok: false, reason: "resolved target directory escapes importer base directory" };
71
+ }
72
+ if (!pathIsUnderBase(importerDir, resolved.path)) {
73
+ return { ok: false, reason: "resolved target directory is outside importer base directory" };
74
+ }
75
+ return { ok: true, targetDirectory: toPosixPath(resolved.path) };
76
+ }
77
+ function isExistingDirectory(absolutePath) {
78
+ try {
79
+ return statSync(absolutePath).isDirectory();
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ function directoryHasScriptFiles(dir) {
86
+ try {
87
+ const items = readdirSync(dir, { withFileTypes: true });
88
+ for (const e of items) {
89
+ if (e.isFile() &&
90
+ SCRIPT_EXTENSION_SET.has(extname(e.name).toLowerCase()) &&
91
+ !e.name.toLowerCase().endsWith(".d.ts"))
92
+ return true;
93
+ if (e.isDirectory() && directoryHasScriptFiles(join(dir, e.name)))
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ function collectEndpointNodes(nodes) {
103
+ const collected = [];
104
+ for (const node of nodes) {
105
+ if (node.type === "endpoint") {
106
+ collected.push(node);
107
+ continue;
108
+ }
109
+ collected.push(...collectEndpointNodes(node.children));
110
+ }
111
+ return collected;
112
+ }
113
+ function mapSnapshotsByRelativePath(snapshots, targetDirectory) {
114
+ const byPath = new Map();
115
+ for (const snapshot of snapshots) {
116
+ const relativePath = toPosixPath(relative(targetDirectory, snapshot.filePath));
117
+ byPath.set(relativePath, snapshot);
118
+ }
119
+ return byPath;
120
+ }
121
+ function validateEndpointContracts(endpoints, snapshotsByPath, api) {
122
+ const violations = [];
123
+ for (const endpoint of endpoints) {
124
+ const snapshot = snapshotsByPath.get(endpoint.path);
125
+ if (!snapshot) {
126
+ violations.push({
127
+ code: "AVM-CONTRACT-001",
128
+ message: `endpoint module not found in TypeInfo snapshot set: ${endpoint.path}`,
129
+ });
130
+ continue;
131
+ }
132
+ const exportedNames = new Set(snapshot.exports.map((exported) => exported.name));
133
+ const missing = REQUIRED_ENDPOINT_EXPORTS.filter((name) => !exportedNames.has(name));
134
+ if (missing.length > 0) {
135
+ violations.push({
136
+ code: "AVM-CONTRACT-002",
137
+ message: `endpoint "${endpoint.path}" missing required export(s): ${missing.join(", ")}`,
138
+ });
139
+ continue;
140
+ }
141
+ const routeExport = snapshot.exports.find((e) => e.name === "route");
142
+ if (!routeExport)
143
+ continue;
144
+ if (!typeNodeIsRouteCompatible(routeExport.type, api)) {
145
+ const hint = "; route must be assignable to Route from @typed/router";
146
+ violations.push({
147
+ code: "AVM-CONTRACT-003",
148
+ message: `endpoint "${endpoint.path}" route: export must be Route (Parse, Param, Join, etc.) from @typed/router${hint}`,
149
+ });
150
+ }
151
+ const handlerExport = snapshot.exports.find((e) => e.name === "handler");
152
+ if (handlerExport) {
153
+ const handlerNode = isCallableNode(handlerExport.type)
154
+ ? (getCallableReturnType(handlerExport.type) ?? handlerExport.type)
155
+ : handlerExport.type;
156
+ const handlerReturnsEffect = api.isAssignableTo(handlerNode, "Effect");
157
+ if (!handlerReturnsEffect) {
158
+ violations.push({
159
+ code: "AVM-CONTRACT-004",
160
+ message: `endpoint "${endpoint.path}" handler: return type must be Effect`,
161
+ });
162
+ }
163
+ }
164
+ const successExport = snapshot.exports.find((e) => e.name === "success");
165
+ if (successExport && !api.isAssignableTo(successExport.type, "Schema")) {
166
+ violations.push({
167
+ code: "AVM-CONTRACT-005",
168
+ message: `endpoint "${endpoint.path}" success: export must be Schema when present`,
169
+ });
170
+ }
171
+ const errorExport = snapshot.exports.find((e) => e.name === "error");
172
+ if (errorExport && !api.isAssignableTo(errorExport.type, "Schema")) {
173
+ violations.push({
174
+ code: "AVM-CONTRACT-006",
175
+ message: `endpoint "${endpoint.path}" error: export must be Schema when present`,
176
+ });
177
+ }
178
+ }
179
+ return violations.sort((a, b) => a.message.localeCompare(b.message, "en"));
180
+ }
181
+ /**
182
+ * Creates the HttpApi virtual module plugin with sync shouldResolve and build behavior.
183
+ */
184
+ export const createHttpApiVirtualModulePlugin = (options = {}) => {
185
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
186
+ const name = options.name ?? DEFAULT_PLUGIN_NAME;
187
+ return {
188
+ name,
189
+ typeTargetSpecs: HTTPAPI_TYPE_TARGET_SPECS,
190
+ shouldResolve(id, importer) {
191
+ const resolved = resolveHttpApiTargetDirectory(id, importer, prefix);
192
+ if (!resolved.ok)
193
+ return false;
194
+ if (!isExistingDirectory(resolved.targetDirectory))
195
+ return false;
196
+ return directoryHasScriptFiles(resolved.targetDirectory);
197
+ },
198
+ build(id, importer, api) {
199
+ const resolved = resolveHttpApiTargetDirectory(id, importer, prefix);
200
+ if (!resolved.ok) {
201
+ return {
202
+ errors: [{ code: "AVM-ID-001", message: resolved.reason, pluginName: name }],
203
+ };
204
+ }
205
+ if (!isExistingDirectory(resolved.targetDirectory)) {
206
+ return {
207
+ errors: [
208
+ {
209
+ code: "AVM-DISC-001",
210
+ message: `target directory does not exist: ${resolveRelativePath(dirname(importer), resolved.targetDirectory)}`,
211
+ pluginName: name,
212
+ },
213
+ ],
214
+ };
215
+ }
216
+ const snapshots = api.directory(API_FILE_GLOBS, {
217
+ baseDir: resolved.targetDirectory,
218
+ recursive: true,
219
+ watch: true,
220
+ });
221
+ if (snapshots.length === 0) {
222
+ return {
223
+ errors: [
224
+ {
225
+ code: "AVM-LEAF-001",
226
+ message: `no API source files discovered in ${resolved.targetDirectory}`,
227
+ pluginName: name,
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ const snapshotsByRelativePath = mapSnapshotsByRelativePath(snapshots, resolved.targetDirectory);
233
+ const relativePaths = [...snapshotsByRelativePath.keys()].sort((a, b) => a.localeCompare(b, "en"));
234
+ const roles = relativePaths.map((path) => classifyHttpApiFileRole(path));
235
+ const tree = buildHttpApiDescriptorTree({ roles });
236
+ const endpoints = collectEndpointNodes(tree.children);
237
+ if (endpoints.length === 0) {
238
+ return {
239
+ errors: [
240
+ {
241
+ code: "AVM-LEAF-001",
242
+ message: `no valid API endpoint leaves discovered in ${resolved.targetDirectory}`,
243
+ pluginName: name,
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ const contractViolations = validateEndpointContracts(endpoints, snapshotsByRelativePath, api);
249
+ if (contractViolations.length > 0) {
250
+ return {
251
+ errors: contractViolations.map((violation) => ({
252
+ code: violation.code,
253
+ message: violation.message,
254
+ pluginName: name,
255
+ })),
256
+ };
257
+ }
258
+ const extractedLiteralsByPath = new Map();
259
+ const optionalExportsByPath = new Map();
260
+ const handlerIsRawByPath = new Map();
261
+ const OPTIONAL_NAMES = ["headers", "body", "success", "error"];
262
+ for (const endpoint of endpoints) {
263
+ const snapshot = snapshotsByRelativePath.get(endpoint.path);
264
+ if (snapshot) {
265
+ const literals = extractEndpointLiterals(snapshot, endpoint.stem);
266
+ extractedLiteralsByPath.set(endpoint.path, literals);
267
+ const exportedNames = new Set(snapshot.exports.map((e) => e.name));
268
+ const present = new Set(OPTIONAL_NAMES.filter((n) => exportedNames.has(n)));
269
+ optionalExportsByPath.set(endpoint.path, present);
270
+ const handlerExport = snapshot.exports.find((e) => e.name === "handler");
271
+ if (handlerExport != null &&
272
+ api.isAssignableTo(handlerExport.type, "HttpServerResponse", [
273
+ { kind: "returnType" },
274
+ { kind: "typeArg", index: 0 },
275
+ ])) {
276
+ handlerIsRawByPath.set(endpoint.path, true);
277
+ }
278
+ }
279
+ }
280
+ const sourceText = emitHttpApiSource({
281
+ tree,
282
+ targetDirectory: resolved.targetDirectory,
283
+ importer,
284
+ extractedLiteralsByPath,
285
+ optionalExportsByPath,
286
+ handlerIsRawByPath,
287
+ });
288
+ if (tree.diagnostics.length > 0) {
289
+ return {
290
+ sourceText,
291
+ warnings: tree.diagnostics.map((diagnostic) => ({
292
+ code: diagnostic.code,
293
+ message: diagnostic.message,
294
+ pluginName: name,
295
+ })),
296
+ };
297
+ }
298
+ return sourceText;
299
+ },
300
+ };
301
+ };
@@ -0,0 +1,23 @@
1
+ import type { VirtualModulePlugin } from "@typed/virtual-modules";
2
+ export interface RouterVirtualModulePluginOptions {
3
+ readonly prefix?: string;
4
+ readonly name?: string;
5
+ }
6
+ export type ParseRouterVirtualModuleIdResult = {
7
+ readonly ok: true;
8
+ readonly relativeDirectory: string;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly reason: string;
12
+ };
13
+ export declare function parseRouterVirtualModuleId(id: string, prefix?: string): ParseRouterVirtualModuleIdResult;
14
+ export type ResolveRouterTargetDirectoryResult = {
15
+ readonly ok: true;
16
+ readonly targetDirectory: string;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly reason: string;
20
+ };
21
+ export declare function resolveRouterTargetDirectory(id: string, importer: string, prefix?: string): ResolveRouterTargetDirectoryResult;
22
+ export declare const createRouterVirtualModulePlugin: (options?: RouterVirtualModulePluginOptions) => VirtualModulePlugin;
23
+ //# sourceMappingURL=RouterVirtualModulePlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterVirtualModulePlugin.d.ts","sourceRoot":"","sources":["../src/RouterVirtualModulePlugin.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AA8B3F,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,gCAAgC,GACxC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,MAAM,EACV,MAAM,GAAE,MAAuB,GAC9B,gCAAgC,CAwBlC;AAED,MAAM,MAAM,kCAAkC,GAC1C;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAuB,GAC9B,kCAAkC,CAiBpC;AAmDD,eAAO,MAAM,+BAA+B,GAC1C,UAAS,gCAAqC,KAC7C,mBAiFF,CAAC"}