@ic-reactor/codegen 0.11.0 → 0.12.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.
@@ -0,0 +1,33 @@
1
+ import { CandidSchema, CandidType, CandidTypeDeclaration } from '@ic-reactor/parser';
2
+
3
+ type JSDocFormatDefinition = {
4
+ regex?: string;
5
+ errorMessage?: string;
6
+ jsonSchemaFormat?: string;
7
+ contentEncoding?: string;
8
+ };
9
+ type CustomJSDocFormatTypes = Record<string, string | JSDocFormatDefinition>;
10
+ declare const BUILT_IN_JSDOC_FORMAT_TYPES: Readonly<Record<string, JSDocFormatDefinition>>;
11
+ declare const BUILT_IN_FORMAT_HELPERS: Readonly<Record<string, string>>;
12
+
13
+ interface GenerateCodecDeclarationsOptions {
14
+ /**
15
+ * Canister name used to derive the service export name.
16
+ * Converted to SCREAMING_SNAKE_CASE (e.g. `"my-backend"` → `MY_BACKEND`).
17
+ * Ignored when `serviceExportName` is set explicitly.
18
+ */
19
+ canisterName?: string;
20
+ /** Explicit service export name. Takes priority over `canisterName`. */
21
+ serviceExportName?: string;
22
+ customJSDocFormatTypes?: CustomJSDocFormatTypes;
23
+ includeCompatibilityExports?: boolean;
24
+ }
25
+ declare function sortDeclarations(declarations: CandidTypeDeclaration[]): CandidTypeDeclaration[];
26
+ declare function renderType(type: CandidType, indent?: string, options?: GenerateCodecDeclarationsOptions): string;
27
+ /**
28
+ * Converts a structured CandidSchema AST into readable `@ic-reactor/cod`
29
+ * codec declarations.
30
+ */
31
+ declare function generateCodecDeclarations(schema: CandidSchema, optionsOrServiceExportName?: string | GenerateCodecDeclarationsOptions): string;
32
+
33
+ export { BUILT_IN_FORMAT_HELPERS, BUILT_IN_JSDOC_FORMAT_TYPES, type CustomJSDocFormatTypes, type GenerateCodecDeclarationsOptions, type JSDocFormatDefinition, generateCodecDeclarations, renderType, sortDeclarations };
@@ -0,0 +1,33 @@
1
+ import { CandidSchema, CandidType, CandidTypeDeclaration } from '@ic-reactor/parser';
2
+
3
+ type JSDocFormatDefinition = {
4
+ regex?: string;
5
+ errorMessage?: string;
6
+ jsonSchemaFormat?: string;
7
+ contentEncoding?: string;
8
+ };
9
+ type CustomJSDocFormatTypes = Record<string, string | JSDocFormatDefinition>;
10
+ declare const BUILT_IN_JSDOC_FORMAT_TYPES: Readonly<Record<string, JSDocFormatDefinition>>;
11
+ declare const BUILT_IN_FORMAT_HELPERS: Readonly<Record<string, string>>;
12
+
13
+ interface GenerateCodecDeclarationsOptions {
14
+ /**
15
+ * Canister name used to derive the service export name.
16
+ * Converted to SCREAMING_SNAKE_CASE (e.g. `"my-backend"` → `MY_BACKEND`).
17
+ * Ignored when `serviceExportName` is set explicitly.
18
+ */
19
+ canisterName?: string;
20
+ /** Explicit service export name. Takes priority over `canisterName`. */
21
+ serviceExportName?: string;
22
+ customJSDocFormatTypes?: CustomJSDocFormatTypes;
23
+ includeCompatibilityExports?: boolean;
24
+ }
25
+ declare function sortDeclarations(declarations: CandidTypeDeclaration[]): CandidTypeDeclaration[];
26
+ declare function renderType(type: CandidType, indent?: string, options?: GenerateCodecDeclarationsOptions): string;
27
+ /**
28
+ * Converts a structured CandidSchema AST into readable `@ic-reactor/cod`
29
+ * codec declarations.
30
+ */
31
+ declare function generateCodecDeclarations(schema: CandidSchema, optionsOrServiceExportName?: string | GenerateCodecDeclarationsOptions): string;
32
+
33
+ export { BUILT_IN_FORMAT_HELPERS, BUILT_IN_JSDOC_FORMAT_TYPES, type CustomJSDocFormatTypes, type GenerateCodecDeclarationsOptions, type JSDocFormatDefinition, generateCodecDeclarations, renderType, sortDeclarations };
@@ -0,0 +1,14 @@
1
+ import {
2
+ BUILT_IN_FORMAT_HELPERS,
3
+ BUILT_IN_JSDOC_FORMAT_TYPES,
4
+ generateCodecDeclarations,
5
+ renderType,
6
+ sortDeclarations
7
+ } from "./chunk-VBCR5IVT.js";
8
+ export {
9
+ BUILT_IN_FORMAT_HELPERS,
10
+ BUILT_IN_JSDOC_FORMAT_TYPES,
11
+ generateCodecDeclarations,
12
+ renderType,
13
+ sortDeclarations
14
+ };
package/llms.txt ADDED
@@ -0,0 +1,28 @@
1
+ # @ic-reactor/codegen — AI Quick Guide
2
+
3
+ Shared generation pipeline used by `@ic-reactor/cli` and
4
+ `@ic-reactor/vite-plugin`.
5
+
6
+ ## Install
7
+
8
+ Most apps should install CLI or Vite plugin instead of this package directly.
9
+
10
+ ```bash
11
+ pnpm add -D @ic-reactor/codegen
12
+ ```
13
+
14
+ ## Main Use Case
15
+
16
+ Use `runCanisterPipeline(...)` when you need programmatic generation in custom
17
+ tooling/automation.
18
+
19
+ ## Generated File Model
20
+
21
+ - `index.generated.ts` is managed output
22
+ - `index.ts` is a stable wrapper
23
+ - app-specific logic should live in wrappers or adjacent modules
24
+
25
+ ## Full References
26
+
27
+ - Full AI guide: https://ic-reactor.b3pay.net/llms-full.txt
28
+ - Codegen docs: https://ic-reactor.b3pay.net/v3/packages/codegen
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/codegen",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Shared code generation utilities for IC Reactor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -8,14 +8,32 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./renderer": {
21
+ "import": {
22
+ "types": "./dist/renderer.d.ts",
23
+ "default": "./dist/renderer.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/renderer.d.cts",
27
+ "default": "./dist/renderer.cjs"
28
+ }
29
+ },
30
+ "./package.json": "./package.json"
15
31
  },
16
32
  "files": [
17
33
  "dist",
18
- "src"
34
+ "src",
35
+ "README.md",
36
+ "llms.txt"
19
37
  ],
20
38
  "keywords": [
21
39
  "internet-computer",
@@ -32,21 +50,22 @@
32
50
  "url": "git+https://github.com/B3Pay/ic-reactor.git",
33
51
  "directory": "packages/codegen"
34
52
  },
53
+ "homepage": "https://ic-reactor.b3pay.net/v3/packages/codegen",
35
54
  "dependencies": {
36
55
  "change-case": "^5.4.4",
37
- "@ic-reactor/parser": "0.4.6"
56
+ "@ic-reactor/parser": "0.4.7"
38
57
  },
39
58
  "devDependencies": {
40
- "@types/node": "^25.5.2",
59
+ "@types/node": "^26.1.1",
41
60
  "tsup": "^8.5.1",
42
- "typescript": "^5.9.3",
43
- "vitest": "^4.1.2"
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^4.1.10"
44
63
  },
45
64
  "scripts": {
46
- "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.json",
47
- "dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
65
+ "build": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --tsconfig tsconfig.json",
66
+ "dev": "tsup src/index.ts src/renderer.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
48
67
  "test": "vitest run",
49
68
  "test:watch": "vitest",
50
- "typecheck": "tsc --noEmit"
69
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
51
70
  }
52
71
  }
@@ -13,6 +13,9 @@ export type BackendService = _SERVICE
13
13
  *
14
14
  * Auto-generated by @ic-reactor/codegen — do not edit.
15
15
  * This file is overwritten whenever generation runs.
16
+ *
17
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
18
+ * factory modules). Avoid editing this managed file directly.
16
19
  */
17
20
  export const backendReactor = new DisplayReactor<BackendService>({
18
21
  clientManager,
@@ -44,6 +47,9 @@ export type WorkflowEngineService = _SERVICE
44
47
  *
45
48
  * Auto-generated by @ic-reactor/codegen — do not edit.
46
49
  * This file is overwritten whenever generation runs.
50
+ *
51
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
52
+ * factory modules). Avoid editing this managed file directly.
47
53
  */
48
54
  export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
49
55
  clientManager,
@@ -75,6 +81,9 @@ export type LedgerService = _SERVICE
75
81
  *
76
82
  * Auto-generated by @ic-reactor/codegen — do not edit.
77
83
  * This file is overwritten whenever generation runs.
84
+ *
85
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
86
+ * factory modules). Avoid editing this managed file directly.
78
87
  */
79
88
  export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
80
89
  clientManager,
@@ -105,6 +114,9 @@ export type BackendService = _SERVICE
105
114
  *
106
115
  * Auto-generated by @ic-reactor/codegen — do not edit.
107
116
  * This file is overwritten whenever generation runs.
117
+ *
118
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
119
+ * factory modules). Avoid editing this managed file directly.
108
120
  */
109
121
  export const backendReactor = new DisplayReactor<BackendService>({
110
122
  clientManager,
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2
2
  import fs from "node:fs"
3
3
  import path from "node:path"
4
- import { generateDeclarations } from "./generators"
4
+ import { generateDeclarations } from "./generators/index.js"
5
5
 
6
6
  describe("Bindgen", () => {
7
7
  const mockDidFile = "mock/test.did"
@@ -43,7 +43,6 @@ ${queryClientImport}
43
43
  */
44
44
  export const clientManager = new ClientManager({
45
45
  queryClient,
46
- withCanisterEnv: true,
47
46
  })
48
47
  `
49
48
  }
@@ -116,6 +116,9 @@ export type ${serviceName} = _SERVICE
116
116
  *
117
117
  * Auto-generated by @ic-reactor/codegen — do not edit.
118
118
  * This file is overwritten whenever generation runs.
119
+ *
120
+ * Keep app-specific logic in the stable \`index.ts\` wrapper (or adjacent
121
+ * factory modules). Avoid editing this managed file directly.
119
122
  */
120
123
  export const ${reactorName} = new ${reactorClass}<${serviceName}>({
121
124
  clientManager,
@@ -133,6 +136,15 @@ export function generateReactorEntryFile(): string {
133
136
  *
134
137
  * Created once by @ic-reactor/codegen and safe to customize.
135
138
  * Keep the re-export below if you want generated exports and types to stay in sync.
139
+ *
140
+ * Recommended customization points:
141
+ * - define reusable query/mutation factories
142
+ * - add app-specific hooks and cache invalidation wiring
143
+ * - compose generated APIs into route loaders/actions
144
+ *
145
+ * Do not edit \`index.generated.ts\`; it is regenerated on each codegen run.
146
+ * AI guide: https://ic-reactor.b3pay.net/llms-full.txt
147
+ * Skill install: npx skills add B3Pay/ic-reactor-skills --full-depth --skill ic-reactor-hooks
136
148
  */
137
149
  export * from "./index.generated"
138
150
  `
package/src/index.ts CHANGED
@@ -26,3 +26,6 @@ export type { MethodInfo, MethodType } from "./parser.js"
26
26
 
27
27
  // Individual Generators (Advanced Usage)
28
28
  export * from "./generators/index.js"
29
+
30
+ // Renderer (Candid Codec)
31
+ export { generateCodecDeclarations } from "./renderer.js"
@@ -0,0 +1,144 @@
1
+ {
2
+ "defaultValidationMessages": {
3
+ "minimum": {
4
+ "template": "Must be at least {value}"
5
+ },
6
+ "maximum": {
7
+ "template": "Must be at most {value}"
8
+ },
9
+ "minLength": {
10
+ "template": "Must be at least {value} character{plural}"
11
+ },
12
+ "maxLength": {
13
+ "template": "Must be at most {value} character{plural}"
14
+ }
15
+ },
16
+ "email": {
17
+ "helper": "email",
18
+ "regex": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
19
+ "jsonSchemaFormat": "email",
20
+ "errorMessage": "Must be a valid email address"
21
+ },
22
+ "date-time": {
23
+ "helper": "dateTime",
24
+ "regex": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))$",
25
+ "jsonSchemaFormat": "date-time",
26
+ "errorMessage": "Must be a valid ISO datetime"
27
+ },
28
+ "datetime": {
29
+ "helper": "datetime",
30
+ "regex": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))$",
31
+ "jsonSchemaFormat": "date-time",
32
+ "errorMessage": "Must be a valid ISO datetime"
33
+ },
34
+ "date": {
35
+ "helper": "date",
36
+ "regex": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
37
+ "jsonSchemaFormat": "date",
38
+ "errorMessage": "Must be a valid ISO date"
39
+ },
40
+ "time": {
41
+ "helper": "time",
42
+ "regex": "^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?$",
43
+ "errorMessage": "Must be a valid ISO time"
44
+ },
45
+ "duration": {
46
+ "helper": "duration",
47
+ "regex": "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$",
48
+ "jsonSchemaFormat": "duration",
49
+ "errorMessage": "Must be a valid ISO duration"
50
+ },
51
+ "url": {
52
+ "helper": "url",
53
+ "regex": "^[a-zA-Z][a-zA-Z\\d+.-]*:.+",
54
+ "jsonSchemaFormat": "uri",
55
+ "errorMessage": "Must be a valid URL"
56
+ },
57
+ "uri": {
58
+ "helper": "uri",
59
+ "regex": "^[a-zA-Z][a-zA-Z\\d+.-]*:.+",
60
+ "jsonSchemaFormat": "uri",
61
+ "errorMessage": "Must be a valid URI"
62
+ },
63
+ "httpsUrl": {
64
+ "helper": "httpsUrl",
65
+ "regex": "^https://.+",
66
+ "jsonSchemaFormat": "uri",
67
+ "errorMessage": "Must be a valid HTTPS URL"
68
+ },
69
+ "ipv4": {
70
+ "helper": "ipv4",
71
+ "regex": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.|$)){4}$",
72
+ "jsonSchemaFormat": "ipv4",
73
+ "errorMessage": "Must be a valid IPv4 address"
74
+ },
75
+ "ipv6": {
76
+ "helper": "ipv6",
77
+ "regex": "^((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|::1|::|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6}))$",
78
+ "jsonSchemaFormat": "ipv6",
79
+ "errorMessage": "Must be a valid IPv6 address"
80
+ },
81
+ "uuid": {
82
+ "helper": "uuid",
83
+ "regex": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
84
+ "jsonSchemaFormat": "uuid",
85
+ "errorMessage": "Must be a valid UUID"
86
+ },
87
+ "guid": {
88
+ "helper": "guid",
89
+ "regex": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
90
+ "jsonSchemaFormat": "uuid",
91
+ "errorMessage": "Must be a valid GUID"
92
+ },
93
+ "base64": {
94
+ "helper": "base64",
95
+ "regex": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$",
96
+ "contentEncoding": "base64",
97
+ "errorMessage": "Must be valid base64"
98
+ },
99
+ "base64url": {
100
+ "helper": "base64url",
101
+ "regex": "^[A-Za-z0-9_-]+$",
102
+ "errorMessage": "Must be valid base64url"
103
+ },
104
+ "cuid": {
105
+ "helper": "cuid",
106
+ "regex": "^c[a-z0-9]{24}$",
107
+ "errorMessage": "Must be a valid CUID"
108
+ },
109
+ "cuid2": {
110
+ "helper": "cuid2",
111
+ "regex": "^[a-z][a-z0-9]*$",
112
+ "errorMessage": "Must be a valid CUID2"
113
+ },
114
+ "ulid": {
115
+ "helper": "ulid",
116
+ "regex": "^[0-9A-HJKMNP-TV-Z]{26}$",
117
+ "errorMessage": "Must be a valid ULID"
118
+ },
119
+ "nanoid": {
120
+ "helper": "nanoid",
121
+ "regex": "^[A-Za-z0-9_-]{21}$",
122
+ "errorMessage": "Must be a valid nanoid"
123
+ },
124
+ "emoji": {
125
+ "helper": "emoji",
126
+ "regex": "^\\p{Extended_Pictographic}+$",
127
+ "errorMessage": "Must contain only emoji characters"
128
+ },
129
+ "cidrv4": {
130
+ "helper": "cidrv4",
131
+ "regex": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)/(?:3[0-2]|[12]?\\d)$",
132
+ "errorMessage": "Must be a valid IPv4 CIDR range"
133
+ },
134
+ "cidrv6": {
135
+ "helper": "cidrv6",
136
+ "regex": "^[0-9A-Fa-f:]+/(?:12[0-8]|1[01]\\d|\\d?\\d)$",
137
+ "errorMessage": "Must be a valid IPv6 CIDR range"
138
+ },
139
+ "mac": {
140
+ "helper": "mac",
141
+ "regex": "^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
142
+ "errorMessage": "Must be a valid MAC address"
143
+ }
144
+ }
@@ -0,0 +1,157 @@
1
+ import type { CandidMetadata } from "@ic-reactor/parser"
2
+ import metadataRules from "./metadata-rules.json"
3
+
4
+ export type JSDocFormatDefinition = {
5
+ regex?: string
6
+ errorMessage?: string
7
+ jsonSchemaFormat?: string
8
+ contentEncoding?: string
9
+ }
10
+
11
+ export type CustomJSDocFormatTypes = Record<
12
+ string,
13
+ string | JSDocFormatDefinition
14
+ >
15
+
16
+ type ValidationBoundRule = {
17
+ template?: string
18
+ }
19
+
20
+ type MetadataRulesFile = {
21
+ defaultValidationMessages?: Record<string, ValidationBoundRule>
22
+ }
23
+
24
+ const metadataRulesFile = metadataRules as MetadataRulesFile
25
+ const defaultValidationMessages =
26
+ metadataRulesFile.defaultValidationMessages ?? {}
27
+
28
+ const rawRuleEntries = Object.entries(metadataRules).filter(
29
+ ([type]) => type !== "defaultValidationMessages"
30
+ ) as Array<
31
+ [
32
+ string,
33
+ {
34
+ helper?: string
35
+ regex?: string
36
+ jsonSchemaFormat?: string
37
+ contentEncoding?: string
38
+ errorMessage?: string
39
+ },
40
+ ]
41
+ >
42
+
43
+ export const BUILT_IN_JSDOC_FORMAT_TYPES: Readonly<
44
+ Record<string, JSDocFormatDefinition>
45
+ > = Object.fromEntries(
46
+ rawRuleEntries.map(([type, rule]) => [
47
+ type,
48
+ {
49
+ regex: rule.regex,
50
+ jsonSchemaFormat: rule.jsonSchemaFormat,
51
+ contentEncoding: rule.contentEncoding,
52
+ errorMessage: rule.errorMessage,
53
+ },
54
+ ])
55
+ ) as Record<string, JSDocFormatDefinition>
56
+
57
+ export const BUILT_IN_FORMAT_HELPERS: Readonly<Record<string, string>> =
58
+ Object.fromEntries(
59
+ rawRuleEntries
60
+ .filter(([, rule]) => rule.helper)
61
+ .map(([type, rule]) => [type, rule.helper as string])
62
+ )
63
+
64
+ export interface FormatResolutionOptions {
65
+ customJSDocFormatTypes?: CustomJSDocFormatTypes
66
+ }
67
+
68
+ export function normalizeFormatDefinition(
69
+ definition: CustomJSDocFormatTypes[string] | undefined
70
+ ): JSDocFormatDefinition | undefined {
71
+ if (!definition) return undefined
72
+ return typeof definition === "string" ? { regex: definition } : definition
73
+ }
74
+
75
+ export function formatDefinitionFor(
76
+ type: string,
77
+ options: FormatResolutionOptions
78
+ ): JSDocFormatDefinition | undefined {
79
+ const builtIn = BUILT_IN_JSDOC_FORMAT_TYPES[type]
80
+ const custom = normalizeFormatDefinition(
81
+ options.customJSDocFormatTypes?.[type]
82
+ )
83
+
84
+ if (!custom) return builtIn
85
+ return {
86
+ ...builtIn,
87
+ ...custom,
88
+ errorMessage: custom.errorMessage ?? builtIn?.errorMessage,
89
+ }
90
+ }
91
+
92
+ function stripUndefined<T extends Record<string, unknown>>(value: T): T {
93
+ return Object.fromEntries(
94
+ Object.entries(value).filter(([, entry]) => entry !== undefined)
95
+ ) as T
96
+ }
97
+
98
+ function defaultBoundMessage(
99
+ kind: "minimum" | "maximum" | "minLength" | "maxLength",
100
+ value: string
101
+ ): string {
102
+ const template =
103
+ defaultValidationMessages[kind]?.template ??
104
+ (kind === "minimum"
105
+ ? "Must be at least {value}"
106
+ : kind === "maximum"
107
+ ? "Must be at most {value}"
108
+ : kind === "minLength"
109
+ ? "Must be at least {value} character{plural}"
110
+ : "Must be at most {value} character{plural}")
111
+
112
+ return template
113
+ .replace("{value}", value)
114
+ .replace("{plural}", value === "1" ? "" : "s")
115
+ }
116
+
117
+ function withDefaultBoundMessage<
118
+ T extends { value: string; message?: string } | undefined,
119
+ >(bound: T, kind: "minimum" | "maximum" | "minLength" | "maxLength"): T {
120
+ if (!bound || bound.message !== undefined) return bound
121
+ return {
122
+ ...bound,
123
+ message: defaultBoundMessage(kind, bound.value),
124
+ } as T
125
+ }
126
+
127
+ export function normalizeValidationMetadata(
128
+ validation: NonNullable<CandidMetadata["validation"]>,
129
+ options: FormatResolutionOptions
130
+ ): NonNullable<CandidMetadata["validation"]> {
131
+ const normalized = {
132
+ ...validation,
133
+ minimum: withDefaultBoundMessage(validation.minimum, "minimum"),
134
+ maximum: withDefaultBoundMessage(validation.maximum, "maximum"),
135
+ minLength: withDefaultBoundMessage(validation.minLength, "minLength"),
136
+ maxLength: withDefaultBoundMessage(validation.maxLength, "maxLength"),
137
+ }
138
+
139
+ const format = validation.format
140
+ const formatDefinition = format
141
+ ? formatDefinitionFor(format.type, options)
142
+ : undefined
143
+
144
+ if (format && formatDefinition) {
145
+ normalized.format = stripUndefined({
146
+ ...format,
147
+ regex: formatDefinition.regex,
148
+ jsonSchemaFormat: formatDefinition.jsonSchemaFormat,
149
+ contentEncoding: formatDefinition.contentEncoding,
150
+ errorMessage: format.message ?? formatDefinition.errorMessage,
151
+ })
152
+ } else if (format) {
153
+ normalized.format = format
154
+ }
155
+
156
+ return normalized
157
+ }
@@ -4,7 +4,7 @@ import {
4
4
  toCamelCase,
5
5
  getReactorName,
6
6
  getServiceTypeName,
7
- } from "./naming"
7
+ } from "./naming.js"
8
8
 
9
9
  describe("Naming Utilities", () => {
10
10
  describe("Base Conversions", () => {
@@ -2,7 +2,7 @@ import fs from "node:fs"
2
2
  import os from "node:os"
3
3
  import path from "node:path"
4
4
  import { afterEach, describe, expect, it } from "vitest"
5
- import { runCanisterPipeline } from "./pipeline"
5
+ import { runCanisterPipeline } from "./pipeline.js"
6
6
 
7
7
  describe("Codegen pipeline", () => {
8
8
  const tempDirs: string[] = []
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { generateReactorEntryFile, generateReactorFile } from "./generators"
2
+ import {
3
+ generateReactorEntryFile,
4
+ generateReactorFile,
5
+ } from "./generators/index.js"
3
6
 
4
7
  describe("Reactor generator", () => {
5
8
  it("keeps default behavior as DisplayReactor", () => {