@warlock.js/core 5.26.0 → 5.27.0

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 (98) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/esm/cli/commands/openapi-generator.action.mjs +43 -0
  3. package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
  4. package/esm/cli/commands/openapi-generator.command.mjs +44 -0
  5. package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
  6. package/esm/cli/framework-cli-commands.mjs +2 -0
  7. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  8. package/esm/dev-server/build-development-openapi-document.mjs +36 -0
  9. package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
  10. package/esm/dev-server/collect-development-api-routes.mjs +27 -0
  11. package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
  12. package/esm/dev-server/dev-logger.mjs +9 -1
  13. package/esm/dev-server/dev-logger.mjs.map +1 -1
  14. package/esm/dev-server/development-server.mjs +6 -3
  15. package/esm/dev-server/development-server.mjs.map +1 -1
  16. package/esm/dev-server/files-orchestrator.mjs +17 -1
  17. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  18. package/esm/dev-server/model-resource-typings.mjs +246 -0
  19. package/esm/dev-server/model-resource-typings.mjs.map +1 -0
  20. package/esm/dev-server/type-generator.mjs +46 -2
  21. package/esm/dev-server/type-generator.mjs.map +1 -1
  22. package/esm/http/index.d.mts +2 -1
  23. package/esm/http/request.d.mts +2 -2
  24. package/esm/http/request.d.mts.map +1 -1
  25. package/esm/http/serialized.d.mts +114 -0
  26. package/esm/http/serialized.d.mts.map +1 -0
  27. package/esm/index.d.mts +8 -3
  28. package/esm/index.mjs +4 -1
  29. package/esm/openapi/build-openapi-document.d.mts +17 -0
  30. package/esm/openapi/build-openapi-document.d.mts.map +1 -0
  31. package/esm/openapi/build-openapi-document.mjs +124 -0
  32. package/esm/openapi/build-openapi-document.mjs.map +1 -0
  33. package/esm/openapi/cast-schema.mjs +89 -0
  34. package/esm/openapi/cast-schema.mjs.map +1 -0
  35. package/esm/openapi/collect-openapi-document.mjs +40 -0
  36. package/esm/openapi/collect-openapi-document.mjs.map +1 -0
  37. package/esm/openapi/development-openapi-provider.d.mts +17 -0
  38. package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
  39. package/esm/openapi/development-openapi-provider.mjs +21 -0
  40. package/esm/openapi/development-openapi-provider.mjs.map +1 -0
  41. package/esm/openapi/error-responses.mjs +67 -0
  42. package/esm/openapi/error-responses.mjs.map +1 -0
  43. package/esm/openapi/index.d.mts +3 -0
  44. package/esm/openapi/index.mjs +4 -0
  45. package/esm/openapi/openapi-child-output.mjs +57 -0
  46. package/esm/openapi/openapi-child-output.mjs.map +1 -0
  47. package/esm/openapi/openapi-protocol.mjs +30 -0
  48. package/esm/openapi/openapi-protocol.mjs.map +1 -0
  49. package/esm/openapi/paths.mjs +63 -0
  50. package/esm/openapi/paths.mjs.map +1 -0
  51. package/esm/openapi/request-schemas.mjs +138 -0
  52. package/esm/openapi/request-schemas.mjs.map +1 -0
  53. package/esm/openapi/response-schemas.mjs +85 -0
  54. package/esm/openapi/response-schemas.mjs.map +1 -0
  55. package/esm/openapi/schema-registry.mjs +143 -0
  56. package/esm/openapi/schema-registry.mjs.map +1 -0
  57. package/esm/openapi/security.mjs +98 -0
  58. package/esm/openapi/security.mjs.map +1 -0
  59. package/esm/openapi/types.d.mts +119 -0
  60. package/esm/openapi/types.d.mts.map +1 -0
  61. package/esm/production/registration-child-runner.mjs +139 -0
  62. package/esm/production/registration-child-runner.mjs.map +1 -0
  63. package/esm/production/route-registration-child.mjs +30 -2
  64. package/esm/production/route-registration-child.mjs.map +1 -1
  65. package/esm/production/route-registration-snapshot.mjs +22 -119
  66. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  67. package/esm/resource/define-resource.d.mts +44 -14
  68. package/esm/resource/define-resource.d.mts.map +1 -1
  69. package/esm/resource/define-resource.mjs +0 -40
  70. package/esm/resource/define-resource.mjs.map +1 -1
  71. package/esm/resource/index.d.mts +1 -1
  72. package/esm/resource/resource.d.mts +4 -4
  73. package/esm/resource/resource.d.mts.map +1 -1
  74. package/esm/resource/resource.mjs.map +1 -1
  75. package/esm/resource/types.d.mts +89 -4
  76. package/esm/resource/types.d.mts.map +1 -1
  77. package/esm/restful/restful.d.mts +5 -5
  78. package/esm/restful/restful.d.mts.map +1 -1
  79. package/esm/router/index.d.mts +1 -1
  80. package/esm/router/named-api-routes-with-responses.mjs +59 -0
  81. package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
  82. package/esm/router/response-schema-types.mjs +100 -0
  83. package/esm/router/response-schema-types.mjs.map +1 -0
  84. package/esm/router/router.d.mts +2 -2
  85. package/esm/router/router.d.mts.map +1 -1
  86. package/esm/router/router.mjs +38 -11
  87. package/esm/router/router.mjs.map +1 -1
  88. package/esm/router/types.d.mts +16 -1
  89. package/esm/router/types.d.mts.map +1 -1
  90. package/llms-full.txt +265 -5
  91. package/llms.txt +3 -2
  92. package/package.json +13 -13
  93. package/skills/README.md +3 -2
  94. package/skills/create-controller/SKILL.md +39 -2
  95. package/skills/define-resource/SKILL.md +100 -1
  96. package/skills/generate-openapi/SKILL.md +120 -0
  97. package/skills/index.md +3 -3
  98. package/skills/validate-input/SKILL.md +2 -2
@@ -0,0 +1,143 @@
1
+ import { castToJsonSchema } from "./cast-schema.mjs";
2
+ import { isLazy } from "@mongez/reinforcements";
3
+
4
+ //#region ../core/src/openapi/schema-registry.ts
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function sanitizeName(name) {
9
+ return name.replace(/[^A-Za-z0-9._-]/g, "");
10
+ }
11
+ /**
12
+ * Owns `components.schemas`: names resource classes (by identity), builds their schemas once
13
+ * and hands out `$ref`s, so nested, lazy and `"self"` references recurse without unrolling.
14
+ */
15
+ var SchemaRegistry = class {
16
+ constructor(resolveName, warn) {
17
+ this.resolveName = resolveName;
18
+ this.warn = warn;
19
+ this.schemas = {};
20
+ this.resourceNames = /* @__PURE__ */ new Map();
21
+ this.sharedNames = /* @__PURE__ */ new Map();
22
+ this.fallbackCounter = 0;
23
+ }
24
+ /**
25
+ * Register a fixed schema (an error shape shared by many operations) under a stable key.
26
+ * Returns its `$ref`; the component name gets a numeric suffix only if a resource took it.
27
+ */
28
+ shared(key, schema) {
29
+ let name = this.sharedNames.get(key);
30
+ if (!name) {
31
+ name = this.claim(key);
32
+ this.sharedNames.set(key, name);
33
+ this.schemas[name] = schema;
34
+ }
35
+ return { $ref: `#/components/schemas/${name}` };
36
+ }
37
+ /**
38
+ * `$ref` to a resource class, building its component on first use.
39
+ */
40
+ resourceRef(resource) {
41
+ const existing = this.resourceNames.get(resource);
42
+ if (existing) return { $ref: `#/components/schemas/${existing}` };
43
+ const name = this.claim(this.nameFor(resource));
44
+ const ref = { $ref: `#/components/schemas/${name}` };
45
+ this.resourceNames.set(resource, name);
46
+ this.schemas[name] = {};
47
+ this.schemas[name] = this.buildResourceSchema(resource, name, ref);
48
+ return ref;
49
+ }
50
+ /**
51
+ * Schema of a cast string, or `{}` and a warning for an unknown cast.
52
+ */
53
+ cast(cast, path, warn = this.warn) {
54
+ const schema = castToJsonSchema(cast);
55
+ if (!schema) {
56
+ warn(`${path}: unknown cast "${cast}" is documented as {}.`);
57
+ return {};
58
+ }
59
+ return schema;
60
+ }
61
+ /**
62
+ * True when the function looks like a resource class: it carries a static `schema` object.
63
+ */
64
+ isResourceClass(value) {
65
+ return typeof value === "function" && isRecord(Reflect.get(value, "schema"));
66
+ }
67
+ nameFor(resource) {
68
+ const resolved = sanitizeName(this.resolveName?.(resource) ?? "");
69
+ if (resolved && resolved !== "default") return resolved;
70
+ const className = typeof resource === "function" ? sanitizeName(resource.name) : "";
71
+ if (className && className !== "default") return className;
72
+ return `Resource${++this.fallbackCounter}`;
73
+ }
74
+ claim(base) {
75
+ if (!(base in this.schemas)) return base;
76
+ let suffix = 2;
77
+ while (`${base}${suffix}` in this.schemas) suffix++;
78
+ return `${base}${suffix}`;
79
+ }
80
+ buildResourceSchema(resource, name, ref) {
81
+ const schema = Reflect.get(resource, "schema");
82
+ if (!isRecord(schema) || Object.keys(schema).length === 0) {
83
+ this.warn(`Resource "${name}" declares no static schema and is documented as {}.`);
84
+ return {};
85
+ }
86
+ const properties = {};
87
+ for (const [key, config] of Object.entries(schema)) properties[key] = this.fieldSchema(config, {
88
+ ownerRef: ref,
89
+ path: `${name}.${key}`
90
+ });
91
+ return {
92
+ type: "object",
93
+ properties,
94
+ required: Object.keys(properties)
95
+ };
96
+ }
97
+ fieldSchema(config, context) {
98
+ if (config === "self") return context.ownerRef;
99
+ if (config === "self[]") return {
100
+ type: "array",
101
+ items: context.ownerRef
102
+ };
103
+ if (typeof config === "string") return this.cast(config, context.path);
104
+ if (isLazy(config)) return this.lazyFieldSchema(config, context);
105
+ if (Array.isArray(config) && config.length === 2 && typeof config[1] === "string") return this.cast(config[1], context.path);
106
+ if (typeof config === "function") {
107
+ if (this.isResourceClass(config)) return this.resourceRef(config);
108
+ this.warn(`${context.path}: a resolver function has no static shape and is documented as {}.`);
109
+ return {};
110
+ }
111
+ if (isRecord(config) && config.__type === "arrayOf" && isRecord(config.schema)) {
112
+ const properties = {};
113
+ for (const [key, item] of Object.entries(config.schema)) properties[key] = this.fieldSchema(item, {
114
+ ...context,
115
+ path: `${context.path}.${key}`
116
+ });
117
+ return {
118
+ type: "array",
119
+ items: {
120
+ type: "object",
121
+ properties,
122
+ required: Object.keys(properties)
123
+ }
124
+ };
125
+ }
126
+ this.warn(`${context.path}: a field builder has no static shape and is documented as {}. Declare it with a cast string to document it.`);
127
+ return {};
128
+ }
129
+ lazyFieldSchema(lazy, context) {
130
+ let resolved;
131
+ try {
132
+ resolved = lazy.resolve();
133
+ } catch (error) {
134
+ this.warn(`${context.path}: a lazy resource reference could not be resolved (${error instanceof Error ? error.message : String(error)}) and is documented as {}.`);
135
+ return {};
136
+ }
137
+ return this.fieldSchema(resolved, context);
138
+ }
139
+ };
140
+
141
+ //#endregion
142
+ export { SchemaRegistry };
143
+ //# sourceMappingURL=schema-registry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-registry.mjs","names":[],"sources":["../../../../../../../core/src/openapi/schema-registry.ts"],"sourcesContent":["import { isLazy } from \"@mongez/reinforcements\";\nimport { castToJsonSchema } from \"./cast-schema\";\nimport type { OpenApiSchema, WarningSink } from \"./types\";\n\ntype ResourceFieldContext = {\n /** `$ref` schema of the resource whose field is being mapped, the target of `\"self\"`. */\n ownerRef: OpenApiSchema;\n /** Display name used in warnings, for example `UserResource.avatar`. */\n path: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction sanitizeName(name: string): string {\n return name.replace(/[^A-Za-z0-9._-]/g, \"\");\n}\n\n/**\n * Owns `components.schemas`: names resource classes (by identity), builds their schemas once\n * and hands out `$ref`s, so nested, lazy and `\"self\"` references recurse without unrolling.\n */\nexport class SchemaRegistry {\n public readonly schemas: Record<string, OpenApiSchema> = {};\n\n private readonly resourceNames = new Map<unknown, string>();\n private readonly sharedNames = new Map<string, string>();\n private fallbackCounter = 0;\n\n public constructor(\n private readonly resolveName: ((resource: unknown) => string | undefined) | undefined,\n private readonly warn: WarningSink,\n ) {}\n\n /**\n * Register a fixed schema (an error shape shared by many operations) under a stable key.\n * Returns its `$ref`; the component name gets a numeric suffix only if a resource took it.\n */\n public shared(key: string, schema: OpenApiSchema): OpenApiSchema {\n let name = this.sharedNames.get(key);\n\n if (!name) {\n name = this.claim(key);\n this.sharedNames.set(key, name);\n this.schemas[name] = schema;\n }\n\n return { $ref: `#/components/schemas/${name}` };\n }\n\n /**\n * `$ref` to a resource class, building its component on first use.\n */\n public resourceRef(resource: unknown): OpenApiSchema {\n const existing = this.resourceNames.get(resource);\n\n if (existing) {\n return { $ref: `#/components/schemas/${existing}` };\n }\n\n const name = this.claim(this.nameFor(resource));\n const ref = { $ref: `#/components/schemas/${name}` };\n\n // Named before its fields are mapped, so a cycle resolves to this same ref.\n this.resourceNames.set(resource, name);\n this.schemas[name] = {};\n this.schemas[name] = this.buildResourceSchema(resource, name, ref);\n\n return ref;\n }\n\n /**\n * Schema of a cast string, or `{}` and a warning for an unknown cast.\n */\n public cast(cast: string, path: string, warn: WarningSink = this.warn): OpenApiSchema {\n const schema = castToJsonSchema(cast);\n\n if (!schema) {\n warn(`${path}: unknown cast \"${cast}\" is documented as {}.`);\n\n return {};\n }\n\n return schema;\n }\n\n /**\n * True when the function looks like a resource class: it carries a static `schema` object.\n */\n public isResourceClass(value: unknown): boolean {\n return typeof value === \"function\" && isRecord(Reflect.get(value, \"schema\"));\n }\n\n private nameFor(resource: unknown): string {\n const resolved = sanitizeName(this.resolveName?.(resource) ?? \"\");\n\n if (resolved && resolved !== \"default\") {\n return resolved;\n }\n\n const className = typeof resource === \"function\" ? sanitizeName(resource.name) : \"\";\n\n if (className && className !== \"default\") {\n return className;\n }\n\n return `Resource${++this.fallbackCounter}`;\n }\n\n private claim(base: string): string {\n if (!(base in this.schemas)) {\n return base;\n }\n\n let suffix = 2;\n\n while (`${base}${suffix}` in this.schemas) {\n suffix++;\n }\n\n return `${base}${suffix}`;\n }\n\n private buildResourceSchema(resource: unknown, name: string, ref: OpenApiSchema): OpenApiSchema {\n const schema = Reflect.get(resource as object, \"schema\");\n\n if (!isRecord(schema) || Object.keys(schema).length === 0) {\n this.warn(`Resource \"${name}\" declares no static schema and is documented as {}.`);\n\n return {};\n }\n\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, config] of Object.entries(schema)) {\n properties[key] = this.fieldSchema(config, { ownerRef: ref, path: `${name}.${key}` });\n }\n\n return { type: \"object\", properties, required: Object.keys(properties) };\n }\n\n private fieldSchema(config: unknown, context: ResourceFieldContext): OpenApiSchema {\n if (config === \"self\") {\n return context.ownerRef;\n }\n\n if (config === \"self[]\") {\n return { type: \"array\", items: context.ownerRef };\n }\n\n if (typeof config === \"string\") {\n return this.cast(config, context.path);\n }\n\n if (isLazy(config)) {\n return this.lazyFieldSchema(config, context);\n }\n\n if (Array.isArray(config) && config.length === 2 && typeof config[1] === \"string\") {\n return this.cast(config[1], context.path);\n }\n\n if (typeof config === \"function\") {\n if (this.isResourceClass(config)) {\n return this.resourceRef(config);\n }\n\n this.warn(`${context.path}: a resolver function has no static shape and is documented as {}.`);\n\n return {};\n }\n\n if (isRecord(config) && config.__type === \"arrayOf\" && isRecord(config.schema)) {\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, item] of Object.entries(config.schema)) {\n properties[key] = this.fieldSchema(item, { ...context, path: `${context.path}.${key}` });\n }\n\n return {\n type: \"array\",\n items: { type: \"object\", properties, required: Object.keys(properties) },\n };\n }\n\n this.warn(\n `${context.path}: a field builder has no static shape and is documented as {}. Declare it with a cast string to document it.`,\n );\n\n return {};\n }\n\n private lazyFieldSchema(lazy: { resolve(): unknown }, context: ResourceFieldContext): OpenApiSchema {\n let resolved: unknown;\n\n try {\n resolved = lazy.resolve();\n } catch (error) {\n this.warn(\n `${context.path}: a lazy resource reference could not be resolved (${\n error instanceof Error ? error.message : String(error)\n }) and is documented as {}.`,\n );\n\n return {};\n }\n\n return this.fieldSchema(resolved, context);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ //#region ../core/src/openapi/security.ts
2
+ /**
3
+ * The marker `authMiddleware()` attaches to the middleware it returns.
4
+ * Core reads it structurally; it never imports the auth package.
5
+ */
6
+ const AUTH_DESCRIPTOR_SYMBOL = Symbol.for("warlock.auth");
7
+ function isAuthSource(value) {
8
+ if (value === "header") return true;
9
+ return typeof value === "object" && value !== null && typeof value.cookie === "string" && value.cookie.length > 0;
10
+ }
11
+ /**
12
+ * Validate an auth descriptor. Returns `undefined` for anything that is not
13
+ * `{ sources: ("header" | { cookie: string })[], userTypes?: string[] }` with at least one source.
14
+ */
15
+ function parseAuthDescriptor(value) {
16
+ if (typeof value !== "object" || value === null) return;
17
+ const { sources, userTypes } = value;
18
+ if (!Array.isArray(sources) || sources.length === 0 || !sources.every(isAuthSource)) return;
19
+ if (userTypes !== void 0 && !(Array.isArray(userTypes) && userTypes.every((type) => typeof type === "string"))) return;
20
+ return {
21
+ sources: [...sources],
22
+ userTypes: Array.isArray(userTypes) ? [...userTypes] : []
23
+ };
24
+ }
25
+ /**
26
+ * Combine the descriptors of every guard on a route (group middleware is already merged into
27
+ * the route): unique sources, union of user types.
28
+ *
29
+ * Middleware without a descriptor is ordinary middleware and is ignored silently. A function
30
+ * that carries a descriptor of the wrong shape is reported through `warn`.
31
+ *
32
+ * @returns `undefined` when the route has no valid auth descriptor.
33
+ */
34
+ function readRouteAuth(middleware, warn) {
35
+ if (!Array.isArray(middleware)) return;
36
+ const sources = [];
37
+ const userTypes = [];
38
+ let found = false;
39
+ for (const fn of middleware) {
40
+ if (typeof fn !== "function") continue;
41
+ const raw = Reflect.get(fn, AUTH_DESCRIPTOR_SYMBOL);
42
+ if (raw === void 0) continue;
43
+ const descriptor = parseAuthDescriptor(raw);
44
+ if (!descriptor) {
45
+ warn("a middleware carries a malformed warlock.auth descriptor; the route is documented without security.");
46
+ continue;
47
+ }
48
+ found = true;
49
+ for (const source of descriptor.sources) if (!sources.some((existing) => typeof existing === "string" || typeof source === "string" ? existing === source : existing.cookie === source.cookie)) sources.push(source);
50
+ for (const type of descriptor.userTypes) if (!userTypes.includes(type)) userTypes.push(type);
51
+ }
52
+ return found ? {
53
+ sources,
54
+ userTypes
55
+ } : void 0;
56
+ }
57
+ /**
58
+ * Collects the security schemes the routes actually use and turns descriptors into
59
+ * per-operation requirements. A dual header + cookie guard is an OR: one requirement each.
60
+ */
61
+ var SecuritySchemes = class {
62
+ constructor() {
63
+ this.schemes = {};
64
+ this.cookieSchemeNames = /* @__PURE__ */ new Map();
65
+ }
66
+ requirementsFor(descriptor) {
67
+ return descriptor.sources.map((source) => {
68
+ if (source === "header") {
69
+ this.schemes.bearerAuth = {
70
+ type: "http",
71
+ scheme: "bearer",
72
+ bearerFormat: "JWT"
73
+ };
74
+ return { bearerAuth: [] };
75
+ }
76
+ return { [this.cookieScheme(source.cookie)]: [] };
77
+ });
78
+ }
79
+ cookieScheme(cookie) {
80
+ const existing = this.cookieSchemeNames.get(cookie);
81
+ if (existing) return existing;
82
+ const name = this.cookieSchemeNames.size === 0 ? "cookieAuth" : `cookieAuth_${cookie.replace(/[^A-Za-z0-9._-]/g, "")}`;
83
+ this.cookieSchemeNames.set(cookie, name);
84
+ this.schemes[name] = {
85
+ type: "apiKey",
86
+ in: "cookie",
87
+ name: cookie
88
+ };
89
+ return name;
90
+ }
91
+ hasSchemes() {
92
+ return Object.keys(this.schemes).length > 0;
93
+ }
94
+ };
95
+
96
+ //#endregion
97
+ export { SecuritySchemes, readRouteAuth };
98
+ //# sourceMappingURL=security.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.mjs","names":[],"sources":["../../../../../../../core/src/openapi/security.ts"],"sourcesContent":["import type { AuthDescriptor, OpenApiSecurityRequirement, OpenApiSecurityScheme, WarningSink } from \"./types\";\n\n/**\n * The marker `authMiddleware()` attaches to the middleware it returns.\n * Core reads it structurally; it never imports the auth package.\n */\nexport const AUTH_DESCRIPTOR_SYMBOL = Symbol.for(\"warlock.auth\");\n\nfunction isAuthSource(value: unknown): value is AuthDescriptor[\"sources\"][number] {\n if (value === \"header\") {\n return true;\n }\n\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { cookie?: unknown }).cookie === \"string\" &&\n (value as { cookie: string }).cookie.length > 0\n );\n}\n\n/**\n * Validate an auth descriptor. Returns `undefined` for anything that is not\n * `{ sources: (\"header\" | { cookie: string })[], userTypes?: string[] }` with at least one source.\n */\nexport function parseAuthDescriptor(value: unknown): AuthDescriptor | undefined {\n if (typeof value !== \"object\" || value === null) {\n return undefined;\n }\n\n const { sources, userTypes } = value as { sources?: unknown; userTypes?: unknown };\n\n if (!Array.isArray(sources) || sources.length === 0 || !sources.every(isAuthSource)) {\n return undefined;\n }\n\n if (userTypes !== undefined && !(Array.isArray(userTypes) && userTypes.every((type) => typeof type === \"string\"))) {\n return undefined;\n }\n\n return { sources: [...sources], userTypes: Array.isArray(userTypes) ? [...userTypes] : [] };\n}\n\n/**\n * Combine the descriptors of every guard on a route (group middleware is already merged into\n * the route): unique sources, union of user types.\n *\n * Middleware without a descriptor is ordinary middleware and is ignored silently. A function\n * that carries a descriptor of the wrong shape is reported through `warn`.\n *\n * @returns `undefined` when the route has no valid auth descriptor.\n */\nexport function readRouteAuth(middleware: unknown, warn: WarningSink): AuthDescriptor | undefined {\n if (!Array.isArray(middleware)) {\n return undefined;\n }\n\n const sources: AuthDescriptor[\"sources\"] = [];\n const userTypes: string[] = [];\n let found = false;\n\n for (const fn of middleware) {\n if (typeof fn !== \"function\") {\n continue;\n }\n\n const raw = Reflect.get(fn, AUTH_DESCRIPTOR_SYMBOL);\n\n if (raw === undefined) {\n continue;\n }\n\n const descriptor = parseAuthDescriptor(raw);\n\n if (!descriptor) {\n warn(\"a middleware carries a malformed warlock.auth descriptor; the route is documented without security.\");\n\n continue;\n }\n\n found = true;\n\n for (const source of descriptor.sources) {\n const duplicate = sources.some((existing) =>\n typeof existing === \"string\" || typeof source === \"string\"\n ? existing === source\n : existing.cookie === source.cookie,\n );\n\n if (!duplicate) {\n sources.push(source);\n }\n }\n\n for (const type of descriptor.userTypes) {\n if (!userTypes.includes(type)) {\n userTypes.push(type);\n }\n }\n }\n\n return found ? { sources, userTypes } : undefined;\n}\n\n/**\n * Collects the security schemes the routes actually use and turns descriptors into\n * per-operation requirements. A dual header + cookie guard is an OR: one requirement each.\n */\nexport class SecuritySchemes {\n public readonly schemes: Record<string, OpenApiSecurityScheme> = {};\n\n private readonly cookieSchemeNames = new Map<string, string>();\n\n public requirementsFor(descriptor: AuthDescriptor): OpenApiSecurityRequirement[] {\n return descriptor.sources.map((source) => {\n if (source === \"header\") {\n this.schemes.bearerAuth = { type: \"http\", scheme: \"bearer\", bearerFormat: \"JWT\" };\n\n return { bearerAuth: [] };\n }\n\n const name = this.cookieScheme(source.cookie);\n\n return { [name]: [] };\n });\n }\n\n private cookieScheme(cookie: string): string {\n const existing = this.cookieSchemeNames.get(cookie);\n\n if (existing) {\n return existing;\n }\n\n // The first cookie keeps the conventional name; a second, different cookie gets its own.\n const name =\n this.cookieSchemeNames.size === 0 ? \"cookieAuth\" : `cookieAuth_${cookie.replace(/[^A-Za-z0-9._-]/g, \"\")}`;\n\n this.cookieSchemeNames.set(cookie, name);\n this.schemes[name] = { type: \"apiKey\", in: \"cookie\", name: cookie };\n\n return name;\n }\n\n public hasSchemes(): boolean {\n return Object.keys(this.schemes).length > 0;\n }\n}\n"],"mappings":";;;;;AAMA,MAAa,yBAAyB,OAAO,IAAI,cAAc;AAE/D,SAAS,aAAa,OAA4D;CAChF,IAAI,UAAU,UACZ,OAAO;CAGT,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA+B,WAAW,YACjD,MAA6B,OAAO,SAAS;AAElD;;;;;AAMA,SAAgB,oBAAoB,OAA4C;CAC9E,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC;CAGF,MAAM,EAAE,SAAS,cAAc;CAE/B,IAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,KAAK,CAAC,QAAQ,MAAM,YAAY,GAChF;CAGF,IAAI,cAAc,UAAa,EAAE,MAAM,QAAQ,SAAS,KAAK,UAAU,OAAO,SAAS,OAAO,SAAS,QAAQ,IAC7G;CAGF,OAAO;EAAE,SAAS,CAAC,GAAG,OAAO;EAAG,WAAW,MAAM,QAAQ,SAAS,IAAI,CAAC,GAAG,SAAS,IAAI,CAAC;CAAE;AAC5F;;;;;;;;;;AAWA,SAAgB,cAAc,YAAqB,MAA+C;CAChG,IAAI,CAAC,MAAM,QAAQ,UAAU,GAC3B;CAGF,MAAM,UAAqC,CAAC;CAC5C,MAAM,YAAsB,CAAC;CAC7B,IAAI,QAAQ;CAEZ,KAAK,MAAM,MAAM,YAAY;EAC3B,IAAI,OAAO,OAAO,YAChB;EAGF,MAAM,MAAM,QAAQ,IAAI,IAAI,sBAAsB;EAElD,IAAI,QAAQ,QACV;EAGF,MAAM,aAAa,oBAAoB,GAAG;EAE1C,IAAI,CAAC,YAAY;GACf,KAAK,qGAAqG;GAE1G;EACF;EAEA,QAAQ;EAER,KAAK,MAAM,UAAU,WAAW,SAO9B,IAAI,CANc,QAAQ,MAAM,aAC9B,OAAO,aAAa,YAAY,OAAO,WAAW,WAC9C,aAAa,SACb,SAAS,WAAW,OAAO,MAGpB,GACX,QAAQ,KAAK,MAAM;EAIvB,KAAK,MAAM,QAAQ,WAAW,WAC5B,IAAI,CAAC,UAAU,SAAS,IAAI,GAC1B,UAAU,KAAK,IAAI;CAGzB;CAEA,OAAO,QAAQ;EAAE;EAAS;CAAU,IAAI;AAC1C;;;;;AAMA,IAAa,kBAAb,MAA6B;;iBACsC,CAAC;2CAE7B,IAAI,IAAoB;;CAE7D,AAAO,gBAAgB,YAA0D;EAC/E,OAAO,WAAW,QAAQ,KAAK,WAAW;GACxC,IAAI,WAAW,UAAU;IACvB,KAAK,QAAQ,aAAa;KAAE,MAAM;KAAQ,QAAQ;KAAU,cAAc;IAAM;IAEhF,OAAO,EAAE,YAAY,CAAC,EAAE;GAC1B;GAIA,OAAO,GAFM,KAAK,aAAa,OAAO,MAEzB,IAAI,CAAC,EAAE;EACtB,CAAC;CACH;CAEA,AAAQ,aAAa,QAAwB;EAC3C,MAAM,WAAW,KAAK,kBAAkB,IAAI,MAAM;EAElD,IAAI,UACF,OAAO;EAIT,MAAM,OACJ,KAAK,kBAAkB,SAAS,IAAI,eAAe,cAAc,OAAO,QAAQ,oBAAoB,EAAE;EAExG,KAAK,kBAAkB,IAAI,QAAQ,IAAI;EACvC,KAAK,QAAQ,QAAQ;GAAE,MAAM;GAAU,IAAI;GAAU,MAAM;EAAO;EAElE,OAAO;CACT;CAEA,AAAO,aAAsB;EAC3B,OAAO,OAAO,KAAK,KAAK,OAAO,CAAC,CAAC,SAAS;CAC5C;AACF"}
@@ -0,0 +1,119 @@
1
+ import { Route } from "../router/types.mjs";
2
+
3
+ //#region ../core/src/openapi/types.d.ts
4
+ /**
5
+ * A JSON Schema fragment (OpenAPI 3.1 schemas are JSON Schema 2020-12).
6
+ */
7
+ type OpenApiSchema = Record<string, unknown>;
8
+ type OpenApiParameter = {
9
+ name: string;
10
+ in: "path" | "query" | "header" | "cookie";
11
+ required?: boolean;
12
+ description?: string;
13
+ style?: "deepObject";
14
+ explode?: boolean;
15
+ schema: OpenApiSchema;
16
+ };
17
+ type OpenApiRequestBody = {
18
+ required?: boolean;
19
+ content: Record<string, {
20
+ schema: OpenApiSchema;
21
+ }>;
22
+ };
23
+ type OpenApiResponse = {
24
+ description: string;
25
+ content?: Record<string, {
26
+ schema: OpenApiSchema;
27
+ }>;
28
+ };
29
+ type OpenApiSecurityRequirement = Record<string, string[]>;
30
+ type OpenApiOperation = {
31
+ operationId: string;
32
+ summary?: string;
33
+ description?: string;
34
+ tags?: string[];
35
+ parameters?: OpenApiParameter[];
36
+ requestBody?: OpenApiRequestBody;
37
+ responses: Record<string, OpenApiResponse>;
38
+ security?: OpenApiSecurityRequirement[];
39
+ };
40
+ type OpenApiSecurityScheme = {
41
+ type: "http";
42
+ scheme: "bearer";
43
+ bearerFormat?: string;
44
+ } | {
45
+ type: "apiKey";
46
+ in: "cookie";
47
+ name: string;
48
+ };
49
+ type OpenApiComponents = {
50
+ schemas?: Record<string, OpenApiSchema>;
51
+ securitySchemes?: Record<string, OpenApiSecurityScheme>;
52
+ };
53
+ type OpenApiDocument = {
54
+ openapi: "3.1.0";
55
+ jsonSchemaDialect: string;
56
+ info: {
57
+ title: string;
58
+ version: string;
59
+ description?: string;
60
+ };
61
+ servers?: {
62
+ url: string;
63
+ }[];
64
+ tags?: {
65
+ name: string;
66
+ }[];
67
+ paths: Record<string, Record<string, OpenApiOperation>>;
68
+ components?: OpenApiComponents;
69
+ };
70
+ /**
71
+ * The `validation.response` configuration keys that shape a failed-validation response.
72
+ * Every key is optional; the documented defaults apply to the missing ones.
73
+ */
74
+ type OpenApiValidationResponseConfig = {
75
+ errors?: string;
76
+ inputKey?: string;
77
+ inputError?: string;
78
+ status?: number;
79
+ };
80
+ /**
81
+ * Everything the builder needs besides the routes. Plain values only, so the same call
82
+ * serves the CLI child and the in-process dev path.
83
+ */
84
+ type OpenApiContext = {
85
+ info: {
86
+ title: string;
87
+ version: string;
88
+ description?: string;
89
+ }; /** Absolute server URLs, first is the default. */
90
+ servers?: string[]; /** Also document page (SSR) routes. Defaults to `false`. */
91
+ includePages?: boolean;
92
+ /**
93
+ * Name a resource class for `components.schemas`, usually its export name
94
+ * (`UserResource`). Return `undefined` for an unknown resource: it is named
95
+ * `Resource1`, `Resource2`, ... in encounter order.
96
+ */
97
+ resolveResourceName?: (resource: unknown) => string | undefined; /** The app's `validation.response` config value. */
98
+ validationResponse?: OpenApiValidationResponseConfig;
99
+ };
100
+ type OpenApiBuildResult = {
101
+ document: OpenApiDocument;
102
+ warnings: string[];
103
+ };
104
+ /** The slice of a route the builder reads. A full `Route` satisfies it. */
105
+ type OpenApiRouteInput = Pick<Route, "method" | "path" | "handler"> & Partial<Pick<Route, "name" | "label" | "description" | "middleware" | "isPage" | "sourceFile">>;
106
+ /**
107
+ * The descriptor `authMiddleware()` attaches under `Symbol.for("warlock.auth")`.
108
+ * Read structurally and validated; core never imports the auth package.
109
+ */
110
+ type AuthDescriptor = {
111
+ sources: ("header" | {
112
+ cookie: string;
113
+ })[];
114
+ userTypes: string[];
115
+ };
116
+ type WarningSink = (message: string) => void;
117
+ //#endregion
118
+ export { AuthDescriptor, OpenApiBuildResult, OpenApiComponents, OpenApiContext, OpenApiDocument, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, OpenApiRouteInput, OpenApiSchema, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiValidationResponseConfig, WarningSink };
119
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/openapi/types.ts"],"mappings":";;;;;AAKA;KAAY,aAAA,GAAgB,MAAM;AAAA,KAEtB,gBAAA;EACV,IAAA;EACA,EAAA;EACA,QAAA;EACA,WAAA;EACA,KAAA;EACA,OAAA;EACA,MAAA,EAAQ,aAAa;AAAA;AAAA,KAGX,kBAAA;EACV,QAAA;EACA,OAAA,EAAS,MAAM;IAAW,MAAA,EAAQ,aAAA;EAAA;AAAA;AAAA,KAGxB,eAAA;EACV,WAAA;EACA,OAAA,GAAU,MAAM;IAAW,MAAA,EAAQ,aAAA;EAAA;AAAA;AAAA,KAGzB,0BAAA,GAA6B,MAAM;AAAA,KAEnC,gBAAA;EACV,WAAA;EACA,OAAA;EACA,WAAA;EACA,IAAA;EACA,UAAA,GAAa,gBAAA;EACb,WAAA,GAAc,kBAAA;EACd,SAAA,EAAW,MAAA,SAAe,eAAA;EAC1B,QAAA,GAAW,0BAAA;AAAA;AAAA,KAGD,qBAAA;EACN,IAAA;EAAc,MAAA;EAAkB,YAAA;AAAA;EAChC,IAAA;EAAgB,EAAA;EAAc,IAAA;AAAA;AAAA,KAExB,iBAAA;EACV,OAAA,GAAU,MAAA,SAAe,aAAA;EACzB,eAAA,GAAkB,MAAA,SAAe,qBAAA;AAAA;AAAA,KAGvB,eAAA;EACV,OAAA;EACA,iBAAA;EACA,IAAA;IAAQ,KAAA;IAAe,OAAA;IAAiB,WAAA;EAAA;EACxC,OAAA;IAAY,GAAA;EAAA;EACZ,IAAA;IAAS,IAAA;EAAA;EACT,KAAA,EAAO,MAAA,SAAe,MAAA,SAAe,gBAAA;EACrC,UAAA,GAAa,iBAAA;AAAA;;;;;KAOH,+BAAA;EACV,MAAA;EACA,QAAA;EACA,UAAA;EACA,MAAA;AAAA;;;;;KAOU,cAAA;EACV,IAAA;IAAQ,KAAA;IAAe,OAAA;IAAiB,WAAA;EAAA,GA/Bb;EAiC3B,OAAA,aAhCyB;EAkCzB,YAAA;EAjCiC;;;;;EAuCjC,mBAAA,IAAuB,QAAA,kCAxCE;EA0CzB,kBAAA,GAAqB,+BAA+B;AAAA;AAAA,KAG1C,kBAAA;EACV,QAAA,EAAU,eAAe;EACzB,QAAA;AAAA;;KAIU,iBAAA,GAAoB,IAAA,CAAK,KAAA,mCACnC,OAAA,CACE,IAAA,CAAK,KAAA;;;;;KAOG,cAAA;EACV,OAAA;IAAuB,MAAA;EAAA;EACvB,SAAA;AAAA;AAAA,KAGU,WAAA,IAAe,OAAe"}
@@ -0,0 +1,139 @@
1
+ import path from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { fork } from "node:child_process";
5
+
6
+ //#region ../core/src/production/registration-child-runner.ts
7
+ const DEFAULT_TIMEOUT_MS = 3e4;
8
+ const MAX_DIAGNOSTIC_BYTES = 16384;
9
+ /** Message a registration child sends when it fails. Shared by every result kind. */
10
+ const REGISTRATION_ERROR_MESSAGE = "route-registration:error";
11
+ /** Message a registration child sends before it imports each module. Shared by every result kind. */
12
+ const REGISTRATION_PROGRESS_MESSAGE = "route-registration:progress";
13
+ /**
14
+ * Run the compiled registration child in a fresh process and wait for its result.
15
+ *
16
+ * This is the fork / IPC / timeout / diagnostics plumbing shared by the route snapshot and
17
+ * the OpenAPI document: the child is forked, sent one request, and must answer with exactly
18
+ * one result message (or an error message) and exit 0. The timeout names the module the
19
+ * child was still importing; the last 16 KiB of child stdout/stderr are attached to failures.
20
+ */
21
+ function runRegistrationChild(options) {
22
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ const startedAt = Date.now();
24
+ const forkProcess = options.forkProcess ?? defaultForkProcess;
25
+ const entry = options.childEntry ?? (options.forkProcess ? "<injected-route-registration-child>" : resolveCompiledChildEntry());
26
+ return new Promise((resolve, reject) => {
27
+ const child = forkProcess(entry, { cwd: options.cwd });
28
+ let settled = false;
29
+ let received;
30
+ let stdout = "";
31
+ let stderr = "";
32
+ let pendingModule;
33
+ const append = (current, chunk) => {
34
+ const remaining = MAX_DIAGNOSTIC_BYTES - Buffer.byteLength(current);
35
+ return remaining <= 0 ? current : current + chunk.toString("utf8", 0, remaining);
36
+ };
37
+ const finish = (error, result) => {
38
+ if (settled) return;
39
+ settled = true;
40
+ clearTimeout(timeout);
41
+ if (child.connected) child.disconnect?.();
42
+ if (error) {
43
+ child.kill?.();
44
+ reject(error);
45
+ } else resolve(result);
46
+ };
47
+ child.stdout?.on("data", (chunk) => {
48
+ stdout = append(stdout, chunk);
49
+ });
50
+ child.stderr?.on("data", (chunk) => {
51
+ stderr = append(stderr, chunk);
52
+ });
53
+ const timeout = setTimeout(() => {
54
+ const elapsedMs = Date.now() - startedAt;
55
+ finish(/* @__PURE__ */ new Error(`Route registration child timed out after ${elapsedMs}ms${pendingModule ? ` while registering ${pendingModule}` : ""}.${formatDiagnostics(stdout, stderr)}`));
56
+ }, timeoutMs);
57
+ timeout.unref?.();
58
+ child.on("error", (error) => {
59
+ finish(/* @__PURE__ */ new Error(`Could not start route registration child: ${error.message}${formatDiagnostics(stdout, stderr)}`));
60
+ });
61
+ child.on("message", (message) => {
62
+ if (!isChildMessage(message, options.resultMessageType)) return;
63
+ if (message.type === "route-registration:error") {
64
+ finish(/* @__PURE__ */ new Error(`Route registration child failed: ${String(message.message)}${formatDiagnostics(stdout, stderr)}`));
65
+ return;
66
+ }
67
+ if (message.type === "route-registration:progress") {
68
+ pendingModule = String(message.module);
69
+ return;
70
+ }
71
+ try {
72
+ const result = options.readResult(message);
73
+ if (received) {
74
+ finish(/* @__PURE__ */ new Error(`Route registration child sent more than one ${options.resultNoun}.`));
75
+ return;
76
+ }
77
+ received = { value: result };
78
+ } catch (error) {
79
+ finish(error);
80
+ }
81
+ });
82
+ child.on("close", (code) => {
83
+ if (settled) return;
84
+ if (code === 0 && received) {
85
+ finish(void 0, received.value);
86
+ return;
87
+ }
88
+ finish(/* @__PURE__ */ new Error(`Route registration child exited ${code ?? "by signal"} without a valid ${options.resultNoun}.${formatDiagnostics(stdout, stderr)}`));
89
+ });
90
+ if (!child.send) {
91
+ finish(/* @__PURE__ */ new Error("Route registration child did not provide an IPC send channel."));
92
+ return;
93
+ }
94
+ child.send(options.request);
95
+ });
96
+ }
97
+ function defaultForkProcess(entry, options) {
98
+ return fork(entry, [], {
99
+ cwd: options.cwd,
100
+ stdio: [
101
+ "ignore",
102
+ "pipe",
103
+ "pipe",
104
+ "ipc"
105
+ ],
106
+ windowsHide: true
107
+ });
108
+ }
109
+ /** Resolve from Core's package root, not an assumed bundler-preserved dirname. */
110
+ function resolveCompiledChildEntry() {
111
+ let directory = path.dirname(fileURLToPath(import.meta.url));
112
+ while (directory !== path.dirname(directory)) {
113
+ const packageJson = path.join(directory, "package.json");
114
+ if (existsSync(packageJson)) try {
115
+ if (JSON.parse(readFileSync(packageJson, "utf8")).name === "@warlock.js/core") {
116
+ const child = path.join(directory, "esm", "production", "route-registration-child.mjs");
117
+ if (existsSync(child)) return child;
118
+ throw new Error(`@warlock.js/core is missing its compiled route-registration child at ${child}. Reinstall a complete package.`);
119
+ }
120
+ } catch (error) {
121
+ if (error instanceof Error && error.message.includes("route-registration child")) throw error;
122
+ }
123
+ directory = path.dirname(directory);
124
+ }
125
+ throw new Error("Could not locate @warlock.js/core package root for the route-registration child.");
126
+ }
127
+ function isChildMessage(value, resultMessageType) {
128
+ if (typeof value !== "object" || value === null || !("type" in value)) return false;
129
+ const type = value.type;
130
+ return type === resultMessageType || type === "route-registration:error" || type === "route-registration:progress";
131
+ }
132
+ function formatDiagnostics(stdout, stderr) {
133
+ if (!stdout && !stderr) return "";
134
+ return `\nChild stdout:\n${stdout}\nChild stderr:\n${stderr}`;
135
+ }
136
+
137
+ //#endregion
138
+ export { runRegistrationChild };
139
+ //# sourceMappingURL=registration-child-runner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration-child-runner.mjs","names":[],"sources":["../../../../../../../core/src/production/registration-child-runner.ts"],"sourcesContent":["import { fork, type ChildProcess, type Serializable } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_DIAGNOSTIC_BYTES = 16_384;\n\n/** Message a registration child sends when it fails. Shared by every result kind. */\nexport const REGISTRATION_ERROR_MESSAGE = \"route-registration:error\";\n\n/** Message a registration child sends before it imports each module. Shared by every result kind. */\nexport const REGISTRATION_PROGRESS_MESSAGE = \"route-registration:progress\";\n\nexport type RegistrationChildProcess = Pick<\n ChildProcess,\n \"send\" | \"kill\" | \"disconnect\" | \"on\" | \"stdout\" | \"stderr\" | \"connected\"\n>;\n\nexport type RegistrationChildForker = (\n entry: string,\n options: { cwd: string },\n) => RegistrationChildProcess;\n\nexport type RegistrationChildRunOptions<TResult> = Readonly<{\n cwd: string;\n timeoutMs?: number;\n childEntry?: string;\n /**\n * Test seam. An injected fork is a protocol seam, not an installation check, so the\n * compiled child is not resolved when it is set.\n */\n forkProcess?: RegistrationChildForker;\n /** The single IPC request the child receives after it starts. */\n request: Serializable;\n /** `type` of the one message that carries the result (for example `route-registration:snapshot`). */\n resultMessageType: string;\n /** What the result is called in error messages (\"snapshot\", \"document\"). */\n resultNoun: string;\n /**\n * Validate and freeze the result message. Throw to reject the run with that error.\n */\n readResult: (message: Record<string, unknown>) => TResult;\n}>;\n\n/**\n * Run the compiled registration child in a fresh process and wait for its result.\n *\n * This is the fork / IPC / timeout / diagnostics plumbing shared by the route snapshot and\n * the OpenAPI document: the child is forked, sent one request, and must answer with exactly\n * one result message (or an error message) and exit 0. The timeout names the module the\n * child was still importing; the last 16 KiB of child stdout/stderr are attached to failures.\n */\nexport function runRegistrationChild<TResult>(\n options: RegistrationChildRunOptions<TResult>,\n): Promise<TResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const startedAt = Date.now();\n const forkProcess = options.forkProcess ?? defaultForkProcess;\n const entry =\n options.childEntry ??\n (options.forkProcess ? \"<injected-route-registration-child>\" : resolveCompiledChildEntry());\n\n return new Promise<TResult>((resolve, reject) => {\n const child = forkProcess(entry, { cwd: options.cwd });\n let settled = false;\n let received: { value: TResult } | undefined;\n let stdout = \"\";\n let stderr = \"\";\n let pendingModule: string | undefined;\n\n const append = (current: string, chunk: Buffer) => {\n const remaining = MAX_DIAGNOSTIC_BYTES - Buffer.byteLength(current);\n\n return remaining <= 0 ? current : current + chunk.toString(\"utf8\", 0, remaining);\n };\n\n const finish = (error?: Error, result?: TResult) => {\n if (settled) {\n return;\n }\n\n settled = true;\n clearTimeout(timeout);\n\n if (child.connected) {\n child.disconnect?.();\n }\n\n if (error) {\n child.kill?.();\n reject(error);\n } else {\n resolve(result as TResult);\n }\n };\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout = append(stdout, chunk);\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderr = append(stderr, chunk);\n });\n\n const timeout = setTimeout(() => {\n const elapsedMs = Date.now() - startedAt;\n\n finish(\n new Error(\n `Route registration child timed out after ${elapsedMs}ms${pendingModule ? ` while registering ${pendingModule}` : \"\"}.${formatDiagnostics(stdout, stderr)}`,\n ),\n );\n }, timeoutMs);\n timeout.unref?.();\n\n child.on(\"error\", (error) => {\n finish(\n new Error(\n `Could not start route registration child: ${error.message}${formatDiagnostics(stdout, stderr)}`,\n ),\n );\n });\n\n child.on(\"message\", (message: unknown) => {\n if (!isChildMessage(message, options.resultMessageType)) {\n return;\n }\n\n if (message.type === REGISTRATION_ERROR_MESSAGE) {\n finish(\n new Error(\n `Route registration child failed: ${String(message.message)}${formatDiagnostics(stdout, stderr)}`,\n ),\n );\n\n return;\n }\n\n if (message.type === REGISTRATION_PROGRESS_MESSAGE) {\n pendingModule = String(message.module);\n\n return;\n }\n\n try {\n const result = options.readResult(message);\n\n if (received) {\n finish(new Error(`Route registration child sent more than one ${options.resultNoun}.`));\n\n return;\n }\n\n received = { value: result };\n } catch (error) {\n finish(error as Error);\n }\n });\n\n child.on(\"close\", (code) => {\n if (settled) {\n return;\n }\n\n if (code === 0 && received) {\n finish(undefined, received.value);\n\n return;\n }\n\n finish(\n new Error(\n `Route registration child exited ${code ?? \"by signal\"} without a valid ${options.resultNoun}.${formatDiagnostics(stdout, stderr)}`,\n ),\n );\n });\n\n if (!child.send) {\n finish(new Error(\"Route registration child did not provide an IPC send channel.\"));\n\n return;\n }\n\n child.send(options.request);\n });\n}\n\nfunction defaultForkProcess(entry: string, options: { cwd: string }): RegistrationChildProcess {\n const forkOptions: NonNullable<Parameters<typeof fork>[2]> & { windowsHide?: boolean } = {\n cwd: options.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\", \"ipc\"],\n windowsHide: true,\n };\n\n return fork(entry, [], forkOptions);\n}\n\n/** Resolve from Core's package root, not an assumed bundler-preserved dirname. */\nfunction resolveCompiledChildEntry(): string {\n let directory = path.dirname(fileURLToPath(import.meta.url));\n\n while (directory !== path.dirname(directory)) {\n const packageJson = path.join(directory, \"package.json\");\n\n if (existsSync(packageJson)) {\n try {\n if (JSON.parse(readFileSync(packageJson, \"utf8\")).name === \"@warlock.js/core\") {\n const child = path.join(directory, \"esm\", \"production\", \"route-registration-child.mjs\");\n\n if (existsSync(child)) {\n return child;\n }\n\n throw new Error(\n `@warlock.js/core is missing its compiled route-registration child at ${child}. Reinstall a complete package.`,\n );\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"route-registration child\")) {\n throw error;\n }\n }\n }\n\n directory = path.dirname(directory);\n }\n\n throw new Error(\n \"Could not locate @warlock.js/core package root for the route-registration child.\",\n );\n}\n\nfunction isChildMessage(\n value: unknown,\n resultMessageType: string,\n): value is Record<string, unknown> & { type: string } {\n if (typeof value !== \"object\" || value === null || !(\"type\" in value)) {\n return false;\n }\n\n const type = (value as { type?: unknown }).type;\n\n return (\n type === resultMessageType ||\n type === REGISTRATION_ERROR_MESSAGE ||\n type === REGISTRATION_PROGRESS_MESSAGE\n );\n}\n\nfunction formatDiagnostics(stdout: string, stderr: string): string {\n if (!stdout && !stderr) {\n return \"\";\n }\n\n return `\\nChild stdout:\\n${stdout}\\nChild stderr:\\n${stderr}`;\n}\n"],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  import { appPath } from "../utils/paths.mjs";
2
2
  import { Application } from "../application/application.mjs";
3
- import { router } from "../router/router.mjs";
4
3
  import "../application/index.mjs";
5
4
  import { loadEnvironmentFiles } from "../utils/load-environment.mjs";
6
5
  import { bootstrap } from "../bootstrap.mjs";
7
6
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
8
7
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
9
8
  import { loadConfigFiles } from "../config/load-config-files.mjs";
9
+ import { collectNamedApiRoutesWithResponses } from "../router/named-api-routes-with-responses.mjs";
10
+ import { OPENAPI_RESULT_MESSAGE, parseOpenApiChildOptions } from "../openapi/openapi-protocol.mjs";
11
+ import { buildOpenApiForChild } from "../openapi/openapi-child-output.mjs";
10
12
  import { fileExistsAsync } from "@warlock.js/fs";
11
13
 
12
14
  //#region ../core/src/production/route-registration-child.ts
@@ -30,11 +32,35 @@ process.once("message", async (message) => {
30
32
  await filesOrchestrator.moduleLoader.loadModule(file, "model");
31
33
  }
32
34
  await filesOrchestrator.moduleLoader.loadAll({ onBeforeLoad: (file) => sendProgress(file.relativePath) });
35
+ if (request.openapi) {
36
+ const { document, warnings } = await buildOpenApiForChild({
37
+ cwd: request.cwd,
38
+ files: filesOrchestrator.files.values(),
39
+ request: request.openapi,
40
+ onWarn: (warning) => {
41
+ process.stderr.write(`[warlock] ${warning}\n`);
42
+ }
43
+ });
44
+ await sendAndExit({
45
+ type: OPENAPI_RESULT_MESSAGE,
46
+ result: {
47
+ version: 1,
48
+ document,
49
+ warnings
50
+ }
51
+ });
52
+ }
33
53
  await sendAndExit({
34
54
  type: "route-registration:snapshot",
35
55
  snapshot: {
36
56
  version: PROTOCOL_VERSION,
37
- routes: router.getNamedApiRoutes()
57
+ routes: await collectNamedApiRoutesWithResponses({
58
+ appRoot: request.cwd,
59
+ files: filesOrchestrator.files.values(),
60
+ onWarn: (warning) => {
61
+ process.stderr.write(`[warlock] ${warning}\n`);
62
+ }
63
+ })
38
64
  }
39
65
  });
40
66
  } catch (error) {
@@ -46,6 +72,8 @@ process.once("message", async (message) => {
46
72
  });
47
73
  function validateRequest(value) {
48
74
  if (typeof value !== "object" || value === null || value.version !== PROTOCOL_VERSION || typeof value.cwd !== "string" || !isOptionalEnvironment(value.environment) || !isOptionalRuntimeStrategy(value.runtimeStrategy)) throw new Error("Route registration child received an invalid protocol request.");
75
+ const openapi = value.openapi;
76
+ if (openapi !== void 0) parseOpenApiChildOptions(openapi);
49
77
  return value;
50
78
  }
51
79
  function isOptionalEnvironment(value) {