@confect/cli 8.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
@@ -1,19 +1,18 @@
1
1
  import type { FunctionSpec, Spec } from "@confect/core";
2
- import { createRequire } from "node:module";
3
- import { pathToFileURL } from "node:url";
4
2
  import { FileSystem, Path } from "@effect/platform";
5
3
  import type { PlatformError } from "@effect/platform/Error";
6
4
  import {
7
5
  Array,
6
+ Context,
8
7
  Effect,
9
8
  HashSet,
10
9
  Option,
11
10
  Order,
12
11
  pipe,
13
12
  Record,
13
+ Ref,
14
14
  String,
15
15
  } from "effect";
16
- import * as esbuild from "esbuild";
17
16
  import * as FunctionPaths from "./FunctionPaths";
18
17
  import * as GroupPath from "./GroupPath";
19
18
  import * as GroupPaths from "./GroupPaths";
@@ -22,6 +21,24 @@ import { ConfectDirectory } from "./ConfectDirectory";
22
21
  import { ConvexDirectory } from "./ConvexDirectory";
23
22
  import * as templates from "./templates";
24
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
+
25
42
  export const removePathExtension = (pathStr: string) =>
26
43
  Effect.gen(function* () {
27
44
  const path = yield* Path.Path;
@@ -29,65 +46,11 @@ export const removePathExtension = (pathStr: string) =>
29
46
  return String.slice(0, -path.extname(pathStr).length)(pathStr);
30
47
  });
31
48
 
32
- export const EXTERNAL_PACKAGES = [
33
- "@confect/core",
34
- "@confect/server",
35
- "effect",
36
- "@effect/*",
37
- ];
38
-
39
- const isExternalImport = (path: string) =>
40
- EXTERNAL_PACKAGES.some((p) => {
41
- if (p.endsWith("/*")) {
42
- return path.startsWith(p.slice(0, -1));
43
- }
44
- return path === p || path.startsWith(p + "/");
45
- });
46
-
47
- const absoluteExternalsPlugin: esbuild.Plugin = {
48
- name: "absolute-externals",
49
- setup(build) {
50
- build.onResolve({ filter: /.*/ }, async (args) => {
51
- if (args.kind !== "import-statement" && args.kind !== "dynamic-import")
52
- return;
53
- if (!isExternalImport(args.path)) return;
54
- // `import.meta.resolve`'s second argument is silently ignored in modern
55
- // Node, so resolution would always walk up from the CLI's bundled file
56
- // (`packages/cli/dist/utils.mjs`) instead of from the user's project.
57
- // Use `createRequire` keyed on the importing file's directory so we
58
- // resolve out of *their* `node_modules`. The synthetic filename is just
59
- // a CommonJS resolution anchor; the file does not need to exist.
60
- const parentFile = pathToFileURL(args.resolveDir + "/_").href;
61
- const require_ = createRequire(parentFile);
62
- const resolvedPath = require_.resolve(args.path);
63
- const resolved = pathToFileURL(resolvedPath).href;
64
- return { path: resolved, external: true };
65
- });
66
- },
67
- };
68
-
69
- /**
70
- * Bundle a TypeScript entry point with esbuild and import the result via a
71
- * data URL. This handles extensionless `.ts` imports regardless of whether the
72
- * user's project sets `"type": "module"` in package.json.
73
- */
74
- 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) =>
75
51
  Effect.gen(function* () {
76
- const result = yield* Effect.promise(() =>
77
- esbuild.build({
78
- entryPoints: [entryPoint],
79
- bundle: true,
80
- write: false,
81
- platform: "node",
82
- format: "esm",
83
- logLevel: "silent",
84
- plugins: [absoluteExternalsPlugin],
85
- }),
86
- );
87
- const code = result.outputFiles[0]!.text;
88
- const dataUrl =
89
- "data:text/javascript;base64," + Buffer.from(code).toString("base64");
90
- return yield* Effect.promise(() => import(dataUrl));
52
+ const withoutExt = yield* removePathExtension(relativePath);
53
+ return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
91
54
  });
92
55
 
93
56
  export const writeFileStringAndLog = (filePath: string, contents: string) =>
@@ -95,12 +58,14 @@ export const writeFileStringAndLog = (filePath: string, contents: string) =>
95
58
  const fs = yield* FileSystem.FileSystem;
96
59
  if (!(yield* fs.exists(filePath))) {
97
60
  yield* fs.writeFileString(filePath, contents);
61
+ yield* markWritten;
98
62
  yield* logFileAdded(filePath);
99
63
  return;
100
64
  }
101
65
  const existing = yield* fs.readFileString(filePath);
102
66
  if (existing !== contents) {
103
67
  yield* fs.writeFileString(filePath, contents);
68
+ yield* markWritten;
104
69
  yield* logFileModified(filePath);
105
70
  }
106
71
  });
@@ -136,28 +101,73 @@ export const writeFileString = (
136
101
 
137
102
  if (!(yield* fs.exists(filePath))) {
138
103
  yield* fs.writeFileString(filePath, contents);
104
+ yield* markWritten;
139
105
  return "Added";
140
106
  }
141
107
  const existing = yield* fs.readFileString(filePath);
142
108
  if (existing !== contents) {
143
109
  yield* fs.writeFileString(filePath, contents);
110
+ yield* markWritten;
144
111
  return "Modified";
145
112
  }
146
113
  return "Unchanged";
147
114
  });
148
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
+
149
156
  export const generateGroupModule = ({
150
157
  groupPath,
151
158
  functionNames,
159
+ registeredFunctionsImportPath,
160
+ useNode = false,
152
161
  }: {
153
162
  groupPath: GroupPath.GroupPath;
154
163
  functionNames: string[];
164
+ registeredFunctionsImportPath: string;
165
+ useNode?: boolean;
155
166
  }) =>
156
167
  Effect.gen(function* () {
157
168
  const fs = yield* FileSystem.FileSystem;
158
169
  const path = yield* Path.Path;
159
170
  const convexDirectory = yield* ConvexDirectory.get;
160
- const confectDirectory = yield* ConfectDirectory.get;
161
171
 
162
172
  const relativeModulePath = yield* GroupPath.modulePath(groupPath);
163
173
  const modulePath = path.join(convexDirectory, relativeModulePath);
@@ -167,42 +177,21 @@ export const generateGroupModule = ({
167
177
  yield* fs.makeDirectory(directoryPath, { recursive: true });
168
178
  }
169
179
 
170
- const isNodeGroup = groupPath.pathSegments[0] === "node";
171
- const registeredFunctionsFileName = isNodeGroup
172
- ? "nodeRegisteredFunctions.ts"
173
- : "registeredFunctions.ts";
174
- const registeredFunctionsPath = path.join(
175
- confectDirectory,
176
- "_generated",
177
- registeredFunctionsFileName,
178
- );
179
- const registeredFunctionsImportPath = yield* removePathExtension(
180
- path.relative(path.dirname(modulePath), registeredFunctionsPath),
181
- );
182
- const registeredFunctionsVariableName = isNodeGroup
183
- ? "nodeRegisteredFunctions"
184
- : "registeredFunctions";
185
-
186
180
  const functionsContentsString = yield* templates.functions({
187
- groupPath,
188
181
  functionNames,
189
182
  registeredFunctionsImportPath,
190
- registeredFunctionsVariableName,
191
- useNode: isNodeGroup,
192
- ...(isNodeGroup
193
- ? {
194
- registeredFunctionsLookupPath: groupPath.pathSegments.slice(1),
195
- }
196
- : {}),
183
+ useNode,
197
184
  });
198
185
 
199
186
  if (!(yield* fs.exists(modulePath))) {
200
187
  yield* fs.writeFileString(modulePath, functionsContentsString);
188
+ yield* markWritten;
201
189
  return "Added" as const;
202
190
  }
203
191
  const existing = yield* fs.readFileString(modulePath);
204
192
  if (existing !== functionsContentsString) {
205
193
  yield* fs.writeFileString(modulePath, functionsContentsString);
194
+ yield* markWritten;
206
195
  return "Modified" as const;
207
196
  }
208
197
  return "Unchanged" as const;
@@ -228,6 +217,7 @@ export const generateFunctions = (spec: Spec.AnyWithProps) =>
228
217
  Effect.gen(function* () {
229
218
  const path = yield* Path.Path;
230
219
  const convexDirectory = yield* ConvexDirectory.get;
220
+ const confectDirectory = yield* ConfectDirectory.get;
231
221
 
232
222
  const groupPathsFromFs = yield* getGroupPathsFromFs;
233
223
  const functionPaths = FunctionPaths.make(spec);
@@ -250,12 +240,30 @@ export const generateFunctions = (spec: Spec.AnyWithProps) =>
250
240
  ),
251
241
  Array.map((fn) => fn.name),
252
242
  );
253
- 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
+ });
254
265
  if (result === "Modified") {
255
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
256
- yield* logFileModified(
257
- path.join(convexDirectory, relativeModulePath),
258
- );
266
+ yield* logFileModified(modulePath);
259
267
  }
260
268
  }),
261
269
  );
@@ -308,7 +316,6 @@ const getGroupPathsFromFs = Effect.gen(function* () {
308
316
 
309
317
  export const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>
310
318
  Effect.gen(function* () {
311
- const fs = yield* FileSystem.FileSystem;
312
319
  const path = yield* Path.Path;
313
320
  const convexDirectory = yield* ConvexDirectory.get;
314
321
 
@@ -320,7 +327,7 @@ export const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>
320
327
 
321
328
  yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);
322
329
 
323
- yield* fs.remove(modulePath);
330
+ yield* removePathIfExists(modulePath);
324
331
  yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);
325
332
  }),
326
333
  ),
@@ -334,6 +341,9 @@ export const writeGroups = (
334
341
  ) =>
335
342
  Effect.forEach(groupPaths, (groupPath) =>
336
343
  Effect.gen(function* () {
344
+ const path = yield* Path.Path;
345
+ const convexDirectory = yield* ConvexDirectory.get;
346
+ const confectDirectory = yield* ConfectDirectory.get;
337
347
  const group = yield* GroupPath.getGroupSpec(spec, groupPath);
338
348
 
339
349
  const functionNames = pipe(
@@ -348,10 +358,25 @@ export const writeGroups = (
348
358
  Array.map((fn) => fn.name),
349
359
  );
350
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
+
351
374
  yield* Effect.logDebug(`Generating group ${groupPath}...`);
352
375
  yield* generateGroupModule({
353
376
  groupPath,
354
377
  functionNames,
378
+ registeredFunctionsImportPath,
379
+ useNode: groupPath.pathSegments[0] === "node",
355
380
  });
356
381
  yield* Effect.logDebug(`Group ${groupPath} generated`);
357
382
  }),