@confect/cli 9.0.0-next.9 → 9.0.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.
package/src/utils.ts DELETED
@@ -1,435 +0,0 @@
1
- import type { FunctionSpec, Spec } from "@confect/core";
2
- import * as FileSystem from "@effect/platform/FileSystem";
3
- import * as Path from "@effect/platform/Path";
4
- import type { PlatformError } from "@effect/platform/Error";
5
- import { pipe } from "effect/Function";
6
- import * as Array from "effect/Array";
7
- import * as Context from "effect/Context";
8
- import * as Effect from "effect/Effect";
9
- import * as HashSet from "effect/HashSet";
10
- import * as Option from "effect/Option";
11
- import * as Order from "effect/Order";
12
- import * as Record from "effect/Record";
13
- import * as Ref from "effect/Ref";
14
- import * as String from "effect/String";
15
- import * as FunctionPaths from "./FunctionPaths";
16
- import * as GroupPath from "./GroupPath";
17
- import * as GroupPaths from "./GroupPaths";
18
- import { logFileAdded, logFileModified, logFileRemoved } from "./log";
19
- import { ConfectDirectory } from "./ConfectDirectory";
20
- import { ConvexDirectory } from "./ConvexDirectory";
21
- import * as templates from "./templates";
22
-
23
- /**
24
- * Tracks whether the current codegen run wrote anything to disk. Set to
25
- * `true` by every helper that actually overwrites a file (skipping the
26
- * content-unchanged case). `codegenHandler` provides a fresh `Ref` per
27
- * run and reads it back to report `anyWritesHappened`; callers outside a
28
- * codegen pass hit the cached default `Ref` and need not interact with
29
- * the tracker.
30
- */
31
- export class WriteTracker extends Context.Reference<WriteTracker>()(
32
- "@confect/cli/WriteTracker",
33
- { defaultValue: () => Ref.unsafeMake(false) },
34
- ) {}
35
-
36
- const markWritten = Effect.gen(function* () {
37
- const tracker = yield* WriteTracker;
38
- yield* Ref.set(tracker, true);
39
- });
40
-
41
- export const removePathExtension = (pathStr: string) =>
42
- Effect.gen(function* () {
43
- const path = yield* Path.Path;
44
-
45
- return String.slice(0, -path.extname(pathStr).length)(pathStr);
46
- });
47
-
48
- /** Ensures a relative path is a valid ESM/TS module specifier (e.g. `spec` → `./spec`). */
49
- export const toModuleImportPath = (relativePath: string) =>
50
- Effect.gen(function* () {
51
- const withoutExt = yield* removePathExtension(relativePath);
52
- return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
53
- });
54
-
55
- export const writeFileStringAndLog = (filePath: string, contents: string) =>
56
- Effect.gen(function* () {
57
- const fs = yield* FileSystem.FileSystem;
58
- if (!(yield* fs.exists(filePath))) {
59
- yield* fs.writeFileString(filePath, contents);
60
- yield* markWritten;
61
- yield* logFileAdded(filePath);
62
- return;
63
- }
64
- const existing = yield* fs.readFileString(filePath);
65
- if (existing !== contents) {
66
- yield* fs.writeFileString(filePath, contents);
67
- yield* markWritten;
68
- yield* logFileModified(filePath);
69
- }
70
- });
71
-
72
- export const findProjectRoot = Effect.gen(function* () {
73
- const fs = yield* FileSystem.FileSystem;
74
- const path = yield* Path.Path;
75
-
76
- const startDir = path.resolve(".");
77
- const root = path.parse(startDir).root;
78
-
79
- const directories = Array.unfold(startDir, (dir) =>
80
- dir === root
81
- ? Option.none()
82
- : Option.some([dir, path.dirname(dir)] as const),
83
- );
84
-
85
- const projectRoot = yield* Effect.findFirst(directories, (dir) =>
86
- fs.exists(path.join(dir, "package.json")),
87
- );
88
-
89
- return Option.getOrElse(projectRoot, () => startDir);
90
- });
91
-
92
- export type WriteChange = "Added" | "Modified" | "Unchanged";
93
-
94
- export const writeFileString = (
95
- filePath: string,
96
- contents: string,
97
- ): Effect.Effect<WriteChange, PlatformError, FileSystem.FileSystem> =>
98
- Effect.gen(function* () {
99
- const fs = yield* FileSystem.FileSystem;
100
-
101
- if (!(yield* fs.exists(filePath))) {
102
- yield* fs.writeFileString(filePath, contents);
103
- yield* markWritten;
104
- return "Added";
105
- }
106
- const existing = yield* fs.readFileString(filePath);
107
- if (existing !== contents) {
108
- yield* fs.writeFileString(filePath, contents);
109
- yield* markWritten;
110
- return "Modified";
111
- }
112
- return "Unchanged";
113
- });
114
-
115
- export const removePathIfExists = (
116
- filePath: string,
117
- ): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>
118
- Effect.gen(function* () {
119
- const fs = yield* FileSystem.FileSystem;
120
-
121
- if (!(yield* fs.exists(filePath))) {
122
- return;
123
- }
124
-
125
- yield* fs
126
- .remove(filePath)
127
- .pipe(
128
- Effect.catchTag("SystemError", (error) =>
129
- error.reason === "NotFound" ? Effect.void : Effect.fail(error),
130
- ),
131
- );
132
- });
133
-
134
- /**
135
- * Bump the mtime of `convex/schema.ts` so the Convex CLI's chokidar watcher
136
- * emits a `change` event after every successful Confect codegen run. Without
137
- * this, a codegen that doesn't change any file content (for example,
138
- * recovering from a transient broken state in `confect/`) leaves Convex
139
- * stuck on its previous error because nothing it observes has changed.
140
- */
141
- export const touchConvexSchema = Effect.gen(function* () {
142
- const fs = yield* FileSystem.FileSystem;
143
- const path = yield* Path.Path;
144
- const convexDirectory = yield* ConvexDirectory.get;
145
- const schemaPath = path.join(convexDirectory, "schema.ts");
146
-
147
- if (!(yield* fs.exists(schemaPath))) {
148
- return;
149
- }
150
-
151
- const now = new Date();
152
- yield* fs.utimes(schemaPath, now, now);
153
- });
154
-
155
- export const generateGroupModule = ({
156
- groupPath,
157
- functionNames,
158
- registeredFunctionsImportPath,
159
- useNode = false,
160
- }: {
161
- groupPath: GroupPath.GroupPath;
162
- functionNames: string[];
163
- registeredFunctionsImportPath: string;
164
- useNode?: boolean;
165
- }) =>
166
- Effect.gen(function* () {
167
- const fs = yield* FileSystem.FileSystem;
168
- const path = yield* Path.Path;
169
- const convexDirectory = yield* ConvexDirectory.get;
170
-
171
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
172
- const modulePath = path.join(convexDirectory, relativeModulePath);
173
-
174
- const directoryPath = path.dirname(modulePath);
175
- if (!(yield* fs.exists(directoryPath))) {
176
- yield* fs.makeDirectory(directoryPath, { recursive: true });
177
- }
178
-
179
- const functionsContentsString = yield* templates.functions({
180
- functionNames,
181
- registeredFunctionsImportPath,
182
- useNode,
183
- });
184
-
185
- if (!(yield* fs.exists(modulePath))) {
186
- yield* fs.writeFileString(modulePath, functionsContentsString);
187
- yield* markWritten;
188
- return "Added" as const;
189
- }
190
- const existing = yield* fs.readFileString(modulePath);
191
- if (existing !== functionsContentsString) {
192
- yield* fs.writeFileString(modulePath, functionsContentsString);
193
- yield* markWritten;
194
- return "Modified" as const;
195
- }
196
- return "Unchanged" as const;
197
- });
198
-
199
- /**
200
- * Compute the module import specifier (relative to `modulePath`) for a group's
201
- * registry file under `confect/_generated/registeredFunctions/`. The registry
202
- * path mirrors the group's path one-to-one (see `registeredFunctionsRelativePath`
203
- * in `LeafModule.ts`) for both Convex and Node groups. Centralizing this here
204
- * keeps the "overlapping" and "new" group branches of `generateFunctions` from
205
- * drifting apart.
206
- */
207
- const registeredFunctionsImportPathForGroup = (
208
- groupPath: GroupPath.GroupPath,
209
- modulePath: string,
210
- ) =>
211
- Effect.gen(function* () {
212
- const path = yield* Path.Path;
213
- const confectDirectory = yield* ConfectDirectory.get;
214
-
215
- const registeredFunctionsPath =
216
- path.join(
217
- confectDirectory,
218
- "_generated",
219
- "registeredFunctions",
220
- ...groupPath.pathSegments,
221
- ) + ".ts";
222
-
223
- return yield* toModuleImportPath(
224
- path.relative(path.dirname(modulePath), registeredFunctionsPath),
225
- );
226
- });
227
-
228
- const logGroupPaths = <R>(
229
- groupPaths: GroupPaths.GroupPaths,
230
- logFn: (fullPath: string) => Effect.Effect<void, never, R>,
231
- ) =>
232
- Effect.gen(function* () {
233
- const path = yield* Path.Path;
234
- const convexDirectory = yield* ConvexDirectory.get;
235
-
236
- yield* Effect.forEach(groupPaths, (gp) =>
237
- Effect.gen(function* () {
238
- const relativeModulePath = yield* GroupPath.modulePath(gp);
239
- yield* logFn(path.join(convexDirectory, relativeModulePath));
240
- }),
241
- );
242
- });
243
-
244
- export const generateFunctions = (spec: Spec.AnyWithProps) =>
245
- Effect.gen(function* () {
246
- const path = yield* Path.Path;
247
- const convexDirectory = yield* ConvexDirectory.get;
248
-
249
- const groupPathsFromFs = yield* getGroupPathsFromFs;
250
- const functionPaths = FunctionPaths.make(spec);
251
- const groupPathsFromSpec = FunctionPaths.groupPaths(functionPaths);
252
-
253
- const overlappingGroupPaths = GroupPaths.GroupPaths.make(
254
- HashSet.intersection(groupPathsFromFs, groupPathsFromSpec),
255
- );
256
- yield* Effect.forEach(overlappingGroupPaths, (groupPath) =>
257
- Effect.gen(function* () {
258
- const group = yield* GroupPath.getGroupSpec(spec, groupPath);
259
- const functionNames = pipe(
260
- group.functions,
261
- Record.values,
262
- Array.sortBy(
263
- Order.mapInput(
264
- Order.string,
265
- (fn: FunctionSpec.AnyWithProps) => fn.name,
266
- ),
267
- ),
268
- Array.map((fn) => fn.name),
269
- );
270
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
271
- const modulePath = path.join(convexDirectory, relativeModulePath);
272
- const registeredFunctionsImportPath =
273
- yield* registeredFunctionsImportPathForGroup(groupPath, modulePath);
274
- const result = yield* generateGroupModule({
275
- groupPath,
276
- functionNames,
277
- registeredFunctionsImportPath,
278
- useNode: group.runtime === "Node",
279
- });
280
- if (result === "Modified") {
281
- yield* logFileModified(modulePath);
282
- }
283
- }),
284
- );
285
-
286
- const extinctGroupPaths = GroupPaths.GroupPaths.make(
287
- HashSet.difference(groupPathsFromFs, groupPathsFromSpec),
288
- );
289
- yield* removeGroups(extinctGroupPaths);
290
- yield* logGroupPaths(extinctGroupPaths, logFileRemoved);
291
-
292
- const newGroupPaths = GroupPaths.GroupPaths.make(
293
- HashSet.difference(groupPathsFromSpec, groupPathsFromFs),
294
- );
295
- yield* writeGroups(spec, newGroupPaths);
296
- yield* logGroupPaths(newGroupPaths, logFileAdded);
297
-
298
- return functionPaths;
299
- });
300
-
301
- const getGroupPathsFromFs = Effect.gen(function* () {
302
- const fs = yield* FileSystem.FileSystem;
303
- const path = yield* Path.Path;
304
- const convexDirectory = yield* ConvexDirectory.get;
305
-
306
- const RESERVED_CONVEX_TS_FILE_NAMES = new Set([
307
- "schema.ts",
308
- "http.ts",
309
- "crons.ts",
310
- "auth.config.ts",
311
- "convex.config.ts",
312
- ]);
313
-
314
- const allConvexPaths = yield* fs.readDirectory(convexDirectory, {
315
- recursive: true,
316
- });
317
- const groupPathArray = yield* pipe(
318
- allConvexPaths,
319
- Array.filter(
320
- (convexPath) =>
321
- path.extname(convexPath) === ".ts" &&
322
- !RESERVED_CONVEX_TS_FILE_NAMES.has(path.basename(convexPath)) &&
323
- path.basename(path.dirname(convexPath)) !== "_generated",
324
- ),
325
- Effect.forEach((groupModulePath) =>
326
- GroupPath.fromGroupModulePath(groupModulePath),
327
- ),
328
- );
329
- return pipe(groupPathArray, HashSet.fromIterable, GroupPaths.GroupPaths.make);
330
- });
331
-
332
- export const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>
333
- Effect.gen(function* () {
334
- const path = yield* Path.Path;
335
- const convexDirectory = yield* ConvexDirectory.get;
336
-
337
- yield* Effect.all(
338
- HashSet.map(groupPaths, (groupPath) =>
339
- Effect.gen(function* () {
340
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
341
- const modulePath = path.join(convexDirectory, relativeModulePath);
342
-
343
- yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);
344
-
345
- yield* removePathIfExists(modulePath);
346
- yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);
347
- }),
348
- ),
349
- { concurrency: "unbounded" },
350
- );
351
- });
352
-
353
- export const writeGroups = (
354
- spec: Spec.AnyWithProps,
355
- groupPaths: GroupPaths.GroupPaths,
356
- ) =>
357
- Effect.forEach(groupPaths, (groupPath) =>
358
- Effect.gen(function* () {
359
- const path = yield* Path.Path;
360
- const convexDirectory = yield* ConvexDirectory.get;
361
- const group = yield* GroupPath.getGroupSpec(spec, groupPath);
362
-
363
- const functionNames = pipe(
364
- group.functions,
365
- Record.values,
366
- Array.sortBy(
367
- Order.mapInput(
368
- Order.string,
369
- (fn: FunctionSpec.AnyWithProps) => fn.name,
370
- ),
371
- ),
372
- Array.map((fn) => fn.name),
373
- );
374
-
375
- const relativeModulePath = yield* GroupPath.modulePath(groupPath);
376
- const modulePath = path.join(convexDirectory, relativeModulePath);
377
- const registeredFunctionsImportPath =
378
- yield* registeredFunctionsImportPathForGroup(groupPath, modulePath);
379
-
380
- yield* Effect.logDebug(`Generating group ${groupPath}...`);
381
- yield* generateGroupModule({
382
- groupPath,
383
- functionNames,
384
- registeredFunctionsImportPath,
385
- useNode: group.runtime === "Node",
386
- });
387
- yield* Effect.logDebug(`Group ${groupPath} generated`);
388
- }),
389
- );
390
-
391
- const generateOptionalFile = (
392
- confectFile: string,
393
- convexFile: string,
394
- generateContents: (importPath: string) => Effect.Effect<string>,
395
- ) =>
396
- Effect.gen(function* () {
397
- const fs = yield* FileSystem.FileSystem;
398
- const path = yield* Path.Path;
399
- const confectDirectory = yield* ConfectDirectory.get;
400
- const convexDirectory = yield* ConvexDirectory.get;
401
-
402
- const confectFilePath = path.join(confectDirectory, confectFile);
403
-
404
- if (!(yield* fs.exists(confectFilePath))) {
405
- return Option.none();
406
- }
407
-
408
- const convexFilePath = path.join(convexDirectory, convexFile);
409
- const relativeImportPath = path.relative(
410
- path.dirname(convexFilePath),
411
- confectFilePath,
412
- );
413
- const importPathWithoutExt = yield* removePathExtension(relativeImportPath);
414
- const contents = yield* generateContents(importPathWithoutExt);
415
- const change = yield* writeFileString(convexFilePath, contents);
416
- return Option.some({ change, convexFilePath });
417
- });
418
-
419
- export const generateHttp = generateOptionalFile(
420
- "http.ts",
421
- "http.ts",
422
- (importPath) => templates.http({ httpImportPath: importPath }),
423
- );
424
-
425
- export const generateCrons = generateOptionalFile(
426
- "crons.ts",
427
- "crons.ts",
428
- (importPath) => templates.crons({ cronsImportPath: importPath }),
429
- );
430
-
431
- export const generateAuthConfig = generateOptionalFile(
432
- "auth.ts",
433
- "auth.config.ts",
434
- (importPath) => templates.authConfig({ authImportPath: importPath }),
435
- );