@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.
- package/CHANGELOG.md +57 -1
- package/dist/BuildError.mjs +101 -0
- package/dist/BuildError.mjs.map +1 -0
- package/dist/Bundler.mjs +91 -0
- package/dist/Bundler.mjs.map +1 -0
- package/dist/CodeBlockWriter.mjs +55 -0
- package/dist/CodeBlockWriter.mjs.map +1 -0
- package/dist/CodegenError.mjs +101 -0
- package/dist/CodegenError.mjs.map +1 -0
- package/dist/FunctionPaths.mjs +1 -1
- package/dist/LeafModule.mjs +170 -0
- package/dist/LeafModule.mjs.map +1 -0
- package/dist/SpecAssemblyNode.mjs +33 -0
- package/dist/SpecAssemblyNode.mjs.map +1 -0
- package/dist/confect/codegen.mjs +292 -72
- package/dist/confect/codegen.mjs.map +1 -1
- package/dist/confect/dev.mjs +234 -180
- package/dist/confect/dev.mjs.map +1 -1
- package/dist/log.mjs +13 -1
- package/dist/log.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/templates.mjs +69 -72
- package/dist/templates.mjs.map +1 -1
- package/dist/utils.mjs +77 -73
- package/dist/utils.mjs.map +1 -1
- package/package.json +4 -3
- package/src/BuildError.ts +210 -0
- package/src/Bundler.ts +144 -0
- package/src/CodeBlockWriter.ts +65 -0
- package/src/CodegenError.ts +376 -0
- package/src/LeafModule.ts +317 -0
- package/src/SpecAssemblyNode.ts +82 -0
- package/src/confect/codegen.ts +511 -141
- package/src/confect/dev.ts +556 -435
- package/src/log.ts +21 -0
- package/src/templates.ts +146 -109
- package/src/utils.ts +118 -93
package/src/confect/dev.ts
CHANGED
|
@@ -1,64 +1,96 @@
|
|
|
1
|
-
import { Spec } from "@confect/core";
|
|
2
1
|
import { Command } from "@effect/cli";
|
|
3
2
|
import { FileSystem, Path } from "@effect/platform";
|
|
4
3
|
import { Ansi, AnsiDoc } from "@effect/printer-ansi";
|
|
5
4
|
import {
|
|
6
5
|
Array,
|
|
6
|
+
Chunk,
|
|
7
|
+
Clock,
|
|
7
8
|
Console,
|
|
8
|
-
Deferred,
|
|
9
9
|
Duration,
|
|
10
10
|
Effect,
|
|
11
11
|
Equal,
|
|
12
|
+
ExecutionStrategy,
|
|
13
|
+
Exit,
|
|
12
14
|
HashSet,
|
|
13
15
|
Match,
|
|
14
16
|
Option,
|
|
17
|
+
Order,
|
|
15
18
|
pipe,
|
|
16
19
|
Queue,
|
|
17
20
|
Ref,
|
|
18
|
-
|
|
21
|
+
Scope,
|
|
19
22
|
Stream,
|
|
20
23
|
String,
|
|
21
24
|
} from "effect";
|
|
22
|
-
import type { ReadonlyRecord } from "effect/Record";
|
|
23
25
|
import * as esbuild from "esbuild";
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import * as
|
|
27
|
-
import { logFailure, logPending, logSuccess } from "../log";
|
|
26
|
+
import { logCoalescedBuildErrors } from "../BuildError";
|
|
27
|
+
import { absoluteExternalsPlugin, EXTERNAL_PACKAGES } from "../Bundler";
|
|
28
|
+
import * as CodegenError from "../CodegenError";
|
|
28
29
|
import { ConfectDirectory } from "../ConfectDirectory";
|
|
29
30
|
import { ConvexDirectory } from "../ConvexDirectory";
|
|
30
|
-
import
|
|
31
|
+
import * as FunctionPaths from "../FunctionPaths";
|
|
32
|
+
import type * as GroupPaths from "../GroupPaths";
|
|
31
33
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
generateHttp,
|
|
37
|
-
removeGroups,
|
|
38
|
-
writeGroups,
|
|
39
|
-
} from "../utils";
|
|
34
|
+
discoverLeafImplFiles,
|
|
35
|
+
isLeafImplPath,
|
|
36
|
+
isLeafSpecPath,
|
|
37
|
+
} from "../LeafModule";
|
|
40
38
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
logFunctionAdded,
|
|
40
|
+
logFunctionRemoved,
|
|
41
|
+
logPending,
|
|
42
|
+
logSuccess,
|
|
43
|
+
} from "../log";
|
|
44
|
+
import { ProjectRoot } from "../ProjectRoot";
|
|
45
|
+
import { generateAuthConfig, generateCrons, generateHttp } from "../utils";
|
|
46
|
+
import { codegenHandler, loadPreviousFunctionPaths } from "./codegen";
|
|
47
|
+
|
|
48
|
+
const GENERATED_SPEC_PATH = "_generated/spec.ts";
|
|
49
|
+
const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
|
|
50
|
+
|
|
51
|
+
// Quiescence window: the sync loop waits this long for further signals
|
|
52
|
+
// after each batch. One user edit fires `onEnd` on every esbuild
|
|
53
|
+
// watcher that touches the file, and rebuild times vary across entry
|
|
54
|
+
// points so the onEnds can be spread over hundreds of milliseconds.
|
|
55
|
+
// The drain keeps extending its wait (bounded by `COALESCE_MAX_WAIT`)
|
|
56
|
+
// until no new signals arrive within the window, collapsing the whole
|
|
57
|
+
// burst into a single codegen cycle.
|
|
58
|
+
const COALESCE_QUIESCENCE = Duration.millis(300);
|
|
59
|
+
|
|
60
|
+
// Upper bound on `drainUntilQuiescent` so a pathological infinite
|
|
61
|
+
// signal stream can't pin the sync loop forever.
|
|
62
|
+
const COALESCE_MAX_WAIT = Duration.seconds(5);
|
|
63
|
+
|
|
64
|
+
// How long to wait for esbuild watchers to react to codegen's own
|
|
65
|
+
// writes (e.g. an updated `_generated/spec.ts`). Added on top of the
|
|
66
|
+
// quiescence drain when codegen reported writes happened.
|
|
67
|
+
const ECHO_COOLDOWN = Duration.millis(500);
|
|
68
|
+
|
|
69
|
+
const emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());
|
|
45
70
|
|
|
46
71
|
type Pending = {
|
|
47
72
|
readonly specDirty: boolean;
|
|
48
|
-
readonly nodeImplDirty: boolean;
|
|
49
73
|
readonly httpDirty: boolean;
|
|
50
74
|
readonly cronsDirty: boolean;
|
|
51
75
|
readonly authDirty: boolean;
|
|
52
76
|
};
|
|
53
77
|
|
|
78
|
+
type PendingKey = keyof Pending;
|
|
79
|
+
|
|
54
80
|
const pendingInit: Pending = {
|
|
55
81
|
specDirty: false,
|
|
56
|
-
nodeImplDirty: false,
|
|
57
82
|
httpDirty: false,
|
|
58
83
|
cronsDirty: false,
|
|
59
84
|
authDirty: false,
|
|
60
85
|
};
|
|
61
86
|
|
|
87
|
+
const isPendingDirty = (p: Pending): boolean =>
|
|
88
|
+
p.specDirty || p.httpDirty || p.cronsDirty || p.authDirty;
|
|
89
|
+
|
|
90
|
+
type WatcherErrors = ReadonlyMap<string, readonly esbuild.Message[]>;
|
|
91
|
+
|
|
92
|
+
const emptyWatcherErrors: WatcherErrors = new Map();
|
|
93
|
+
|
|
62
94
|
const changeChar = (change: "Added" | "Removed" | "Modified") =>
|
|
63
95
|
Match.value(change).pipe(
|
|
64
96
|
Match.when("Added", () => ({ char: "+", color: Ansi.green })),
|
|
@@ -97,211 +129,234 @@ const logFileChangeIndented = (
|
|
|
97
129
|
);
|
|
98
130
|
});
|
|
99
131
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
const logFunctionPathDiff = (
|
|
133
|
+
previous: FunctionPaths.FunctionPaths,
|
|
134
|
+
current: FunctionPaths.FunctionPaths,
|
|
135
|
+
) =>
|
|
136
|
+
Effect.gen(function* () {
|
|
137
|
+
const {
|
|
138
|
+
functionsAdded,
|
|
139
|
+
functionsRemoved,
|
|
140
|
+
groupsRemoved,
|
|
141
|
+
groupsAdded,
|
|
142
|
+
groupsChanged,
|
|
143
|
+
} = FunctionPaths.diff(previous, current);
|
|
144
|
+
|
|
145
|
+
const logForGroups = (
|
|
146
|
+
groupPaths: GroupPaths.GroupPaths,
|
|
147
|
+
fnPaths: FunctionPaths.FunctionPaths,
|
|
148
|
+
logFn: typeof logFunctionAdded,
|
|
149
|
+
) =>
|
|
150
|
+
Effect.forEach(groupPaths, (gp) =>
|
|
151
|
+
Effect.forEach(
|
|
152
|
+
Array.fromIterable(
|
|
153
|
+
HashSet.filter(fnPaths, (fp) => Equal.equals(fp.groupPath, gp)),
|
|
110
154
|
),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
AnsiDoc.render({ style: "pretty" }),
|
|
115
|
-
),
|
|
116
|
-
);
|
|
155
|
+
logFn,
|
|
156
|
+
),
|
|
157
|
+
);
|
|
117
158
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
AnsiDoc.annotate(Ansi.blackBright),
|
|
159
|
+
yield* logForGroups(groupsRemoved, functionsRemoved, logFunctionRemoved);
|
|
160
|
+
yield* logForGroups(groupsAdded, functionsAdded, logFunctionAdded);
|
|
161
|
+
yield* Effect.forEach(groupsChanged, (gp) =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
yield* Effect.forEach(
|
|
164
|
+
Array.fromIterable(
|
|
165
|
+
HashSet.filter(functionsAdded, (fp) =>
|
|
166
|
+
Equal.equals(fp.groupPath, gp),
|
|
167
|
+
),
|
|
128
168
|
),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
logFunctionAdded,
|
|
170
|
+
);
|
|
171
|
+
yield* Effect.forEach(
|
|
172
|
+
Array.fromIterable(
|
|
173
|
+
HashSet.filter(functionsRemoved, (fp) =>
|
|
174
|
+
Equal.equals(fp.groupPath, gp),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
logFunctionRemoved,
|
|
178
|
+
);
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
135
182
|
|
|
136
183
|
export const dev = Command.make("dev", {}, () =>
|
|
137
184
|
Effect.gen(function* () {
|
|
138
185
|
yield* logPending("Performing initial sync…");
|
|
139
|
-
const
|
|
186
|
+
const previousFunctionPaths = yield* loadPreviousFunctionPaths;
|
|
187
|
+
const initialResult = yield* codegenHandler.pipe(
|
|
188
|
+
Effect.tap(({ functionPaths }) =>
|
|
189
|
+
logFunctionPathDiff(previousFunctionPaths, functionPaths),
|
|
190
|
+
),
|
|
191
|
+
Effect.tap(() => logSuccess("Generated files are up-to-date")),
|
|
192
|
+
CodegenError.catchAndLog,
|
|
193
|
+
);
|
|
194
|
+
const initialFunctionPaths = Option.match(initialResult, {
|
|
195
|
+
onNone: () => emptyFunctionPaths,
|
|
196
|
+
onSome: ({ functionPaths }) => functionPaths,
|
|
197
|
+
});
|
|
140
198
|
|
|
141
199
|
const pendingRef = yield* Ref.make<Pending>(pendingInit);
|
|
142
200
|
const signal = yield* Queue.sliding<void>(1);
|
|
143
|
-
const
|
|
201
|
+
const restartQueue = yield* Queue.sliding<void>(1);
|
|
202
|
+
const watcherErrorsRef = yield* Ref.make<WatcherErrors>(emptyWatcherErrors);
|
|
144
203
|
|
|
145
204
|
yield* Effect.all(
|
|
146
205
|
[
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
206
|
+
Effect.scoped(
|
|
207
|
+
entryPointsWatcher(
|
|
208
|
+
signal,
|
|
209
|
+
pendingRef,
|
|
210
|
+
restartQueue,
|
|
211
|
+
watcherErrorsRef,
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
confectStructureWatcher(signal, pendingRef, restartQueue),
|
|
215
|
+
syncLoop(signal, pendingRef, initialFunctionPaths, watcherErrorsRef),
|
|
150
216
|
],
|
|
151
217
|
{ concurrency: "unbounded" },
|
|
152
218
|
);
|
|
153
219
|
}),
|
|
154
220
|
).pipe(Command.withDescription("Start the Confect development server"));
|
|
155
221
|
|
|
222
|
+
const esbuildMessageKey = (m: esbuild.Message): string =>
|
|
223
|
+
`${m.location?.file ?? ""}:${m.location?.line ?? ""}:${m.location?.column ?? ""}:${m.text}`;
|
|
224
|
+
|
|
225
|
+
const allMessages = (errors: WatcherErrors): ReadonlyArray<esbuild.Message> =>
|
|
226
|
+
pipe(Array.fromIterable(errors.values()), Array.flatten);
|
|
227
|
+
|
|
228
|
+
const dedupeWatcherErrors = (
|
|
229
|
+
errors: WatcherErrors,
|
|
230
|
+
): ReadonlyArray<esbuild.Message> =>
|
|
231
|
+
pipe(
|
|
232
|
+
allMessages(errors),
|
|
233
|
+
Array.dedupeWith(
|
|
234
|
+
(messageA, messageB) =>
|
|
235
|
+
esbuildMessageKey(messageA) === esbuildMessageKey(messageB),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const watcherErrorsSignature = (errors: WatcherErrors): string =>
|
|
240
|
+
pipe(
|
|
241
|
+
allMessages(errors),
|
|
242
|
+
Array.map(esbuildMessageKey),
|
|
243
|
+
Array.dedupe,
|
|
244
|
+
Array.sort(Order.string),
|
|
245
|
+
Array.join("\n"),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Log any watcher errors that haven't already been logged at their
|
|
250
|
+
* current signature. Suppresses the per-watcher fanout that happens
|
|
251
|
+
* when one root cause (e.g. a missing import) breaks every entry
|
|
252
|
+
* point's build at the same source location.
|
|
253
|
+
*/
|
|
254
|
+
const logChangedWatcherErrors = (
|
|
255
|
+
watcherErrorsRef: Ref.Ref<WatcherErrors>,
|
|
256
|
+
lastLoggedSignatureRef: Ref.Ref<string>,
|
|
257
|
+
) =>
|
|
258
|
+
Effect.gen(function* () {
|
|
259
|
+
const errors = yield* Ref.get(watcherErrorsRef);
|
|
260
|
+
const signature = watcherErrorsSignature(errors);
|
|
261
|
+
const previous = yield* Ref.get(lastLoggedSignatureRef);
|
|
262
|
+
if (signature === previous) return;
|
|
263
|
+
yield* Ref.set(lastLoggedSignatureRef, signature);
|
|
264
|
+
if (errors.size === 0) return;
|
|
265
|
+
yield* logCoalescedBuildErrors(dedupeWatcherErrors(errors));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Block until the signal queue has been quiet for `quiescence`. esbuild
|
|
270
|
+
* watchers' `onEnd` events for a single user edit can be spread across
|
|
271
|
+
* hundreds of milliseconds, so a fixed window misses late arrivals.
|
|
272
|
+
* Bounded by `maxWait` so pathological signal floods can't pin the
|
|
273
|
+
* loop forever.
|
|
274
|
+
*/
|
|
275
|
+
const drainUntilQuiescent = (
|
|
276
|
+
signal: Queue.Queue<void>,
|
|
277
|
+
quiescence: Duration.Duration,
|
|
278
|
+
maxWait: Duration.Duration,
|
|
279
|
+
) =>
|
|
280
|
+
Effect.gen(function* () {
|
|
281
|
+
const start = yield* Clock.currentTimeMillis;
|
|
282
|
+
const maxMillis = Duration.toMillis(maxWait);
|
|
283
|
+
yield* Effect.iterate(true as boolean, {
|
|
284
|
+
while: (keepGoing) => keepGoing,
|
|
285
|
+
body: () =>
|
|
286
|
+
Effect.gen(function* () {
|
|
287
|
+
yield* Effect.sleep(quiescence);
|
|
288
|
+
const drained = yield* Queue.takeAll(signal);
|
|
289
|
+
if (Chunk.isEmpty(drained)) return false;
|
|
290
|
+
const now = yield* Clock.currentTimeMillis;
|
|
291
|
+
return now - start < maxMillis;
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
156
296
|
const syncLoop = (
|
|
157
297
|
signal: Queue.Queue<void>,
|
|
158
298
|
pendingRef: Ref.Ref<Pending>,
|
|
159
299
|
initialFunctionPaths: FunctionPaths.FunctionPaths,
|
|
300
|
+
watcherErrorsRef: Ref.Ref<WatcherErrors>,
|
|
160
301
|
) =>
|
|
161
302
|
Effect.gen(function* () {
|
|
162
303
|
const functionPathsRef = yield* Ref.make(initialFunctionPaths);
|
|
163
|
-
const
|
|
304
|
+
const lastLoggedErrorsRef = yield* Ref.make<string>("");
|
|
164
305
|
|
|
165
306
|
return yield* Effect.forever(
|
|
166
307
|
Effect.gen(function* () {
|
|
167
|
-
yield* Effect.logDebug("Running sync loop
|
|
308
|
+
yield* Effect.logDebug("Running sync loop…");
|
|
309
|
+
// Wait for the first signal of a burst, then keep absorbing
|
|
310
|
+
// follow-up signals from other watchers' onEnds until the queue
|
|
311
|
+
// stays quiet for `COALESCE_QUIESCENCE`.
|
|
168
312
|
yield* Queue.take(signal);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
() => isDone,
|
|
313
|
+
yield* drainUntilQuiescent(
|
|
314
|
+
signal,
|
|
315
|
+
COALESCE_QUIESCENCE,
|
|
316
|
+
COALESCE_MAX_WAIT,
|
|
174
317
|
);
|
|
175
|
-
|
|
318
|
+
|
|
319
|
+
yield* logChangedWatcherErrors(watcherErrorsRef, lastLoggedErrorsRef);
|
|
176
320
|
|
|
177
321
|
const pending = yield* Ref.getAndSet(pendingRef, pendingInit);
|
|
178
322
|
|
|
179
|
-
if (pending
|
|
180
|
-
|
|
181
|
-
|
|
323
|
+
if (!isPendingDirty(pending)) {
|
|
324
|
+
// No-op signal (e.g. a late echo after a previous cycle
|
|
325
|
+
// already drained). Stay silent.
|
|
326
|
+
return;
|
|
182
327
|
}
|
|
183
328
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
relativeModulePath,
|
|
216
|
-
);
|
|
217
|
-
yield* logFileChangeIndented("Removed", filePath);
|
|
218
|
-
yield* Effect.forEach(
|
|
219
|
-
Array.fromIterable(
|
|
220
|
-
HashSet.filter(functionsRemoved, (fp) =>
|
|
221
|
-
Equal.equals(fp.groupPath, gp),
|
|
222
|
-
),
|
|
223
|
-
),
|
|
224
|
-
logFunctionRemovedIndented,
|
|
225
|
-
);
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Added groups
|
|
230
|
-
yield* writeGroups(spec, groupsAdded);
|
|
231
|
-
yield* Effect.forEach(groupsAdded, (gp) =>
|
|
232
|
-
Effect.gen(function* () {
|
|
233
|
-
const relativeModulePath =
|
|
234
|
-
yield* GroupPath.modulePath(gp);
|
|
235
|
-
const filePath = path.join(
|
|
236
|
-
convexDirectory,
|
|
237
|
-
relativeModulePath,
|
|
238
|
-
);
|
|
239
|
-
yield* logFileChangeIndented("Added", filePath);
|
|
240
|
-
yield* Effect.forEach(
|
|
241
|
-
Array.fromIterable(
|
|
242
|
-
HashSet.filter(functionsAdded, (fp) =>
|
|
243
|
-
Equal.equals(fp.groupPath, gp),
|
|
244
|
-
),
|
|
245
|
-
),
|
|
246
|
-
logFunctionAddedIndented,
|
|
247
|
-
);
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// Changed groups
|
|
252
|
-
yield* writeGroups(spec, groupsChanged);
|
|
253
|
-
yield* Effect.forEach(groupsChanged, (gp) =>
|
|
254
|
-
Effect.gen(function* () {
|
|
255
|
-
const relativeModulePath =
|
|
256
|
-
yield* GroupPath.modulePath(gp);
|
|
257
|
-
const filePath = path.join(
|
|
258
|
-
convexDirectory,
|
|
259
|
-
relativeModulePath,
|
|
260
|
-
);
|
|
261
|
-
yield* logFileChangeIndented("Modified", filePath);
|
|
262
|
-
yield* Effect.forEach(
|
|
263
|
-
Array.fromIterable(
|
|
264
|
-
HashSet.filter(functionsAdded, (fp) =>
|
|
265
|
-
Equal.equals(fp.groupPath, gp),
|
|
266
|
-
),
|
|
267
|
-
),
|
|
268
|
-
logFunctionAddedIndented,
|
|
269
|
-
);
|
|
270
|
-
yield* Effect.forEach(
|
|
271
|
-
Array.fromIterable(
|
|
272
|
-
HashSet.filter(functionsRemoved, (fp) =>
|
|
273
|
-
Equal.equals(fp.groupPath, gp),
|
|
274
|
-
),
|
|
275
|
-
),
|
|
276
|
-
logFunctionRemovedIndented,
|
|
277
|
-
);
|
|
278
|
-
}),
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
yield* Ref.set(functionPathsRef, current);
|
|
282
|
-
|
|
283
|
-
return Option.some(undefined);
|
|
284
|
-
}),
|
|
285
|
-
),
|
|
286
|
-
Effect.catchTag("SpecImportFailedError", () =>
|
|
287
|
-
logFailure("Spec import failed").pipe(
|
|
288
|
-
Effect.as(Option.none()),
|
|
289
|
-
),
|
|
290
|
-
),
|
|
291
|
-
Effect.catchTag("SpecFileDoesNotExportSpecError", () =>
|
|
292
|
-
logFailure(
|
|
293
|
-
"Spec file does not default export a Convex spec",
|
|
294
|
-
).pipe(Effect.as(Option.none())),
|
|
295
|
-
),
|
|
296
|
-
Effect.catchTag("NodeSpecFileDoesNotExportSpecError", () =>
|
|
297
|
-
logFailure(
|
|
298
|
-
"Node spec file does not default export a Node spec",
|
|
299
|
-
).pipe(Effect.as(Option.none())),
|
|
300
|
-
),
|
|
301
|
-
),
|
|
302
|
-
onFalse: () => Effect.succeed(Option.some(undefined)),
|
|
303
|
-
},
|
|
304
|
-
);
|
|
329
|
+
yield* logPending("Dependencies may have changed, reloading…");
|
|
330
|
+
|
|
331
|
+
if (pending.specDirty) {
|
|
332
|
+
const current = yield* codegenHandler.pipe(
|
|
333
|
+
Effect.tap(({ functionPaths: nextFunctionPaths }) =>
|
|
334
|
+
Effect.gen(function* () {
|
|
335
|
+
const previous = yield* Ref.get(functionPathsRef);
|
|
336
|
+
yield* logFunctionPathDiff(previous, nextFunctionPaths);
|
|
337
|
+
yield* Ref.set(functionPathsRef, nextFunctionPaths);
|
|
338
|
+
}),
|
|
339
|
+
),
|
|
340
|
+
CodegenError.catchAndLog,
|
|
341
|
+
);
|
|
342
|
+
if (Option.isNone(current)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Drain any stragglers from this cycle's burst (slow watchers
|
|
346
|
+
// whose onEnd fired after the first quiescence) plus, when
|
|
347
|
+
// codegen wrote, the echo signals esbuild emits in response
|
|
348
|
+
// to our writes. Reset `pendingRef` so those drained signals
|
|
349
|
+
// don't carry a dirty flag into the next cycle.
|
|
350
|
+
if (current.value.anyWritesHappened) {
|
|
351
|
+
yield* Effect.sleep(ECHO_COOLDOWN);
|
|
352
|
+
}
|
|
353
|
+
yield* drainUntilQuiescent(
|
|
354
|
+
signal,
|
|
355
|
+
COALESCE_QUIESCENCE,
|
|
356
|
+
COALESCE_MAX_WAIT,
|
|
357
|
+
);
|
|
358
|
+
yield* Ref.set(pendingRef, pendingInit);
|
|
359
|
+
}
|
|
305
360
|
|
|
306
361
|
const dirtyOptionalFiles = [
|
|
307
362
|
...(pending.httpDirty
|
|
@@ -319,237 +374,350 @@ const syncLoop = (
|
|
|
319
374
|
? Effect.all(dirtyOptionalFiles, { concurrency: "unbounded" })
|
|
320
375
|
: Effect.void;
|
|
321
376
|
|
|
322
|
-
yield*
|
|
323
|
-
onSome: () => logSuccess("Generated files are up-to-date"),
|
|
324
|
-
onNone: () => Effect.void,
|
|
325
|
-
});
|
|
377
|
+
yield* logSuccess("Generated files are up-to-date");
|
|
326
378
|
}),
|
|
327
379
|
);
|
|
328
380
|
});
|
|
329
381
|
|
|
330
|
-
|
|
382
|
+
interface EntryPoint {
|
|
383
|
+
readonly absolutePath: string;
|
|
384
|
+
readonly displayPath: string;
|
|
385
|
+
readonly pendingKey: PendingKey;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Every file whose import graph codegen should react to. Each one becomes
|
|
390
|
+
* its own scoped esbuild watcher; the union of their watches gives us
|
|
391
|
+
* dependency-aware tracking of anything reachable from `confect/`,
|
|
392
|
+
* including files outside `confect/`.
|
|
393
|
+
*/
|
|
394
|
+
const discoverEntryPoints = Effect.gen(function* () {
|
|
331
395
|
const fs = yield* FileSystem.FileSystem;
|
|
332
396
|
const path = yield* Path.Path;
|
|
397
|
+
const projectRoot = yield* ProjectRoot.get;
|
|
333
398
|
const confectDirectory = yield* ConfectDirectory.get;
|
|
334
|
-
const specPath = yield* getSpecPath;
|
|
335
|
-
const specModule = yield* bundleAndImport(specPath).pipe(
|
|
336
|
-
Effect.mapError((error) => new SpecImportFailedError({ error })),
|
|
337
|
-
);
|
|
338
|
-
const spec = specModule.default;
|
|
339
399
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return path.join(confectDirectory, "nodeSpec.ts");
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
const loadNodeSpec = Effect.gen(function* () {
|
|
370
|
-
const fs = yield* FileSystem.FileSystem;
|
|
371
|
-
const nodeSpecPath = yield* getNodeSpecPath;
|
|
372
|
-
|
|
373
|
-
if (!(yield* fs.exists(nodeSpecPath))) {
|
|
374
|
-
return Option.none();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const nodeSpecModule = yield* bundleAndImport(nodeSpecPath).pipe(
|
|
378
|
-
Effect.mapError((error) => new SpecImportFailedError({ error })),
|
|
400
|
+
const tryEntry = (relativePath: string, pendingKey: PendingKey) =>
|
|
401
|
+
Effect.gen(function* () {
|
|
402
|
+
const absolutePath = path.join(confectDirectory, relativePath);
|
|
403
|
+
if (!(yield* fs.exists(absolutePath))) {
|
|
404
|
+
return Option.none<EntryPoint>();
|
|
405
|
+
}
|
|
406
|
+
return Option.some<EntryPoint>({
|
|
407
|
+
absolutePath,
|
|
408
|
+
displayPath: path.relative(projectRoot, absolutePath),
|
|
409
|
+
pendingKey,
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const fixedEntryOptions = yield* Effect.all([
|
|
414
|
+
tryEntry(GENERATED_SPEC_PATH, "specDirty"),
|
|
415
|
+
tryEntry(GENERATED_NODE_SPEC_PATH, "specDirty"),
|
|
416
|
+
tryEntry("schema.ts", "specDirty"),
|
|
417
|
+
tryEntry("http.ts", "httpDirty"),
|
|
418
|
+
tryEntry("crons.ts", "cronsDirty"),
|
|
419
|
+
tryEntry("auth.ts", "authDirty"),
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
const implRelativePaths = yield* discoverLeafImplFiles;
|
|
423
|
+
const implEntryOptions = yield* Effect.forEach(
|
|
424
|
+
implRelativePaths,
|
|
425
|
+
(relativePath) => tryEntry(relativePath, "specDirty"),
|
|
379
426
|
);
|
|
380
|
-
const nodeSpec = nodeSpecModule.default;
|
|
381
|
-
|
|
382
|
-
if (!Spec.isNodeSpec(nodeSpec)) {
|
|
383
|
-
return yield* new NodeSpecFileDoesNotExportSpecError();
|
|
384
|
-
}
|
|
385
427
|
|
|
386
|
-
return
|
|
428
|
+
return Array.getSomes([...fixedEntryOptions, ...implEntryOptions]);
|
|
387
429
|
});
|
|
388
430
|
|
|
389
|
-
const esbuildOptions = (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
431
|
+
const esbuildOptions = (
|
|
432
|
+
entry: EntryPoint,
|
|
433
|
+
signal: Queue.Queue<void>,
|
|
434
|
+
pendingRef: Ref.Ref<Pending>,
|
|
435
|
+
watcherErrorsRef: Ref.Ref<WatcherErrors>,
|
|
436
|
+
) => {
|
|
437
|
+
// First `onEnd` fires when esbuild finishes the watcher's initial
|
|
438
|
+
// build. At startup that's an echo of the just-completed initial
|
|
439
|
+
// codegen pass; for a watcher spawned mid-session (e.g. a newly
|
|
440
|
+
// added impl) it's an echo of the codegen run that triggered the
|
|
441
|
+
// restart. Either way, the entry's contents were already accounted
|
|
442
|
+
// for, so we record any errors but don't flip dirty or push a
|
|
443
|
+
// signal — only genuine subsequent rebuilds should do that.
|
|
444
|
+
const initialBuildSeenRef = Ref.unsafeMake(false);
|
|
445
|
+
return {
|
|
446
|
+
entryPoints: [entry.absolutePath],
|
|
447
|
+
bundle: true,
|
|
448
|
+
write: false,
|
|
449
|
+
metafile: true,
|
|
450
|
+
platform: "node" as const,
|
|
451
|
+
format: "esm" as const,
|
|
452
|
+
logLevel: "silent" as const,
|
|
453
|
+
external: EXTERNAL_PACKAGES,
|
|
454
|
+
plugins: [
|
|
455
|
+
absoluteExternalsPlugin,
|
|
456
|
+
{
|
|
457
|
+
name: "notify-rebuild",
|
|
458
|
+
setup(build: esbuild.PluginBuild) {
|
|
459
|
+
build.onEnd((result) => {
|
|
406
460
|
Effect.runPromise(
|
|
407
461
|
Effect.gen(function* () {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
color: true,
|
|
412
|
-
terminalWidth: 80,
|
|
413
|
-
}),
|
|
414
|
-
);
|
|
415
|
-
const output = formatBuildErrors(
|
|
416
|
-
result.errors,
|
|
417
|
-
formattedMessages,
|
|
462
|
+
const wasInitial = yield* Ref.getAndSet(
|
|
463
|
+
initialBuildSeenRef,
|
|
464
|
+
true,
|
|
418
465
|
);
|
|
419
|
-
|
|
420
|
-
yield*
|
|
466
|
+
const isInitial = !wasInitial;
|
|
467
|
+
yield* Ref.update(watcherErrorsRef, (current) => {
|
|
468
|
+
const next = new Map(current);
|
|
469
|
+
if (result.errors.length > 0) {
|
|
470
|
+
next.set(entry.absolutePath, result.errors);
|
|
471
|
+
} else {
|
|
472
|
+
next.delete(entry.absolutePath);
|
|
473
|
+
}
|
|
474
|
+
return next;
|
|
475
|
+
});
|
|
476
|
+
if (isInitial && result.errors.length === 0) return;
|
|
477
|
+
yield* Ref.update(pendingRef, (p) => ({
|
|
478
|
+
...p,
|
|
479
|
+
[entry.pendingKey]: true,
|
|
480
|
+
}));
|
|
481
|
+
yield* Queue.offer(signal, undefined);
|
|
421
482
|
}),
|
|
422
483
|
);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const createSpecWatcher = (entryPoint: string) =>
|
|
431
|
-
Stream.asyncPush<void>(
|
|
432
|
-
(emit) =>
|
|
433
|
-
Effect.acquireRelease(
|
|
434
|
-
Effect.promise(async () => {
|
|
435
|
-
const opts = esbuildOptions(entryPoint);
|
|
436
|
-
const plugin = opts.plugins[0];
|
|
437
|
-
const originalSetup = plugin!.setup!;
|
|
438
|
-
(plugin as { setup: (build: esbuild.PluginBuild) => void }).setup = (
|
|
439
|
-
build,
|
|
440
|
-
) => {
|
|
441
|
-
(build as { _emit?: (v: void) => void })._emit = () =>
|
|
442
|
-
emit.single();
|
|
443
|
-
return originalSetup(build);
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const ctx = await esbuild.context({
|
|
447
|
-
...opts,
|
|
448
|
-
plugins: [plugin],
|
|
449
484
|
});
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
};
|
|
450
490
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
491
|
+
const createEntryPointWatcher = (
|
|
492
|
+
entry: EntryPoint,
|
|
493
|
+
signal: Queue.Queue<void>,
|
|
494
|
+
pendingRef: Ref.Ref<Pending>,
|
|
495
|
+
watcherErrorsRef: Ref.Ref<WatcherErrors>,
|
|
496
|
+
) =>
|
|
497
|
+
Effect.acquireRelease(
|
|
498
|
+
Effect.promise(async () => {
|
|
499
|
+
const ctx = await esbuild.context(
|
|
500
|
+
esbuildOptions(entry, signal, pendingRef, watcherErrorsRef),
|
|
501
|
+
);
|
|
502
|
+
await ctx.watch();
|
|
503
|
+
return ctx;
|
|
504
|
+
}),
|
|
505
|
+
(ctx) =>
|
|
506
|
+
Effect.gen(function* () {
|
|
507
|
+
yield* Effect.promise(() => ctx.dispose());
|
|
508
|
+
// Clear any errors recorded by this watcher so a disposed
|
|
509
|
+
// watcher can't leave stale errors visible to the sync loop.
|
|
510
|
+
yield* Ref.update(watcherErrorsRef, (current) => {
|
|
511
|
+
if (!current.has(entry.absolutePath)) return current;
|
|
512
|
+
const next = new Map(current);
|
|
513
|
+
next.delete(entry.absolutePath);
|
|
514
|
+
return next;
|
|
515
|
+
});
|
|
516
|
+
yield* Effect.logDebug(
|
|
517
|
+
`esbuild watcher disposed: ${entry.displayPath}`,
|
|
518
|
+
);
|
|
519
|
+
}),
|
|
460
520
|
);
|
|
461
521
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Holds one scoped esbuild watcher per entry point and reconciles the set
|
|
524
|
+
* whenever something offers to `restartQueue`. Adding or removing an entry
|
|
525
|
+
* point only spawns/disposes the affected watcher; unchanged entries keep
|
|
526
|
+
* their existing context, so a structural change doesn't churn watchers
|
|
527
|
+
* for unrelated files.
|
|
528
|
+
*/
|
|
529
|
+
const entryPointsWatcher = (
|
|
465
530
|
signal: Queue.Queue<void>,
|
|
466
531
|
pendingRef: Ref.Ref<Pending>,
|
|
467
|
-
|
|
532
|
+
restartQueue: Queue.Queue<void>,
|
|
533
|
+
watcherErrorsRef: Ref.Ref<WatcherErrors>,
|
|
468
534
|
) =>
|
|
469
|
-
Effect.
|
|
470
|
-
Effect.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const nodeSpecWatcher = nodeSpecExists
|
|
478
|
-
? createSpecWatcher(nodeSpecPath)
|
|
479
|
-
: Stream.empty;
|
|
480
|
-
|
|
481
|
-
const specChanges = pipe(
|
|
482
|
-
Stream.merge(specWatcher, nodeSpecWatcher),
|
|
483
|
-
Stream.map((): SpecWatcherEvent => "change"),
|
|
535
|
+
Effect.gen(function* () {
|
|
536
|
+
const parentScope = yield* Effect.scope;
|
|
537
|
+
const scopesRef = yield* Ref.make(new Map<string, Scope.CloseableScope>());
|
|
538
|
+
|
|
539
|
+
const sync = Effect.gen(function* () {
|
|
540
|
+
const desired = yield* discoverEntryPoints;
|
|
541
|
+
const desiredByPath = new Map(
|
|
542
|
+
desired.map((entryPoint) => [entryPoint.absolutePath, entryPoint]),
|
|
484
543
|
);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
544
|
+
const current = yield* Ref.get(scopesRef);
|
|
545
|
+
|
|
546
|
+
yield* Effect.forEach(
|
|
547
|
+
Array.fromIterable(current),
|
|
548
|
+
([absolutePath, childScope]) =>
|
|
549
|
+
desiredByPath.has(absolutePath)
|
|
550
|
+
? Effect.void
|
|
551
|
+
: Scope.close(childScope, Exit.void).pipe(
|
|
552
|
+
Effect.andThen(
|
|
553
|
+
Ref.update(scopesRef, (scopes) => {
|
|
554
|
+
const updated = new Map(scopes);
|
|
555
|
+
updated.delete(absolutePath);
|
|
556
|
+
return updated;
|
|
557
|
+
}),
|
|
558
|
+
),
|
|
559
|
+
),
|
|
488
560
|
);
|
|
489
561
|
|
|
490
|
-
yield*
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
562
|
+
yield* Effect.forEach(desired, (entry) =>
|
|
563
|
+
Effect.gen(function* () {
|
|
564
|
+
const existing = yield* Ref.get(scopesRef);
|
|
565
|
+
if (existing.has(entry.absolutePath)) return;
|
|
566
|
+
|
|
567
|
+
const childScope = yield* Scope.fork(
|
|
568
|
+
parentScope,
|
|
569
|
+
ExecutionStrategy.sequential,
|
|
570
|
+
);
|
|
571
|
+
yield* createEntryPointWatcher(
|
|
572
|
+
entry,
|
|
573
|
+
signal,
|
|
574
|
+
pendingRef,
|
|
575
|
+
watcherErrorsRef,
|
|
576
|
+
).pipe(Scope.extend(childScope));
|
|
577
|
+
yield* Ref.update(scopesRef, (scopes) => {
|
|
578
|
+
const updated = new Map(scopes);
|
|
579
|
+
updated.set(entry.absolutePath, childScope);
|
|
580
|
+
return updated;
|
|
581
|
+
});
|
|
582
|
+
}),
|
|
502
583
|
);
|
|
503
|
-
})
|
|
504
|
-
);
|
|
584
|
+
});
|
|
505
585
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
AnsiDoc.text(error?.text ?? ""),
|
|
513
|
-
AnsiDoc.annotate(Ansi.red),
|
|
514
|
-
AnsiDoc.render({ style: "pretty" }),
|
|
515
|
-
);
|
|
516
|
-
const replaced = pipe(
|
|
517
|
-
Array.findFirstIndex(lines, (l) => pipe(l, String.trim, String.isNonEmpty)),
|
|
518
|
-
Option.match({
|
|
519
|
-
onNone: () => lines,
|
|
520
|
-
onSome: (index) => Array.modify(lines, index, () => redErrorText),
|
|
521
|
-
}),
|
|
522
|
-
);
|
|
523
|
-
return pipe(replaced, Array.join("\n"));
|
|
524
|
-
};
|
|
586
|
+
yield* sync;
|
|
587
|
+
|
|
588
|
+
return yield* Effect.forever(
|
|
589
|
+
Queue.take(restartQueue).pipe(Effect.andThen(sync)),
|
|
590
|
+
);
|
|
591
|
+
});
|
|
525
592
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)
|
|
593
|
+
/**
|
|
594
|
+
* Single recursive `fs.watch` on `confect/`. Flips the matching dirty flag
|
|
595
|
+
* for any change to an entry-point-shaped file (so codegen runs without
|
|
596
|
+
* waiting on a newly spawned esbuild watcher), and offers to
|
|
597
|
+
* `restartQueue` when an entry point is created or removed so the watcher
|
|
598
|
+
* manager picks up the new set.
|
|
599
|
+
*/
|
|
600
|
+
const confectStructureWatcher = (
|
|
601
|
+
signal: Queue.Queue<void>,
|
|
602
|
+
pendingRef: Ref.Ref<Pending>,
|
|
603
|
+
restartQueue: Queue.Queue<void>,
|
|
604
|
+
) =>
|
|
605
|
+
Effect.gen(function* () {
|
|
606
|
+
const fs = yield* FileSystem.FileSystem;
|
|
607
|
+
const path = yield* Path.Path;
|
|
608
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
609
|
+
|
|
610
|
+
yield* pipe(
|
|
611
|
+
fs.watch(confectDirectory, { recursive: true }),
|
|
612
|
+
Stream.debounce(Duration.millis(200)),
|
|
613
|
+
Stream.runForEach((event) =>
|
|
614
|
+
handleConfectChange({
|
|
615
|
+
relativePath: path.relative(confectDirectory, event.path),
|
|
616
|
+
eventTag: event._tag,
|
|
617
|
+
signal,
|
|
618
|
+
pendingRef,
|
|
619
|
+
restartQueue,
|
|
620
|
+
}),
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const TOP_LEVEL_OPTIONAL_KEYS: ReadonlyMap<string, PendingKey> = new Map([
|
|
626
|
+
["http.ts", "httpDirty"],
|
|
627
|
+
["crons.ts", "cronsDirty"],
|
|
628
|
+
["auth.ts", "authDirty"],
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
const flipDirtyAndSignal = (
|
|
632
|
+
pendingRef: Ref.Ref<Pending>,
|
|
633
|
+
signal: Queue.Queue<void>,
|
|
634
|
+
key: PendingKey,
|
|
635
|
+
restartQueue: Queue.Queue<void>,
|
|
636
|
+
restart: boolean,
|
|
637
|
+
) =>
|
|
530
638
|
pipe(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
639
|
+
Ref.update(pendingRef, (p) => ({ ...p, [key]: true })),
|
|
640
|
+
Effect.andThen(Queue.offer(signal, undefined)),
|
|
641
|
+
Effect.andThen(
|
|
642
|
+
restart ? Queue.offer(restartQueue, undefined) : Effect.void,
|
|
643
|
+
),
|
|
535
644
|
);
|
|
536
645
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
646
|
+
const handleConfectChange = ({
|
|
647
|
+
relativePath,
|
|
648
|
+
eventTag,
|
|
649
|
+
signal,
|
|
650
|
+
pendingRef,
|
|
651
|
+
restartQueue,
|
|
652
|
+
}: {
|
|
653
|
+
relativePath: string;
|
|
654
|
+
eventTag: "Create" | "Update" | "Remove";
|
|
655
|
+
signal: Queue.Queue<void>;
|
|
656
|
+
pendingRef: Ref.Ref<Pending>;
|
|
657
|
+
restartQueue: Queue.Queue<void>;
|
|
658
|
+
}) => {
|
|
659
|
+
// _generated/ files are written by codegen itself; reacting to them here
|
|
660
|
+
// would form a loop. The esbuild watchers track the generated specs as
|
|
661
|
+
// entry points, so changes there flow back through `notify-rebuild`.
|
|
662
|
+
if (relativePath.split(/[/\\]/).includes("_generated")) {
|
|
663
|
+
return Effect.void;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!relativePath.endsWith(".ts")) {
|
|
667
|
+
return Effect.void;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const isLifecycleChange = eventTag !== "Update";
|
|
671
|
+
|
|
672
|
+
const topLevelKey = TOP_LEVEL_OPTIONAL_KEYS.get(relativePath);
|
|
673
|
+
if (topLevelKey !== undefined) {
|
|
674
|
+
return flipDirtyAndSignal(
|
|
675
|
+
pendingRef,
|
|
676
|
+
signal,
|
|
677
|
+
topLevelKey,
|
|
678
|
+
restartQueue,
|
|
679
|
+
isLifecycleChange,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (relativePath === "schema.ts") {
|
|
684
|
+
return flipDirtyAndSignal(
|
|
685
|
+
pendingRef,
|
|
686
|
+
signal,
|
|
687
|
+
"specDirty",
|
|
688
|
+
restartQueue,
|
|
689
|
+
isLifecycleChange,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (isLeafSpecPath(relativePath) || isLeafImplPath(relativePath)) {
|
|
694
|
+
return flipDirtyAndSignal(
|
|
695
|
+
pendingRef,
|
|
696
|
+
signal,
|
|
697
|
+
"specDirty",
|
|
698
|
+
restartQueue,
|
|
699
|
+
isLifecycleChange,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
541
702
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
703
|
+
// Any other `.ts` under `confect/` (helpers like `tables/Notes.ts`).
|
|
704
|
+
// Updates to such files are handled by the esbuild watcher for whichever
|
|
705
|
+
// entry point imports them — its onEnd flips the right dirty flag.
|
|
706
|
+
// Creates are our safety net: when a previously-missing import is added,
|
|
707
|
+
// esbuild may not have its parent directory on a poll path, so we
|
|
708
|
+
// re-run codegen on Create here.
|
|
709
|
+
if (eventTag === "Create") {
|
|
710
|
+
return flipDirtyAndSignal(
|
|
711
|
+
pendingRef,
|
|
712
|
+
signal,
|
|
713
|
+
"specDirty",
|
|
714
|
+
restartQueue,
|
|
715
|
+
false,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
546
718
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
{
|
|
550
|
-
error: Schema.Unknown,
|
|
551
|
-
},
|
|
552
|
-
) {}
|
|
719
|
+
return Effect.void;
|
|
720
|
+
};
|
|
553
721
|
|
|
554
722
|
const syncOptionalFile = (generate: typeof generateHttp, convexFile: string) =>
|
|
555
723
|
pipe(
|
|
@@ -579,50 +747,3 @@ const syncOptionalFile = (generate: typeof generateHttp, convexFile: string) =>
|
|
|
579
747
|
}),
|
|
580
748
|
),
|
|
581
749
|
);
|
|
582
|
-
|
|
583
|
-
const optionalConfectFiles: ReadonlyRecord<string, keyof Pending> = {
|
|
584
|
-
"http.ts": "httpDirty",
|
|
585
|
-
"crons.ts": "cronsDirty",
|
|
586
|
-
"auth.ts": "authDirty",
|
|
587
|
-
"nodeSpec.ts": "specDirty",
|
|
588
|
-
"nodeImpl.ts": "nodeImplDirty",
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
const confectDirectoryWatcher = (
|
|
592
|
-
signal: Queue.Queue<void>,
|
|
593
|
-
pendingRef: Ref.Ref<Pending>,
|
|
594
|
-
specWatcherRestartQueue: Queue.Queue<void>,
|
|
595
|
-
) =>
|
|
596
|
-
Effect.gen(function* () {
|
|
597
|
-
const fs = yield* FileSystem.FileSystem;
|
|
598
|
-
const path = yield* Path.Path;
|
|
599
|
-
const confectDirectory = yield* ConfectDirectory.get;
|
|
600
|
-
|
|
601
|
-
yield* pipe(
|
|
602
|
-
fs.watch(confectDirectory),
|
|
603
|
-
Stream.runForEach((event) => {
|
|
604
|
-
const basename = path.basename(event.path);
|
|
605
|
-
const pendingKey = optionalConfectFiles[basename];
|
|
606
|
-
|
|
607
|
-
if (pendingKey !== undefined) {
|
|
608
|
-
return pipe(
|
|
609
|
-
pendingRef,
|
|
610
|
-
Ref.update((pending) => {
|
|
611
|
-
const next = { ...pending, [pendingKey]: true };
|
|
612
|
-
if (basename === "nodeImpl.ts") {
|
|
613
|
-
return { ...next, specDirty: true };
|
|
614
|
-
}
|
|
615
|
-
return next;
|
|
616
|
-
}),
|
|
617
|
-
Effect.andThen(Queue.offer(signal, undefined)),
|
|
618
|
-
Effect.andThen(
|
|
619
|
-
basename === "nodeSpec.ts"
|
|
620
|
-
? Queue.offer(specWatcherRestartQueue, undefined)
|
|
621
|
-
: Effect.void,
|
|
622
|
-
),
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
return Effect.void;
|
|
626
|
-
}),
|
|
627
|
-
);
|
|
628
|
-
});
|