@confect/cli 7.0.0 → 9.0.0-next.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/src/log.ts CHANGED
@@ -5,6 +5,27 @@ import type * as FunctionPath from "./FunctionPath";
5
5
  import * as GroupPath from "./GroupPath";
6
6
  import { ProjectRoot } from "./ProjectRoot";
7
7
 
8
+ // --- Path styling ---
9
+
10
+ /**
11
+ * Render a relative path as an AnsiDoc with the directory portion
12
+ * dimmed (`Ansi.blackBright`) and the file leaf rendered in the
13
+ * default terminal color. Used inline anywhere a file path appears
14
+ * in a CLI message.
15
+ */
16
+ export const formatPathDoc = (relativePath: string): AnsiDoc.AnsiDoc => {
17
+ const lastSep = Math.max(
18
+ relativePath.lastIndexOf("/"),
19
+ relativePath.lastIndexOf("\\"),
20
+ );
21
+ const dir = lastSep < 0 ? "" : relativePath.slice(0, lastSep + 1);
22
+ const leaf = lastSep < 0 ? relativePath : relativePath.slice(lastSep + 1);
23
+ return AnsiDoc.hcat([
24
+ pipe(AnsiDoc.text(dir), AnsiDoc.annotate(Ansi.blackBright)),
25
+ AnsiDoc.text(leaf),
26
+ ]);
27
+ };
28
+
8
29
  // --- File operation logs ---
9
30
 
10
31
  const logFile = (char: string, color: Ansi.Ansi) => (fullPath: string) =>
package/src/templates.ts CHANGED
@@ -1,42 +1,36 @@
1
- import type { Options as CodeBlockWriterOptions } from "code-block-writer";
2
- import CodeBlockWriter_ from "code-block-writer";
3
- import { Array, Effect } from "effect";
4
- import type * as GroupPath from "./GroupPath";
1
+ import { Array, Effect, Option } from "effect";
2
+ import { CodeBlockWriter } from "./CodeBlockWriter";
3
+ import {
4
+ collectImportBindings,
5
+ type SpecAssemblyNode,
6
+ } from "./SpecAssemblyNode";
5
7
 
6
8
  export const functions = ({
7
- groupPath,
8
9
  functionNames,
9
10
  registeredFunctionsImportPath,
10
- registeredFunctionsVariableName = "registeredFunctions",
11
- registeredFunctionsLookupPath,
12
11
  useNode = false,
13
12
  }: {
14
- groupPath: GroupPath.GroupPath;
15
13
  functionNames: string[];
16
14
  registeredFunctionsImportPath: string;
17
- registeredFunctionsVariableName?: string;
18
- registeredFunctionsLookupPath?: readonly string[];
19
15
  useNode?: boolean;
20
16
  }) =>
21
17
  Effect.gen(function* () {
22
18
  const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
23
19
 
24
- const lookupPath = registeredFunctionsLookupPath ?? groupPath.pathSegments;
25
-
26
20
  if (useNode) {
27
21
  yield* cbw.writeLine(`"use node";`);
28
22
  yield* cbw.blankLine();
29
23
  }
30
24
 
31
25
  yield* cbw.writeLine(
32
- `import ${registeredFunctionsVariableName} from "${registeredFunctionsImportPath}";`,
26
+ `import registeredFunctions from "${registeredFunctionsImportPath}";`,
33
27
  );
34
28
  yield* cbw.newLine();
35
- for (const functionName of functionNames) {
36
- yield* cbw.writeLine(
37
- `export const ${functionName} = ${registeredFunctionsVariableName}.${Array.join([...lookupPath, functionName], ".")};`,
38
- );
39
- }
29
+ yield* Effect.forEach(functionNames, (functionName) =>
30
+ cbw.writeLine(
31
+ `export const ${functionName} = registeredFunctions.${functionName};`,
32
+ ),
33
+ );
40
34
 
41
35
  return yield* cbw.toString();
42
36
  });
@@ -92,21 +86,24 @@ export const refs = ({
92
86
  nodeSpecImportPath,
93
87
  }: {
94
88
  specImportPath: string;
95
- nodeSpecImportPath?: string;
89
+ nodeSpecImportPath: Option.Option<string>;
96
90
  }) =>
97
91
  Effect.gen(function* () {
98
92
  const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
99
93
 
100
94
  yield* cbw.writeLine(`import { Refs } from "@confect/core";`);
101
95
  yield* cbw.writeLine(`import spec from "${specImportPath}";`);
102
- if (nodeSpecImportPath !== undefined) {
103
- yield* cbw.writeLine(`import nodeSpec from "${nodeSpecImportPath}";`);
104
- }
96
+ yield* Option.match(nodeSpecImportPath, {
97
+ onNone: () => Effect.void,
98
+ onSome: (nodeSpecImportPath_) =>
99
+ cbw.writeLine(`import nodeSpec from "${nodeSpecImportPath_}";`),
100
+ });
105
101
  yield* cbw.blankLine();
106
102
  yield* cbw.writeLine(
107
- nodeSpecImportPath !== undefined
108
- ? `export default Refs.make(spec, nodeSpec);`
109
- : `export default Refs.make(spec);`,
103
+ Option.match(nodeSpecImportPath, {
104
+ onSome: () => `export default Refs.make(spec, nodeSpec);`,
105
+ onNone: () => `export default Refs.make(spec);`,
106
+ }),
110
107
  );
111
108
 
112
109
  return yield* cbw.toString();
@@ -151,45 +148,43 @@ export const nodeApi = ({
151
148
  return yield* cbw.toString();
152
149
  });
153
150
 
154
- export const registeredFunctions = ({
151
+ export const registeredFunctionsForGroup = ({
152
+ apiImportPath,
153
+ groupPathDot,
155
154
  implImportPath,
155
+ layerExportName,
156
+ useNode = false,
156
157
  }: {
158
+ apiImportPath: string;
159
+ groupPathDot: string;
157
160
  implImportPath: string;
161
+ layerExportName: string;
162
+ useNode?: boolean;
158
163
  }) =>
159
164
  Effect.gen(function* () {
160
165
  const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
161
166
 
162
- yield* cbw.writeLine(
163
- `import { RegisteredConvexFunction, RegisteredFunctions } from "@confect/server";`,
164
- );
165
- yield* cbw.writeLine(`import impl from "${implImportPath}";`);
166
- yield* cbw.blankLine();
167
- yield* cbw.writeLine(
168
- `export default RegisteredFunctions.make(impl, RegisteredConvexFunction.make);`,
169
- );
170
-
171
- return yield* cbw.toString();
172
- });
173
-
174
- export const nodeRegisteredFunctions = ({
175
- nodeImplImportPath,
176
- }: {
177
- nodeImplImportPath: string;
178
- }) =>
179
- Effect.gen(function* () {
180
- const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
167
+ if (useNode) {
168
+ yield* cbw.writeLine(
169
+ `import { RegisteredFunctions } from "@confect/server";`,
170
+ );
171
+ yield* cbw.writeLine(
172
+ `import { RegisteredNodeFunction } from "@confect/server/node";`,
173
+ );
174
+ } else {
175
+ yield* cbw.writeLine(
176
+ `import { RegisteredConvexFunction, RegisteredFunctions } from "@confect/server";`,
177
+ );
178
+ }
181
179
 
182
- yield* cbw.writeLine(
183
- `import { RegisteredFunctions } from "@confect/server";`,
184
- );
185
- yield* cbw.writeLine(
186
- `import { RegisteredNodeFunction } from "@confect/server/node";`,
187
- );
188
- yield* cbw.blankLine();
189
- yield* cbw.writeLine(`import nodeImpl from "${nodeImplImportPath}";`);
180
+ yield* cbw.writeLine(`import api from "${apiImportPath}";`);
181
+ yield* cbw.writeLine(`import ${layerExportName} from "${implImportPath}";`);
190
182
  yield* cbw.blankLine();
183
+ const quotedGroupPath = `"${groupPathDot.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
191
184
  yield* cbw.writeLine(
192
- `export default RegisteredFunctions.make(nodeImpl, RegisteredNodeFunction.make);`,
185
+ useNode
186
+ ? `export default RegisteredFunctions.buildForGroup(api, ${quotedGroupPath}, ${layerExportName}, RegisteredNodeFunction.make);`
187
+ : `export default RegisteredFunctions.buildForGroup(api, ${quotedGroupPath}, ${layerExportName}, RegisteredConvexFunction.make);`,
193
188
  );
194
189
 
195
190
  return yield* cbw.toString();
@@ -382,64 +377,101 @@ export const services = ({ schemaImportPath }: { schemaImportPath: string }) =>
382
377
  return yield* cbw.toString();
383
378
  });
384
379
 
385
- class CodeBlockWriter {
386
- private readonly writer: CodeBlockWriter_;
387
-
388
- constructor(opts?: Partial<CodeBlockWriterOptions>) {
389
- this.writer = new CodeBlockWriter_(opts);
390
- }
391
-
392
- indent<E = never, R = never>(
393
- eff: Effect.Effect<void, E, R>,
394
- ): Effect.Effect<void, E, R> {
395
- return Effect.gen(this, function* () {
396
- const indentationLevel = this.writer.getIndentationLevel();
397
- this.writer.setIndentationLevel(indentationLevel + 1);
398
- yield* eff;
399
- this.writer.setIndentationLevel(indentationLevel);
400
- });
401
- }
380
+ const writeChildAddGroupAt = (
381
+ cbw: CodeBlockWriter,
382
+ child: SpecAssemblyNode,
383
+ groupFactory: string,
384
+ ): Effect.Effect<void> =>
385
+ Effect.gen(function* () {
386
+ yield* cbw.write(".addGroupAt(");
387
+ yield* cbw.quote(child.segment);
388
+ yield* cbw.write(", ");
389
+ yield* writeGroupAssembly(cbw, child, groupFactory);
390
+ yield* cbw.write(")");
391
+ });
402
392
 
403
- writeLine<E = never, R = never>(line: string): Effect.Effect<void, E, R> {
404
- return Effect.sync(() => {
405
- this.writer.writeLine(line);
406
- });
407
- }
393
+ const writeGroupFactoryCall = (
394
+ cbw: CodeBlockWriter,
395
+ node: SpecAssemblyNode,
396
+ groupFactory: string,
397
+ ): Effect.Effect<void> =>
398
+ Effect.gen(function* () {
399
+ yield* cbw.write(groupFactory);
400
+ yield* cbw.write("(");
401
+ yield* cbw.quote(node.segment);
402
+ yield* cbw.write(")");
408
403
 
409
- write<E = never, R = never>(text: string): Effect.Effect<void, E, R> {
410
- return Effect.sync(() => {
411
- this.writer.write(text);
412
- });
413
- }
404
+ yield* Effect.forEach(node.children, (child) =>
405
+ writeChildAddGroupAt(cbw, child, groupFactory),
406
+ );
407
+ });
414
408
 
415
- quote<E = never, R = never>(text: string): Effect.Effect<void, E, R> {
416
- return Effect.sync(() => {
417
- this.writer.quote(text);
418
- });
419
- }
420
-
421
- conditionalWriteLine<E = never, R = never>(
422
- condition: boolean,
423
- text: string,
424
- ): Effect.Effect<void, E, R> {
425
- return Effect.sync(() => {
426
- this.writer.conditionalWriteLine(condition, text);
427
- });
428
- }
409
+ const writeGroupAssembly: (
410
+ cbw: CodeBlockWriter,
411
+ node: SpecAssemblyNode,
412
+ groupFactory: string,
413
+ ) => Effect.Effect<void> = (cbw, node, groupFactory) =>
414
+ node.children.length === 0
415
+ ? Option.match(node.importBinding, {
416
+ onNone: () => writeGroupFactoryCall(cbw, node, groupFactory),
417
+ onSome: (binding) => cbw.write(binding.exportName),
418
+ })
419
+ : writeGroupFactoryCall(cbw, node, groupFactory);
420
+
421
+ const writeRootAddAt = (
422
+ cbw: CodeBlockWriter,
423
+ node: SpecAssemblyNode,
424
+ groupFactory: string,
425
+ ): Effect.Effect<void> =>
426
+ Effect.gen(function* () {
427
+ yield* cbw.write(".addAt(");
428
+ yield* cbw.quote(node.segment);
429
+ yield* cbw.write(", ");
429
430
 
430
- newLine<E = never, R = never>(): Effect.Effect<void, E, R> {
431
- return Effect.sync(() => {
432
- this.writer.newLine();
433
- });
434
- }
431
+ yield* writeGroupAssembly(cbw, node, groupFactory);
435
432
 
436
- blankLine<E = never, R = never>(): Effect.Effect<void, E, R> {
437
- return Effect.sync(() => {
438
- this.writer.blankLine();
439
- });
440
- }
433
+ yield* cbw.write(")");
434
+ });
435
+
436
+ export const assembledSpec = ({
437
+ nodes,
438
+ runtime,
439
+ }: {
440
+ nodes: ReadonlyArray<SpecAssemblyNode>;
441
+ runtime: "Convex" | "Node";
442
+ }) =>
443
+ Effect.gen(function* () {
444
+ const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
445
+
446
+ const needsGroupSpec = Array.some(
447
+ nodes,
448
+ (node) => node.children.length > 0,
449
+ );
450
+ yield* cbw.writeLine(
451
+ needsGroupSpec
452
+ ? `import { GroupSpec, Spec } from "@confect/core";`
453
+ : `import { Spec } from "@confect/core";`,
454
+ );
455
+
456
+ yield* Effect.forEach(collectImportBindings(nodes), (binding) =>
457
+ cbw.writeLine(
458
+ `import ${binding.exportName} from "${binding.importPath}";`,
459
+ ),
460
+ );
461
+
462
+ yield* cbw.blankLine();
441
463
 
442
- toString<E = never, R = never>(): Effect.Effect<string, E, R> {
443
- return Effect.sync(() => this.writer.toString());
444
- }
445
- }
464
+ const specFactory =
465
+ runtime === "Convex" ? "Spec.make()" : "Spec.makeNode()";
466
+ const groupFactory =
467
+ runtime === "Convex" ? "GroupSpec.makeAt" : "GroupSpec.makeNodeAt";
468
+
469
+ yield* cbw.write(`export default ${specFactory}`);
470
+ yield* Effect.forEach(nodes, (node) =>
471
+ writeRootAddAt(cbw, node, groupFactory),
472
+ );
473
+ yield* cbw.write(";");
474
+ yield* cbw.newLine();
475
+
476
+ return yield* cbw.toString();
477
+ });
package/src/utils.ts CHANGED
@@ -3,15 +3,16 @@ import { FileSystem, Path } from "@effect/platform";
3
3
  import type { PlatformError } from "@effect/platform/Error";
4
4
  import {
5
5
  Array,
6
+ Context,
6
7
  Effect,
7
8
  HashSet,
8
9
  Option,
9
10
  Order,
10
11
  pipe,
11
12
  Record,
13
+ Ref,
12
14
  String,
13
15
  } from "effect";
14
- import * as esbuild from "esbuild";
15
16
  import * as FunctionPaths from "./FunctionPaths";
16
17
  import * as GroupPath from "./GroupPath";
17
18
  import * as GroupPaths from "./GroupPaths";
@@ -20,6 +21,24 @@ import { ConfectDirectory } from "./ConfectDirectory";
20
21
  import { ConvexDirectory } from "./ConvexDirectory";
21
22
  import * as templates from "./templates";
22
23
 
24
+ /**
25
+ * Tracks whether the current codegen run wrote anything to disk. Set to
26
+ * `true` by every helper that actually overwrites a file (skipping the
27
+ * content-unchanged case). `codegenHandler` provides a fresh `Ref` per
28
+ * run and reads it back to report `anyWritesHappened`; callers outside a
29
+ * codegen pass hit the cached default `Ref` and need not interact with
30
+ * the tracker.
31
+ */
32
+ export class WriteTracker extends Context.Reference<WriteTracker>()(
33
+ "@confect/cli/WriteTracker",
34
+ { defaultValue: () => Ref.unsafeMake(false) },
35
+ ) {}
36
+
37
+ const markWritten = Effect.gen(function* () {
38
+ const tracker = yield* WriteTracker;
39
+ yield* Ref.set(tracker, true);
40
+ });
41
+
23
42
  export const removePathExtension = (pathStr: string) =>
24
43
  Effect.gen(function* () {
25
44
  const path = yield* Path.Path;
@@ -27,59 +46,11 @@ export const removePathExtension = (pathStr: string) =>
27
46
  return String.slice(0, -path.extname(pathStr).length)(pathStr);
28
47
  });
29
48
 
30
- export const EXTERNAL_PACKAGES = [
31
- "@confect/core",
32
- "@confect/server",
33
- "effect",
34
- "@effect/*",
35
- ];
36
-
37
- const isExternalImport = (path: string) =>
38
- EXTERNAL_PACKAGES.some((p) => {
39
- if (p.endsWith("/*")) {
40
- return path.startsWith(p.slice(0, -1));
41
- }
42
- return path === p || path.startsWith(p + "/");
43
- });
44
-
45
- const absoluteExternalsPlugin: esbuild.Plugin = {
46
- name: "absolute-externals",
47
- setup(build) {
48
- build.onResolve({ filter: /.*/ }, async (args) => {
49
- if (args.kind !== "import-statement" && args.kind !== "dynamic-import")
50
- return;
51
- if (!isExternalImport(args.path)) return;
52
- const resolved = import.meta.resolve(
53
- args.path,
54
- "file://" + args.resolveDir + "/",
55
- );
56
- return { path: resolved, external: true };
57
- });
58
- },
59
- };
60
-
61
- /**
62
- * Bundle a TypeScript entry point with esbuild and import the result via a
63
- * data URL. This handles extensionless `.ts` imports regardless of whether the
64
- * user's project sets `"type": "module"` in package.json.
65
- */
66
- export const bundleAndImport = (entryPoint: string) =>
49
+ /** Ensures a relative path is a valid ESM/TS module specifier (e.g. `spec` → `./spec`). */
50
+ export const toModuleImportPath = (relativePath: string) =>
67
51
  Effect.gen(function* () {
68
- const result = yield* Effect.promise(() =>
69
- esbuild.build({
70
- entryPoints: [entryPoint],
71
- bundle: true,
72
- write: false,
73
- platform: "node",
74
- format: "esm",
75
- logLevel: "silent",
76
- plugins: [absoluteExternalsPlugin],
77
- }),
78
- );
79
- const code = result.outputFiles[0]!.text;
80
- const dataUrl =
81
- "data:text/javascript;base64," + Buffer.from(code).toString("base64");
82
- return yield* Effect.promise(() => import(dataUrl));
52
+ const withoutExt = yield* removePathExtension(relativePath);
53
+ return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
83
54
  });
84
55
 
85
56
  export const writeFileStringAndLog = (filePath: string, contents: string) =>
@@ -87,12 +58,14 @@ export const writeFileStringAndLog = (filePath: string, contents: string) =>
87
58
  const fs = yield* FileSystem.FileSystem;
88
59
  if (!(yield* fs.exists(filePath))) {
89
60
  yield* fs.writeFileString(filePath, contents);
61
+ yield* markWritten;
90
62
  yield* logFileAdded(filePath);
91
63
  return;
92
64
  }
93
65
  const existing = yield* fs.readFileString(filePath);
94
66
  if (existing !== contents) {
95
67
  yield* fs.writeFileString(filePath, contents);
68
+ yield* markWritten;
96
69
  yield* logFileModified(filePath);
97
70
  }
98
71
  });
@@ -128,28 +101,73 @@ export const writeFileString = (
128
101
 
129
102
  if (!(yield* fs.exists(filePath))) {
130
103
  yield* fs.writeFileString(filePath, contents);
104
+ yield* markWritten;
131
105
  return "Added";
132
106
  }
133
107
  const existing = yield* fs.readFileString(filePath);
134
108
  if (existing !== contents) {
135
109
  yield* fs.writeFileString(filePath, contents);
110
+ yield* markWritten;
136
111
  return "Modified";
137
112
  }
138
113
  return "Unchanged";
139
114
  });
140
115
 
116
+ export const removePathIfExists = (
117
+ filePath: string,
118
+ ): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
119
+ Effect.gen(function* () {
120
+ const fs = yield* FileSystem.FileSystem;
121
+
122
+ if (!(yield* fs.exists(filePath))) {
123
+ return;
124
+ }
125
+
126
+ yield* fs
127
+ .remove(filePath)
128
+ .pipe(
129
+ Effect.catchTag("SystemError", (error) =>
130
+ error.reason === "NotFound" ? Effect.void : Effect.fail(error),
131
+ ),
132
+ );
133
+ });
134
+
135
+ /**
136
+ * Bump the mtime of `convex/schema.ts` so the Convex CLI's chokidar watcher
137
+ * emits a `change` event after every successful Confect codegen run. Without
138
+ * this, a codegen that doesn't change any file content (for example,
139
+ * recovering from a transient broken state in `confect/`) leaves Convex
140
+ * stuck on its previous error because nothing it observes has changed.
141
+ */
142
+ export const touchConvexSchema = Effect.gen(function* () {
143
+ const fs = yield* FileSystem.FileSystem;
144
+ const path = yield* Path.Path;
145
+ const convexDirectory = yield* ConvexDirectory.get;
146
+ const schemaPath = path.join(convexDirectory, "schema.ts");
147
+
148
+ if (!(yield* fs.exists(schemaPath))) {
149
+ return;
150
+ }
151
+
152
+ const now = new Date();
153
+ yield* fs.utimes(schemaPath, now, now);
154
+ });
155
+
141
156
  export const generateGroupModule = ({
142
157
  groupPath,
143
158
  functionNames,
159
+ registeredFunctionsImportPath,
160
+ useNode = false,
144
161
  }: {
145
162
  groupPath: GroupPath.GroupPath;
146
163
  functionNames: string[];
164
+ registeredFunctionsImportPath: string;
165
+ useNode?: boolean;
147
166
  }) =>
148
167
  Effect.gen(function* () {
149
168
  const fs = yield* FileSystem.FileSystem;
150
169
  const path = yield* Path.Path;
151
170
  const convexDirectory = yield* ConvexDirectory.get;
152
- const confectDirectory = yield* ConfectDirectory.get;
153
171
 
154
172
  const relativeModulePath = yield* GroupPath.modulePath(groupPath);
155
173
  const modulePath = path.join(convexDirectory, relativeModulePath);
@@ -159,42 +177,21 @@ export const generateGroupModule = ({
159
177
  yield* fs.makeDirectory(directoryPath, { recursive: true });
160
178
  }
161
179
 
162
- const isNodeGroup = groupPath.pathSegments[0] === "node";
163
- const registeredFunctionsFileName = isNodeGroup
164
- ? "nodeRegisteredFunctions.ts"
165
- : "registeredFunctions.ts";
166
- const registeredFunctionsPath = path.join(
167
- confectDirectory,
168
- "_generated",
169
- registeredFunctionsFileName,
170
- );
171
- const registeredFunctionsImportPath = yield* removePathExtension(
172
- path.relative(path.dirname(modulePath), registeredFunctionsPath),
173
- );
174
- const registeredFunctionsVariableName = isNodeGroup
175
- ? "nodeRegisteredFunctions"
176
- : "registeredFunctions";
177
-
178
180
  const functionsContentsString = yield* templates.functions({
179
- groupPath,
180
181
  functionNames,
181
182
  registeredFunctionsImportPath,
182
- registeredFunctionsVariableName,
183
- useNode: isNodeGroup,
184
- ...(isNodeGroup
185
- ? {
186
- registeredFunctionsLookupPath: groupPath.pathSegments.slice(1),
187
- }
188
- : {}),
183
+ useNode,
189
184
  });
190
185
 
191
186
  if (!(yield* fs.exists(modulePath))) {
192
187
  yield* fs.writeFileString(modulePath, functionsContentsString);
188
+ yield* markWritten;
193
189
  return "Added" as const;
194
190
  }
195
191
  const existing = yield* fs.readFileString(modulePath);
196
192
  if (existing !== functionsContentsString) {
197
193
  yield* fs.writeFileString(modulePath, functionsContentsString);
194
+ yield* markWritten;
198
195
  return "Modified" as const;
199
196
  }
200
197
  return "Unchanged" as const;
@@ -220,6 +217,7 @@ export const generateFunctions = (spec: Spec.AnyWithProps) =>
220
217
  Effect.gen(function* () {
221
218
  const path = yield* Path.Path;
222
219
  const convexDirectory = yield* ConvexDirectory.get;
220
+ const confectDirectory = yield* ConfectDirectory.get;
223
221
 
224
222
  const groupPathsFromFs = yield* getGroupPathsFromFs;
225
223
  const functionPaths = FunctionPaths.make(spec);
@@ -242,12 +240,30 @@ export const generateFunctions = (spec: Spec.AnyWithProps) =>
242
240
  ),
243
241
  Array.map((fn) => fn.name),
244
242
  );
245
- const result = yield* generateGroupModule({ groupPath, functionNames });
243
+ const relativeModulePath = yield* GroupPath.modulePath(groupPath);
244
+ const modulePath = path.join(convexDirectory, relativeModulePath);
245
+ const registrySegments =
246
+ groupPath.pathSegments[0] === "node"
247
+ ? groupPath.pathSegments.slice(1)
248
+ : groupPath.pathSegments;
249
+ const registeredFunctionsPath =
250
+ path.join(
251
+ confectDirectory,
252
+ "_generated",
253
+ "registeredFunctions",
254
+ ...registrySegments,
255
+ ) + ".ts";
256
+ const registeredFunctionsImportPath = yield* toModuleImportPath(
257
+ path.relative(path.dirname(modulePath), registeredFunctionsPath),
258
+ );
259
+ const result = yield* generateGroupModule({
260
+ groupPath,
261
+ functionNames,
262
+ registeredFunctionsImportPath,
263
+ useNode: groupPath.pathSegments[0] === "node",
264
+ });
246
265
  if (result === "Modified") {
247
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
248
- yield* logFileModified(
249
- path.join(convexDirectory, relativeModulePath),
250
- );
266
+ yield* logFileModified(modulePath);
251
267
  }
252
268
  }),
253
269
  );
@@ -300,7 +316,6 @@ const getGroupPathsFromFs = Effect.gen(function* () {
300
316
 
301
317
  export const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>
302
318
  Effect.gen(function* () {
303
- const fs = yield* FileSystem.FileSystem;
304
319
  const path = yield* Path.Path;
305
320
  const convexDirectory = yield* ConvexDirectory.get;
306
321
 
@@ -312,7 +327,7 @@ export const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>
312
327
 
313
328
  yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);
314
329
 
315
- yield* fs.remove(modulePath);
330
+ yield* removePathIfExists(modulePath);
316
331
  yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);
317
332
  }),
318
333
  ),
@@ -326,6 +341,9 @@ export const writeGroups = (
326
341
  ) =>
327
342
  Effect.forEach(groupPaths, (groupPath) =>
328
343
  Effect.gen(function* () {
344
+ const path = yield* Path.Path;
345
+ const convexDirectory = yield* ConvexDirectory.get;
346
+ const confectDirectory = yield* ConfectDirectory.get;
329
347
  const group = yield* GroupPath.getGroupSpec(spec, groupPath);
330
348
 
331
349
  const functionNames = pipe(
@@ -340,10 +358,25 @@ export const writeGroups = (
340
358
  Array.map((fn) => fn.name),
341
359
  );
342
360
 
361
+ const relativeModulePath = yield* GroupPath.modulePath(groupPath);
362
+ const modulePath = path.join(convexDirectory, relativeModulePath);
363
+ const registeredFunctionsPath =
364
+ path.join(
365
+ confectDirectory,
366
+ "_generated",
367
+ "registeredFunctions",
368
+ ...groupPath.pathSegments,
369
+ ) + ".ts";
370
+ const registeredFunctionsImportPath = yield* toModuleImportPath(
371
+ path.relative(path.dirname(modulePath), registeredFunctionsPath),
372
+ );
373
+
343
374
  yield* Effect.logDebug(`Generating group ${groupPath}...`);
344
375
  yield* generateGroupModule({
345
376
  groupPath,
346
377
  functionNames,
378
+ registeredFunctionsImportPath,
379
+ useNode: groupPath.pathSegments[0] === "node",
347
380
  });
348
381
  yield* Effect.logDebug(`Group ${groupPath} generated`);
349
382
  }),