@ic-reactor/codegen 0.4.0 → 0.5.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.
- package/README.md +45 -0
- package/dist/index.cjs +168 -65
- package/dist/index.d.cts +193 -27
- package/dist/index.d.ts +193 -27
- package/dist/index.js +165 -65
- package/package.json +1 -1
- package/src/bindgen.test.ts +1 -1
- package/src/generators/client.ts +49 -0
- package/src/generators/declarations.ts +112 -0
- package/src/generators/index.ts +18 -0
- package/src/generators/reactor.ts +82 -0
- package/src/index.ts +13 -11
- package/src/naming.test.ts +6 -20
- package/src/naming.ts +21 -43
- package/src/parser.ts +79 -0
- package/src/pipeline.ts +144 -0
- package/src/types.ts +65 -0
- package/src/bindgen.ts +0 -101
- package/src/templates/__snapshots__/client.test.ts.snap +0 -35
- package/src/templates/__snapshots__/reactor.test.ts.snap +0 -28
- package/src/templates/client.test.ts +0 -20
- package/src/templates/client.ts +0 -37
- package/src/templates/reactor.test.ts +0 -23
- package/src/templates/reactor.ts +0 -99
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarations Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates TypeScript declaration files (.js IDL factory + .d.ts types)
|
|
5
|
+
* from a Candid .did file using @ic-reactor/parser.
|
|
6
|
+
*
|
|
7
|
+
* Output structure:
|
|
8
|
+
* <outDir>/declarations/<name>.js — IDL factory
|
|
9
|
+
* <outDir>/declarations/<name>.d.ts — TypeScript types
|
|
10
|
+
* <outDir>/declarations/<name>.did — Copy of the source .did file
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { didToJs, didToTs } from "@ic-reactor/parser"
|
|
14
|
+
import path from "node:path"
|
|
15
|
+
import fs from "node:fs"
|
|
16
|
+
import type { GeneratorResult } from "../types.js"
|
|
17
|
+
|
|
18
|
+
export interface DeclarationsGeneratorOptions {
|
|
19
|
+
/** Absolute path to the .did file */
|
|
20
|
+
didFile: string
|
|
21
|
+
/** Absolute path to the output directory (declarations/ will be created inside) */
|
|
22
|
+
outDir: string
|
|
23
|
+
/** Canister name (used only for error messages) */
|
|
24
|
+
canisterName: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DeclarationsGeneratorResult {
|
|
28
|
+
success: boolean
|
|
29
|
+
declarationsDir: string
|
|
30
|
+
files: GeneratorResult[]
|
|
31
|
+
error?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate TypeScript declarations from a Candid file.
|
|
36
|
+
*
|
|
37
|
+
* Always cleans and regenerates the declarations directory to ensure
|
|
38
|
+
* it's in sync with the source .did file.
|
|
39
|
+
*/
|
|
40
|
+
export async function generateDeclarations(
|
|
41
|
+
options: DeclarationsGeneratorOptions
|
|
42
|
+
): Promise<DeclarationsGeneratorResult> {
|
|
43
|
+
const { didFile, outDir, canisterName } = options
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(didFile)) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
declarationsDir: "",
|
|
49
|
+
files: [],
|
|
50
|
+
error: `DID file not found: ${didFile}`,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const declarationsDir = path.join(outDir, "declarations")
|
|
55
|
+
const baseName = path.basename(didFile, ".did") // e.g. "backend" from "backend.did"
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Read the DID content before any directory manipulation
|
|
59
|
+
const didContent = fs.readFileSync(didFile, "utf-8")
|
|
60
|
+
|
|
61
|
+
// Ensure output dir exists
|
|
62
|
+
if (!fs.existsSync(outDir)) {
|
|
63
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Clean and recreate declarations dir for a fresh generation
|
|
67
|
+
if (fs.existsSync(declarationsDir)) {
|
|
68
|
+
fs.rmSync(declarationsDir, { recursive: true, force: true })
|
|
69
|
+
}
|
|
70
|
+
fs.mkdirSync(declarationsDir, { recursive: true })
|
|
71
|
+
|
|
72
|
+
const jsContent = didToJs(didContent)
|
|
73
|
+
const tsContent = didToTs(didContent)
|
|
74
|
+
|
|
75
|
+
const jsPath = path.join(declarationsDir, `${baseName}.js`)
|
|
76
|
+
const dtsPath = path.join(declarationsDir, `${baseName}.d.ts`)
|
|
77
|
+
const didCopyPath = path.join(declarationsDir, `${baseName}.did`)
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(jsPath, jsContent)
|
|
80
|
+
fs.writeFileSync(dtsPath, tsContent)
|
|
81
|
+
fs.writeFileSync(didCopyPath, didContent)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
declarationsDir,
|
|
86
|
+
files: [
|
|
87
|
+
{ success: true, filePath: jsPath },
|
|
88
|
+
{ success: true, filePath: dtsPath },
|
|
89
|
+
{ success: true, filePath: didCopyPath },
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
declarationsDir,
|
|
97
|
+
files: [],
|
|
98
|
+
error: `[${canisterName}] Failed to generate declarations: ${message}`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if declarations already exist for a canister.
|
|
105
|
+
*/
|
|
106
|
+
export function declarationsExist(
|
|
107
|
+
outDir: string,
|
|
108
|
+
canisterName: string
|
|
109
|
+
): boolean {
|
|
110
|
+
const dtsPath = path.join(outDir, "declarations", `${canisterName}.d.ts`)
|
|
111
|
+
return fs.existsSync(dtsPath)
|
|
112
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator exports
|
|
3
|
+
*
|
|
4
|
+
* All individual generators are exported here.
|
|
5
|
+
* The pipeline composes them; CLI and vite-plugin use them via the pipeline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { generateDeclarations, declarationsExist } from "./declarations.js"
|
|
9
|
+
export type {
|
|
10
|
+
DeclarationsGeneratorOptions,
|
|
11
|
+
DeclarationsGeneratorResult,
|
|
12
|
+
} from "./declarations.js"
|
|
13
|
+
|
|
14
|
+
export { generateReactorFile } from "./reactor.js"
|
|
15
|
+
export type { ReactorGeneratorOptions } from "./reactor.js"
|
|
16
|
+
|
|
17
|
+
export { generateClientFile } from "./client.js"
|
|
18
|
+
export type { ClientGeneratorOptions } from "./client.js"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactor File Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates the main `index.ts` for a canister — a DisplayReactor instance
|
|
5
|
+
* plus the full set of typed hooks via `createActorHooks`.
|
|
6
|
+
*
|
|
7
|
+
* Generated output example (for canister "backend"):
|
|
8
|
+
*
|
|
9
|
+
* import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
10
|
+
* import { clientManager } from "../../clients"
|
|
11
|
+
* import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
12
|
+
*
|
|
13
|
+
* export type BackendService = _SERVICE
|
|
14
|
+
*
|
|
15
|
+
* export const backendReactor = new DisplayReactor<BackendService>({ ... })
|
|
16
|
+
*
|
|
17
|
+
* export const {
|
|
18
|
+
* useActorQuery: useBackendQuery,
|
|
19
|
+
* ...
|
|
20
|
+
* } = createActorHooks(backendReactor)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from "node:path"
|
|
24
|
+
import { toPascalCase, getReactorName, getServiceTypeName } from "../naming.js"
|
|
25
|
+
|
|
26
|
+
export interface ReactorGeneratorOptions {
|
|
27
|
+
/** Canister name (e.g. "backend") */
|
|
28
|
+
canisterName: string
|
|
29
|
+
/**
|
|
30
|
+
* Path to the .did file. Used to derive the declarations import path.
|
|
31
|
+
* Can be relative or absolute — only the basename is used.
|
|
32
|
+
*/
|
|
33
|
+
didFile: string
|
|
34
|
+
/**
|
|
35
|
+
* Import path for the client manager, relative from the generated reactor file.
|
|
36
|
+
* Default: "../../clients"
|
|
37
|
+
*/
|
|
38
|
+
clientManagerPath?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate the content of a canister's `index.ts` reactor file.
|
|
43
|
+
*/
|
|
44
|
+
export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
45
|
+
const { canisterName, didFile, clientManagerPath = "../../clients" } = options
|
|
46
|
+
|
|
47
|
+
const pascalName = toPascalCase(canisterName)
|
|
48
|
+
const reactorName = getReactorName(canisterName)
|
|
49
|
+
const serviceName = getServiceTypeName(canisterName)
|
|
50
|
+
|
|
51
|
+
// Derive the declarations import path from the .did filename
|
|
52
|
+
const baseName = path.basename(didFile, ".did")
|
|
53
|
+
const declarationsPath = `./declarations/${baseName}`
|
|
54
|
+
|
|
55
|
+
return `import { DisplayReactor, createActorHooks } from "@ic-reactor/react"
|
|
56
|
+
import { clientManager } from "${clientManagerPath}"
|
|
57
|
+
import { idlFactory, type _SERVICE } from "${declarationsPath}"
|
|
58
|
+
|
|
59
|
+
export type ${serviceName} = _SERVICE
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ${pascalName} Reactor
|
|
63
|
+
*
|
|
64
|
+
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
65
|
+
* Re-run \`ic-reactor generate\` (or the Vite plugin) to regenerate.
|
|
66
|
+
*/
|
|
67
|
+
export const ${reactorName} = new DisplayReactor<${serviceName}>({
|
|
68
|
+
clientManager,
|
|
69
|
+
idlFactory,
|
|
70
|
+
name: "${canisterName}",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const {
|
|
74
|
+
useActorQuery: use${pascalName}Query,
|
|
75
|
+
useActorSuspenseQuery: use${pascalName}SuspenseQuery,
|
|
76
|
+
useActorInfiniteQuery: use${pascalName}InfiniteQuery,
|
|
77
|
+
useActorSuspenseInfiniteQuery: use${pascalName}SuspenseInfiniteQuery,
|
|
78
|
+
useActorMutation: use${pascalName}Mutation,
|
|
79
|
+
useActorMethod: use${pascalName}Method,
|
|
80
|
+
} = createActorHooks(${reactorName})
|
|
81
|
+
`
|
|
82
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
* @ic-reactor/codegen
|
|
3
3
|
*
|
|
4
4
|
* Shared code generation utilities for IC Reactor.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by @ic-reactor/cli and @ic-reactor/vite-plugin.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
export {
|
|
8
|
+
// Core Types
|
|
9
|
+
export type { CanisterConfig, CodegenConfig, GeneratorResult } from "./types.js"
|
|
10
|
+
|
|
11
|
+
// Pipeline (Primary Entry Point)
|
|
12
|
+
export { runCanisterPipeline } from "./pipeline.js"
|
|
13
|
+
export type { PipelineOptions, PipelineResult } from "./pipeline.js"
|
|
10
14
|
|
|
11
|
-
//
|
|
12
|
-
export {
|
|
13
|
-
export type { BindgenOptions, BindgenResult } from "./bindgen.js"
|
|
15
|
+
// Utilities
|
|
16
|
+
export { toPascalCase, getReactorName, getServiceTypeName } from "./naming.js"
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
export type { ReactorGeneratorOptions } from "./templates/reactor.js"
|
|
18
|
+
export { parseDIDFile, extractMethods } from "./parser.js"
|
|
19
|
+
export type { MethodInfo, MethodType } from "./parser.js"
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
+
// Individual Generators (Advanced Usage)
|
|
22
|
+
export * from "./generators/index.js"
|
package/src/naming.test.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { describe, it, expect } from "vitest"
|
|
|
2
2
|
import {
|
|
3
3
|
toPascalCase,
|
|
4
4
|
toCamelCase,
|
|
5
|
-
getHookFileName,
|
|
6
|
-
getHookExportName,
|
|
7
|
-
getReactHookName,
|
|
8
5
|
getReactorName,
|
|
9
6
|
getServiceTypeName,
|
|
10
7
|
} from "./naming"
|
|
@@ -25,25 +22,14 @@ describe("Naming Utilities", () => {
|
|
|
25
22
|
})
|
|
26
23
|
|
|
27
24
|
describe("Domain-Specific Naming", () => {
|
|
28
|
-
it("generates
|
|
29
|
-
expect(
|
|
30
|
-
expect(
|
|
31
|
-
"updateItemMutation.ts"
|
|
32
|
-
)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it("generates hook export names", () => {
|
|
36
|
-
expect(getHookExportName("get_user", "query")).toBe("getUserQuery")
|
|
37
|
-
expect(getHookExportName("update_item", "mutation")).toBe(
|
|
38
|
-
"updateItemMutation"
|
|
39
|
-
)
|
|
25
|
+
it("generates reactor names", () => {
|
|
26
|
+
expect(getReactorName("backend")).toBe("backendReactor")
|
|
27
|
+
expect(getReactorName("my-canister")).toBe("myCanisterReactor")
|
|
40
28
|
})
|
|
41
29
|
|
|
42
|
-
it("generates
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
45
|
-
"useUpdateItemMutation"
|
|
46
|
-
)
|
|
30
|
+
it("generates service type names", () => {
|
|
31
|
+
expect(getServiceTypeName("backend")).toBe("BackendService")
|
|
32
|
+
expect(getServiceTypeName("update_item")).toBe("UpdateItemService")
|
|
47
33
|
})
|
|
48
34
|
|
|
49
35
|
it("generates reactor names", () => {
|
package/src/naming.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Naming utilities for code generation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Pure functions that convert canister/method names to correctly cased
|
|
5
|
+
* identifiers used throughout generated code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { camelCase, pascalCase } from "change-case"
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
// BASE CASE CONVERSIONS
|
|
12
|
-
//
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Convert string to PascalCase
|
|
15
|
+
* Convert string to PascalCase.
|
|
16
16
|
* @example toPascalCase("get_message") → "GetMessage"
|
|
17
17
|
* @example toPascalCase("my-canister") → "MyCanister"
|
|
18
18
|
*/
|
|
@@ -21,7 +21,7 @@ export function toPascalCase(str: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Convert string to camelCase
|
|
24
|
+
* Convert string to camelCase.
|
|
25
25
|
* @example toCamelCase("get_message") → "getMessage"
|
|
26
26
|
* @example toCamelCase("my-canister") → "myCanister"
|
|
27
27
|
*/
|
|
@@ -29,55 +29,33 @@ export function toCamelCase(str: string): string {
|
|
|
29
29
|
return camelCase(str)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
33
|
// DOMAIN-SPECIFIC NAMING
|
|
34
|
-
//
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Generate
|
|
38
|
-
* @example getHookFileName("get_message", "query") → "getMessageQuery.ts"
|
|
39
|
-
*/
|
|
40
|
-
export function getHookFileName(methodName: string, hookType: string): string {
|
|
41
|
-
const camelMethod = toCamelCase(methodName)
|
|
42
|
-
const pascalType = toPascalCase(hookType)
|
|
43
|
-
return `${camelMethod}${pascalType}.ts`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Generate hook export name
|
|
48
|
-
* @example getHookExportName("get_message", "query") → "getMessageQuery"
|
|
49
|
-
*/
|
|
50
|
-
export function getHookExportName(
|
|
51
|
-
methodName: string,
|
|
52
|
-
hookType: string
|
|
53
|
-
): string {
|
|
54
|
-
const camelMethod = toCamelCase(methodName)
|
|
55
|
-
const pascalType = toPascalCase(hookType)
|
|
56
|
-
return `${camelMethod}${pascalType}`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Generate React hook name (with use prefix)
|
|
61
|
-
* @example getReactHookName("get_message", "query") → "useGetMessageQuery"
|
|
62
|
-
*/
|
|
63
|
-
export function getReactHookName(methodName: string, hookType: string): string {
|
|
64
|
-
const pascalMethod = toPascalCase(methodName)
|
|
65
|
-
const pascalType = toPascalCase(hookType)
|
|
66
|
-
return `use${pascalMethod}${pascalType}`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Generate reactor variable name
|
|
37
|
+
* Generate the reactor variable name for a canister.
|
|
71
38
|
* @example getReactorName("backend") → "backendReactor"
|
|
39
|
+
* @example getReactorName("my_canister") → "myCanisterReactor"
|
|
72
40
|
*/
|
|
73
41
|
export function getReactorName(canisterName: string): string {
|
|
74
42
|
return `${toCamelCase(canisterName)}Reactor`
|
|
75
43
|
}
|
|
76
44
|
|
|
77
45
|
/**
|
|
78
|
-
* Generate service type name
|
|
46
|
+
* Generate the service type name for a canister.
|
|
79
47
|
* @example getServiceTypeName("backend") → "BackendService"
|
|
48
|
+
* @example getServiceTypeName("my_canister") → "MyCanisterService"
|
|
80
49
|
*/
|
|
81
50
|
export function getServiceTypeName(canisterName: string): string {
|
|
82
51
|
return `${toPascalCase(canisterName)}Service`
|
|
83
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate the hook name prefix (PascalCase canister name).
|
|
56
|
+
* Used when naming the destructured hooks: `use<Prefix>Query`, etc.
|
|
57
|
+
* @example getHookPrefix("my_canister") → "MyCanister"
|
|
58
|
+
*/
|
|
59
|
+
export function getHookPrefix(canisterName: string): string {
|
|
60
|
+
return toPascalCase(canisterName)
|
|
61
|
+
}
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candid Parser Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parses Candid .did files to extract service method signatures.
|
|
5
|
+
* Used by the CLI for listing methods and by advanced generators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { didToJs } from "@ic-reactor/parser"
|
|
9
|
+
import fs from "node:fs"
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// TYPES
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type MethodType = "query" | "mutation"
|
|
16
|
+
|
|
17
|
+
export interface MethodInfo {
|
|
18
|
+
/** Method name as it appears in the Candid service */
|
|
19
|
+
name: string
|
|
20
|
+
/** "query" for read-only calls, "mutation" for update calls */
|
|
21
|
+
type: MethodType
|
|
22
|
+
/** True if the method takes at least one argument */
|
|
23
|
+
hasArgs: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// PARSERS
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract method information from raw Candid source text.
|
|
32
|
+
*
|
|
33
|
+
* Compiles the Candid to JS and inspects the IDL.Service definition.
|
|
34
|
+
*/
|
|
35
|
+
export function extractMethods(didContent: string): MethodInfo[] {
|
|
36
|
+
try {
|
|
37
|
+
const jsContent = didToJs(didContent)
|
|
38
|
+
|
|
39
|
+
const methods: MethodInfo[] = []
|
|
40
|
+
|
|
41
|
+
// Find the IDL.Service({...}) body
|
|
42
|
+
const serviceMatch = /IDL\.Service\(\{([\s\S]*?)\}\)/.exec(jsContent)
|
|
43
|
+
if (!serviceMatch) return methods
|
|
44
|
+
|
|
45
|
+
const serviceBody = serviceMatch[1]
|
|
46
|
+
|
|
47
|
+
// Match each method: 'methodName': IDL.Func([args], [rets], [annotations])
|
|
48
|
+
const methodRegex =
|
|
49
|
+
/['"]([\w]+)['"]\s*:\s*IDL\.Func\(\s*\[(.*?)\],\s*\[.*?\],\s*\[(.*?)\]\)/g
|
|
50
|
+
|
|
51
|
+
let match: RegExpExecArray | null
|
|
52
|
+
while ((match = methodRegex.exec(serviceBody)) !== null) {
|
|
53
|
+
const [, name, args, annotations] = match
|
|
54
|
+
methods.push({
|
|
55
|
+
name,
|
|
56
|
+
type: annotations.includes("'query'") ? "query" : "mutation",
|
|
57
|
+
hasArgs: args.trim().length > 0,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return methods
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
64
|
+
throw new Error(`Failed to parse Candid: ${msg}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a .did file from disk and return its methods.
|
|
70
|
+
*
|
|
71
|
+
* @throws if the file does not exist or fails to parse
|
|
72
|
+
*/
|
|
73
|
+
export function parseDIDFile(didFilePath: string): MethodInfo[] {
|
|
74
|
+
if (!fs.existsSync(didFilePath)) {
|
|
75
|
+
throw new Error(`DID file not found: ${didFilePath}`)
|
|
76
|
+
}
|
|
77
|
+
const content = fs.readFileSync(didFilePath, "utf-8")
|
|
78
|
+
return extractMethods(content)
|
|
79
|
+
}
|
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegen Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates all generators for a single canister.
|
|
5
|
+
* This is the primary entry point used by @ic-reactor/cli and @ic-reactor/vite-plugin.
|
|
6
|
+
*
|
|
7
|
+
* Pipeline steps (in order):
|
|
8
|
+
* 1. Resolve paths (didFile, outDir)
|
|
9
|
+
* 2. Generate declarations (JS + .d.ts + .did copy)
|
|
10
|
+
* 3. Generate reactor file (index.ts)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs"
|
|
14
|
+
import path from "node:path"
|
|
15
|
+
import type { CanisterConfig, CodegenConfig, GeneratorResult } from "./types.js"
|
|
16
|
+
import { generateDeclarations } from "./generators/declarations.js"
|
|
17
|
+
import { generateReactorFile } from "./generators/reactor.js"
|
|
18
|
+
|
|
19
|
+
export interface PipelineOptions {
|
|
20
|
+
/** Canister name and config */
|
|
21
|
+
canisterConfig: CanisterConfig
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to the project root.
|
|
24
|
+
* Relative paths in `CanisterConfig` are resolved from here.
|
|
25
|
+
*/
|
|
26
|
+
projectRoot: string
|
|
27
|
+
/**
|
|
28
|
+
* Global codegen config (for fallback outDir and clientManagerPath).
|
|
29
|
+
*/
|
|
30
|
+
globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath">
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PipelineResult {
|
|
34
|
+
canisterName: string
|
|
35
|
+
success: boolean
|
|
36
|
+
/** All files the pipeline attempted to write */
|
|
37
|
+
files: GeneratorResult[]
|
|
38
|
+
error?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run the full codegen pipeline for a single canister.
|
|
43
|
+
*
|
|
44
|
+
* @returns PipelineResult with per-file details
|
|
45
|
+
*/
|
|
46
|
+
export async function runCanisterPipeline(
|
|
47
|
+
options: PipelineOptions
|
|
48
|
+
): Promise<PipelineResult> {
|
|
49
|
+
const { canisterConfig, projectRoot, globalConfig } = options
|
|
50
|
+
const { name, didFile, clientManagerPath } = canisterConfig
|
|
51
|
+
|
|
52
|
+
const files: GeneratorResult[] = []
|
|
53
|
+
|
|
54
|
+
// ── Resolve paths ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const resolvedDidFile = path.isAbsolute(didFile)
|
|
57
|
+
? didFile
|
|
58
|
+
: path.resolve(projectRoot, didFile)
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(resolvedDidFile)) {
|
|
61
|
+
return {
|
|
62
|
+
canisterName: name,
|
|
63
|
+
success: false,
|
|
64
|
+
files: [],
|
|
65
|
+
error: `DID file not found: ${resolvedDidFile}`,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Per-canister outDir overrides global outDir
|
|
70
|
+
const canisterOutDir =
|
|
71
|
+
canisterConfig.outDir != null
|
|
72
|
+
? path.isAbsolute(canisterConfig.outDir)
|
|
73
|
+
? canisterConfig.outDir
|
|
74
|
+
: path.resolve(projectRoot, canisterConfig.outDir)
|
|
75
|
+
: path.resolve(projectRoot, globalConfig.outDir, name)
|
|
76
|
+
|
|
77
|
+
// clientManagerPath falls back to global, then a safe default
|
|
78
|
+
const resolvedClientManagerPath =
|
|
79
|
+
clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients"
|
|
80
|
+
|
|
81
|
+
// ── Step 1: Declarations ───────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const declResult = await generateDeclarations({
|
|
85
|
+
didFile: resolvedDidFile,
|
|
86
|
+
outDir: canisterOutDir,
|
|
87
|
+
canisterName: name,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (!declResult.success) {
|
|
91
|
+
return {
|
|
92
|
+
canisterName: name,
|
|
93
|
+
success: false,
|
|
94
|
+
files,
|
|
95
|
+
error: declResult.error,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
files.push(...declResult.files)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
canisterName: name,
|
|
103
|
+
success: false,
|
|
104
|
+
files,
|
|
105
|
+
error: `Declarations step failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Step 2: Reactor file ───────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const reactorPath = path.join(canisterOutDir, "index.ts")
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const reactorContent = generateReactorFile({
|
|
115
|
+
canisterName: name,
|
|
116
|
+
didFile: resolvedDidFile,
|
|
117
|
+
clientManagerPath: resolvedClientManagerPath,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
fs.mkdirSync(canisterOutDir, { recursive: true })
|
|
121
|
+
fs.writeFileSync(reactorPath, reactorContent)
|
|
122
|
+
|
|
123
|
+
files.push({ success: true, filePath: reactorPath })
|
|
124
|
+
} catch (err) {
|
|
125
|
+
files.push({
|
|
126
|
+
success: false,
|
|
127
|
+
filePath: reactorPath,
|
|
128
|
+
error: `Reactor generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
canisterName: name,
|
|
133
|
+
success: false,
|
|
134
|
+
files,
|
|
135
|
+
error: `Reactor step failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
canisterName: name,
|
|
141
|
+
success: true,
|
|
142
|
+
files,
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ic-reactor/codegen — Core Types
|
|
3
|
+
*
|
|
4
|
+
* All shared types used across generators, pipeline, CLI, and vite-plugin.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
// CONFIGURATION TYPES
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Per-canister configuration.
|
|
13
|
+
* `name` is always required — it drives all naming in generators.
|
|
14
|
+
*/
|
|
15
|
+
export interface CanisterConfig {
|
|
16
|
+
/** Canister name (used for variable names, file names, etc.) */
|
|
17
|
+
name: string
|
|
18
|
+
/** Path to the .did file (relative to project root, or absolute) */
|
|
19
|
+
didFile: string
|
|
20
|
+
/** Override output dir for this specific canister */
|
|
21
|
+
outDir?: string
|
|
22
|
+
/**
|
|
23
|
+
* Import path to the ClientManager (relative from the generated reactor file).
|
|
24
|
+
* Example: "../../clients" → `import { clientManager } from "../../clients"`
|
|
25
|
+
*/
|
|
26
|
+
clientManagerPath?: string
|
|
27
|
+
/** Optional fixed canister ID */
|
|
28
|
+
canisterId?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Top-level codegen / CLI configuration (stored in `ic-reactor.json`).
|
|
33
|
+
*/
|
|
34
|
+
export interface CodegenConfig {
|
|
35
|
+
/** JSON Schema reference */
|
|
36
|
+
$schema?: string
|
|
37
|
+
/**
|
|
38
|
+
* Default output directory for all canisters (relative to project root).
|
|
39
|
+
* Individual canisters can override via `CanisterConfig.outDir`.
|
|
40
|
+
*/
|
|
41
|
+
outDir: string
|
|
42
|
+
/**
|
|
43
|
+
* Default import path for the ClientManager (relative from generated files).
|
|
44
|
+
* Individual canisters can override via `CanisterConfig.clientManagerPath`.
|
|
45
|
+
*/
|
|
46
|
+
clientManagerPath?: string
|
|
47
|
+
/** Canister configurations, keyed by canister name */
|
|
48
|
+
canisters: Record<string, CanisterConfig>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
+
// GENERATOR TYPES
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of a single file-writing generator step.
|
|
57
|
+
*/
|
|
58
|
+
export interface GeneratorResult {
|
|
59
|
+
success: boolean
|
|
60
|
+
/** Absolute path of the file that was (or would have been) written */
|
|
61
|
+
filePath: string
|
|
62
|
+
/** If true, skipped because the file already existed */
|
|
63
|
+
skipped?: boolean
|
|
64
|
+
error?: string
|
|
65
|
+
}
|