@confect/cli 1.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +7 -0
  3. package/dist/FunctionPath.mjs +19 -0
  4. package/dist/FunctionPath.mjs.map +1 -0
  5. package/dist/FunctionPaths.mjs +41 -0
  6. package/dist/FunctionPaths.mjs.map +1 -0
  7. package/dist/GroupPath.mjs +52 -0
  8. package/dist/GroupPath.mjs.map +1 -0
  9. package/dist/GroupPaths.mjs +9 -0
  10. package/dist/GroupPaths.mjs.map +1 -0
  11. package/dist/_virtual/rolldown_runtime.mjs +28 -0
  12. package/dist/cliApp.mjs +13 -0
  13. package/dist/cliApp.mjs.map +1 -0
  14. package/dist/confect/codegen.mjs +111 -0
  15. package/dist/confect/codegen.mjs.map +1 -0
  16. package/dist/confect/dev.mjs +253 -0
  17. package/dist/confect/dev.mjs.map +1 -0
  18. package/dist/confect.mjs +14 -0
  19. package/dist/confect.mjs.map +1 -0
  20. package/dist/index.d.mts +1 -0
  21. package/dist/index.mjs +19 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/log.mjs +25 -0
  24. package/dist/log.mjs.map +1 -0
  25. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeCommandExecutor.mjs +12 -0
  26. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeCommandExecutor.mjs.map +1 -0
  27. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeFileSystem.mjs +15 -0
  28. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeFileSystem.mjs.map +1 -0
  29. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodePath.mjs +25 -0
  30. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodePath.mjs.map +1 -0
  31. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeRuntime.mjs +12 -0
  32. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeRuntime.mjs.map +1 -0
  33. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeTerminal.mjs +17 -0
  34. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/NodeTerminal.mjs.map +1 -0
  35. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/commandExecutor.mjs +129 -0
  36. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/commandExecutor.mjs.map +1 -0
  37. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/error.mjs +43 -0
  38. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/error.mjs.map +1 -0
  39. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/fileSystem.mjs +329 -0
  40. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/fileSystem.mjs.map +1 -0
  41. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/path.mjs +51 -0
  42. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/path.mjs.map +1 -0
  43. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/runtime.mjs +31 -0
  44. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/runtime.mjs.map +1 -0
  45. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/sink.mjs +24 -0
  46. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/sink.mjs.map +1 -0
  47. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/stream.mjs +91 -0
  48. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/stream.mjs.map +1 -0
  49. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/terminal.mjs +75 -0
  50. package/dist/node_modules/.pnpm/@effect_platform-node-shared@0.53.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effe_e0eeb3aae5ffec3060feb87d17ffb17c/node_modules/@effect/platform-node-shared/dist/esm/internal/terminal.mjs.map +1 -0
  51. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeContext.mjs +21 -0
  52. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeContext.mjs.map +1 -0
  53. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeFileSystem.mjs +15 -0
  54. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeFileSystem.mjs.map +1 -0
  55. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeRuntime.mjs +15 -0
  56. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeRuntime.mjs.map +1 -0
  57. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeWorker.mjs +27 -0
  58. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/NodeWorker.mjs.map +1 -0
  59. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/internal/worker.mjs +71 -0
  60. package/dist/node_modules/.pnpm/@effect_platform-node@0.100.0_@effect_cluster@0.52.9_@effect_platform@0.93.2_effect@3.1_a15ca1802d939cd85cc564105ef862ac/node_modules/@effect/platform-node/dist/esm/internal/worker.mjs.map +1 -0
  61. package/dist/packages/cli/package.mjs +6 -0
  62. package/dist/packages/cli/package.mjs.map +1 -0
  63. package/dist/packages/core/dist/Spec.mjs +23 -0
  64. package/dist/packages/core/dist/Spec.mjs.map +1 -0
  65. package/dist/packages/core/dist/_virtual/rolldown_runtime.mjs +14 -0
  66. package/dist/packages/core/dist/_virtual/rolldown_runtime.mjs.map +1 -0
  67. package/dist/services/ConfectDirectory.mjs +31 -0
  68. package/dist/services/ConfectDirectory.mjs.map +1 -0
  69. package/dist/services/ConvexDirectory.mjs +41 -0
  70. package/dist/services/ConvexDirectory.mjs.map +1 -0
  71. package/dist/services/ProjectRoot.mjs +35 -0
  72. package/dist/services/ProjectRoot.mjs.map +1 -0
  73. package/dist/templates.mjs +204 -0
  74. package/dist/templates.mjs.map +1 -0
  75. package/dist/utils.mjs +162 -0
  76. package/dist/utils.mjs.map +1 -0
  77. package/package.json +86 -0
  78. package/src/FunctionPath.ts +28 -0
  79. package/src/FunctionPaths.ts +103 -0
  80. package/src/GroupPath.ts +117 -0
  81. package/src/GroupPaths.ts +7 -0
  82. package/src/cliApp.ts +8 -0
  83. package/src/confect/codegen.ts +228 -0
  84. package/src/confect/dev.ts +611 -0
  85. package/src/confect.ts +19 -0
  86. package/src/index.ts +22 -0
  87. package/src/log.ts +106 -0
  88. package/src/services/ConfectDirectory.ts +41 -0
  89. package/src/services/ConvexDirectory.ts +67 -0
  90. package/src/services/ProjectRoot.ts +49 -0
  91. package/src/templates.ts +380 -0
  92. package/src/utils.ts +332 -0
@@ -0,0 +1,611 @@
1
+ import { Spec } from "@confect/core";
2
+ import { Command } from "@effect/cli";
3
+ import { FileSystem, Path } from "@effect/platform";
4
+ import { Ansi, AnsiDoc } from "@effect/printer-ansi";
5
+ import {
6
+ Array,
7
+ Console,
8
+ Data,
9
+ Duration,
10
+ Effect,
11
+ Equal,
12
+ HashSet,
13
+ Match,
14
+ Option,
15
+ pipe,
16
+ Queue,
17
+ Ref,
18
+ Schema,
19
+ Stream,
20
+ String,
21
+ } from "effect";
22
+ import type { ReadonlyRecord } from "effect/Record";
23
+ import * as esbuild from "esbuild";
24
+ import * as tsx from "tsx/esm/api";
25
+ import type * as FunctionPath from "../FunctionPath";
26
+ import * as FunctionPaths from "../FunctionPaths";
27
+ import * as GroupPath from "../GroupPath";
28
+ import { logCompleted, logFailed } from "../log";
29
+ import { ConfectDirectory } from "../services/ConfectDirectory";
30
+ import { ConvexDirectory } from "../services/ConvexDirectory";
31
+ import { ProjectRoot } from "../services/ProjectRoot";
32
+ import {
33
+ generateAuthConfig,
34
+ generateConvexConfig,
35
+ generateCrons,
36
+ generateHttp,
37
+ removeGroups,
38
+ writeGroups,
39
+ } from "../utils";
40
+ import { codegenHandler } from "./codegen";
41
+
42
+ type Pending = {
43
+ readonly specDirty: boolean;
44
+ readonly httpDirty: boolean;
45
+ readonly appDirty: boolean;
46
+ readonly cronsDirty: boolean;
47
+ readonly authDirty: boolean;
48
+ };
49
+
50
+ const pendingInit: Pending = {
51
+ specDirty: false,
52
+ httpDirty: false,
53
+ appDirty: false,
54
+ cronsDirty: false,
55
+ authDirty: false,
56
+ };
57
+
58
+ type FileChange = Data.TaggedEnum<{
59
+ OptionalFile: {
60
+ readonly change: "Added" | "Removed" | "Modified";
61
+ readonly filePath: string;
62
+ };
63
+ GroupModule: {
64
+ readonly change: "Added" | "Removed" | "Modified";
65
+ readonly filePath: string;
66
+ readonly functionsAdded: ReadonlyArray<FunctionPath.FunctionPath>;
67
+ readonly functionsRemoved: ReadonlyArray<FunctionPath.FunctionPath>;
68
+ };
69
+ }>;
70
+
71
+ const FileChange = Data.taggedEnum<FileChange>();
72
+
73
+ const logChangeReport = (changes: ReadonlyArray<FileChange>) =>
74
+ Effect.gen(function* () {
75
+ yield* logCompleted("Generated files are up-to-date");
76
+
77
+ yield* Effect.when(
78
+ Effect.forEach(changes, (change) =>
79
+ FileChange.$match(change, {
80
+ OptionalFile: ({ change: c, filePath }) =>
81
+ logFileChangeIndented(c, filePath),
82
+ GroupModule: ({
83
+ change: c,
84
+ filePath,
85
+ functionsAdded,
86
+ functionsRemoved,
87
+ }) =>
88
+ Effect.gen(function* () {
89
+ yield* logFileChangeIndented(c, filePath);
90
+ yield* Effect.forEach(functionsAdded, logFunctionAddedIndented);
91
+ yield* Effect.forEach(
92
+ functionsRemoved,
93
+ logFunctionRemovedIndented,
94
+ );
95
+ }),
96
+ }),
97
+ ),
98
+ () => Array.isNonEmptyReadonlyArray(changes),
99
+ );
100
+ });
101
+
102
+ const changeChar = (change: "Added" | "Removed" | "Modified") =>
103
+ Match.value(change).pipe(
104
+ Match.when("Added", () => ({ char: "+", color: Ansi.green })),
105
+ Match.when("Removed", () => ({ char: "-", color: Ansi.red })),
106
+ Match.when("Modified", () => ({ char: "~", color: Ansi.yellow })),
107
+ Match.exhaustive,
108
+ );
109
+
110
+ const logFileChangeIndented = (
111
+ change: "Added" | "Removed" | "Modified",
112
+ fullPath: string,
113
+ ) =>
114
+ Effect.gen(function* () {
115
+ const projectRoot = yield* ProjectRoot.get;
116
+ const path = yield* Path.Path;
117
+
118
+ const prefix = projectRoot + path.sep;
119
+ const suffix = pipe(fullPath, String.startsWith(prefix))
120
+ ? pipe(fullPath, String.slice(prefix.length))
121
+ : fullPath;
122
+
123
+ const { char, color } = changeChar(change);
124
+
125
+ yield* Console.log(
126
+ pipe(
127
+ AnsiDoc.text(" "),
128
+ AnsiDoc.cat(pipe(AnsiDoc.char(char), AnsiDoc.annotate(color))),
129
+ AnsiDoc.catWithSpace(
130
+ AnsiDoc.hcat([
131
+ pipe(AnsiDoc.text(prefix), AnsiDoc.annotate(Ansi.blackBright)),
132
+ pipe(AnsiDoc.text(suffix), AnsiDoc.annotate(color)),
133
+ ]),
134
+ ),
135
+ AnsiDoc.render({ style: "pretty" }),
136
+ ),
137
+ );
138
+ });
139
+
140
+ const logFunctionAddedIndented = (functionPath: FunctionPath.FunctionPath) =>
141
+ Console.log(
142
+ pipe(
143
+ AnsiDoc.text(" "),
144
+ AnsiDoc.cat(pipe(AnsiDoc.char("+"), AnsiDoc.annotate(Ansi.green))),
145
+ AnsiDoc.catWithSpace(
146
+ AnsiDoc.hcat([
147
+ pipe(
148
+ AnsiDoc.text(GroupPath.toString(functionPath.groupPath) + "."),
149
+ AnsiDoc.annotate(Ansi.blackBright),
150
+ ),
151
+ pipe(AnsiDoc.text(functionPath.name), AnsiDoc.annotate(Ansi.green)),
152
+ ]),
153
+ ),
154
+ AnsiDoc.render({ style: "pretty" }),
155
+ ),
156
+ );
157
+
158
+ const logFunctionRemovedIndented = (functionPath: FunctionPath.FunctionPath) =>
159
+ Console.log(
160
+ pipe(
161
+ AnsiDoc.text(" "),
162
+ AnsiDoc.cat(pipe(AnsiDoc.char("-"), AnsiDoc.annotate(Ansi.red))),
163
+ AnsiDoc.catWithSpace(
164
+ AnsiDoc.hcat([
165
+ pipe(
166
+ AnsiDoc.text(GroupPath.toString(functionPath.groupPath) + "."),
167
+ AnsiDoc.annotate(Ansi.blackBright),
168
+ ),
169
+ pipe(AnsiDoc.text(functionPath.name), AnsiDoc.annotate(Ansi.red)),
170
+ ]),
171
+ ),
172
+ AnsiDoc.render({ style: "pretty" }),
173
+ ),
174
+ );
175
+
176
+ export const dev = Command.make("dev", {}, () =>
177
+ Effect.gen(function* () {
178
+ const initialFunctionPaths = yield* codegenHandler;
179
+
180
+ const pendingRef = yield* Ref.make<Pending>(pendingInit);
181
+ const signal = yield* Queue.sliding<void>(1);
182
+
183
+ yield* Effect.all(
184
+ [
185
+ specFileWatcher(signal, pendingRef),
186
+ confectDirectoryWatcher(signal, pendingRef),
187
+ syncLoop(signal, pendingRef, initialFunctionPaths),
188
+ ],
189
+ { concurrency: "unbounded" },
190
+ );
191
+ }),
192
+ ).pipe(Command.withDescription("Start the Confect development server"));
193
+
194
+ const syncLoop = (
195
+ signal: Queue.Queue<void>,
196
+ pendingRef: Ref.Ref<Pending>,
197
+ initialFunctionPaths: FunctionPaths.FunctionPaths,
198
+ ) =>
199
+ Effect.gen(function* () {
200
+ const functionPathsRef = yield* Ref.make(initialFunctionPaths);
201
+ const changesRef = yield* Ref.make<ReadonlyArray<FileChange>>([]);
202
+
203
+ return yield* Effect.forever(
204
+ Effect.gen(function* () {
205
+ yield* Effect.logDebug("Running sync loop...");
206
+ yield* Queue.take(signal);
207
+
208
+ const pending = yield* Ref.getAndSet(pendingRef, pendingInit);
209
+
210
+ const specResult: Option.Option<ReadonlyArray<FileChange>> =
211
+ yield* Effect.if(pending.specDirty, {
212
+ onTrue: () =>
213
+ loadSpec.pipe(
214
+ Effect.andThen(
215
+ Effect.fn(function* (spec) {
216
+ yield* Effect.logDebug("Spec loaded");
217
+
218
+ const previous = yield* Ref.get(functionPathsRef);
219
+
220
+ const path = yield* Path.Path;
221
+ const convexDirectory = yield* ConvexDirectory.get;
222
+
223
+ const current = FunctionPaths.make(spec);
224
+ const {
225
+ functionsAdded,
226
+ functionsRemoved,
227
+ groupsRemoved,
228
+ groupsAdded,
229
+ groupsChanged,
230
+ } = FunctionPaths.diff(previous, current);
231
+
232
+ // Removed groups
233
+ yield* removeGroups(groupsRemoved);
234
+ const removedChanges = yield* Effect.forEach(
235
+ groupsRemoved,
236
+ (gp) =>
237
+ Effect.gen(function* () {
238
+ const relativeModulePath =
239
+ yield* GroupPath.modulePath(gp);
240
+ return FileChange.GroupModule({
241
+ change: "Removed",
242
+ filePath: path.join(
243
+ convexDirectory,
244
+ relativeModulePath,
245
+ ),
246
+ functionsAdded: [],
247
+ functionsRemoved: Array.fromIterable(
248
+ HashSet.filter(functionsRemoved, (fp) =>
249
+ Equal.equals(fp.groupPath, gp),
250
+ ),
251
+ ),
252
+ });
253
+ }),
254
+ );
255
+
256
+ // Added groups
257
+ yield* writeGroups(spec, groupsAdded);
258
+ const addedChanges = yield* Effect.forEach(
259
+ groupsAdded,
260
+ (gp) =>
261
+ Effect.gen(function* () {
262
+ const relativeModulePath =
263
+ yield* GroupPath.modulePath(gp);
264
+ return FileChange.GroupModule({
265
+ change: "Added",
266
+ filePath: path.join(
267
+ convexDirectory,
268
+ relativeModulePath,
269
+ ),
270
+ functionsAdded: Array.fromIterable(
271
+ HashSet.filter(functionsAdded, (fp) =>
272
+ Equal.equals(fp.groupPath, gp),
273
+ ),
274
+ ),
275
+ functionsRemoved: [],
276
+ });
277
+ }),
278
+ );
279
+
280
+ // Changed groups
281
+ yield* writeGroups(spec, groupsChanged);
282
+ const changedChanges = yield* Effect.forEach(
283
+ groupsChanged,
284
+ (gp) =>
285
+ Effect.gen(function* () {
286
+ const relativeModulePath =
287
+ yield* GroupPath.modulePath(gp);
288
+ return FileChange.GroupModule({
289
+ change: "Modified",
290
+ filePath: path.join(
291
+ convexDirectory,
292
+ relativeModulePath,
293
+ ),
294
+ functionsAdded: Array.fromIterable(
295
+ HashSet.filter(functionsAdded, (fp) =>
296
+ Equal.equals(fp.groupPath, gp),
297
+ ),
298
+ ),
299
+ functionsRemoved: Array.fromIterable(
300
+ HashSet.filter(functionsRemoved, (fp) =>
301
+ Equal.equals(fp.groupPath, gp),
302
+ ),
303
+ ),
304
+ });
305
+ }),
306
+ );
307
+
308
+ yield* Ref.set(functionPathsRef, current);
309
+
310
+ return Option.some([
311
+ ...removedChanges,
312
+ ...addedChanges,
313
+ ...changedChanges,
314
+ ]);
315
+ }),
316
+ ),
317
+ Effect.catchTag("SpecImportFailedError", () =>
318
+ logFailed("Spec import failed").pipe(
319
+ Effect.as(Option.none()),
320
+ ),
321
+ ),
322
+ Effect.catchTag("SpecFileDoesNotExportSpecError", () =>
323
+ logFailed("Spec file does not default export a spec").pipe(
324
+ Effect.as(Option.none()),
325
+ ),
326
+ ),
327
+ ),
328
+ onFalse: () => Effect.succeed(Option.some([])),
329
+ });
330
+
331
+ const specChanges = Option.getOrElse(specResult, () => []);
332
+
333
+ const dirtyOptionalFiles = [
334
+ ...(pending.httpDirty
335
+ ? [syncOptionalFile(generateHttp, "http.ts")]
336
+ : []),
337
+ ...(pending.appDirty
338
+ ? [syncOptionalFile(generateConvexConfig, "convex.config.ts")]
339
+ : []),
340
+ ...(pending.cronsDirty
341
+ ? [syncOptionalFile(generateCrons, "crons.ts")]
342
+ : []),
343
+ ...(pending.authDirty
344
+ ? [syncOptionalFile(generateAuthConfig, "auth.config.ts")]
345
+ : []),
346
+ ];
347
+
348
+ const optionalChanges: ReadonlyArray<FileChange> =
349
+ Array.isNonEmptyReadonlyArray(dirtyOptionalFiles)
350
+ ? yield* pipe(
351
+ Effect.all(dirtyOptionalFiles, {
352
+ concurrency: "unbounded",
353
+ }),
354
+ Effect.map(Array.getSomes),
355
+ )
356
+ : [];
357
+
358
+ yield* Ref.update(changesRef, (prev) => [
359
+ ...prev,
360
+ ...specChanges,
361
+ ...optionalChanges,
362
+ ]);
363
+
364
+ yield* Option.match(specResult, {
365
+ onSome: () =>
366
+ Effect.gen(function* () {
367
+ const pendingSize = yield* Queue.size(signal);
368
+ yield* Effect.when(
369
+ Effect.gen(function* () {
370
+ const allChanges = yield* Ref.getAndSet(changesRef, []);
371
+ yield* logChangeReport(allChanges);
372
+ }),
373
+ () => pendingSize === 0,
374
+ );
375
+ }),
376
+ onNone: () => Ref.set(changesRef, []),
377
+ });
378
+ }),
379
+ );
380
+ });
381
+
382
+ const loadSpec = Effect.gen(function* () {
383
+ const path = yield* Path.Path;
384
+ const specPathUrl = yield* path.toFileUrl(yield* getSpecPath);
385
+ const specModule = yield* Effect.tryPromise({
386
+ try: () => tsx.tsImport(specPathUrl.href, import.meta.url),
387
+ catch: (error) => new SpecImportFailedError({ error }),
388
+ });
389
+ const spec = specModule.default;
390
+
391
+ if (Spec.isSpec(spec)) {
392
+ return spec;
393
+ } else {
394
+ return yield* Effect.fail(new SpecFileDoesNotExportSpecError());
395
+ }
396
+ });
397
+
398
+ const getSpecPath = Effect.gen(function* () {
399
+ const path = yield* Path.Path;
400
+ const confectDirectory = yield* ConfectDirectory.get;
401
+
402
+ return path.join(confectDirectory, "spec.ts");
403
+ });
404
+
405
+ const specFileWatcher = (
406
+ signal: Queue.Queue<void>,
407
+ pendingRef: Ref.Ref<Pending>,
408
+ ) =>
409
+ Effect.gen(function* () {
410
+ const specPath = yield* getSpecPath;
411
+
412
+ const specChanges: Stream.Stream<void> = Stream.asyncPush(
413
+ (emit) =>
414
+ Effect.acquireRelease(
415
+ Effect.promise(async () => {
416
+ const ctx = await esbuild.context({
417
+ entryPoints: [specPath],
418
+ bundle: true,
419
+ write: false,
420
+ metafile: true,
421
+ platform: "node",
422
+ format: "esm",
423
+ logLevel: "silent",
424
+ external: [
425
+ "@confect/core",
426
+ "@confect/server",
427
+ "effect",
428
+ "@effect/*",
429
+ ],
430
+ plugins: [
431
+ {
432
+ name: "notify-rebuild",
433
+ setup(build) {
434
+ build.onEnd((result) => {
435
+ if (result.errors.length === 0) {
436
+ emit.single();
437
+ } else {
438
+ Effect.runPromise(
439
+ Effect.gen(function* () {
440
+ yield* logFailed("Build errors");
441
+ const formattedMessages = yield* Effect.promise(
442
+ () =>
443
+ esbuild.formatMessages(result.errors, {
444
+ kind: "error",
445
+ color: true,
446
+ terminalWidth: 80,
447
+ }),
448
+ );
449
+ const output = formatBuildErrors(
450
+ result.errors,
451
+ formattedMessages,
452
+ );
453
+ yield* Console.error("\n" + output + "\n");
454
+ }),
455
+ );
456
+ }
457
+ });
458
+ },
459
+ },
460
+ ],
461
+ });
462
+
463
+ await ctx.watch();
464
+
465
+ return ctx;
466
+ }),
467
+ (ctx) =>
468
+ Effect.promise(() => ctx.dispose()).pipe(
469
+ Effect.tap(() => Effect.logDebug("esbuild watcher disposed")),
470
+ ),
471
+ ),
472
+ { bufferSize: 1, strategy: "sliding" },
473
+ );
474
+
475
+ yield* pipe(
476
+ specChanges,
477
+ Stream.debounce(Duration.millis(200)),
478
+ Stream.runForEach(() =>
479
+ Ref.update(pendingRef, (pending) => ({
480
+ ...pending,
481
+ specDirty: true,
482
+ })).pipe(Effect.andThen(Queue.offer(signal, undefined))),
483
+ ),
484
+ );
485
+ });
486
+
487
+ const formatBuildError = (
488
+ error: esbuild.Message | undefined,
489
+ formattedMessage: string,
490
+ ): string => {
491
+ const lines = String.split(formattedMessage, "\n");
492
+ const redErrorText = pipe(
493
+ AnsiDoc.text(error?.text ?? ""),
494
+ AnsiDoc.annotate(Ansi.red),
495
+ AnsiDoc.render({ style: "pretty" }),
496
+ );
497
+ const replaced = pipe(
498
+ Array.findFirstIndex(lines, (l) => pipe(l, String.trim, String.isNonEmpty)),
499
+ Option.match({
500
+ onNone: () => lines,
501
+ onSome: (idx) => Array.modify(lines, idx, () => redErrorText),
502
+ }),
503
+ );
504
+ return pipe(
505
+ replaced,
506
+ Array.map((l) => (pipe(l, String.trim, String.isNonEmpty) ? ` ${l}` : l)),
507
+ Array.join("\n"),
508
+ );
509
+ };
510
+
511
+ const formatBuildErrors = (
512
+ errors: readonly esbuild.Message[],
513
+ formattedMessages: readonly string[],
514
+ ): string =>
515
+ pipe(
516
+ formattedMessages,
517
+ Array.map((message, i) => formatBuildError(errors[i], message)),
518
+ Array.join(""),
519
+ String.trimEnd,
520
+ );
521
+
522
+ export class SpecFileDoesNotExportSpecError extends Schema.TaggedError<SpecFileDoesNotExportSpecError>(
523
+ "SpecFileDoesNotExportSpecError",
524
+ )("SpecFileDoesNotExportSpecError", {}) {}
525
+
526
+ export class SpecImportFailedError extends Schema.TaggedError<SpecImportFailedError>(
527
+ "SpecImportFailedError",
528
+ )("SpecImportFailedError", {
529
+ error: Schema.Unknown,
530
+ }) {}
531
+
532
+ const syncOptionalFile = (generate: typeof generateHttp, convexFile: string) =>
533
+ pipe(
534
+ generate,
535
+ Effect.andThen(
536
+ Option.match({
537
+ onSome: ({ change, convexFilePath }) =>
538
+ Match.value(change).pipe(
539
+ Match.when("Unchanged", () => Effect.succeed(Option.none())),
540
+ Match.whenOr("Added", "Modified", (addedOrModified) =>
541
+ Effect.succeed(
542
+ Option.some(
543
+ FileChange.OptionalFile({
544
+ change: addedOrModified,
545
+ filePath: convexFilePath,
546
+ }),
547
+ ),
548
+ ),
549
+ ),
550
+ Match.exhaustive,
551
+ ),
552
+ onNone: () =>
553
+ Effect.gen(function* () {
554
+ const fs = yield* FileSystem.FileSystem;
555
+ const path = yield* Path.Path;
556
+ const convexDirectory = yield* ConvexDirectory.get;
557
+ const convexFilePath = path.join(convexDirectory, convexFile);
558
+
559
+ if (yield* fs.exists(convexFilePath)) {
560
+ yield* fs.remove(convexFilePath);
561
+
562
+ return Option.some(
563
+ FileChange.OptionalFile({
564
+ change: "Removed",
565
+ filePath: convexFilePath,
566
+ }),
567
+ );
568
+ } else {
569
+ return Option.none();
570
+ }
571
+ }),
572
+ }),
573
+ ),
574
+ );
575
+
576
+ const optionalConfectFiles: ReadonlyRecord<string, keyof Pending> = {
577
+ "http.ts": "httpDirty",
578
+ "app.ts": "appDirty",
579
+ "crons.ts": "cronsDirty",
580
+ "auth.ts": "authDirty",
581
+ };
582
+
583
+ const confectDirectoryWatcher = (
584
+ signal: Queue.Queue<void>,
585
+ pendingRef: Ref.Ref<Pending>,
586
+ ) =>
587
+ Effect.gen(function* () {
588
+ const fs = yield* FileSystem.FileSystem;
589
+ const confectDirectory = yield* ConfectDirectory.get;
590
+
591
+ yield* pipe(
592
+ fs.watch(confectDirectory),
593
+ Stream.runForEach((event) =>
594
+ pipe(
595
+ Option.fromNullable(optionalConfectFiles[event.path]),
596
+ Option.match({
597
+ onNone: () => Effect.void,
598
+ onSome: (pendingKey) =>
599
+ pipe(
600
+ pendingRef,
601
+ Ref.update((pending) => ({
602
+ ...pending,
603
+ [pendingKey]: true,
604
+ })),
605
+ Effect.andThen(Queue.offer(signal, undefined)),
606
+ ),
607
+ }),
608
+ ),
609
+ ),
610
+ );
611
+ });
package/src/confect.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Command } from "@effect/cli";
2
+ import { Layer } from "effect";
3
+ import { codegen } from "./confect/codegen";
4
+ import { dev } from "./confect/dev";
5
+ import { ConfectDirectory } from "./services/ConfectDirectory";
6
+ import { ConvexDirectory } from "./services/ConvexDirectory";
7
+ import { ProjectRoot } from "./services/ProjectRoot";
8
+
9
+ export const confect = Command.make("confect").pipe(
10
+ Command.withDescription("Generate and sync Confect files with Convex"),
11
+ Command.withSubcommands([codegen, dev]),
12
+ Command.provide(
13
+ Layer.mergeAll(
14
+ ConfectDirectory.Default,
15
+ ProjectRoot.Default,
16
+ ConvexDirectory.Default,
17
+ ),
18
+ ),
19
+ );
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
+ import { Effect } from "effect";
5
+ import { cliApp } from "./cliApp";
6
+
7
+ // Track if we received SIGINT so we can re-raise it after cleanup.
8
+ // This ensures proper terminal state restoration when run via e.g. `pnpm`.
9
+ let interrupted = false;
10
+ process.prependListener("SIGINT", () => {
11
+ interrupted = true;
12
+ });
13
+ process.on("exit", () => {
14
+ if (interrupted) {
15
+ process.kill(process.pid, "SIGINT");
16
+ }
17
+ });
18
+
19
+ cliApp(process.argv).pipe(
20
+ Effect.provide(NodeContext.layer),
21
+ NodeRuntime.runMain,
22
+ );