@ic-reactor/codegen 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.cjs +226 -625
- package/dist/index.d.cts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +216 -26
- package/package.json +3 -13
- package/src/generators/reactor.ts +15 -4
- package/src/index.ts +19 -3
- package/src/pipeline.ts +36 -9
- package/src/validate.test.ts +393 -0
- package/src/validate.ts +369 -0
- package/dist/chunk-VBCR5IVT.js +0 -609
- package/dist/renderer.cjs +0 -639
- package/dist/renderer.d.cts +0 -33
- package/dist/renderer.d.ts +0 -33
- package/dist/renderer.js +0 -14
- package/src/metadata-rules.json +0 -144
- package/src/metadata.ts +0 -157
- package/src/renderer.test.ts +0 -573
- package/src/renderer.ts +0 -515
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export { generateCodecDeclarations } from './renderer.cjs';
|
|
2
|
-
import '@ic-reactor/parser';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* @ic-reactor/codegen — Core Types
|
|
6
3
|
*
|
|
@@ -143,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
|
|
|
143
140
|
*/
|
|
144
141
|
declare function getServiceTypeName(canisterName: string): string;
|
|
145
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Config validation for the codegen pipeline.
|
|
145
|
+
*
|
|
146
|
+
* Every value validated here arrives from a project's `ic-reactor.json` or from
|
|
147
|
+
* `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
|
|
148
|
+
* user may have merely cloned. The pipeline turns those values into filesystem
|
|
149
|
+
* paths it recursively deletes and into source text it writes into the user's
|
|
150
|
+
* bundle, so they are treated as untrusted input rather than as configuration.
|
|
151
|
+
*
|
|
152
|
+
* Validation lives here, at the pipeline's own entry point, rather than in the
|
|
153
|
+
* CLI's interactive prompt: the prompt only covers one of the three entry paths
|
|
154
|
+
* (hand-edited config and plugin options bypass it entirely).
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a canister config would produce an unsafe path or unsafe
|
|
159
|
+
* generated source. Callers convert this into a `PipelineResult` error.
|
|
160
|
+
*/
|
|
161
|
+
declare class CodegenConfigError extends Error {
|
|
162
|
+
readonly name = "CodegenConfigError";
|
|
163
|
+
constructor(message: string);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Characters allowed in a canister name.
|
|
167
|
+
*
|
|
168
|
+
* A canister name becomes a directory segment, so this excludes path
|
|
169
|
+
* separators, quotes, whitespace and control characters. It deliberately does
|
|
170
|
+
* NOT require a leading letter: `dfx` places no restriction on canister names,
|
|
171
|
+
* and real projects use `_private` and `my.canister`, both of which derive
|
|
172
|
+
* perfectly good identifiers. Identifier validity is enforced separately, on
|
|
173
|
+
* the *derived* names, by {@link assertSafeCanisterName}.
|
|
174
|
+
*/
|
|
175
|
+
declare const CANISTER_NAME_PATTERN: RegExp;
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a canister name is safe to use as a path segment and as the stem
|
|
178
|
+
* of a generated identifier.
|
|
179
|
+
*
|
|
180
|
+
* Rejecting a leading digit here is what stops `2048_game` from reaching the
|
|
181
|
+
* generator, where it would become `export const 2048GameReactor` — invalid
|
|
182
|
+
* TypeScript emitted with a success status.
|
|
183
|
+
*/
|
|
184
|
+
declare function assertSafeCanisterName(name: unknown): asserts name is string;
|
|
185
|
+
/**
|
|
186
|
+
* Assert that a module specifier we interpolate into an `import` statement is a
|
|
187
|
+
* relative path or a bare package name — never a URL.
|
|
188
|
+
*
|
|
189
|
+
* A `https://…` specifier here would make the generated module pull code from a
|
|
190
|
+
* remote host at import time, inside the user's own bundle.
|
|
191
|
+
*/
|
|
192
|
+
declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
|
|
193
|
+
/** Reactor classes the generator knows how to emit an import for. */
|
|
194
|
+
declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
|
|
195
|
+
/** Runtime targets the generator knows how to emit. */
|
|
196
|
+
declare const CODEGEN_TARGETS: readonly CodegenTarget[];
|
|
197
|
+
/**
|
|
198
|
+
* Assert a config value is one of a closed set.
|
|
199
|
+
*
|
|
200
|
+
* `mode` and `target` are interpolated into emitted source as bare identifiers
|
|
201
|
+
* and module specifiers, so an unrecognized value is not merely unsupported —
|
|
202
|
+
* it is injected.
|
|
203
|
+
*/
|
|
204
|
+
declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
|
|
205
|
+
/**
|
|
206
|
+
* Assert that an already-resolved absolute path lies inside `projectRoot`.
|
|
207
|
+
*
|
|
208
|
+
* Containment is decided on the real on-disk locations: `path.resolve` does not
|
|
209
|
+
* follow symlinks, so a purely lexical comparison can be walked out of via a
|
|
210
|
+
* symlink placed inside the project root.
|
|
211
|
+
*
|
|
212
|
+
* @param original - the path as the user wrote it, for the error message
|
|
213
|
+
*/
|
|
214
|
+
declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve `outDir` against `projectRoot` and assert the result stays inside it.
|
|
217
|
+
*
|
|
218
|
+
* The pipeline recursively deletes `<outDir>/declarations` before every
|
|
219
|
+
* generation, so an `outDir` that escapes the project root turns a config file
|
|
220
|
+
* into an arbitrary-directory delete.
|
|
221
|
+
*/
|
|
222
|
+
declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
|
|
223
|
+
interface ValidatedCanisterPaths {
|
|
224
|
+
/** The validated canister name. */
|
|
225
|
+
name: string;
|
|
226
|
+
/** Absolute output directory, guaranteed to be inside `projectRoot`. */
|
|
227
|
+
outDir: string;
|
|
228
|
+
/** Validated module specifier for the client manager import. */
|
|
229
|
+
clientManagerPath: string;
|
|
230
|
+
}
|
|
231
|
+
interface ValidateCanisterConfigOptions {
|
|
232
|
+
name: unknown;
|
|
233
|
+
/** Per-canister `outDir`, if the config sets one. */
|
|
234
|
+
canisterOutDir?: unknown;
|
|
235
|
+
/** Global `outDir`; the canister name is appended to it when used. */
|
|
236
|
+
globalOutDir: unknown;
|
|
237
|
+
clientManagerPath: unknown;
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
/** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
|
|
240
|
+
mode?: unknown;
|
|
241
|
+
/** Resolved runtime target (`canisterConfig.target` / global `target`). */
|
|
242
|
+
target?: unknown;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate one canister's config and return the resolved, contained paths the
|
|
246
|
+
* pipeline should use.
|
|
247
|
+
*
|
|
248
|
+
* Call this before any filesystem work: it is the single choke point that all
|
|
249
|
+
* three entry paths (CLI, vite plugin, direct API) share.
|
|
250
|
+
*/
|
|
251
|
+
declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
|
|
252
|
+
|
|
146
253
|
/**
|
|
147
254
|
* Candid Parser Utilities
|
|
148
255
|
*
|
|
@@ -286,4 +393,4 @@ interface ClientGeneratorOptions {
|
|
|
286
393
|
*/
|
|
287
394
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
288
395
|
|
|
289
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
396
|
+
export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export { generateCodecDeclarations } from './renderer.js';
|
|
2
|
-
import '@ic-reactor/parser';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* @ic-reactor/codegen — Core Types
|
|
6
3
|
*
|
|
@@ -143,6 +140,116 @@ declare function getReactorName(canisterName: string): string;
|
|
|
143
140
|
*/
|
|
144
141
|
declare function getServiceTypeName(canisterName: string): string;
|
|
145
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Config validation for the codegen pipeline.
|
|
145
|
+
*
|
|
146
|
+
* Every value validated here arrives from a project's `ic-reactor.json` or from
|
|
147
|
+
* `@ic-reactor/vite-plugin` options — that is, from a file in a repository the
|
|
148
|
+
* user may have merely cloned. The pipeline turns those values into filesystem
|
|
149
|
+
* paths it recursively deletes and into source text it writes into the user's
|
|
150
|
+
* bundle, so they are treated as untrusted input rather than as configuration.
|
|
151
|
+
*
|
|
152
|
+
* Validation lives here, at the pipeline's own entry point, rather than in the
|
|
153
|
+
* CLI's interactive prompt: the prompt only covers one of the three entry paths
|
|
154
|
+
* (hand-edited config and plugin options bypass it entirely).
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a canister config would produce an unsafe path or unsafe
|
|
159
|
+
* generated source. Callers convert this into a `PipelineResult` error.
|
|
160
|
+
*/
|
|
161
|
+
declare class CodegenConfigError extends Error {
|
|
162
|
+
readonly name = "CodegenConfigError";
|
|
163
|
+
constructor(message: string);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Characters allowed in a canister name.
|
|
167
|
+
*
|
|
168
|
+
* A canister name becomes a directory segment, so this excludes path
|
|
169
|
+
* separators, quotes, whitespace and control characters. It deliberately does
|
|
170
|
+
* NOT require a leading letter: `dfx` places no restriction on canister names,
|
|
171
|
+
* and real projects use `_private` and `my.canister`, both of which derive
|
|
172
|
+
* perfectly good identifiers. Identifier validity is enforced separately, on
|
|
173
|
+
* the *derived* names, by {@link assertSafeCanisterName}.
|
|
174
|
+
*/
|
|
175
|
+
declare const CANISTER_NAME_PATTERN: RegExp;
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a canister name is safe to use as a path segment and as the stem
|
|
178
|
+
* of a generated identifier.
|
|
179
|
+
*
|
|
180
|
+
* Rejecting a leading digit here is what stops `2048_game` from reaching the
|
|
181
|
+
* generator, where it would become `export const 2048GameReactor` — invalid
|
|
182
|
+
* TypeScript emitted with a success status.
|
|
183
|
+
*/
|
|
184
|
+
declare function assertSafeCanisterName(name: unknown): asserts name is string;
|
|
185
|
+
/**
|
|
186
|
+
* Assert that a module specifier we interpolate into an `import` statement is a
|
|
187
|
+
* relative path or a bare package name — never a URL.
|
|
188
|
+
*
|
|
189
|
+
* A `https://…` specifier here would make the generated module pull code from a
|
|
190
|
+
* remote host at import time, inside the user's own bundle.
|
|
191
|
+
*/
|
|
192
|
+
declare function assertSafeModuleSpecifier(label: string, specifier: unknown): asserts specifier is string;
|
|
193
|
+
/** Reactor classes the generator knows how to emit an import for. */
|
|
194
|
+
declare const REACTOR_CLASS_NAMES: readonly ReactorClassName[];
|
|
195
|
+
/** Runtime targets the generator knows how to emit. */
|
|
196
|
+
declare const CODEGEN_TARGETS: readonly CodegenTarget[];
|
|
197
|
+
/**
|
|
198
|
+
* Assert a config value is one of a closed set.
|
|
199
|
+
*
|
|
200
|
+
* `mode` and `target` are interpolated into emitted source as bare identifiers
|
|
201
|
+
* and module specifiers, so an unrecognized value is not merely unsupported —
|
|
202
|
+
* it is injected.
|
|
203
|
+
*/
|
|
204
|
+
declare function assertOneOf<T extends string>(label: string, value: unknown, allowed: readonly T[]): asserts value is T;
|
|
205
|
+
/**
|
|
206
|
+
* Assert that an already-resolved absolute path lies inside `projectRoot`.
|
|
207
|
+
*
|
|
208
|
+
* Containment is decided on the real on-disk locations: `path.resolve` does not
|
|
209
|
+
* follow symlinks, so a purely lexical comparison can be walked out of via a
|
|
210
|
+
* symlink placed inside the project root.
|
|
211
|
+
*
|
|
212
|
+
* @param original - the path as the user wrote it, for the error message
|
|
213
|
+
*/
|
|
214
|
+
declare function assertContainedPath(label: string, resolved: string, projectRoot: string, original?: string): void;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve `outDir` against `projectRoot` and assert the result stays inside it.
|
|
217
|
+
*
|
|
218
|
+
* The pipeline recursively deletes `<outDir>/declarations` before every
|
|
219
|
+
* generation, so an `outDir` that escapes the project root turns a config file
|
|
220
|
+
* into an arbitrary-directory delete.
|
|
221
|
+
*/
|
|
222
|
+
declare function resolveContainedOutDir(label: string, outDir: unknown, projectRoot: string): string;
|
|
223
|
+
interface ValidatedCanisterPaths {
|
|
224
|
+
/** The validated canister name. */
|
|
225
|
+
name: string;
|
|
226
|
+
/** Absolute output directory, guaranteed to be inside `projectRoot`. */
|
|
227
|
+
outDir: string;
|
|
228
|
+
/** Validated module specifier for the client manager import. */
|
|
229
|
+
clientManagerPath: string;
|
|
230
|
+
}
|
|
231
|
+
interface ValidateCanisterConfigOptions {
|
|
232
|
+
name: unknown;
|
|
233
|
+
/** Per-canister `outDir`, if the config sets one. */
|
|
234
|
+
canisterOutDir?: unknown;
|
|
235
|
+
/** Global `outDir`; the canister name is appended to it when used. */
|
|
236
|
+
globalOutDir: unknown;
|
|
237
|
+
clientManagerPath: unknown;
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
/** Resolved reactor class (`canisterConfig.mode`), if the config sets one. */
|
|
240
|
+
mode?: unknown;
|
|
241
|
+
/** Resolved runtime target (`canisterConfig.target` / global `target`). */
|
|
242
|
+
target?: unknown;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validate one canister's config and return the resolved, contained paths the
|
|
246
|
+
* pipeline should use.
|
|
247
|
+
*
|
|
248
|
+
* Call this before any filesystem work: it is the single choke point that all
|
|
249
|
+
* three entry paths (CLI, vite plugin, direct API) share.
|
|
250
|
+
*/
|
|
251
|
+
declare function assertSafeCanisterConfig(options: ValidateCanisterConfigOptions): ValidatedCanisterPaths;
|
|
252
|
+
|
|
146
253
|
/**
|
|
147
254
|
* Candid Parser Utilities
|
|
148
255
|
*
|
|
@@ -286,4 +393,4 @@ interface ClientGeneratorOptions {
|
|
|
286
393
|
*/
|
|
287
394
|
declare function generateClientFile(options?: ClientGeneratorOptions): string;
|
|
288
395
|
|
|
289
|
-
export { type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, type ReactorClassName, type ReactorGeneratorOptions, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, runCanisterPipeline, toPascalCase };
|
|
396
|
+
export { CANISTER_NAME_PATTERN, CODEGEN_TARGETS, type CanisterConfig, type ClientGeneratorOptions, type CodegenConfig, CodegenConfigError, type CodegenTarget, type DeclarationsGeneratorOptions, type DeclarationsGeneratorResult, type GeneratorResult, type MethodInfo, type MethodType, type PipelineOptions, type PipelineResult, REACTOR_CLASS_NAMES, type ReactorClassName, type ReactorGeneratorOptions, type ValidateCanisterConfigOptions, type ValidatedCanisterPaths, assertContainedPath, assertOneOf, assertSafeCanisterConfig, assertSafeCanisterName, assertSafeModuleSpecifier, declarationsExist, extractMethods, generateClientFile, generateDeclarations, generateReactorEntryFile, generateReactorFile, getReactorName, getServiceTypeName, parseDIDFile, resolveContainedOutDir, runCanisterPipeline, toPascalCase };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
generateCodecDeclarations
|
|
3
|
-
} from "./chunk-VBCR5IVT.js";
|
|
4
|
-
|
|
5
1
|
// src/pipeline.ts
|
|
6
|
-
import
|
|
7
|
-
import
|
|
2
|
+
import fs3 from "fs";
|
|
3
|
+
import path4 from "path";
|
|
8
4
|
|
|
9
5
|
// src/generators/declarations.ts
|
|
10
6
|
import { didToJs, didToTs } from "@ic-reactor/parser";
|
|
@@ -80,6 +76,9 @@ function getReactorName(canisterName) {
|
|
|
80
76
|
function getServiceTypeName(canisterName) {
|
|
81
77
|
return `${toPascalCase(canisterName)}Service`;
|
|
82
78
|
}
|
|
79
|
+
function getHookPrefix(canisterName) {
|
|
80
|
+
return toPascalCase(canisterName);
|
|
81
|
+
}
|
|
83
82
|
|
|
84
83
|
// src/generators/reactor.ts
|
|
85
84
|
function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
@@ -91,6 +90,10 @@ function getReactorClassImportSource(reactorClass, runtimeTarget) {
|
|
|
91
90
|
case "CandidDisplayReactor":
|
|
92
91
|
case "MetadataDisplayReactor":
|
|
93
92
|
return "@ic-reactor/candid";
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
96
|
+
);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
function generateReactorFile(options) {
|
|
@@ -124,9 +127,9 @@ export const {
|
|
|
124
127
|
useActorMethod: use${pascalName}Method,
|
|
125
128
|
} = createActorHooks(${reactorName})
|
|
126
129
|
` : "";
|
|
127
|
-
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from
|
|
128
|
-
import { clientManager } from
|
|
129
|
-
import { idlFactory, type _SERVICE } from
|
|
130
|
+
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
131
|
+
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
132
|
+
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
130
133
|
|
|
131
134
|
export type ${serviceName} = _SERVICE
|
|
132
135
|
|
|
@@ -142,7 +145,7 @@ export type ${serviceName} = _SERVICE
|
|
|
142
145
|
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
143
146
|
clientManager,
|
|
144
147
|
idlFactory,
|
|
145
|
-
${canisterIdLine} name:
|
|
148
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
146
149
|
})${hookExports || "\n"}`;
|
|
147
150
|
}
|
|
148
151
|
function generateReactorEntryFile() {
|
|
@@ -165,6 +168,162 @@ export * from "./index.generated"
|
|
|
165
168
|
`;
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
// src/validate.ts
|
|
172
|
+
import fs2 from "fs";
|
|
173
|
+
import path3 from "path";
|
|
174
|
+
var CodegenConfigError = class extends Error {
|
|
175
|
+
name = "CodegenConfigError";
|
|
176
|
+
constructor(message) {
|
|
177
|
+
super(message);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var CANISTER_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
181
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
182
|
+
var MAX_CANISTER_NAME_LENGTH = 64;
|
|
183
|
+
var HAS_URI_SCHEME = /^(?:[A-Za-z][A-Za-z0-9+.-]*:|\/\/)/;
|
|
184
|
+
var BREAKS_OUT_OF_LITERAL = /["'`\\\u0000-\u001f\u2028\u2029]/;
|
|
185
|
+
function assertSafeCanisterName(name) {
|
|
186
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
187
|
+
throw new CodegenConfigError(
|
|
188
|
+
`Invalid canister name: expected a non-empty string, received ${name === void 0 ? "undefined" : JSON.stringify(name)}. Check the "canisters" entries in your ic-reactor.json (or the plugin's "canisters" option).`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (name.length > MAX_CANISTER_NAME_LENGTH) {
|
|
192
|
+
throw new CodegenConfigError(
|
|
193
|
+
`Invalid canister name ${JSON.stringify(name)}: must be at most ${MAX_CANISTER_NAME_LENGTH} characters.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!CANISTER_NAME_PATTERN.test(name)) {
|
|
197
|
+
throw new CodegenConfigError(
|
|
198
|
+
`Invalid canister name ${JSON.stringify(name)}: may contain only letters, digits, "_", "." and "-" (${CANISTER_NAME_PATTERN.source}). Canister names become directory names, so path separators, quotes and whitespace are not allowed.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (name === "." || name === "..") {
|
|
202
|
+
throw new CodegenConfigError(
|
|
203
|
+
`Invalid canister name ${JSON.stringify(name)}: refers to a directory, not a canister.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (toPascalCase(name).length === 0) {
|
|
207
|
+
throw new CodegenConfigError(
|
|
208
|
+
`Invalid canister name ${JSON.stringify(name)}: contains no letters or digits, so it collapses to an empty identifier in generated code.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const derived = [
|
|
212
|
+
["reactor constant", getReactorName(name)],
|
|
213
|
+
["service type", getServiceTypeName(name)],
|
|
214
|
+
["hook name", `use${getHookPrefix(name)}Query`]
|
|
215
|
+
];
|
|
216
|
+
for (const [what, identifier] of derived) {
|
|
217
|
+
if (!IDENTIFIER_PATTERN.test(identifier)) {
|
|
218
|
+
throw new CodegenConfigError(
|
|
219
|
+
`Invalid canister name ${JSON.stringify(name)}: it derives the ${what} ${JSON.stringify(identifier)}, which is not a valid TypeScript identifier. Rename the canister so it does not begin with a digit.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function assertSafeModuleSpecifier(label, specifier) {
|
|
225
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
226
|
+
throw new CodegenConfigError(
|
|
227
|
+
`Invalid ${label}: expected a non-empty string, received ${specifier === void 0 ? "undefined" : JSON.stringify(specifier)}.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (BREAKS_OUT_OF_LITERAL.test(specifier)) {
|
|
231
|
+
throw new CodegenConfigError(
|
|
232
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not contain quotes, backslashes or control characters.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (/^\s|\s$/.test(specifier)) {
|
|
236
|
+
throw new CodegenConfigError(
|
|
237
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must not begin or end with whitespace.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (HAS_URI_SCHEME.test(specifier.trim())) {
|
|
241
|
+
throw new CodegenConfigError(
|
|
242
|
+
`Invalid ${label} ${JSON.stringify(specifier)}: must be a relative path ("./\u2026", "../\u2026") or a bare package name. URLs are not allowed \u2014 the generated file imports from this specifier.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
var REACTOR_CLASS_NAMES = [
|
|
247
|
+
"Reactor",
|
|
248
|
+
"DisplayReactor",
|
|
249
|
+
"CandidReactor",
|
|
250
|
+
"CandidDisplayReactor",
|
|
251
|
+
"MetadataDisplayReactor"
|
|
252
|
+
];
|
|
253
|
+
var CODEGEN_TARGETS = ["react", "core"];
|
|
254
|
+
function assertOneOf(label, value, allowed) {
|
|
255
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
256
|
+
throw new CodegenConfigError(
|
|
257
|
+
`Invalid ${label} ${JSON.stringify(value)}: must be one of ${allowed.map((a) => JSON.stringify(a)).join(", ")}.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function realpathAllowingMissing(target) {
|
|
262
|
+
let current = path3.resolve(target);
|
|
263
|
+
const missing = [];
|
|
264
|
+
for (; ; ) {
|
|
265
|
+
try {
|
|
266
|
+
return path3.join(fs2.realpathSync(current), ...missing);
|
|
267
|
+
} catch {
|
|
268
|
+
const parent = path3.dirname(current);
|
|
269
|
+
if (parent === current) return path3.resolve(target);
|
|
270
|
+
missing.unshift(path3.basename(current));
|
|
271
|
+
current = parent;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function assertContainedPath(label, resolved, projectRoot, original = resolved) {
|
|
276
|
+
const relative = path3.relative(
|
|
277
|
+
realpathAllowingMissing(projectRoot),
|
|
278
|
+
realpathAllowingMissing(resolved)
|
|
279
|
+
);
|
|
280
|
+
const [firstSegment] = relative.split(/[\\/]/);
|
|
281
|
+
if (firstSegment === ".." || path3.isAbsolute(relative)) {
|
|
282
|
+
throw new CodegenConfigError(
|
|
283
|
+
`Invalid ${label} ${JSON.stringify(original)}: resolves to ${JSON.stringify(resolved)}, which is outside the project root ${JSON.stringify(path3.resolve(projectRoot))}. Generated output must stay inside the project \u2014 generated directories are deleted and rewritten on every run.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function resolveContainedOutDir(label, outDir, projectRoot) {
|
|
288
|
+
if (typeof outDir !== "string" || outDir.length === 0) {
|
|
289
|
+
throw new CodegenConfigError(
|
|
290
|
+
`Invalid ${label}: expected a non-empty string, received ${outDir === void 0 ? "undefined" : JSON.stringify(outDir)}.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const resolved = path3.isAbsolute(outDir) ? path3.resolve(outDir) : path3.resolve(projectRoot, outDir);
|
|
294
|
+
assertContainedPath(label, resolved, projectRoot, outDir);
|
|
295
|
+
return resolved;
|
|
296
|
+
}
|
|
297
|
+
function assertSafeCanisterConfig(options) {
|
|
298
|
+
const {
|
|
299
|
+
name,
|
|
300
|
+
canisterOutDir,
|
|
301
|
+
globalOutDir,
|
|
302
|
+
clientManagerPath,
|
|
303
|
+
projectRoot,
|
|
304
|
+
mode,
|
|
305
|
+
target
|
|
306
|
+
} = options;
|
|
307
|
+
assertSafeCanisterName(name);
|
|
308
|
+
assertSafeModuleSpecifier("clientManagerPath", clientManagerPath);
|
|
309
|
+
if (mode != null) assertOneOf("mode", mode, REACTOR_CLASS_NAMES);
|
|
310
|
+
if (target != null) assertOneOf("target", target, CODEGEN_TARGETS);
|
|
311
|
+
const outDir = canisterOutDir != null ? resolveContainedOutDir(
|
|
312
|
+
`outDir for canister ${JSON.stringify(name)}`,
|
|
313
|
+
canisterOutDir,
|
|
314
|
+
projectRoot
|
|
315
|
+
) : path3.join(
|
|
316
|
+
resolveContainedOutDir("outDir", globalOutDir, projectRoot),
|
|
317
|
+
name
|
|
318
|
+
);
|
|
319
|
+
assertContainedPath(
|
|
320
|
+
`output directory for canister ${JSON.stringify(name)}`,
|
|
321
|
+
outDir,
|
|
322
|
+
projectRoot
|
|
323
|
+
);
|
|
324
|
+
return { name, outDir, clientManagerPath };
|
|
325
|
+
}
|
|
326
|
+
|
|
168
327
|
// src/pipeline.ts
|
|
169
328
|
function resolveReactorClass(canisterConfig) {
|
|
170
329
|
return canisterConfig.mode ?? "DisplayReactor";
|
|
@@ -190,8 +349,30 @@ async function runCanisterPipeline(options) {
|
|
|
190
349
|
} = options;
|
|
191
350
|
const { name, didFile, clientManagerPath } = canisterConfig;
|
|
192
351
|
const files = [];
|
|
193
|
-
|
|
194
|
-
|
|
352
|
+
let validated;
|
|
353
|
+
try {
|
|
354
|
+
validated = assertSafeCanisterConfig({
|
|
355
|
+
name,
|
|
356
|
+
canisterOutDir: canisterConfig.outDir,
|
|
357
|
+
globalOutDir: globalConfig.outDir,
|
|
358
|
+
clientManagerPath: clientManagerPath ?? globalConfig.clientManagerPath ?? "../../clients",
|
|
359
|
+
projectRoot,
|
|
360
|
+
mode: canisterConfig.mode,
|
|
361
|
+
target: canisterConfig.target ?? globalConfig.target
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err instanceof CodegenConfigError) {
|
|
365
|
+
return {
|
|
366
|
+
canisterName: typeof name === "string" ? name : String(name),
|
|
367
|
+
success: false,
|
|
368
|
+
files,
|
|
369
|
+
error: err.message
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
throw err;
|
|
373
|
+
}
|
|
374
|
+
const resolvedDidFile = path4.isAbsolute(didFile) ? didFile : path4.resolve(projectRoot, didFile);
|
|
375
|
+
if (!fs3.existsSync(resolvedDidFile)) {
|
|
195
376
|
return {
|
|
196
377
|
canisterName: name,
|
|
197
378
|
success: false,
|
|
@@ -199,8 +380,8 @@ async function runCanisterPipeline(options) {
|
|
|
199
380
|
error: `DID file not found: ${resolvedDidFile}`
|
|
200
381
|
};
|
|
201
382
|
}
|
|
202
|
-
const canisterOutDir =
|
|
203
|
-
const resolvedClientManagerPath =
|
|
383
|
+
const canisterOutDir = validated.outDir;
|
|
384
|
+
const resolvedClientManagerPath = validated.clientManagerPath;
|
|
204
385
|
try {
|
|
205
386
|
const declResult = await generateDeclarations({
|
|
206
387
|
didFile: resolvedDidFile,
|
|
@@ -231,8 +412,8 @@ async function runCanisterPipeline(options) {
|
|
|
231
412
|
files
|
|
232
413
|
};
|
|
233
414
|
}
|
|
234
|
-
const reactorPath =
|
|
235
|
-
const entryPath =
|
|
415
|
+
const reactorPath = path4.join(canisterOutDir, "index.generated.ts");
|
|
416
|
+
const entryPath = path4.join(canisterOutDir, "index.ts");
|
|
236
417
|
const reactorClass = resolveReactorClass(canisterConfig);
|
|
237
418
|
const runtimeTarget = resolveRuntimeTarget(canisterConfig, globalConfig);
|
|
238
419
|
try {
|
|
@@ -245,16 +426,16 @@ async function runCanisterPipeline(options) {
|
|
|
245
426
|
reactorClass
|
|
246
427
|
});
|
|
247
428
|
const entryContent = generateReactorEntryFile();
|
|
248
|
-
|
|
249
|
-
|
|
429
|
+
fs3.mkdirSync(canisterOutDir, { recursive: true });
|
|
430
|
+
fs3.writeFileSync(reactorPath, reactorContent);
|
|
250
431
|
files.push({ success: true, filePath: reactorPath });
|
|
251
|
-
if (!
|
|
252
|
-
|
|
432
|
+
if (!fs3.existsSync(entryPath)) {
|
|
433
|
+
fs3.writeFileSync(entryPath, entryContent);
|
|
253
434
|
files.push({ success: true, filePath: entryPath });
|
|
254
435
|
} else {
|
|
255
|
-
const existingEntryContent =
|
|
436
|
+
const existingEntryContent = fs3.readFileSync(entryPath, "utf-8");
|
|
256
437
|
if (isLegacyGeneratedIndexFile(existingEntryContent) || isManagedEntryWrapper(existingEntryContent, entryContent)) {
|
|
257
|
-
|
|
438
|
+
fs3.writeFileSync(entryPath, entryContent);
|
|
258
439
|
files.push({ success: true, filePath: entryPath });
|
|
259
440
|
} else {
|
|
260
441
|
files.push({ success: true, filePath: entryPath, skipped: true });
|
|
@@ -282,7 +463,7 @@ async function runCanisterPipeline(options) {
|
|
|
282
463
|
|
|
283
464
|
// src/parser.ts
|
|
284
465
|
import { didToJs as didToJs2 } from "@ic-reactor/parser";
|
|
285
|
-
import
|
|
466
|
+
import fs4 from "fs";
|
|
286
467
|
function extractMethods(didContent) {
|
|
287
468
|
try {
|
|
288
469
|
const jsContent = didToJs2(didContent);
|
|
@@ -307,10 +488,10 @@ function extractMethods(didContent) {
|
|
|
307
488
|
}
|
|
308
489
|
}
|
|
309
490
|
function parseDIDFile(didFilePath) {
|
|
310
|
-
if (!
|
|
491
|
+
if (!fs4.existsSync(didFilePath)) {
|
|
311
492
|
throw new Error(`DID file not found: ${didFilePath}`);
|
|
312
493
|
}
|
|
313
|
-
const content =
|
|
494
|
+
const content = fs4.readFileSync(didFilePath, "utf-8");
|
|
314
495
|
return extractMethods(content);
|
|
315
496
|
}
|
|
316
497
|
|
|
@@ -335,16 +516,25 @@ export const clientManager = new ClientManager({
|
|
|
335
516
|
`;
|
|
336
517
|
}
|
|
337
518
|
export {
|
|
519
|
+
CANISTER_NAME_PATTERN,
|
|
520
|
+
CODEGEN_TARGETS,
|
|
521
|
+
CodegenConfigError,
|
|
522
|
+
REACTOR_CLASS_NAMES,
|
|
523
|
+
assertContainedPath,
|
|
524
|
+
assertOneOf,
|
|
525
|
+
assertSafeCanisterConfig,
|
|
526
|
+
assertSafeCanisterName,
|
|
527
|
+
assertSafeModuleSpecifier,
|
|
338
528
|
declarationsExist,
|
|
339
529
|
extractMethods,
|
|
340
530
|
generateClientFile,
|
|
341
|
-
generateCodecDeclarations,
|
|
342
531
|
generateDeclarations,
|
|
343
532
|
generateReactorEntryFile,
|
|
344
533
|
generateReactorFile,
|
|
345
534
|
getReactorName,
|
|
346
535
|
getServiceTypeName,
|
|
347
536
|
parseDIDFile,
|
|
537
|
+
resolveContainedOutDir,
|
|
348
538
|
runCanisterPipeline,
|
|
349
539
|
toPascalCase
|
|
350
540
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/codegen",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Shared code generation utilities for IC Reactor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -17,16 +17,6 @@
|
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
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
20
|
"./package.json": "./package.json"
|
|
31
21
|
},
|
|
32
22
|
"files": [
|
|
@@ -62,8 +52,8 @@
|
|
|
62
52
|
"vitest": "^4.1.10"
|
|
63
53
|
},
|
|
64
54
|
"scripts": {
|
|
65
|
-
"build": "tsup src/index.ts
|
|
66
|
-
"dev": "tsup src/index.ts
|
|
55
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
|
|
56
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
|
|
67
57
|
"test": "vitest run",
|
|
68
58
|
"test:watch": "vitest",
|
|
69
59
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
@@ -60,6 +60,14 @@ function getReactorClassImportSource(
|
|
|
60
60
|
case "CandidDisplayReactor":
|
|
61
61
|
case "MetadataDisplayReactor":
|
|
62
62
|
return "@ic-reactor/candid"
|
|
63
|
+
default:
|
|
64
|
+
// The pipeline validates `mode` against a closed set before we are
|
|
65
|
+
// reached. Failing closed here means a caller that skips validation gets
|
|
66
|
+
// an error rather than an unknown class name interpolated into source.
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unknown reactor class ${JSON.stringify(reactorClass)}. Expected one of: ` +
|
|
69
|
+
`Reactor, DisplayReactor, CandidReactor, CandidDisplayReactor, MetadataDisplayReactor.`
|
|
70
|
+
)
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -105,9 +113,12 @@ export const {
|
|
|
105
113
|
`
|
|
106
114
|
: ""
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
// Every interpolation into emitted source is JSON.stringify'd. The pipeline
|
|
117
|
+
// validates these values before we are called; quoting them here as well
|
|
118
|
+
// means a future caller that skips validation cannot inject source text.
|
|
119
|
+
return `${runtimeTarget === "react" ? 'import { createActorHooks } from "@ic-reactor/react"\n' : ""}import { ${reactorClass} } from ${JSON.stringify(reactorImportSource)}
|
|
120
|
+
import { clientManager } from ${JSON.stringify(clientManagerPath)}
|
|
121
|
+
import { idlFactory, type _SERVICE } from ${JSON.stringify(declarationsPath)}
|
|
111
122
|
|
|
112
123
|
export type ${serviceName} = _SERVICE
|
|
113
124
|
|
|
@@ -123,7 +134,7 @@ export type ${serviceName} = _SERVICE
|
|
|
123
134
|
export const ${reactorName} = new ${reactorClass}<${serviceName}>({
|
|
124
135
|
clientManager,
|
|
125
136
|
idlFactory,
|
|
126
|
-
${canisterIdLine} name:
|
|
137
|
+
${canisterIdLine} name: ${JSON.stringify(canisterName)},
|
|
127
138
|
})${hookExports || "\n"}`
|
|
128
139
|
}
|
|
129
140
|
|