@ic-reactor/codegen 0.12.1 → 0.13.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 +14 -3
- package/dist/index.cjs +316 -201
- package/dist/index.d.cts +20 -36
- package/dist/index.d.ts +20 -36
- package/dist/index.js +315 -199
- package/package.json +7 -3
- package/src/generators/declarations.ts +164 -21
- package/src/generators/reactor.ts +5 -3
- package/src/index.ts +1 -3
- package/src/pipeline.ts +155 -10
- package/src/validate.ts +38 -0
- package/src/__snapshots__/bindgen.test.ts.snap +0 -18
- package/src/__snapshots__/reactor.test.ts.snap +0 -127
- package/src/bindgen.test.ts +0 -85
- package/src/naming.test.ts +0 -45
- package/src/parser.ts +0 -79
- package/src/pipeline.test.ts +0 -355
- package/src/reactor.test.ts +0 -84
- package/src/validate.test.ts +0 -393
|
@@ -14,6 +14,7 @@ import { didToJs, didToTs } from "@ic-reactor/parser"
|
|
|
14
14
|
import path from "node:path"
|
|
15
15
|
import fs from "node:fs"
|
|
16
16
|
import type { GeneratorResult } from "../types.js"
|
|
17
|
+
import { CodegenConfigError, resolveDeclarationsBaseName } from "../validate.js"
|
|
17
18
|
|
|
18
19
|
export interface DeclarationsGeneratorOptions {
|
|
19
20
|
/** Absolute path to the .did file */
|
|
@@ -31,18 +32,95 @@ export interface DeclarationsGeneratorResult {
|
|
|
31
32
|
error?: string
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Move `from` onto `to`, replacing whatever is already there.
|
|
37
|
+
*
|
|
38
|
+
* `rename(2)` refuses to replace a non-empty directory, so the previous output
|
|
39
|
+
* is moved aside first and only removed once the new directory is in place. If
|
|
40
|
+
* the second rename fails the old directory is put back, so the observable
|
|
41
|
+
* states are "old output" and "new output" — never "no output".
|
|
42
|
+
*/
|
|
43
|
+
function replaceDirectory(from: string, to: string): void {
|
|
44
|
+
if (!fs.existsSync(to)) {
|
|
45
|
+
fs.renameSync(from, to)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Same parent as `to`, so this is a rename within one filesystem. Dot-prefixed
|
|
50
|
+
// because a crash between the two renames below strands this directory next
|
|
51
|
+
// to the real one: `declarations.old-XYZ` sits inside the project's source
|
|
52
|
+
// tree and TypeScript would compile it, producing duplicate declarations on
|
|
53
|
+
// top of whatever the crash already caused.
|
|
54
|
+
const displaced = fs.mkdtempSync(
|
|
55
|
+
path.join(path.dirname(to), `.${path.basename(to)}.old-`)
|
|
56
|
+
)
|
|
57
|
+
// mkdtemp created the directory; rename needs the name to be free.
|
|
58
|
+
fs.rmdirSync(displaced)
|
|
59
|
+
fs.renameSync(to, displaced)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
fs.renameSync(from, to)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
fs.renameSync(displaced, to)
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.rmSync(displaced, { recursive: true, force: true })
|
|
69
|
+
}
|
|
70
|
+
|
|
34
71
|
/**
|
|
35
72
|
* Generate TypeScript declarations from a Candid file.
|
|
36
73
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
74
|
+
* Generation happens in a staging directory that is swapped over the existing
|
|
75
|
+
* `declarations/` only after every file has been written. The parser runs on
|
|
76
|
+
* user-authored Candid and throws on a syntax error — under the vite plugin
|
|
77
|
+
* that is a *normal* watch-mode event, one keystroke in a .did file — so
|
|
78
|
+
* deleting the previous output before parsing turned every typo into a broken
|
|
79
|
+
* build with no declarations at all. A failure now leaves the previous
|
|
80
|
+
* declarations byte-identical.
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* The export the generated reactor imports. Its presence in `didToJs` output is
|
|
84
|
+
* the authoritative "this .did describes a service" test — see the note in
|
|
85
|
+
* generateDeclarations.
|
|
86
|
+
*/
|
|
87
|
+
const HAS_IDL_FACTORY = /\bexport\s+const\s+idlFactory\b/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Marker naming the canister an output directory belongs to. Dot-prefixed so
|
|
91
|
+
* it stays out of the generated surface consumers import.
|
|
39
92
|
*/
|
|
93
|
+
export const OWNER_FILE = ".ic-reactor-owner"
|
|
94
|
+
|
|
40
95
|
export async function generateDeclarations(
|
|
41
96
|
options: DeclarationsGeneratorOptions
|
|
42
97
|
): Promise<DeclarationsGeneratorResult> {
|
|
43
98
|
const { didFile, outDir, canisterName } = options
|
|
44
99
|
|
|
45
|
-
|
|
100
|
+
const declarationsDir = path.join(outDir, "declarations")
|
|
101
|
+
|
|
102
|
+
// Checked before the file itself: an absolute `didFile` ending in ".." is not
|
|
103
|
+
// normalized by the caller and does exist, so without this the diagnostic
|
|
104
|
+
// would be about the file type rather than about the path being a directory.
|
|
105
|
+
let baseName: string
|
|
106
|
+
try {
|
|
107
|
+
baseName = resolveDeclarationsBaseName(didFile)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof CodegenConfigError) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
declarationsDir: "",
|
|
113
|
+
files: [],
|
|
114
|
+
error: `[${canisterName}] ${error.message}`,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let didStat: fs.Stats
|
|
121
|
+
try {
|
|
122
|
+
didStat = fs.statSync(didFile)
|
|
123
|
+
} catch {
|
|
46
124
|
return {
|
|
47
125
|
success: false,
|
|
48
126
|
declarationsDir: "",
|
|
@@ -51,34 +129,72 @@ export async function generateDeclarations(
|
|
|
51
129
|
}
|
|
52
130
|
}
|
|
53
131
|
|
|
54
|
-
|
|
55
|
-
|
|
132
|
+
// A fifo or character device reads forever. Refusing anything that is not a
|
|
133
|
+
// regular file turns an indefinite hang into a diagnosable error.
|
|
134
|
+
if (!didStat.isFile()) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
declarationsDir: "",
|
|
138
|
+
files: [],
|
|
139
|
+
error: `[${canisterName}] DID path is not a regular file: ${didFile}`,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let staging: string | undefined
|
|
56
144
|
|
|
57
145
|
try {
|
|
58
146
|
// Read the DID content before any directory manipulation
|
|
59
147
|
const didContent = fs.readFileSync(didFile, "utf-8")
|
|
60
148
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fs.mkdirSync(outDir, { recursive: true })
|
|
64
|
-
}
|
|
149
|
+
const jsContent = didToJs(didContent)
|
|
150
|
+
const tsContent = didToTs(didContent)
|
|
65
151
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
152
|
+
// A .did that parses but declares no service is a normal thing to have — a
|
|
153
|
+
// shared types file, say — but it compiles to a module with no `idlFactory`
|
|
154
|
+
// and no `_SERVICE`, which is exactly what the generated reactor imports.
|
|
155
|
+
// Caught here the caller gets the .did path; not caught, the consumer's
|
|
156
|
+
// build fails on a file we just reported as generated.
|
|
157
|
+
//
|
|
158
|
+
// The test is on the GENERATED OUTPUT, not on `parseDid(...).service`. That
|
|
159
|
+
// AST field is only populated for an inline body; it is null for every
|
|
160
|
+
// alias form — `service : S;`, `service : (args) -> S;`, `service name : S;`
|
|
161
|
+
// — which the parser nonetheless compiles to a complete idlFactory. Gating
|
|
162
|
+
// on the AST field rejected those valid files outright.
|
|
163
|
+
if (!HAS_IDL_FACTORY.test(jsContent)) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
declarationsDir,
|
|
167
|
+
files: [],
|
|
168
|
+
error:
|
|
169
|
+
`[${canisterName}] ${didFile} produces no idlFactory, so there is no service ` +
|
|
170
|
+
`to generate against. Point this canister at the .did file that declares its ` +
|
|
171
|
+
`service.`,
|
|
172
|
+
}
|
|
69
173
|
}
|
|
70
|
-
fs.mkdirSync(declarationsDir, { recursive: true })
|
|
71
174
|
|
|
72
|
-
|
|
73
|
-
|
|
175
|
+
// Ensure output dir exists — it is also the staging directory's parent, so
|
|
176
|
+
// the swap below stays a same-filesystem rename.
|
|
177
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
178
|
+
|
|
179
|
+
// Record which canister owns this directory. The pipeline reads it to stop
|
|
180
|
+
// a second canister generating into the same outDir and wiping the first
|
|
181
|
+
// one's declarations. It lives here rather than in index.generated.ts
|
|
182
|
+
// because `--bindgen-only` never writes that file, and the declarations
|
|
183
|
+
// replacement it skips past is exactly the destructive step.
|
|
184
|
+
fs.writeFileSync(path.join(outDir, OWNER_FILE), `${canisterName}\n`)
|
|
185
|
+
|
|
186
|
+
staging = fs.mkdtempSync(path.join(outDir, ".declarations.tmp-"))
|
|
74
187
|
|
|
75
188
|
const jsPath = path.join(declarationsDir, `${baseName}.js`)
|
|
76
189
|
const dtsPath = path.join(declarationsDir, `${baseName}.d.ts`)
|
|
77
190
|
const didCopyPath = path.join(declarationsDir, `${baseName}.did`)
|
|
78
191
|
|
|
79
|
-
fs.writeFileSync(
|
|
80
|
-
fs.writeFileSync(
|
|
81
|
-
fs.writeFileSync(
|
|
192
|
+
fs.writeFileSync(path.join(staging, `${baseName}.js`), jsContent)
|
|
193
|
+
fs.writeFileSync(path.join(staging, `${baseName}.d.ts`), tsContent)
|
|
194
|
+
fs.writeFileSync(path.join(staging, `${baseName}.did`), didContent)
|
|
195
|
+
|
|
196
|
+
replaceDirectory(staging, declarationsDir)
|
|
197
|
+
staging = undefined
|
|
82
198
|
|
|
83
199
|
return {
|
|
84
200
|
success: true,
|
|
@@ -97,16 +213,43 @@ export async function generateDeclarations(
|
|
|
97
213
|
files: [],
|
|
98
214
|
error: `[${canisterName}] Failed to generate declarations: ${message}`,
|
|
99
215
|
}
|
|
216
|
+
} finally {
|
|
217
|
+
// Reached on the failure path and on an interrupted swap; a completed swap
|
|
218
|
+
// has already cleared `staging`.
|
|
219
|
+
if (staging) fs.rmSync(staging, { recursive: true, force: true })
|
|
100
220
|
}
|
|
101
221
|
}
|
|
102
222
|
|
|
103
223
|
/**
|
|
104
224
|
* Check if declarations already exist for a canister.
|
|
225
|
+
*
|
|
226
|
+
* Declarations are written under the *.did basename*, which need not equal the
|
|
227
|
+
* canister name — `{ name: "backend", didFile: "service.did" }` writes
|
|
228
|
+
* `declarations/service.d.ts`. Pass `didFile` for an exact answer; without it
|
|
229
|
+
* this falls back to any `.d.ts` in the directory, because a bare
|
|
230
|
+
* `<canisterName>.d.ts` check reports "missing" for perfectly good output.
|
|
105
231
|
*/
|
|
106
232
|
export function declarationsExist(
|
|
107
233
|
outDir: string,
|
|
108
|
-
canisterName: string
|
|
234
|
+
canisterName: string,
|
|
235
|
+
didFile?: string
|
|
109
236
|
): boolean {
|
|
110
|
-
const
|
|
111
|
-
|
|
237
|
+
const declarationsDir = path.join(outDir, "declarations")
|
|
238
|
+
|
|
239
|
+
if (didFile !== undefined) {
|
|
240
|
+
const baseName = path.basename(didFile, ".did")
|
|
241
|
+
return fs.existsSync(path.join(declarationsDir, `${baseName}.d.ts`))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(path.join(declarationsDir, `${canisterName}.d.ts`))) {
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
return fs
|
|
250
|
+
.readdirSync(declarationsDir)
|
|
251
|
+
.some((entry) => entry.endsWith(".d.ts"))
|
|
252
|
+
} catch {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
112
255
|
}
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* } = createActorHooks(backendReactor)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import path from "node:path"
|
|
24
23
|
import { toPascalCase, getReactorName, getServiceTypeName } from "../naming.js"
|
|
25
24
|
import type { CodegenTarget, ReactorClassName } from "../types.js"
|
|
25
|
+
import { resolveDeclarationsBaseName } from "../validate.js"
|
|
26
26
|
|
|
27
27
|
export interface ReactorGeneratorOptions {
|
|
28
28
|
/** Canister name (e.g. "backend") */
|
|
@@ -88,8 +88,10 @@ export function generateReactorFile(options: ReactorGeneratorOptions): string {
|
|
|
88
88
|
const reactorName = getReactorName(canisterName)
|
|
89
89
|
const serviceName = getServiceTypeName(canisterName)
|
|
90
90
|
|
|
91
|
-
// Derive the declarations import path from the .did filename
|
|
92
|
-
|
|
91
|
+
// Derive the declarations import path from the .did filename. The helper
|
|
92
|
+
// rejects stems like "." and ".." rather than emitting an import of a
|
|
93
|
+
// directory — this runs even for callers that bypass the pipeline.
|
|
94
|
+
const baseName = resolveDeclarationsBaseName(didFile)
|
|
93
95
|
const declarationsPath = `./declarations/${baseName}`
|
|
94
96
|
const reactorImportSource = getReactorClassImportSource(
|
|
95
97
|
reactorClass,
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
assertContainedPath,
|
|
31
31
|
assertOneOf,
|
|
32
32
|
resolveContainedOutDir,
|
|
33
|
+
resolveDeclarationsBaseName,
|
|
33
34
|
CodegenConfigError,
|
|
34
35
|
CANISTER_NAME_PATTERN,
|
|
35
36
|
REACTOR_CLASS_NAMES,
|
|
@@ -40,8 +41,5 @@ export type {
|
|
|
40
41
|
ValidateCanisterConfigOptions,
|
|
41
42
|
} from "./validate.js"
|
|
42
43
|
|
|
43
|
-
export { parseDIDFile, extractMethods } from "./parser.js"
|
|
44
|
-
export type { MethodInfo, MethodType } from "./parser.js"
|
|
45
|
-
|
|
46
44
|
// Individual Generators (Advanced Usage)
|
|
47
45
|
export * from "./generators/index.js"
|
package/src/pipeline.ts
CHANGED
|
@@ -20,12 +20,17 @@ import type {
|
|
|
20
20
|
GeneratorResult,
|
|
21
21
|
ReactorClassName,
|
|
22
22
|
} from "./types.js"
|
|
23
|
-
import { generateDeclarations } from "./generators/declarations.js"
|
|
23
|
+
import { generateDeclarations, OWNER_FILE } from "./generators/declarations.js"
|
|
24
24
|
import {
|
|
25
25
|
generateReactorEntryFile,
|
|
26
26
|
generateReactorFile,
|
|
27
27
|
} from "./generators/reactor.js"
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
assertSafeCanisterConfig,
|
|
30
|
+
CodegenConfigError,
|
|
31
|
+
resolveDeclarationsBaseName,
|
|
32
|
+
} from "./validate.js"
|
|
33
|
+
import { getReactorName, getServiceTypeName } from "./naming.js"
|
|
29
34
|
|
|
30
35
|
export interface PipelineOptions {
|
|
31
36
|
/** Canister name and config */
|
|
@@ -61,13 +66,114 @@ function normalizeFileContent(content: string): string {
|
|
|
61
66
|
return content.replace(/\r\n/g, "\n").trim()
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
/** Header line every version of this generator has written into its output. */
|
|
70
|
+
const GENERATED_MARKER = "Auto-generated by @ic-reactor/codegen"
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The reactor's `name:` property as this generator emits it. Canister names are
|
|
74
|
+
* restricted to `CANISTER_NAME_PATTERN`, so the emitted line is exactly this.
|
|
75
|
+
*/
|
|
76
|
+
const GENERATED_CANISTER_NAME = /^ {2}name: "([A-Za-z0-9_.-]+)",$/m
|
|
77
|
+
|
|
78
|
+
/** Every `export …` statement, one per line, as the generator writes them. */
|
|
79
|
+
const EXPORT_STATEMENT = /^export\b.*$/gm
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Decide whether an existing `index.ts` is still the pre-split generated
|
|
83
|
+
* reactor file, which this pipeline migrates to the thin wrapper.
|
|
84
|
+
*
|
|
85
|
+
* The old test — the header marker plus the substring `createActorHooks(` —
|
|
86
|
+
* matched any file that had *ever* been generated, including one the user has
|
|
87
|
+
* since filled with their own factories and hooks. Migration would overwrite it
|
|
88
|
+
* with six lines of boilerplate and report the write as an ordinary success.
|
|
89
|
+
*
|
|
90
|
+
* So the shape is checked, not just the header: the file must still declare the
|
|
91
|
+
* reactor and hooks for *this* canister and must export nothing the old
|
|
92
|
+
* generator did not write. Anything the user added — another `export const`, an
|
|
93
|
+
* `export * from`, a re-export — takes the file out of scope and it is
|
|
94
|
+
* preserved. Edits that add no export are still possible, which is why the
|
|
95
|
+
* caller keeps a backup before writing.
|
|
96
|
+
*/
|
|
97
|
+
function isLegacyGeneratedIndexFile(
|
|
98
|
+
content: string,
|
|
99
|
+
canisterName: string
|
|
100
|
+
): boolean {
|
|
101
|
+
if (!content.includes(GENERATED_MARKER)) return false
|
|
102
|
+
|
|
103
|
+
const reactorName = getReactorName(canisterName)
|
|
104
|
+
const serviceName = getServiceTypeName(canisterName)
|
|
105
|
+
|
|
106
|
+
// The two lines that make this the generated reactor for this canister.
|
|
107
|
+
if (!content.includes(`export const ${reactorName} = new `)) return false
|
|
108
|
+
if (!content.includes(`= createActorHooks(${reactorName})`)) return false
|
|
109
|
+
|
|
110
|
+
const allowed = [
|
|
111
|
+
`export type ${serviceName} = _SERVICE`,
|
|
112
|
+
`export const ${reactorName} = new `,
|
|
113
|
+
// Opens the destructured hook block; its members are not export statements.
|
|
114
|
+
"export const {",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
const statements = normalizeFileContent(content).match(EXPORT_STATEMENT) ?? []
|
|
118
|
+
|
|
119
|
+
return statements.every((statement) =>
|
|
120
|
+
allowed.some((prefix) => statement.startsWith(prefix))
|
|
68
121
|
)
|
|
69
122
|
}
|
|
70
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Copy `filePath` next to itself before it is overwritten, and return the
|
|
126
|
+
* backup path.
|
|
127
|
+
*
|
|
128
|
+
* Migration decides from the file's contents that it is safe to replace. That
|
|
129
|
+
* inference can be wrong for an edit that adds no export, and the file being
|
|
130
|
+
* replaced may be the only copy of work the user has not committed, so the
|
|
131
|
+
* original is kept rather than deleted. An existing backup is never clobbered.
|
|
132
|
+
*/
|
|
133
|
+
function backUpFile(filePath: string): string {
|
|
134
|
+
let backupPath = `${filePath}.bak`
|
|
135
|
+
for (let n = 2; fs.existsSync(backupPath); n += 1) {
|
|
136
|
+
backupPath = `${filePath}.bak.${n}`
|
|
137
|
+
}
|
|
138
|
+
fs.copyFileSync(filePath, backupPath)
|
|
139
|
+
return backupPath
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read the canister name out of an existing `index.generated.ts`, if the file
|
|
144
|
+
* exists and this package wrote it.
|
|
145
|
+
*
|
|
146
|
+
* Two canisters pointed at the same `outDir` overwrite each other's
|
|
147
|
+
* `index.generated.ts` and — since the declarations directory is replaced
|
|
148
|
+
* wholesale — each other's declarations too, with both runs reporting success.
|
|
149
|
+
* The generated file records which canister owns the directory, so the second
|
|
150
|
+
* canister can be stopped before it destroys the first one's output.
|
|
151
|
+
*/
|
|
152
|
+
function findOutDirOwner(outDir: string): string | undefined {
|
|
153
|
+
// Written by generateDeclarations, so it is present for `--bindgen-only`
|
|
154
|
+
// runs too — those skip index.generated.ts but still replace declarations/,
|
|
155
|
+
// which is the step that destroys the other canister's output.
|
|
156
|
+
try {
|
|
157
|
+
const owner = fs.readFileSync(path.join(outDir, OWNER_FILE), "utf-8").trim()
|
|
158
|
+
if (owner) return owner
|
|
159
|
+
} catch {
|
|
160
|
+
// fall through to the legacy marker
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Directories generated before the marker existed only carry the canister
|
|
164
|
+
// name inside index.generated.ts.
|
|
165
|
+
let content: string
|
|
166
|
+
try {
|
|
167
|
+
content = fs.readFileSync(path.join(outDir, "index.generated.ts"), "utf-8")
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!content.includes(GENERATED_MARKER)) return undefined
|
|
173
|
+
|
|
174
|
+
return GENERATED_CANISTER_NAME.exec(content)?.[1]
|
|
175
|
+
}
|
|
176
|
+
|
|
71
177
|
function isManagedEntryWrapper(
|
|
72
178
|
content: string,
|
|
73
179
|
expectedEntryContent: string
|
|
@@ -149,10 +255,45 @@ export async function runCanisterPipeline(
|
|
|
149
255
|
}
|
|
150
256
|
}
|
|
151
257
|
|
|
258
|
+
// The .did basename names the generated declaration files and is the tail of
|
|
259
|
+
// the import specifier the reactor emits, so it is validated here, before any
|
|
260
|
+
// filesystem work, rather than surfacing as a broken import later.
|
|
261
|
+
try {
|
|
262
|
+
resolveDeclarationsBaseName(resolvedDidFile)
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof CodegenConfigError) {
|
|
265
|
+
return {
|
|
266
|
+
canisterName: name,
|
|
267
|
+
success: false,
|
|
268
|
+
files,
|
|
269
|
+
error: `[${name}] ${err.message}`,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw err
|
|
273
|
+
}
|
|
274
|
+
|
|
152
275
|
// Per-canister outDir overrides global outDir; both are resolved and
|
|
153
276
|
// containment-checked by assertSafeCanisterConfig above.
|
|
154
277
|
const canisterOutDir = validated.outDir
|
|
155
278
|
|
|
279
|
+
// Refuse to generate into a directory another canister already owns. This has
|
|
280
|
+
// to happen before the declarations step, which replaces `declarations/`
|
|
281
|
+
// wholesale and would otherwise take the other canister's output with it.
|
|
282
|
+
const owner = findOutDirOwner(canisterOutDir)
|
|
283
|
+
if (owner !== undefined && owner !== name) {
|
|
284
|
+
return {
|
|
285
|
+
canisterName: name,
|
|
286
|
+
success: false,
|
|
287
|
+
files,
|
|
288
|
+
error:
|
|
289
|
+
`[${name}] Output directory ${canisterOutDir} was generated for canister ` +
|
|
290
|
+
`"${owner}". Two canisters cannot share an outDir — each run replaces the ` +
|
|
291
|
+
`declarations directory and index.generated.ts, so they would overwrite each ` +
|
|
292
|
+
`other. Give each canister its own "outDir". If you renamed "${owner}" to ` +
|
|
293
|
+
`"${name}", delete that directory and regenerate.`,
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
156
297
|
// clientManagerPath falls back to global, then a safe default
|
|
157
298
|
const resolvedClientManagerPath = validated.clientManagerPath
|
|
158
299
|
|
|
@@ -220,11 +361,15 @@ export async function runCanisterPipeline(
|
|
|
220
361
|
} else {
|
|
221
362
|
const existingEntryContent = fs.readFileSync(entryPath, "utf-8")
|
|
222
363
|
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
) {
|
|
364
|
+
if (isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
365
|
+
fs.writeFileSync(entryPath, entryContent)
|
|
366
|
+
files.push({ success: true, filePath: entryPath })
|
|
367
|
+
} else if (isLegacyGeneratedIndexFile(existingEntryContent, name)) {
|
|
368
|
+
// Migration replaces a file the user owns, on the strength of a
|
|
369
|
+
// content match. Keep the original so a wrong match is recoverable.
|
|
370
|
+
const backupPath = backUpFile(entryPath)
|
|
227
371
|
fs.writeFileSync(entryPath, entryContent)
|
|
372
|
+
files.push({ success: true, filePath: backupPath })
|
|
228
373
|
files.push({ success: true, filePath: entryPath })
|
|
229
374
|
} else {
|
|
230
375
|
files.push({ success: true, filePath: entryPath, skipped: true })
|
package/src/validate.ts
CHANGED
|
@@ -167,6 +167,44 @@ export function assertSafeModuleSpecifier(
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Derive the declarations file stem from a `.did` path and assert it is usable
|
|
172
|
+
* both as a file name and as the tail of the import specifier we emit.
|
|
173
|
+
*
|
|
174
|
+
* `path.basename` is happy to hand back `"."` or `".."`: a `didFile` of
|
|
175
|
+
* `"canisters/.."` makes the generated reactor `import … from
|
|
176
|
+
* "./declarations/.."`, a *directory* reference — reported as a successful
|
|
177
|
+
* generation, rejected by the consumer's bundler. Validating the derived
|
|
178
|
+
* specifier here is the same treatment `clientManagerPath` gets, for the same
|
|
179
|
+
* reason: it is config-supplied text that ends up inside an `import`.
|
|
180
|
+
*/
|
|
181
|
+
export function resolveDeclarationsBaseName(didFile: unknown): string {
|
|
182
|
+
if (typeof didFile !== "string" || didFile.length === 0) {
|
|
183
|
+
throw new CodegenConfigError(
|
|
184
|
+
`Invalid didFile: expected a non-empty string, received ${
|
|
185
|
+
didFile === undefined ? "undefined" : JSON.stringify(didFile)
|
|
186
|
+
}.`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseName = path.basename(didFile, ".did")
|
|
191
|
+
|
|
192
|
+
if (baseName === "" || baseName === "." || baseName === "..") {
|
|
193
|
+
throw new CodegenConfigError(
|
|
194
|
+
`Invalid didFile ${JSON.stringify(didFile)}: its file name ${JSON.stringify(baseName)} ` +
|
|
195
|
+
`refers to a directory, not a Candid file. Generated declarations are named after the ` +
|
|
196
|
+
`.did file, so this would emit an import of a directory.`
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assertSafeModuleSpecifier(
|
|
201
|
+
`declarations import derived from didFile ${JSON.stringify(didFile)}`,
|
|
202
|
+
`./declarations/${baseName}`
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return baseName
|
|
206
|
+
}
|
|
207
|
+
|
|
170
208
|
/** Reactor classes the generator knows how to emit an import for. */
|
|
171
209
|
export const REACTOR_CLASS_NAMES: readonly ReactorClassName[] = [
|
|
172
210
|
"Reactor",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`Bindgen > generateDeclarations creates correct files > js-declarations 1`] = `
|
|
4
|
-
"export const idlFactory = ({ IDL }) => {
|
|
5
|
-
return IDL.Service({ 'greet' : IDL.Func([IDL.Text], [IDL.Text], ['query']) });
|
|
6
|
-
};
|
|
7
|
-
export const init = ({ IDL }) => { return []; };"
|
|
8
|
-
`;
|
|
9
|
-
|
|
10
|
-
exports[`Bindgen > generateDeclarations creates correct files > ts-declarations 1`] = `
|
|
11
|
-
"import type { Principal } from '@icp-sdk/core/principal';
|
|
12
|
-
import type { ActorMethod } from '@icp-sdk/core/agent';
|
|
13
|
-
import type { IDL } from '@icp-sdk/core/candid';
|
|
14
|
-
|
|
15
|
-
export interface _SERVICE { 'greet' : ActorMethod<[string], string> }
|
|
16
|
-
export declare const idlFactory: IDL.InterfaceFactory;
|
|
17
|
-
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];"
|
|
18
|
-
`;
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`Reactor generator > keeps default behavior as DisplayReactor > display-reactor-index 1`] = `
|
|
4
|
-
"import { createActorHooks } from "@ic-reactor/react"
|
|
5
|
-
import { DisplayReactor } from "@ic-reactor/react"
|
|
6
|
-
import { clientManager } from "../../clients"
|
|
7
|
-
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
8
|
-
|
|
9
|
-
export type BackendService = _SERVICE
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Backend Reactor
|
|
13
|
-
*
|
|
14
|
-
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
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.
|
|
19
|
-
*/
|
|
20
|
-
export const backendReactor = new DisplayReactor<BackendService>({
|
|
21
|
-
clientManager,
|
|
22
|
-
idlFactory,
|
|
23
|
-
name: "backend",
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export const {
|
|
27
|
-
useActorQuery: useBackendQuery,
|
|
28
|
-
useActorSuspenseQuery: useBackendSuspenseQuery,
|
|
29
|
-
useActorInfiniteQuery: useBackendInfiniteQuery,
|
|
30
|
-
useActorSuspenseInfiniteQuery: useBackendSuspenseInfiniteQuery,
|
|
31
|
-
useActorMutation: useBackendMutation,
|
|
32
|
-
useActorMethod: useBackendMethod,
|
|
33
|
-
} = createActorHooks(backendReactor)
|
|
34
|
-
"
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
exports[`Reactor generator > supports Reactor mode generation > reactor-index 1`] = `
|
|
38
|
-
"import { createActorHooks } from "@ic-reactor/react"
|
|
39
|
-
import { Reactor } from "@ic-reactor/react"
|
|
40
|
-
import { clientManager } from "../../clients"
|
|
41
|
-
import { idlFactory, type _SERVICE } from "./declarations/workflow_engine"
|
|
42
|
-
|
|
43
|
-
export type WorkflowEngineService = _SERVICE
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* WorkflowEngine Reactor
|
|
47
|
-
*
|
|
48
|
-
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
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.
|
|
53
|
-
*/
|
|
54
|
-
export const workflowEngineReactor = new Reactor<WorkflowEngineService>({
|
|
55
|
-
clientManager,
|
|
56
|
-
idlFactory,
|
|
57
|
-
name: "workflow_engine",
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
export const {
|
|
61
|
-
useActorQuery: useWorkflowEngineQuery,
|
|
62
|
-
useActorSuspenseQuery: useWorkflowEngineSuspenseQuery,
|
|
63
|
-
useActorInfiniteQuery: useWorkflowEngineInfiniteQuery,
|
|
64
|
-
useActorSuspenseInfiniteQuery: useWorkflowEngineSuspenseInfiniteQuery,
|
|
65
|
-
useActorMutation: useWorkflowEngineMutation,
|
|
66
|
-
useActorMethod: useWorkflowEngineMethod,
|
|
67
|
-
} = createActorHooks(workflowEngineReactor)
|
|
68
|
-
"
|
|
69
|
-
`;
|
|
70
|
-
|
|
71
|
-
exports[`Reactor generator > supports candid reactor subclasses > metadata-display-reactor-index 1`] = `
|
|
72
|
-
"import { createActorHooks } from "@ic-reactor/react"
|
|
73
|
-
import { MetadataDisplayReactor } from "@ic-reactor/candid"
|
|
74
|
-
import { clientManager } from "../../clients"
|
|
75
|
-
import { idlFactory, type _SERVICE } from "./declarations/ledger"
|
|
76
|
-
|
|
77
|
-
export type LedgerService = _SERVICE
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Ledger Reactor
|
|
81
|
-
*
|
|
82
|
-
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
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.
|
|
87
|
-
*/
|
|
88
|
-
export const ledgerReactor = new MetadataDisplayReactor<LedgerService>({
|
|
89
|
-
clientManager,
|
|
90
|
-
idlFactory,
|
|
91
|
-
name: "ledger",
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
export const {
|
|
95
|
-
useActorQuery: useLedgerQuery,
|
|
96
|
-
useActorSuspenseQuery: useLedgerSuspenseQuery,
|
|
97
|
-
useActorInfiniteQuery: useLedgerInfiniteQuery,
|
|
98
|
-
useActorSuspenseInfiniteQuery: useLedgerSuspenseInfiniteQuery,
|
|
99
|
-
useActorMutation: useLedgerMutation,
|
|
100
|
-
useActorMethod: useLedgerMethod,
|
|
101
|
-
} = createActorHooks(ledgerReactor)
|
|
102
|
-
"
|
|
103
|
-
`;
|
|
104
|
-
|
|
105
|
-
exports[`Reactor generator > supports core target generation without React hooks > core-display-reactor-index 1`] = `
|
|
106
|
-
"import { DisplayReactor } from "@ic-reactor/core"
|
|
107
|
-
import { clientManager } from "../../clients"
|
|
108
|
-
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
109
|
-
|
|
110
|
-
export type BackendService = _SERVICE
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Backend Reactor
|
|
114
|
-
*
|
|
115
|
-
* Auto-generated by @ic-reactor/codegen — do not edit.
|
|
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.
|
|
120
|
-
*/
|
|
121
|
-
export const backendReactor = new DisplayReactor<BackendService>({
|
|
122
|
-
clientManager,
|
|
123
|
-
idlFactory,
|
|
124
|
-
name: "backend",
|
|
125
|
-
})
|
|
126
|
-
"
|
|
127
|
-
`;
|