@confect/cli 9.0.0-next.8 → 9.0.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +187 -1
  2. package/dist/Bundler.mjs +1 -4
  3. package/dist/Bundler.mjs.map +1 -1
  4. package/dist/CodegenError.mjs +3 -13
  5. package/dist/CodegenError.mjs.map +1 -1
  6. package/dist/LeafModule.mjs +9 -22
  7. package/dist/LeafModule.mjs.map +1 -1
  8. package/dist/SpecAssemblyNode.mjs +1 -8
  9. package/dist/SpecAssemblyNode.mjs.map +1 -1
  10. package/dist/confect/codegen.mjs +26 -69
  11. package/dist/confect/codegen.mjs.map +1 -1
  12. package/dist/confect/dev.mjs +0 -3
  13. package/dist/confect/dev.mjs.map +1 -1
  14. package/dist/log.mjs +2 -2
  15. package/dist/log.mjs.map +1 -1
  16. package/dist/package.mjs +1 -1
  17. package/dist/templates.mjs +5 -14
  18. package/dist/templates.mjs.map +1 -1
  19. package/dist/utils.mjs +32 -22
  20. package/dist/utils.mjs.map +1 -1
  21. package/package.json +4 -21
  22. package/dist/index.d.mts +0 -1
  23. package/src/BuildError.ts +0 -217
  24. package/src/Bundler.ts +0 -145
  25. package/src/CodeBlockWriter.ts +0 -65
  26. package/src/CodegenError.ts +0 -443
  27. package/src/ConfectDirectory.ts +0 -45
  28. package/src/ConvexDirectory.ts +0 -72
  29. package/src/FunctionPath.ts +0 -27
  30. package/src/FunctionPaths.ts +0 -107
  31. package/src/GroupPath.ts +0 -116
  32. package/src/GroupPaths.ts +0 -7
  33. package/src/LeafModule.ts +0 -323
  34. package/src/ProjectRoot.ts +0 -55
  35. package/src/SpecAssemblyNode.ts +0 -88
  36. package/src/TableModule.ts +0 -157
  37. package/src/cliApp.ts +0 -8
  38. package/src/confect/codegen.ts +0 -908
  39. package/src/confect/dev.ts +0 -790
  40. package/src/confect.ts +0 -19
  41. package/src/index.ts +0 -23
  42. package/src/log.ts +0 -110
  43. package/src/templates.ts +0 -633
  44. package/src/utils.ts +0 -428
@@ -1,908 +0,0 @@
1
- import { Spec, type GroupSpec } from "@confect/core";
2
- import * as Command from "@effect/cli/Command";
3
- import * as FileSystem from "@effect/platform/FileSystem";
4
- import * as Path from "@effect/platform/Path";
5
- import * as Array from "effect/Array";
6
- import * as Effect from "effect/Effect";
7
- import * as Either from "effect/Either";
8
- import * as HashSet from "effect/HashSet";
9
- import * as Match from "effect/Match";
10
- import * as Option from "effect/Option";
11
- import * as Ref from "effect/Ref";
12
- import * as Bundler from "../Bundler";
13
- import * as CodegenError from "../CodegenError";
14
- import {
15
- LegacySchemaFileError,
16
- MissingImplFileError,
17
- MissingSpecFileError,
18
- ParentChildNameCollisionError,
19
- } from "../CodegenError";
20
- import { ConfectDirectory } from "../ConfectDirectory";
21
- import { ConvexDirectory } from "../ConvexDirectory";
22
- import * as FunctionPaths from "../FunctionPaths";
23
- import {
24
- discoverLeafImplFiles,
25
- discoverLeafSpecFiles,
26
- implPathForSpec,
27
- registeredFunctionsRelativePath,
28
- specPathForImpl,
29
- toLeafModule,
30
- toNodeRegistryLeaf,
31
- validateImpl,
32
- validateSpec,
33
- type LeafModule,
34
- } from "../LeafModule";
35
- import {
36
- logFileAdded,
37
- logFileModified,
38
- logFileRemoved,
39
- logPending,
40
- logSuccess,
41
- logWarn,
42
- } from "../log";
43
- import {
44
- assemblyNodesFromLeaves,
45
- partitionByRuntime,
46
- type SpecAssemblyNode,
47
- } from "../SpecAssemblyNode";
48
- import * as TableModule from "../TableModule";
49
- import * as templates from "../templates";
50
- import {
51
- generateAuthConfig,
52
- generateCrons,
53
- generateFunctions,
54
- generateHttp,
55
- removePathIfExists,
56
- toModuleImportPath,
57
- touchConvexSchema,
58
- writeFileStringAndLog,
59
- WriteTracker,
60
- } from "../utils";
61
-
62
- const GENERATED_DIRNAME = "_generated";
63
-
64
- const GENERATED_SPEC_PATH = Effect.andThen(Path.Path, (path) =>
65
- path.join(GENERATED_DIRNAME, "spec.ts"),
66
- );
67
- const GENERATED_NODE_SPEC_PATH = Effect.andThen(Path.Path, (path) =>
68
- path.join(GENERATED_DIRNAME, "nodeSpec.ts"),
69
- );
70
- const GENERATED_SCHEMA_PATH = Effect.andThen(Path.Path, (path) =>
71
- path.join(GENERATED_DIRNAME, "schema.ts"),
72
- );
73
- const GENERATED_CONVEX_SCHEMA_PATH = Effect.andThen(Path.Path, (path) =>
74
- path.join(GENERATED_DIRNAME, "convexSchema.ts"),
75
- );
76
- const GENERATED_ID_PATH = Effect.andThen(Path.Path, (path) =>
77
- path.join(GENERATED_DIRNAME, "id.ts"),
78
- );
79
- const GENERATED_TABLES_DIRNAME = Effect.andThen(Path.Path, (path) =>
80
- path.join(GENERATED_DIRNAME, "tables"),
81
- );
82
-
83
- const LEGACY_PATHS = Effect.gen(function* () {
84
- const path = yield* Path.Path;
85
-
86
- return [
87
- "spec.ts",
88
- "nodeSpec.ts",
89
- "impl.ts",
90
- "nodeImpl.ts",
91
- path.join(GENERATED_DIRNAME, "registeredFunctions.ts"),
92
- path.join(GENERATED_DIRNAME, "nodeRegisteredFunctions.ts"),
93
- path.join(GENERATED_DIRNAME, "impl.ts"),
94
- path.join(GENERATED_DIRNAME, "nodeImpl.ts"),
95
- ];
96
- });
97
-
98
- export const codegen = Command.make("codegen", {}, () =>
99
- Effect.gen(function* () {
100
- yield* logPending("Performing initial sync…");
101
- yield* codegenHandler.pipe(
102
- Effect.asVoid,
103
- Effect.tap(() => logSuccess("Generated files are up-to-date")),
104
- CodegenError.tapAndLog,
105
- );
106
- }),
107
- ).pipe(
108
- Command.withDescription(
109
- "Generate `confect/_generated` files and the contents of the `convex` directory (except `convex.config.ts` and `tsconfig.json`)",
110
- ),
111
- );
112
-
113
- export const codegenHandler = Effect.gen(function* () {
114
- const tracker = yield* Ref.make(false);
115
-
116
- const functionPaths = yield* runCodegen.pipe(
117
- Effect.provideService(WriteTracker, tracker),
118
- );
119
-
120
- const anyWritesHappened = yield* Ref.get(tracker);
121
- return { functionPaths, anyWritesHappened };
122
- });
123
-
124
- const runCodegen = Effect.gen(function* () {
125
- yield* generateConfectGeneratedDirectory;
126
- // Reject a legacy `confect/schema.ts` up front so the user-facing
127
- // migration message surfaces before any bundler error from impl
128
- // validation (each impl imports `_generated/schema.ts`).
129
- yield* rejectLegacySchemaFile;
130
- // List `confect/tables/*.ts` (filename-only — no bundling yet) so the
131
- // `_generated/id.ts` constructor can be emitted *before* we bundle any
132
- // user-authored table module. Tables import from `_generated/id.ts` for
133
- // cross-table id refs, so it must exist on disk first.
134
- const tableModules = yield* TableModule.discover;
135
- yield* warnIfNoTables(tableModules);
136
- yield* generateIdConstructor(tableModules);
137
- // Now that `_generated/id.ts` is on disk, bundle each table module and
138
- // check its default export is an `UnnamedTable`. Surface diagnostics
139
- // here (rather than later) so they appear before impl-validation noise.
140
- yield* TableModule.validate(tableModules);
141
- yield* generateTableWrappers(tableModules);
142
- yield* removeObsoleteTableWrappers(tableModules);
143
- yield* generateRuntimeSchema(tableModules);
144
- const { leaves, groupSpecsByRelativePath } =
145
- yield* loadAndValidateLeafModules;
146
- yield* removeLegacyFiles;
147
- yield* validateNoParentChildNameCollisions(leaves, groupSpecsByRelativePath);
148
- yield* generateAssembledSpecs(leaves);
149
- // `_generated/api.ts` / `nodeApi.ts` are no longer imported by generated or
150
- // impl code (impls take the database schema from `_generated/schema`
151
- // directly), so remove any copies left over from earlier versions before
152
- // impl validation runs.
153
- yield* Effect.all(
154
- [
155
- removeGeneratedApi,
156
- generateRefs,
157
- removeGeneratedNodeApi,
158
- generateServices,
159
- generateConvexSchema(tableModules),
160
- ],
161
- { concurrency: "unbounded" },
162
- );
163
- yield* validateImplModules(leaves);
164
- yield* generateGroupRegisteredFunctions(leaves);
165
- yield* removeObsoleteRegisteredFunctions(leaves);
166
- const [functionPaths] = yield* Effect.all(
167
- [
168
- generateFunctionModules,
169
- generateConvexSchemaReexport,
170
- logGenerated(generateHttp),
171
- logGenerated(generateCrons),
172
- logGenerated(generateAuthConfig),
173
- ],
174
- { concurrency: "unbounded" },
175
- );
176
- yield* touchConvexSchema;
177
- return functionPaths;
178
- });
179
-
180
- const generateConfectGeneratedDirectory = Effect.gen(function* () {
181
- const fs = yield* FileSystem.FileSystem;
182
- const path = yield* Path.Path;
183
- const confectDirectory = yield* ConfectDirectory.get;
184
-
185
- if (!(yield* fs.exists(path.join(confectDirectory, "_generated")))) {
186
- yield* fs.makeDirectory(path.join(confectDirectory, "_generated"), {
187
- recursive: true,
188
- });
189
- yield* logFileAdded(path.join(confectDirectory, "_generated") + "/");
190
- }
191
- });
192
-
193
- const loadAndValidateLeafModules = Effect.gen(function* () {
194
- const fs = yield* FileSystem.FileSystem;
195
- const path = yield* Path.Path;
196
- const confectDirectory = yield* ConfectDirectory.get;
197
- const specFiles = yield* discoverLeafSpecFiles;
198
-
199
- const results = yield* Effect.forEach(specFiles, (specRelativePath) =>
200
- Effect.gen(function* () {
201
- const leaf = yield* toLeafModule(specRelativePath);
202
- const groupSpec = yield* validateSpec(leaf);
203
-
204
- const implRelativePath = yield* implPathForSpec(specRelativePath);
205
- const implAbsolutePath = path.join(confectDirectory, implRelativePath);
206
- if (!(yield* fs.exists(implAbsolutePath))) {
207
- return yield* new MissingImplFileError({
208
- specPath: specRelativePath,
209
- expectedImplPath: implRelativePath,
210
- });
211
- }
212
-
213
- return { leaf, groupSpec };
214
- }),
215
- );
216
-
217
- yield* validateOrphanImpls(specFiles);
218
-
219
- const leaves = Array.map(results, ({ leaf }) => leaf);
220
- const groupSpecsByRelativePath = new Map(
221
- Array.map(results, ({ leaf, groupSpec }) => [leaf.relativePath, groupSpec]),
222
- );
223
-
224
- return { leaves, groupSpecsByRelativePath };
225
- });
226
-
227
- /**
228
- * Walk the assembly tree and fail with a {@link ParentChildNameCollisionError}
229
- * when a parent leaf declares a function or subgroup whose name matches a
230
- * sibling subdirectory spec's segment. Without this check the colliding
231
- * descendant would overwrite the parent's entry in the assembled
232
- * `GroupSpec.groups` map at runtime, surfacing as a confusing
233
- * `Refs.make` error rather than a codegen-time diagnostic.
234
- */
235
- export const validateNoParentChildNameCollisions = (
236
- leaves: ReadonlyArray<LeafModule>,
237
- groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
238
- ) =>
239
- Effect.gen(function* () {
240
- const { convex, node } = partitionByRuntime(leaves);
241
- const convexNodes = assemblyNodesFromLeaves(convex);
242
- const nodeNodes = assemblyNodesFromLeaves(
243
- Array.map(node, toNodeRegistryLeaf),
244
- );
245
- yield* Effect.forEach(convexNodes, (n) =>
246
- checkAssemblyNodeForCollisions(n, groupSpecsByRelativePath),
247
- );
248
- yield* Effect.forEach(nodeNodes, (n) =>
249
- checkAssemblyNodeForCollisions(n, groupSpecsByRelativePath),
250
- );
251
- });
252
-
253
- const checkAssemblyNodeForCollisions = (
254
- node: SpecAssemblyNode,
255
- groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
256
- ): Effect.Effect<void, ParentChildNameCollisionError> =>
257
- Effect.gen(function* () {
258
- yield* Option.match(node.importBinding, {
259
- onNone: () => Effect.void,
260
- onSome: (binding) =>
261
- Effect.gen(function* () {
262
- if (node.children.length === 0) return;
263
- const parentRelativePath = bindingToRelativeSpecPath(
264
- binding.importPath,
265
- );
266
- const parentGroupSpec =
267
- groupSpecsByRelativePath.get(parentRelativePath);
268
- if (parentGroupSpec === undefined) return;
269
- yield* Effect.forEach(node.children, (child) => {
270
- if (
271
- Object.prototype.hasOwnProperty.call(
272
- parentGroupSpec.functions,
273
- child.segment,
274
- )
275
- ) {
276
- return Effect.fail(
277
- new ParentChildNameCollisionError({
278
- parentSpecPath: parentRelativePath,
279
- childSpecPath: childRepresentativeSpecPath(child),
280
- collisionName: child.segment,
281
- collisionKind: "function",
282
- }),
283
- );
284
- }
285
- if (
286
- Object.prototype.hasOwnProperty.call(
287
- parentGroupSpec.groups,
288
- child.segment,
289
- )
290
- ) {
291
- return Effect.fail(
292
- new ParentChildNameCollisionError({
293
- parentSpecPath: parentRelativePath,
294
- childSpecPath: childRepresentativeSpecPath(child),
295
- collisionName: child.segment,
296
- collisionKind: "group",
297
- }),
298
- );
299
- }
300
- return Effect.void;
301
- });
302
- }),
303
- });
304
- yield* Effect.forEach(node.children, (child) =>
305
- checkAssemblyNodeForCollisions(child, groupSpecsByRelativePath),
306
- );
307
- });
308
-
309
- /**
310
- * `LeafModule.specImportPath` is the import path used from inside the
311
- * generated `_generated/spec.ts` (e.g. `"../notes.spec"`). Strip the
312
- * `../` prefix and re-add the `.ts` extension to recover the leaf's
313
- * confect-relative spec path used as the key in
314
- * `groupSpecsByRelativePath`.
315
- */
316
- const bindingToRelativeSpecPath = (importPath: string): string => {
317
- const withoutDotDot = importPath.startsWith("../")
318
- ? importPath.slice(3)
319
- : importPath;
320
- return `${withoutDotDot}.ts`;
321
- };
322
-
323
- /**
324
- * A child assembly node may itself be a parent without a leaf (when the
325
- * actual leaves live only in deeper subdirectories). In that case we
326
- * surface the first descendant leaf as a representative path so the
327
- * error message points at something the user actually wrote.
328
- */
329
- const childRepresentativeSpecPath = (node: SpecAssemblyNode): string => {
330
- if (Option.isSome(node.importBinding)) {
331
- return bindingToRelativeSpecPath(node.importBinding.value.importPath);
332
- }
333
- for (const child of node.children) {
334
- return childRepresentativeSpecPath(child);
335
- }
336
- return node.segment;
337
- };
338
-
339
- const validateOrphanImpls = (specFiles: ReadonlyArray<string>) =>
340
- Effect.gen(function* () {
341
- const fs = yield* FileSystem.FileSystem;
342
- const path = yield* Path.Path;
343
- const confectDirectory = yield* ConfectDirectory.get;
344
- const implFiles = yield* discoverLeafImplFiles;
345
- const specPaths = new Set(specFiles);
346
-
347
- yield* Effect.forEach(implFiles, (implRelativePath) =>
348
- Effect.gen(function* () {
349
- const specRelativePath = yield* specPathForImpl(implRelativePath);
350
- if (specPaths.has(specRelativePath)) {
351
- return;
352
- }
353
-
354
- const specAbsolutePath = path.join(confectDirectory, specRelativePath);
355
- if (!(yield* fs.exists(specAbsolutePath))) {
356
- return yield* new MissingSpecFileError({
357
- implPath: implRelativePath,
358
- expectedSpecPath: specRelativePath,
359
- });
360
- }
361
- }),
362
- );
363
- });
364
-
365
- const removeLegacyFiles = Effect.gen(function* () {
366
- const fs = yield* FileSystem.FileSystem;
367
- const path = yield* Path.Path;
368
- const confectDirectory = yield* ConfectDirectory.get;
369
- const legacyPaths = yield* LEGACY_PATHS;
370
-
371
- yield* Effect.forEach(legacyPaths, (relativePath) =>
372
- Effect.gen(function* () {
373
- const absolutePath = path.join(confectDirectory, relativePath);
374
- if (yield* fs.exists(absolutePath)) {
375
- yield* removePathIfExists(absolutePath);
376
- yield* logFileRemoved(absolutePath);
377
- }
378
- }),
379
- );
380
- });
381
-
382
- const generateAssembledSpecs = (leaves: ReadonlyArray<LeafModule>) =>
383
- Effect.gen(function* () {
384
- const path = yield* Path.Path;
385
- const confectDirectory = yield* ConfectDirectory.get;
386
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
387
- const generatedNodeSpecPath = yield* GENERATED_NODE_SPEC_PATH;
388
- const { convex, node } = partitionByRuntime(leaves);
389
-
390
- if (convex.length > 0) {
391
- const nodes = assemblyNodesFromLeaves(convex);
392
- const specContents = yield* templates.assembledSpec({
393
- nodes,
394
- runtime: "Convex",
395
- });
396
- yield* writeFileStringAndLog(
397
- path.join(confectDirectory, generatedSpecPath),
398
- specContents,
399
- );
400
- }
401
-
402
- if (node.length > 0) {
403
- const nodes = assemblyNodesFromLeaves(
404
- Array.map(node, toNodeRegistryLeaf),
405
- );
406
- const nodeSpecContents = yield* templates.assembledSpec({
407
- nodes,
408
- runtime: "Node",
409
- });
410
- yield* writeFileStringAndLog(
411
- path.join(confectDirectory, generatedNodeSpecPath),
412
- nodeSpecContents,
413
- );
414
- }
415
- });
416
-
417
- const validateImplModules = (leaves: ReadonlyArray<LeafModule>) =>
418
- Effect.forEach(leaves, validateImpl);
419
-
420
- const generateGroupRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
421
- Effect.gen(function* () {
422
- const path = yield* Path.Path;
423
- const confectDirectory = yield* ConfectDirectory.get;
424
-
425
- yield* Effect.forEach(leaves, (leaf) =>
426
- Effect.gen(function* () {
427
- const registryRelativePath =
428
- yield* registeredFunctionsRelativePath(leaf);
429
- const registryPath = path.join(
430
- confectDirectory,
431
- "_generated",
432
- registryRelativePath,
433
- );
434
- const registryDir = path.dirname(registryPath);
435
- const fs = yield* FileSystem.FileSystem;
436
- if (!(yield* fs.exists(registryDir))) {
437
- yield* fs.makeDirectory(registryDir, { recursive: true });
438
- }
439
-
440
- const implRelativePath = yield* implPathForSpec(leaf.relativePath);
441
- const schemaImportPath = yield* toModuleImportPath(
442
- path.relative(
443
- path.dirname(registryPath),
444
- path.join(confectDirectory, "_generated", "schema.ts"),
445
- ),
446
- );
447
- // The group's own leaf spec (sibling of its impl), referenced
448
- // type-only by the registry to shape its returned record.
449
- const specImportPath = yield* toModuleImportPath(
450
- path.relative(
451
- path.dirname(registryPath),
452
- path.join(confectDirectory, leaf.relativePath),
453
- ),
454
- );
455
- const implImportPath = yield* toModuleImportPath(
456
- path.relative(
457
- path.dirname(registryPath),
458
- path.join(confectDirectory, implRelativePath),
459
- ),
460
- );
461
-
462
- const contents = yield* templates.registeredFunctionsForGroup({
463
- schemaImportPath,
464
- specImportPath,
465
- implImportPath,
466
- layerExportName: leaf.exportName,
467
- useNode: leaf.runtime === "Node",
468
- });
469
-
470
- yield* writeFileStringAndLog(registryPath, contents);
471
- }),
472
- );
473
- });
474
-
475
- const removeObsoleteRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
476
- Effect.gen(function* () {
477
- const fs = yield* FileSystem.FileSystem;
478
- const path = yield* Path.Path;
479
- const confectDirectory = yield* ConfectDirectory.get;
480
- const registryRoot = path.join(
481
- confectDirectory,
482
- "_generated",
483
- "registeredFunctions",
484
- );
485
-
486
- if (!(yield* fs.exists(registryRoot))) {
487
- return;
488
- }
489
-
490
- const expected = new Set(
491
- yield* Effect.forEach(leaves, (leaf) =>
492
- registeredFunctionsRelativePath(leaf),
493
- ),
494
- );
495
-
496
- const existing = yield* fs.readDirectory(registryRoot, { recursive: true });
497
- yield* Effect.forEach(existing, (relativePath) => {
498
- if (path.extname(relativePath) !== ".ts") {
499
- return Effect.void;
500
- }
501
- const normalized = path.join("registeredFunctions", relativePath);
502
- if (!expected.has(normalized)) {
503
- return Effect.gen(function* () {
504
- const absolutePath = path.join(registryRoot, relativePath);
505
- if (yield* fs.exists(absolutePath)) {
506
- yield* removePathIfExists(absolutePath);
507
- yield* logFileRemoved(absolutePath);
508
- }
509
- });
510
- }
511
- return Effect.void;
512
- });
513
- });
514
-
515
- const getGeneratedSpecPath = Effect.gen(function* () {
516
- const path = yield* Path.Path;
517
- const confectDirectory = yield* ConfectDirectory.get;
518
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
519
- return path.join(confectDirectory, generatedSpecPath);
520
- });
521
-
522
- const getGeneratedNodeSpecPath = Effect.gen(function* () {
523
- const path = yield* Path.Path;
524
- const confectDirectory = yield* ConfectDirectory.get;
525
- const generatedNodeSpecPath = yield* GENERATED_NODE_SPEC_PATH;
526
- return path.join(confectDirectory, generatedNodeSpecPath);
527
- });
528
-
529
- const loadGeneratedSpec = Effect.gen(function* () {
530
- const specPath = yield* getGeneratedSpecPath;
531
- const { module: specModule } = yield* Bundler.bundle(specPath);
532
- const spec = specModule.default;
533
-
534
- if (!Spec.isConvexSpec(spec)) {
535
- return yield* Effect.dieMessage(
536
- "_generated/spec.ts does not export a valid Convex Spec",
537
- );
538
- }
539
-
540
- return spec;
541
- });
542
-
543
- const loadGeneratedNodeSpec = Effect.gen(function* () {
544
- const fs = yield* FileSystem.FileSystem;
545
- const nodeSpecPath = yield* getGeneratedNodeSpecPath;
546
-
547
- if (!(yield* fs.exists(nodeSpecPath))) {
548
- return Option.none<Spec.AnyWithPropsWithRuntime<"Node">>();
549
- }
550
-
551
- const { module: nodeSpecModule } = yield* Bundler.bundle(nodeSpecPath);
552
- const nodeSpec = nodeSpecModule.default;
553
-
554
- if (!Spec.isNodeSpec(nodeSpec)) {
555
- return yield* Effect.dieMessage(
556
- "_generated/nodeSpec.ts does not export a valid Node Spec",
557
- );
558
- }
559
-
560
- return Option.some(nodeSpec);
561
- });
562
-
563
- const emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());
564
-
565
- export const loadPreviousFunctionPaths = Effect.gen(function* () {
566
- const fs = yield* FileSystem.FileSystem;
567
- const specPath = yield* getGeneratedSpecPath;
568
-
569
- if (!(yield* fs.exists(specPath))) {
570
- return emptyFunctionPaths;
571
- }
572
-
573
- const specEither = yield* loadGeneratedSpec.pipe(Effect.either);
574
-
575
- return yield* Either.match(specEither, {
576
- onLeft: () => Effect.succeed(emptyFunctionPaths),
577
- onRight: (spec) =>
578
- Effect.gen(function* () {
579
- const nodeSpecOption = yield* loadGeneratedNodeSpec;
580
- const mergedSpec = Option.match(nodeSpecOption, {
581
- onNone: () => spec,
582
- onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),
583
- });
584
- return FunctionPaths.make(mergedSpec);
585
- }),
586
- });
587
- });
588
-
589
- /**
590
- * Remove a now-obsolete `_generated/<name>.ts` if present (and log it), for
591
- * projects upgrading from a version that still emitted it.
592
- */
593
- const removeObsoleteGeneratedFile = (fileName: string) =>
594
- Effect.gen(function* () {
595
- const fs = yield* FileSystem.FileSystem;
596
- const path = yield* Path.Path;
597
- const confectDirectory = yield* ConfectDirectory.get;
598
- const filePath = path.join(confectDirectory, "_generated", fileName);
599
-
600
- if (yield* fs.exists(filePath)) {
601
- yield* removePathIfExists(filePath);
602
- yield* logFileRemoved(filePath);
603
- }
604
- });
605
-
606
- // `_generated/api.ts` is no longer imported by generated or impl code: impls
607
- // take the database schema (`_generated/schema`) directly, and per-group
608
- // registries reference the spec type-only. Remove any stale copy.
609
- const removeGeneratedApi = removeObsoleteGeneratedFile("api.ts");
610
-
611
- // `_generated/nodeApi.ts` is obsolete for the same reason.
612
- const removeGeneratedNodeApi = removeObsoleteGeneratedFile("nodeApi.ts");
613
-
614
- const generateFunctionModules = Effect.gen(function* () {
615
- const spec = yield* loadGeneratedSpec;
616
- const nodeSpecOption = yield* loadGeneratedNodeSpec;
617
-
618
- const mergedSpec = Option.match(nodeSpecOption, {
619
- onNone: () => spec,
620
- onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),
621
- });
622
-
623
- return yield* generateFunctions(mergedSpec);
624
- });
625
-
626
- /**
627
- * The user-authored `confect/schema.ts` is no longer supported: codegen now
628
- * owns both `_generated/schema.ts` (runtime) and `_generated/convexSchema.ts`
629
- * (deploy), derived from a single scan of `confect/tables/*.ts`. Detect a
630
- * stray file and fail with a clear migration message — leaving it in place
631
- * would silently shadow the codegen-owned `_generated/schema.ts` /
632
- * `_generated/convexSchema.ts`.
633
- */
634
- const rejectLegacySchemaFile = Effect.gen(function* () {
635
- const fs = yield* FileSystem.FileSystem;
636
- const path = yield* Path.Path;
637
- const confectDirectory = yield* ConfectDirectory.get;
638
- const legacyPath = path.join(confectDirectory, "schema.ts");
639
-
640
- if (yield* fs.exists(legacyPath)) {
641
- return yield* new LegacySchemaFileError({ schemaPath: "schema.ts" });
642
- }
643
- });
644
-
645
- /**
646
- * Surface a yellow `⚠` warning when codegen sees no tables — either the
647
- * `confect/tables/` directory is missing or it contains no `.ts` files.
648
- * Generation still succeeds (emitting an empty `DatabaseSchema` and
649
- * `defineSchema({})`), since action-only / table-free Confect backends
650
- * are legal — but the warning catches the much more common case of a
651
- * typoed directory or files placed under the wrong root.
652
- */
653
- const warnIfNoTables = (
654
- tableModules: ReadonlyArray<TableModule.TableModule>,
655
- ) =>
656
- tableModules.length === 0
657
- ? logWarn(
658
- `No tables discovered in \`confect/${TableModule.TABLES_DIRNAME}/\`. ` +
659
- `Generating an empty schema; add a \`Table.make(...)\` module under that ` +
660
- `directory unless this backend is intentionally tables-free.`,
661
- )
662
- : Effect.void;
663
-
664
- const tableModuleBindings = (
665
- tableModules: ReadonlyArray<TableModule.TableModule>,
666
- generatedFilePath: string,
667
- ) =>
668
- Effect.gen(function* () {
669
- const path = yield* Path.Path;
670
- const confectDirectory = yield* ConfectDirectory.get;
671
- const generatedDir = path.dirname(generatedFilePath);
672
-
673
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
674
-
675
- return yield* Effect.forEach(tableModules, (tm) =>
676
- Effect.gen(function* () {
677
- const wrapperAbsolutePath = path.join(
678
- confectDirectory,
679
- generatedTablesDirname,
680
- `${tm.tableName}.ts`,
681
- );
682
- const importPath = yield* toModuleImportPath(
683
- path.relative(generatedDir, wrapperAbsolutePath),
684
- );
685
- return {
686
- importPath,
687
- tableName: tm.tableName,
688
- };
689
- }),
690
- );
691
- });
692
-
693
- const generateIdConstructor = (
694
- tableModules: ReadonlyArray<TableModule.TableModule>,
695
- ) =>
696
- Effect.gen(function* () {
697
- const path = yield* Path.Path;
698
- const confectDirectory = yield* ConfectDirectory.get;
699
- const generatedIdPath = yield* GENERATED_ID_PATH;
700
- const idPath = path.join(confectDirectory, generatedIdPath);
701
-
702
- const tableNames = tableModules.map((tm) => tm.tableName);
703
- const contents = yield* templates.id({ tableNames });
704
-
705
- yield* writeFileStringAndLog(idPath, contents);
706
- });
707
-
708
- const generateTableWrappers = (
709
- tableModules: ReadonlyArray<TableModule.TableModule>,
710
- ) =>
711
- Effect.gen(function* () {
712
- const fs = yield* FileSystem.FileSystem;
713
- const path = yield* Path.Path;
714
- const confectDirectory = yield* ConfectDirectory.get;
715
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
716
- const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
717
-
718
- if (!(yield* fs.exists(wrappersDir))) {
719
- yield* fs.makeDirectory(wrappersDir, { recursive: true });
720
- }
721
-
722
- yield* Effect.forEach(
723
- tableModules,
724
- (tm) =>
725
- Effect.gen(function* () {
726
- const wrapperPath = path.join(
727
- confectDirectory,
728
- generatedTablesDirname,
729
- `${tm.tableName}.ts`,
730
- );
731
- const unnamedAbsolutePath = path.join(
732
- confectDirectory,
733
- tm.relativePath,
734
- );
735
- const unnamedImportPath = yield* toModuleImportPath(
736
- path.relative(path.dirname(wrapperPath), unnamedAbsolutePath),
737
- );
738
- const contents = yield* templates.tableWrapper({
739
- tableName: tm.tableName,
740
- unnamedImportPath,
741
- });
742
- yield* writeFileStringAndLog(wrapperPath, contents);
743
- }),
744
- { concurrency: "unbounded" },
745
- );
746
- });
747
-
748
- /**
749
- * Remove any stale `_generated/tables/*.ts` wrapper whose source table
750
- * has been deleted or renamed. Mirrors `removeObsoleteRegisteredFunctions`
751
- * for the wrapper directory.
752
- */
753
- const removeObsoleteTableWrappers = (
754
- tableModules: ReadonlyArray<TableModule.TableModule>,
755
- ) =>
756
- Effect.gen(function* () {
757
- const fs = yield* FileSystem.FileSystem;
758
- const path = yield* Path.Path;
759
- const confectDirectory = yield* ConfectDirectory.get;
760
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
761
- const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
762
-
763
- if (!(yield* fs.exists(wrappersDir))) {
764
- return;
765
- }
766
-
767
- const expected = new Set(tableModules.map((tm) => `${tm.tableName}.ts`));
768
- const existing = yield* fs.readDirectory(wrappersDir, { recursive: true });
769
- yield* Effect.forEach(existing, (entry) => {
770
- if (path.extname(entry) !== ".ts") {
771
- return Effect.void;
772
- }
773
- if (!expected.has(entry)) {
774
- return Effect.gen(function* () {
775
- const absolutePath = path.join(wrappersDir, entry);
776
- if (yield* fs.exists(absolutePath)) {
777
- yield* removePathIfExists(absolutePath);
778
- yield* logFileRemoved(absolutePath);
779
- }
780
- });
781
- }
782
- return Effect.void;
783
- });
784
- });
785
-
786
- const generateRuntimeSchema = (
787
- tableModules: ReadonlyArray<TableModule.TableModule>,
788
- ) =>
789
- Effect.gen(function* () {
790
- const path = yield* Path.Path;
791
- const confectDirectory = yield* ConfectDirectory.get;
792
- const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
793
- const schemaPath = path.join(confectDirectory, generatedSchemaPath);
794
-
795
- const bindings = yield* tableModuleBindings(tableModules, schemaPath);
796
- const contents = yield* templates.runtimeSchema({ tableModules: bindings });
797
-
798
- yield* writeFileStringAndLog(schemaPath, contents);
799
- });
800
-
801
- const generateConvexSchema = (
802
- tableModules: ReadonlyArray<TableModule.TableModule>,
803
- ) =>
804
- Effect.gen(function* () {
805
- const path = yield* Path.Path;
806
- const confectDirectory = yield* ConfectDirectory.get;
807
- const generatedConvexSchemaPath = yield* GENERATED_CONVEX_SCHEMA_PATH;
808
- const convexSchemaPath = path.join(
809
- confectDirectory,
810
- generatedConvexSchemaPath,
811
- );
812
-
813
- const bindings = yield* tableModuleBindings(tableModules, convexSchemaPath);
814
- const contents = yield* templates.convexSchema({ tableModules: bindings });
815
-
816
- yield* writeFileStringAndLog(convexSchemaPath, contents);
817
- });
818
-
819
- const generateConvexSchemaReexport = Effect.gen(function* () {
820
- const path = yield* Path.Path;
821
- const confectDirectory = yield* ConfectDirectory.get;
822
- const convexDirectory = yield* ConvexDirectory.get;
823
- const generatedConvexSchemaRelativePath = yield* GENERATED_CONVEX_SCHEMA_PATH;
824
-
825
- const convexSchemaPath = path.join(convexDirectory, "schema.ts");
826
- const generatedConvexSchemaPath = path.join(
827
- confectDirectory,
828
- generatedConvexSchemaRelativePath,
829
- );
830
-
831
- const convexSchemaImportPath = yield* toModuleImportPath(
832
- path.relative(path.dirname(convexSchemaPath), generatedConvexSchemaPath),
833
- );
834
-
835
- const schemaContents = yield* templates.schema({
836
- convexSchemaImportPath,
837
- });
838
-
839
- yield* writeFileStringAndLog(convexSchemaPath, schemaContents);
840
- });
841
-
842
- const generateServices = Effect.gen(function* () {
843
- const path = yield* Path.Path;
844
- const confectDirectory = yield* ConfectDirectory.get;
845
-
846
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
847
-
848
- const servicesPath = path.join(confectGeneratedDirectory, "services.ts");
849
- const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
850
- const schemaImportPath = yield* toModuleImportPath(
851
- path.relative(
852
- path.dirname(servicesPath),
853
- path.join(confectDirectory, generatedSchemaPath),
854
- ),
855
- );
856
-
857
- const servicesContentsString = yield* templates.services({
858
- schemaImportPath,
859
- });
860
-
861
- yield* writeFileStringAndLog(servicesPath, servicesContentsString);
862
- });
863
-
864
- const generateRefs = Effect.gen(function* () {
865
- const fs = yield* FileSystem.FileSystem;
866
- const path = yield* Path.Path;
867
- const confectDirectory = yield* ConfectDirectory.get;
868
-
869
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
870
- const refsPath = path.join(confectGeneratedDirectory, "refs.ts");
871
- const refsDir = path.dirname(refsPath);
872
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
873
-
874
- const specImportPath = yield* toModuleImportPath(
875
- path.relative(refsDir, path.join(confectDirectory, generatedSpecPath)),
876
- );
877
-
878
- const nodeSpecPath = yield* getGeneratedNodeSpecPath;
879
- const nodeSpecExists = yield* fs.exists(nodeSpecPath);
880
- const nodeSpecImportPath = nodeSpecExists
881
- ? Option.some(
882
- yield* toModuleImportPath(path.relative(refsDir, nodeSpecPath)),
883
- )
884
- : Option.none<string>();
885
-
886
- const refsContents = yield* templates.refs({
887
- specImportPath,
888
- nodeSpecImportPath,
889
- });
890
-
891
- yield* writeFileStringAndLog(refsPath, refsContents);
892
- });
893
-
894
- const logGenerated = (effect: typeof generateHttp) =>
895
- effect.pipe(
896
- Effect.tap(
897
- Option.match({
898
- onNone: () => Effect.void,
899
- onSome: ({ change, convexFilePath }) =>
900
- Match.value(change).pipe(
901
- Match.when("Added", () => logFileAdded(convexFilePath)),
902
- Match.when("Modified", () => logFileModified(convexFilePath)),
903
- Match.when("Unchanged", () => Effect.void),
904
- Match.exhaustive,
905
- ),
906
- }),
907
- ),
908
- );