@confect/cli 8.0.0 → 9.0.0-next.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.
@@ -1,7 +1,20 @@
1
- import { DatabaseSchema, Spec } from "@confect/core";
1
+ import { type GroupSpec, Spec } from "@confect/core";
2
+ import * as DatabaseSchema from "@confect/server/DatabaseSchema";
2
3
  import { Command } from "@effect/cli";
3
4
  import { FileSystem, Path } from "@effect/platform";
4
- import { Effect, Match, Option } from "effect";
5
+ import { Array, Effect, Either, HashSet, Match, Option, Ref } from "effect";
6
+ import { fromBundlerError } from "../BuildError";
7
+ import * as CodegenError from "../CodegenError";
8
+ import {
9
+ MissingImplFileError,
10
+ MissingSchemaFileError,
11
+ MissingSpecFileError,
12
+ ParentChildNameCollisionError,
13
+ SchemaInvalidDefaultExportError,
14
+ } from "../CodegenError";
15
+ import { ConfectDirectory } from "../ConfectDirectory";
16
+ import { ConvexDirectory } from "../ConvexDirectory";
17
+ import * as FunctionPaths from "../FunctionPaths";
5
18
  import {
6
19
  logFileAdded,
7
20
  logFileModified,
@@ -9,66 +22,97 @@ import {
9
22
  logPending,
10
23
  logSuccess,
11
24
  } from "../log";
12
- import { ConfectDirectory } from "../ConfectDirectory";
13
- import { ConvexDirectory } from "../ConvexDirectory";
25
+ import {
26
+ discoverLeafImplFiles,
27
+ discoverLeafSpecFiles,
28
+ implPathForSpec,
29
+ registeredFunctionsRelativePath,
30
+ specPathForImpl,
31
+ toLeafModule,
32
+ toNodeRegistryLeaf,
33
+ validateImpl,
34
+ validateSpec,
35
+ type LeafModule,
36
+ } from "../LeafModule";
37
+ import {
38
+ assemblyNodesFromLeaves,
39
+ partitionByRuntime,
40
+ type SpecAssemblyNode,
41
+ } from "../SpecAssemblyNode";
14
42
  import * as templates from "../templates";
43
+ import * as Bundler from "../Bundler";
15
44
  import {
16
- bundleAndImport,
17
45
  generateAuthConfig,
18
46
  generateCrons,
19
47
  generateFunctions,
20
48
  generateHttp,
21
49
  removePathExtension,
50
+ removePathIfExists,
51
+ toModuleImportPath,
52
+ touchConvexSchema,
22
53
  writeFileStringAndLog,
54
+ WriteTracker,
23
55
  } from "../utils";
24
56
 
25
- const getNodeSpecPath = Effect.gen(function* () {
26
- const path = yield* Path.Path;
27
- const confectDirectory = yield* ConfectDirectory.get;
28
- return path.join(confectDirectory, "nodeSpec.ts");
29
- });
30
-
31
- const loadNodeSpec = Effect.gen(function* () {
32
- const fs = yield* FileSystem.FileSystem;
33
- const nodeSpecPath = yield* getNodeSpecPath;
34
-
35
- if (!(yield* fs.exists(nodeSpecPath))) {
36
- return Option.none<Spec.AnyWithPropsWithRuntime<"Node">>();
37
- }
38
-
39
- const nodeSpecModule = yield* bundleAndImport(nodeSpecPath);
40
- const nodeSpec = nodeSpecModule.default;
41
-
42
- if (!Spec.isNodeSpec(nodeSpec)) {
43
- return yield* Effect.die("nodeSpec.ts does not export a valid Node Spec");
44
- }
45
-
46
- return Option.some(nodeSpec);
47
- });
57
+ const GENERATED_SPEC_PATH = "_generated/spec.ts";
58
+ const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
59
+
60
+ const LEGACY_PATHS = [
61
+ "spec.ts",
62
+ "nodeSpec.ts",
63
+ "impl.ts",
64
+ "nodeImpl.ts",
65
+ "notesAndRandom.impl.ts",
66
+ "groups.impl.ts",
67
+ "_generated/registeredFunctions.ts",
68
+ "_generated/nodeRegisteredFunctions.ts",
69
+ "_generated/impl.ts",
70
+ "_generated/nodeImpl.ts",
71
+ ];
48
72
 
49
73
  export const codegen = Command.make("codegen", {}, () =>
50
74
  Effect.gen(function* () {
51
75
  yield* logPending("Performing initial sync…");
52
- yield* codegenHandler;
53
- yield* logSuccess("Generated files are up-to-date");
76
+ yield* codegenHandler.pipe(
77
+ Effect.asVoid,
78
+ Effect.tap(() => logSuccess("Generated files are up-to-date")),
79
+ CodegenError.tapAndLog,
80
+ );
54
81
  }),
55
82
  ).pipe(
56
83
  Command.withDescription(
57
- "Generate `confect/_generated` files and the contents of the `convex` directory (except `tsconfig.json`)",
84
+ "Generate `confect/_generated` files and the contents of the `convex` directory (except `convex.config.ts` and `tsconfig.json`)",
58
85
  ),
59
86
  );
60
87
 
61
88
  export const codegenHandler = Effect.gen(function* () {
89
+ const tracker = yield* Ref.make(false);
90
+
91
+ const functionPaths = yield* runCodegen.pipe(
92
+ Effect.provideService(WriteTracker, tracker),
93
+ );
94
+
95
+ const anyWritesHappened = yield* Ref.get(tracker);
96
+ return { functionPaths, anyWritesHappened };
97
+ });
98
+
99
+ const runCodegen = Effect.gen(function* () {
62
100
  yield* generateConfectGeneratedDirectory;
101
+ // Validate schema first so its missing-file / invalid-default-export
102
+ // diagnostics surface ahead of impl bundling, which transitively depends
103
+ // on schema via `_generated/api.ts` and would otherwise blow up with a
104
+ // less actionable bundler error.
105
+ yield* validateSchema;
106
+ const { leaves, groupSpecsByRelativePath } =
107
+ yield* loadAndValidateLeafModules;
108
+ yield* removeLegacyFiles;
109
+ yield* validateNoParentChildNameCollisions(leaves, groupSpecsByRelativePath);
110
+ yield* generateAssembledSpecs(leaves);
111
+ yield* validateImplModules(leaves);
112
+ yield* generateGroupRegisteredFunctions(leaves);
113
+ yield* removeObsoleteRegisteredFunctions(leaves);
63
114
  yield* Effect.all(
64
- [
65
- generateApi,
66
- generateRefs,
67
- generateRegisteredFunctions,
68
- generateNodeApi,
69
- generateNodeRegisteredFunctions,
70
- generateServices,
71
- ],
115
+ [generateApi, generateRefs, generateNodeApi, generateServices],
72
116
  { concurrency: "unbounded" },
73
117
  );
74
118
  const [functionPaths] = yield* Effect.all(
@@ -81,6 +125,7 @@ export const codegenHandler = Effect.gen(function* () {
81
125
  ],
82
126
  { concurrency: "unbounded" },
83
127
  );
128
+ yield* touchConvexSchema;
84
129
  return functionPaths;
85
130
  });
86
131
 
@@ -97,6 +142,390 @@ const generateConfectGeneratedDirectory = Effect.gen(function* () {
97
142
  }
98
143
  });
99
144
 
145
+ const loadAndValidateLeafModules = Effect.gen(function* () {
146
+ const fs = yield* FileSystem.FileSystem;
147
+ const path = yield* Path.Path;
148
+ const confectDirectory = yield* ConfectDirectory.get;
149
+ const specFiles = yield* discoverLeafSpecFiles;
150
+
151
+ const results = yield* Effect.forEach(specFiles, (specRelativePath) =>
152
+ Effect.gen(function* () {
153
+ const leaf = yield* toLeafModule(specRelativePath);
154
+ const groupSpec = yield* validateSpec(leaf);
155
+
156
+ const implRelativePath = yield* implPathForSpec(specRelativePath);
157
+ const implAbsolutePath = path.join(confectDirectory, implRelativePath);
158
+ if (!(yield* fs.exists(implAbsolutePath))) {
159
+ return yield* new MissingImplFileError({
160
+ specPath: specRelativePath,
161
+ expectedImplPath: implRelativePath,
162
+ });
163
+ }
164
+
165
+ return { leaf, groupSpec };
166
+ }),
167
+ );
168
+
169
+ yield* validateOrphanImpls(specFiles);
170
+
171
+ const leaves = Array.map(results, ({ leaf }) => leaf);
172
+ const groupSpecsByRelativePath = new Map(
173
+ Array.map(results, ({ leaf, groupSpec }) => [leaf.relativePath, groupSpec]),
174
+ );
175
+
176
+ return { leaves, groupSpecsByRelativePath };
177
+ });
178
+
179
+ /**
180
+ * Walk the assembly tree and fail with a {@link ParentChildNameCollisionError}
181
+ * when a parent leaf declares a function or subgroup whose name matches a
182
+ * sibling subdirectory spec's segment. Without this check the colliding
183
+ * descendant would overwrite the parent's entry in the assembled
184
+ * `GroupSpec.groups` map at runtime, surfacing as a confusing
185
+ * `Refs.make` error rather than a codegen-time diagnostic.
186
+ */
187
+ export const validateNoParentChildNameCollisions = (
188
+ leaves: ReadonlyArray<LeafModule>,
189
+ groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
190
+ ) =>
191
+ Effect.gen(function* () {
192
+ const { convex, node } = partitionByRuntime(leaves);
193
+ const convexNodes = assemblyNodesFromLeaves(convex);
194
+ const nodeNodes = assemblyNodesFromLeaves(
195
+ Array.map(node, toNodeRegistryLeaf),
196
+ );
197
+ yield* Effect.forEach(convexNodes, (n) =>
198
+ checkAssemblyNodeForCollisions(n, groupSpecsByRelativePath),
199
+ );
200
+ yield* Effect.forEach(nodeNodes, (n) =>
201
+ checkAssemblyNodeForCollisions(n, groupSpecsByRelativePath),
202
+ );
203
+ });
204
+
205
+ const checkAssemblyNodeForCollisions = (
206
+ node: SpecAssemblyNode,
207
+ groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
208
+ ): Effect.Effect<void, ParentChildNameCollisionError> =>
209
+ Effect.gen(function* () {
210
+ yield* Option.match(node.importBinding, {
211
+ onNone: () => Effect.void,
212
+ onSome: (binding) =>
213
+ Effect.gen(function* () {
214
+ if (node.children.length === 0) return;
215
+ const parentRelativePath = bindingToRelativeSpecPath(
216
+ binding.importPath,
217
+ );
218
+ const parentGroupSpec =
219
+ groupSpecsByRelativePath.get(parentRelativePath);
220
+ if (parentGroupSpec === undefined) return;
221
+ yield* Effect.forEach(node.children, (child) => {
222
+ if (
223
+ Object.prototype.hasOwnProperty.call(
224
+ parentGroupSpec.functions,
225
+ child.segment,
226
+ )
227
+ ) {
228
+ return Effect.fail(
229
+ new ParentChildNameCollisionError({
230
+ parentSpecPath: parentRelativePath,
231
+ childSpecPath: childRepresentativeSpecPath(child),
232
+ collisionName: child.segment,
233
+ collisionKind: "function",
234
+ }),
235
+ );
236
+ }
237
+ if (
238
+ Object.prototype.hasOwnProperty.call(
239
+ parentGroupSpec.groups,
240
+ child.segment,
241
+ )
242
+ ) {
243
+ return Effect.fail(
244
+ new ParentChildNameCollisionError({
245
+ parentSpecPath: parentRelativePath,
246
+ childSpecPath: childRepresentativeSpecPath(child),
247
+ collisionName: child.segment,
248
+ collisionKind: "group",
249
+ }),
250
+ );
251
+ }
252
+ return Effect.void;
253
+ });
254
+ }),
255
+ });
256
+ yield* Effect.forEach(node.children, (child) =>
257
+ checkAssemblyNodeForCollisions(child, groupSpecsByRelativePath),
258
+ );
259
+ });
260
+
261
+ /**
262
+ * `LeafModule.specImportPath` is the import path used from inside the
263
+ * generated `_generated/spec.ts` (e.g. `"../notes.spec"`). Strip the
264
+ * `../` prefix and re-add the `.ts` extension to recover the leaf's
265
+ * confect-relative spec path used as the key in
266
+ * `groupSpecsByRelativePath`.
267
+ */
268
+ const bindingToRelativeSpecPath = (importPath: string): string => {
269
+ const withoutDotDot = importPath.startsWith("../")
270
+ ? importPath.slice(3)
271
+ : importPath;
272
+ return `${withoutDotDot}.ts`;
273
+ };
274
+
275
+ /**
276
+ * A child assembly node may itself be a parent without a leaf (when the
277
+ * actual leaves live only in deeper subdirectories). In that case we
278
+ * surface the first descendant leaf as a representative path so the
279
+ * error message points at something the user actually wrote.
280
+ */
281
+ const childRepresentativeSpecPath = (node: SpecAssemblyNode): string => {
282
+ if (Option.isSome(node.importBinding)) {
283
+ return bindingToRelativeSpecPath(node.importBinding.value.importPath);
284
+ }
285
+ for (const child of node.children) {
286
+ return childRepresentativeSpecPath(child);
287
+ }
288
+ return node.segment;
289
+ };
290
+
291
+ const validateOrphanImpls = (specFiles: ReadonlyArray<string>) =>
292
+ Effect.gen(function* () {
293
+ const fs = yield* FileSystem.FileSystem;
294
+ const path = yield* Path.Path;
295
+ const confectDirectory = yield* ConfectDirectory.get;
296
+ const implFiles = yield* discoverLeafImplFiles;
297
+ const specPaths = new Set(specFiles);
298
+
299
+ yield* Effect.forEach(implFiles, (implRelativePath) =>
300
+ Effect.gen(function* () {
301
+ const specRelativePath = yield* specPathForImpl(implRelativePath);
302
+ if (specPaths.has(specRelativePath)) {
303
+ return;
304
+ }
305
+
306
+ const specAbsolutePath = path.join(confectDirectory, specRelativePath);
307
+ if (!(yield* fs.exists(specAbsolutePath))) {
308
+ return yield* new MissingSpecFileError({
309
+ implPath: implRelativePath,
310
+ expectedSpecPath: specRelativePath,
311
+ });
312
+ }
313
+ }),
314
+ );
315
+ });
316
+
317
+ const removeLegacyFiles = Effect.gen(function* () {
318
+ const fs = yield* FileSystem.FileSystem;
319
+ const path = yield* Path.Path;
320
+ const confectDirectory = yield* ConfectDirectory.get;
321
+
322
+ yield* Effect.forEach(LEGACY_PATHS, (relativePath) =>
323
+ Effect.gen(function* () {
324
+ const absolutePath = path.join(confectDirectory, relativePath);
325
+ if (yield* fs.exists(absolutePath)) {
326
+ yield* removePathIfExists(absolutePath);
327
+ yield* logFileRemoved(absolutePath);
328
+ }
329
+ }),
330
+ );
331
+ });
332
+
333
+ const generateAssembledSpecs = (leaves: ReadonlyArray<LeafModule>) =>
334
+ Effect.gen(function* () {
335
+ const path = yield* Path.Path;
336
+ const confectDirectory = yield* ConfectDirectory.get;
337
+ const { convex, node } = partitionByRuntime(leaves);
338
+
339
+ if (convex.length > 0) {
340
+ const nodes = assemblyNodesFromLeaves(convex);
341
+ const specContents = yield* templates.assembledSpec({
342
+ nodes,
343
+ runtime: "Convex",
344
+ });
345
+ yield* writeFileStringAndLog(
346
+ path.join(confectDirectory, GENERATED_SPEC_PATH),
347
+ specContents,
348
+ );
349
+ }
350
+
351
+ if (node.length > 0) {
352
+ const nodes = assemblyNodesFromLeaves(
353
+ Array.map(node, toNodeRegistryLeaf),
354
+ );
355
+ const nodeSpecContents = yield* templates.assembledSpec({
356
+ nodes,
357
+ runtime: "Node",
358
+ });
359
+ yield* writeFileStringAndLog(
360
+ path.join(confectDirectory, GENERATED_NODE_SPEC_PATH),
361
+ nodeSpecContents,
362
+ );
363
+ }
364
+ });
365
+
366
+ const validateImplModules = (leaves: ReadonlyArray<LeafModule>) =>
367
+ Effect.forEach(leaves, validateImpl);
368
+
369
+ const generateGroupRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
370
+ Effect.gen(function* () {
371
+ const path = yield* Path.Path;
372
+ const confectDirectory = yield* ConfectDirectory.get;
373
+
374
+ yield* Effect.forEach(leaves, (leaf) =>
375
+ Effect.gen(function* () {
376
+ const registryRelativePath =
377
+ yield* registeredFunctionsRelativePath(leaf);
378
+ const registryPath = path.join(
379
+ confectDirectory,
380
+ "_generated",
381
+ registryRelativePath,
382
+ );
383
+ const registryDir = path.dirname(registryPath);
384
+ const fs = yield* FileSystem.FileSystem;
385
+ if (!(yield* fs.exists(registryDir))) {
386
+ yield* fs.makeDirectory(registryDir, { recursive: true });
387
+ }
388
+
389
+ const implRelativePath = yield* implPathForSpec(leaf.relativePath);
390
+ const apiFileName = leaf.runtime === "Node" ? "nodeApi.ts" : "api.ts";
391
+ const apiImportPath = yield* toModuleImportPath(
392
+ path.relative(
393
+ path.dirname(registryPath),
394
+ path.join(confectDirectory, "_generated", apiFileName),
395
+ ),
396
+ );
397
+ const implImportPath = yield* toModuleImportPath(
398
+ path.relative(
399
+ path.dirname(registryPath),
400
+ path.join(confectDirectory, implRelativePath),
401
+ ),
402
+ );
403
+
404
+ const contents = yield* templates.registeredFunctionsForGroup({
405
+ apiImportPath,
406
+ groupPathDot: leaf.registryGroupPathDot,
407
+ implImportPath,
408
+ layerExportName: leaf.exportName,
409
+ useNode: leaf.runtime === "Node",
410
+ });
411
+
412
+ yield* writeFileStringAndLog(registryPath, contents);
413
+ }),
414
+ );
415
+ });
416
+
417
+ const removeObsoleteRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
418
+ Effect.gen(function* () {
419
+ const fs = yield* FileSystem.FileSystem;
420
+ const path = yield* Path.Path;
421
+ const confectDirectory = yield* ConfectDirectory.get;
422
+ const registryRoot = path.join(
423
+ confectDirectory,
424
+ "_generated",
425
+ "registeredFunctions",
426
+ );
427
+
428
+ if (!(yield* fs.exists(registryRoot))) {
429
+ return;
430
+ }
431
+
432
+ const expected = new Set(
433
+ yield* Effect.forEach(leaves, (leaf) =>
434
+ registeredFunctionsRelativePath(leaf),
435
+ ),
436
+ );
437
+
438
+ const existing = yield* fs.readDirectory(registryRoot, { recursive: true });
439
+ yield* Effect.forEach(existing, (relativePath) => {
440
+ if (path.extname(relativePath) !== ".ts") {
441
+ return Effect.void;
442
+ }
443
+ const normalized = path.join("registeredFunctions", relativePath);
444
+ if (!expected.has(normalized)) {
445
+ return Effect.gen(function* () {
446
+ const absolutePath = path.join(registryRoot, relativePath);
447
+ if (yield* fs.exists(absolutePath)) {
448
+ yield* removePathIfExists(absolutePath);
449
+ yield* logFileRemoved(absolutePath);
450
+ }
451
+ });
452
+ }
453
+ return Effect.void;
454
+ });
455
+ });
456
+
457
+ const getGeneratedSpecPath = Effect.gen(function* () {
458
+ const path = yield* Path.Path;
459
+ const confectDirectory = yield* ConfectDirectory.get;
460
+ return path.join(confectDirectory, GENERATED_SPEC_PATH);
461
+ });
462
+
463
+ const getGeneratedNodeSpecPath = Effect.gen(function* () {
464
+ const path = yield* Path.Path;
465
+ const confectDirectory = yield* ConfectDirectory.get;
466
+ return path.join(confectDirectory, GENERATED_NODE_SPEC_PATH);
467
+ });
468
+
469
+ const loadGeneratedSpec = Effect.gen(function* () {
470
+ const specPath = yield* getGeneratedSpecPath;
471
+ const { module: specModule } = yield* Bundler.bundle(specPath);
472
+ const spec = specModule.default;
473
+
474
+ if (!Spec.isConvexSpec(spec)) {
475
+ return yield* Effect.dieMessage(
476
+ "_generated/spec.ts does not export a valid Convex Spec",
477
+ );
478
+ }
479
+
480
+ return spec;
481
+ });
482
+
483
+ const loadGeneratedNodeSpec = Effect.gen(function* () {
484
+ const fs = yield* FileSystem.FileSystem;
485
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
486
+
487
+ if (!(yield* fs.exists(nodeSpecPath))) {
488
+ return Option.none<Spec.AnyWithPropsWithRuntime<"Node">>();
489
+ }
490
+
491
+ const { module: nodeSpecModule } = yield* Bundler.bundle(nodeSpecPath);
492
+ const nodeSpec = nodeSpecModule.default;
493
+
494
+ if (!Spec.isNodeSpec(nodeSpec)) {
495
+ return yield* Effect.dieMessage(
496
+ "_generated/nodeSpec.ts does not export a valid Node Spec",
497
+ );
498
+ }
499
+
500
+ return Option.some(nodeSpec);
501
+ });
502
+
503
+ const emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());
504
+
505
+ export const loadPreviousFunctionPaths = Effect.gen(function* () {
506
+ const fs = yield* FileSystem.FileSystem;
507
+ const specPath = yield* getGeneratedSpecPath;
508
+
509
+ if (!(yield* fs.exists(specPath))) {
510
+ return emptyFunctionPaths;
511
+ }
512
+
513
+ const specEither = yield* loadGeneratedSpec.pipe(Effect.either);
514
+
515
+ return yield* Either.match(specEither, {
516
+ onLeft: () => Effect.succeed(emptyFunctionPaths),
517
+ onRight: (spec) =>
518
+ Effect.gen(function* () {
519
+ const nodeSpecOption = yield* loadGeneratedNodeSpec;
520
+ const mergedSpec = Option.match(nodeSpecOption, {
521
+ onNone: () => spec,
522
+ onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),
523
+ });
524
+ return FunctionPaths.make(mergedSpec);
525
+ }),
526
+ });
527
+ });
528
+
100
529
  const generateApi = Effect.gen(function* () {
101
530
  const path = yield* Path.Path;
102
531
  const confectDirectory = yield* ConfectDirectory.get;
@@ -104,12 +533,12 @@ const generateApi = Effect.gen(function* () {
104
533
  const apiPath = path.join(confectDirectory, "_generated", "api.ts");
105
534
  const apiDir = path.dirname(apiPath);
106
535
 
107
- const schemaImportPath = yield* removePathExtension(
536
+ const schemaImportPath = yield* toModuleImportPath(
108
537
  path.relative(apiDir, path.join(confectDirectory, "schema.ts")),
109
538
  );
110
539
 
111
- const specImportPath = yield* removePathExtension(
112
- path.relative(apiDir, path.join(confectDirectory, "spec.ts")),
540
+ const specImportPath = yield* toModuleImportPath(
541
+ path.relative(apiDir, path.join(confectDirectory, GENERATED_SPEC_PATH)),
113
542
  );
114
543
 
115
544
  const apiContents = yield* templates.api({
@@ -125,12 +554,12 @@ export const generateNodeApi = Effect.gen(function* () {
125
554
  const path = yield* Path.Path;
126
555
  const confectDirectory = yield* ConfectDirectory.get;
127
556
 
128
- const nodeSpecPath = yield* getNodeSpecPath;
557
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
129
558
  const nodeApiPath = path.join(confectDirectory, "_generated", "nodeApi.ts");
130
559
 
131
560
  if (!(yield* fs.exists(nodeSpecPath))) {
132
561
  if (yield* fs.exists(nodeApiPath)) {
133
- yield* fs.remove(nodeApiPath);
562
+ yield* removePathIfExists(nodeApiPath);
134
563
  yield* logFileRemoved(nodeApiPath);
135
564
  }
136
565
  return;
@@ -138,11 +567,11 @@ export const generateNodeApi = Effect.gen(function* () {
138
567
 
139
568
  const nodeApiDir = path.dirname(nodeApiPath);
140
569
 
141
- const schemaImportPath = yield* removePathExtension(
570
+ const schemaImportPath = yield* toModuleImportPath(
142
571
  path.relative(nodeApiDir, path.join(confectDirectory, "schema.ts")),
143
572
  );
144
573
 
145
- const nodeSpecImportPath = yield* removePathExtension(
574
+ const nodeSpecImportPath = yield* toModuleImportPath(
146
575
  path.relative(nodeApiDir, nodeSpecPath),
147
576
  );
148
577
 
@@ -155,47 +584,52 @@ export const generateNodeApi = Effect.gen(function* () {
155
584
  });
156
585
 
157
586
  const generateFunctionModules = Effect.gen(function* () {
158
- const fs = yield* FileSystem.FileSystem;
159
- const path = yield* Path.Path;
160
- const confectDirectory = yield* ConfectDirectory.get;
161
-
162
- const specPath = path.join(confectDirectory, "spec.ts");
163
-
164
- const specModule = yield* bundleAndImport(specPath);
165
- const spec = specModule.default;
166
-
167
- if (!Spec.isConvexSpec(spec)) {
168
- return yield* Effect.die("spec.ts does not export a valid Convex Spec");
169
- }
170
-
171
- const nodeImplPath = path.join(confectDirectory, "nodeImpl.ts");
172
- const nodeImplExists = yield* fs.exists(nodeImplPath);
173
- const nodeSpecOption = yield* loadNodeSpec;
587
+ const spec = yield* loadGeneratedSpec;
588
+ const nodeSpecOption = yield* loadGeneratedNodeSpec;
174
589
 
175
590
  const mergedSpec = Option.match(nodeSpecOption, {
176
591
  onNone: () => spec,
177
- onSome: (nodeSpec) => (nodeImplExists ? Spec.merge(spec, nodeSpec) : spec),
592
+ onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),
178
593
  });
179
594
 
180
595
  return yield* generateFunctions(mergedSpec);
181
596
  });
182
597
 
183
- const generateSchema = Effect.gen(function* () {
598
+ export const validateSchema = Effect.gen(function* () {
599
+ const fs = yield* FileSystem.FileSystem;
184
600
  const path = yield* Path.Path;
185
601
  const confectDirectory = yield* ConfectDirectory.get;
186
- const convexDirectory = yield* ConvexDirectory.get;
187
-
188
602
  const confectSchemaPath = path.join(confectDirectory, "schema.ts");
189
603
 
190
- yield* bundleAndImport(confectSchemaPath).pipe(
191
- Effect.andThen((schemaModule) => {
604
+ if (!(yield* fs.exists(confectSchemaPath))) {
605
+ return yield* new MissingSchemaFileError({ schemaPath: "schema.ts" });
606
+ }
607
+
608
+ yield* Bundler.bundle(confectSchemaPath).pipe(
609
+ Effect.mapError((error) => fromBundlerError("schema.ts", error)),
610
+ Effect.andThen(({ module: schemaModule }) => {
192
611
  const defaultExport = schemaModule.default;
193
612
 
194
613
  return DatabaseSchema.isDatabaseSchema(defaultExport)
195
614
  ? Effect.succeed(defaultExport)
196
- : Effect.die("Invalid schema module");
615
+ : Effect.fail(
616
+ new SchemaInvalidDefaultExportError({
617
+ schemaPath: "schema.ts",
618
+ }),
619
+ );
197
620
  }),
198
621
  );
622
+ });
623
+
624
+ const generateSchema = Effect.gen(function* () {
625
+ const path = yield* Path.Path;
626
+ const confectDirectory = yield* ConfectDirectory.get;
627
+ const convexDirectory = yield* ConvexDirectory.get;
628
+
629
+ const confectSchemaPath = path.join(confectDirectory, "schema.ts");
630
+
631
+ // `validateSchema` runs once at the top of `runCodegen`; no need to
632
+ // bundle the schema again here.
199
633
 
200
634
  const convexSchemaPath = path.join(convexDirectory, "schema.ts");
201
635
 
@@ -230,72 +664,6 @@ const generateServices = Effect.gen(function* () {
230
664
  yield* writeFileStringAndLog(servicesPath, servicesContentsString);
231
665
  });
232
666
 
233
- const generateRegisteredFunctions = Effect.gen(function* () {
234
- const path = yield* Path.Path;
235
- const confectDirectory = yield* ConfectDirectory.get;
236
-
237
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
238
-
239
- const registeredFunctionsPath = path.join(
240
- confectGeneratedDirectory,
241
- "registeredFunctions.ts",
242
- );
243
- const implImportPath = yield* removePathExtension(
244
- path.relative(
245
- path.dirname(registeredFunctionsPath),
246
- path.join(confectDirectory, "impl.ts"),
247
- ),
248
- );
249
-
250
- const registeredFunctionsContents = yield* templates.registeredFunctions({
251
- implImportPath,
252
- });
253
-
254
- yield* writeFileStringAndLog(
255
- registeredFunctionsPath,
256
- registeredFunctionsContents,
257
- );
258
- });
259
-
260
- export const generateNodeRegisteredFunctions = Effect.gen(function* () {
261
- const fs = yield* FileSystem.FileSystem;
262
- const path = yield* Path.Path;
263
- const confectDirectory = yield* ConfectDirectory.get;
264
-
265
- const nodeImplPath = path.join(confectDirectory, "nodeImpl.ts");
266
- const nodeSpecPath = yield* getNodeSpecPath;
267
- const nodeRegisteredFunctionsPath = path.join(
268
- confectDirectory,
269
- "_generated",
270
- "nodeRegisteredFunctions.ts",
271
- );
272
-
273
- const nodeImplExists = yield* fs.exists(nodeImplPath);
274
- const nodeSpecExists = yield* fs.exists(nodeSpecPath);
275
-
276
- if (!nodeImplExists || !nodeSpecExists) {
277
- if (yield* fs.exists(nodeRegisteredFunctionsPath)) {
278
- yield* fs.remove(nodeRegisteredFunctionsPath);
279
- yield* logFileRemoved(nodeRegisteredFunctionsPath);
280
- }
281
- return;
282
- }
283
-
284
- const nodeImplImportPath = yield* removePathExtension(
285
- path.relative(path.dirname(nodeRegisteredFunctionsPath), nodeImplPath),
286
- );
287
-
288
- const nodeRegisteredFunctionsContents =
289
- yield* templates.nodeRegisteredFunctions({
290
- nodeImplImportPath,
291
- });
292
-
293
- yield* writeFileStringAndLog(
294
- nodeRegisteredFunctionsPath,
295
- nodeRegisteredFunctionsContents,
296
- );
297
- });
298
-
299
667
  const generateRefs = Effect.gen(function* () {
300
668
  const fs = yield* FileSystem.FileSystem;
301
669
  const path = yield* Path.Path;
@@ -305,19 +673,21 @@ const generateRefs = Effect.gen(function* () {
305
673
  const refsPath = path.join(confectGeneratedDirectory, "refs.ts");
306
674
  const refsDir = path.dirname(refsPath);
307
675
 
308
- const specImportPath = yield* removePathExtension(
309
- path.relative(refsDir, path.join(confectDirectory, "spec.ts")),
676
+ const specImportPath = yield* toModuleImportPath(
677
+ path.relative(refsDir, path.join(confectDirectory, GENERATED_SPEC_PATH)),
310
678
  );
311
679
 
312
- const nodeSpecPath = yield* getNodeSpecPath;
680
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
313
681
  const nodeSpecExists = yield* fs.exists(nodeSpecPath);
314
682
  const nodeSpecImportPath = nodeSpecExists
315
- ? yield* removePathExtension(path.relative(refsDir, nodeSpecPath))
316
- : null;
683
+ ? Option.some(
684
+ yield* toModuleImportPath(path.relative(refsDir, nodeSpecPath)),
685
+ )
686
+ : Option.none<string>();
317
687
 
318
688
  const refsContents = yield* templates.refs({
319
689
  specImportPath,
320
- ...(nodeSpecImportPath === null ? {} : { nodeSpecImportPath }),
690
+ nodeSpecImportPath,
321
691
  });
322
692
 
323
693
  yield* writeFileStringAndLog(refsPath, refsContents);