@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/dist/confect/dev.mjs
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
|
-
import { modulePath, toString } from "../GroupPath.mjs";
|
|
2
1
|
import { ProjectRoot } from "../ProjectRoot.mjs";
|
|
3
|
-
import {
|
|
2
|
+
import { logFunctionAdded, logFunctionRemoved, logPending, logSuccess } from "../log.mjs";
|
|
3
|
+
import { logCoalescedBuildErrors } from "../BuildError.mjs";
|
|
4
|
+
import { catchAndLog } from "../CodegenError.mjs";
|
|
4
5
|
import { ConvexDirectory } from "../ConvexDirectory.mjs";
|
|
5
6
|
import { ConfectDirectory } from "../ConfectDirectory.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { EXTERNAL_PACKAGES,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { FunctionPaths, diff } from "../FunctionPaths.mjs";
|
|
8
|
+
import { EXTERNAL_PACKAGES, absoluteExternalsPlugin } from "../Bundler.mjs";
|
|
9
|
+
import { generateAuthConfig, generateCrons, generateHttp } from "../utils.mjs";
|
|
10
|
+
import { discoverLeafImplFiles, isLeafImplPath, isLeafSpecPath } from "../LeafModule.mjs";
|
|
11
|
+
import { codegenHandler, loadPreviousFunctionPaths } from "./codegen.mjs";
|
|
12
|
+
import { Array, Chunk, Clock, Console, Duration, Effect, Equal, ExecutionStrategy, Exit, HashSet, Match, Option, Order, Queue, Ref, Scope, Stream, String, pipe } from "effect";
|
|
10
13
|
import { Command } from "@effect/cli";
|
|
11
|
-
import { Spec } from "@confect/core";
|
|
12
14
|
import { FileSystem, Path } from "@effect/platform";
|
|
13
15
|
import { Ansi, AnsiDoc } from "@effect/printer-ansi";
|
|
14
16
|
import * as esbuild from "esbuild";
|
|
15
17
|
|
|
16
18
|
//#region src/confect/dev.ts
|
|
19
|
+
const GENERATED_SPEC_PATH = "_generated/spec.ts";
|
|
20
|
+
const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
|
|
21
|
+
const COALESCE_QUIESCENCE = Duration.millis(300);
|
|
22
|
+
const COALESCE_MAX_WAIT = Duration.seconds(5);
|
|
23
|
+
const ECHO_COOLDOWN = Duration.millis(500);
|
|
24
|
+
const emptyFunctionPaths = FunctionPaths.make(HashSet.empty());
|
|
17
25
|
const pendingInit = {
|
|
18
26
|
specDirty: false,
|
|
19
|
-
nodeImplDirty: false,
|
|
20
27
|
httpDirty: false,
|
|
21
28
|
cronsDirty: false,
|
|
22
29
|
authDirty: false
|
|
23
30
|
};
|
|
31
|
+
const isPendingDirty = (p) => p.specDirty || p.httpDirty || p.cronsDirty || p.authDirty;
|
|
32
|
+
const emptyWatcherErrors = /* @__PURE__ */ new Map();
|
|
24
33
|
const changeChar = (change) => Match.value(change).pipe(Match.when("Added", () => ({
|
|
25
34
|
char: "+",
|
|
26
35
|
color: Ansi.green
|
|
@@ -37,182 +46,255 @@ const logFileChangeIndented = (change, fullPath) => Effect.gen(function* () {
|
|
|
37
46
|
const { char, color } = changeChar(change);
|
|
38
47
|
yield* Console.log(pipe(AnsiDoc.char(char), AnsiDoc.annotate(color), AnsiDoc.catWithSpace(AnsiDoc.hcat([pipe(AnsiDoc.text(prefix), AnsiDoc.annotate(Ansi.blackBright)), pipe(AnsiDoc.text(suffix), AnsiDoc.annotate(color))])), AnsiDoc.render({ style: "pretty" })));
|
|
39
48
|
});
|
|
40
|
-
const
|
|
41
|
-
const
|
|
49
|
+
const logFunctionPathDiff = (previous, current) => Effect.gen(function* () {
|
|
50
|
+
const { functionsAdded, functionsRemoved, groupsRemoved, groupsAdded, groupsChanged } = diff(previous, current);
|
|
51
|
+
const logForGroups = (groupPaths, fnPaths, logFn) => Effect.forEach(groupPaths, (gp) => Effect.forEach(Array.fromIterable(HashSet.filter(fnPaths, (fp) => Equal.equals(fp.groupPath, gp))), logFn));
|
|
52
|
+
yield* logForGroups(groupsRemoved, functionsRemoved, logFunctionRemoved);
|
|
53
|
+
yield* logForGroups(groupsAdded, functionsAdded, logFunctionAdded);
|
|
54
|
+
yield* Effect.forEach(groupsChanged, (gp) => Effect.gen(function* () {
|
|
55
|
+
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAdded);
|
|
56
|
+
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemoved);
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
42
59
|
const dev = Command.make("dev", {}, () => Effect.gen(function* () {
|
|
43
60
|
yield* logPending("Performing initial sync…");
|
|
44
|
-
const
|
|
61
|
+
const previousFunctionPaths = yield* loadPreviousFunctionPaths;
|
|
62
|
+
const initialResult = yield* codegenHandler.pipe(Effect.tap(({ functionPaths }) => logFunctionPathDiff(previousFunctionPaths, functionPaths)), Effect.tap(() => logSuccess("Generated files are up-to-date")), catchAndLog);
|
|
63
|
+
const initialFunctionPaths = Option.match(initialResult, {
|
|
64
|
+
onNone: () => emptyFunctionPaths,
|
|
65
|
+
onSome: ({ functionPaths }) => functionPaths
|
|
66
|
+
});
|
|
45
67
|
const pendingRef = yield* Ref.make(pendingInit);
|
|
46
68
|
const signal = yield* Queue.sliding(1);
|
|
47
|
-
const
|
|
69
|
+
const restartQueue = yield* Queue.sliding(1);
|
|
70
|
+
const watcherErrorsRef = yield* Ref.make(emptyWatcherErrors);
|
|
48
71
|
yield* Effect.all([
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
syncLoop(signal, pendingRef, initialFunctionPaths)
|
|
72
|
+
Effect.scoped(entryPointsWatcher(signal, pendingRef, restartQueue, watcherErrorsRef)),
|
|
73
|
+
confectStructureWatcher(signal, pendingRef, restartQueue),
|
|
74
|
+
syncLoop(signal, pendingRef, initialFunctionPaths, watcherErrorsRef)
|
|
52
75
|
], { concurrency: "unbounded" });
|
|
53
76
|
})).pipe(Command.withDescription("Start the Confect development server"));
|
|
54
|
-
const
|
|
77
|
+
const esbuildMessageKey = (m) => `${m.location?.file ?? ""}:${m.location?.line ?? ""}:${m.location?.column ?? ""}:${m.text}`;
|
|
78
|
+
const allMessages = (errors) => pipe(Array.fromIterable(errors.values()), Array.flatten);
|
|
79
|
+
const dedupeWatcherErrors = (errors) => pipe(allMessages(errors), Array.dedupeWith((messageA, messageB) => esbuildMessageKey(messageA) === esbuildMessageKey(messageB)));
|
|
80
|
+
const watcherErrorsSignature = (errors) => pipe(allMessages(errors), Array.map(esbuildMessageKey), Array.dedupe, Array.sort(Order.string), Array.join("\n"));
|
|
81
|
+
/**
|
|
82
|
+
* Log any watcher errors that haven't already been logged at their
|
|
83
|
+
* current signature. Suppresses the per-watcher fanout that happens
|
|
84
|
+
* when one root cause (e.g. a missing import) breaks every entry
|
|
85
|
+
* point's build at the same source location.
|
|
86
|
+
*/
|
|
87
|
+
const logChangedWatcherErrors = (watcherErrorsRef, lastLoggedSignatureRef) => Effect.gen(function* () {
|
|
88
|
+
const errors = yield* Ref.get(watcherErrorsRef);
|
|
89
|
+
const signature = watcherErrorsSignature(errors);
|
|
90
|
+
if (signature === (yield* Ref.get(lastLoggedSignatureRef))) return;
|
|
91
|
+
yield* Ref.set(lastLoggedSignatureRef, signature);
|
|
92
|
+
if (errors.size === 0) return;
|
|
93
|
+
yield* logCoalescedBuildErrors(dedupeWatcherErrors(errors));
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Block until the signal queue has been quiet for `quiescence`. esbuild
|
|
97
|
+
* watchers' `onEnd` events for a single user edit can be spread across
|
|
98
|
+
* hundreds of milliseconds, so a fixed window misses late arrivals.
|
|
99
|
+
* Bounded by `maxWait` so pathological signal floods can't pin the
|
|
100
|
+
* loop forever.
|
|
101
|
+
*/
|
|
102
|
+
const drainUntilQuiescent = (signal, quiescence, maxWait) => Effect.gen(function* () {
|
|
103
|
+
const start = yield* Clock.currentTimeMillis;
|
|
104
|
+
const maxMillis = Duration.toMillis(maxWait);
|
|
105
|
+
yield* Effect.iterate(true, {
|
|
106
|
+
while: (keepGoing) => keepGoing,
|
|
107
|
+
body: () => Effect.gen(function* () {
|
|
108
|
+
yield* Effect.sleep(quiescence);
|
|
109
|
+
const drained = yield* Queue.takeAll(signal);
|
|
110
|
+
if (Chunk.isEmpty(drained)) return false;
|
|
111
|
+
return (yield* Clock.currentTimeMillis) - start < maxMillis;
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const syncLoop = (signal, pendingRef, initialFunctionPaths, watcherErrorsRef) => Effect.gen(function* () {
|
|
55
116
|
const functionPathsRef = yield* Ref.make(initialFunctionPaths);
|
|
56
|
-
const
|
|
117
|
+
const lastLoggedErrorsRef = yield* Ref.make("");
|
|
57
118
|
return yield* Effect.forever(Effect.gen(function* () {
|
|
58
|
-
yield* Effect.logDebug("Running sync loop
|
|
119
|
+
yield* Effect.logDebug("Running sync loop…");
|
|
59
120
|
yield* Queue.take(signal);
|
|
60
|
-
|
|
61
|
-
yield*
|
|
62
|
-
yield* Deferred.succeed(initialSyncDone, void 0);
|
|
121
|
+
yield* drainUntilQuiescent(signal, COALESCE_QUIESCENCE, COALESCE_MAX_WAIT);
|
|
122
|
+
yield* logChangedWatcherErrors(watcherErrorsRef, lastLoggedErrorsRef);
|
|
63
123
|
const pending = yield* Ref.getAndSet(pendingRef, pendingInit);
|
|
64
|
-
if (pending
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
if (!isPendingDirty(pending)) return;
|
|
125
|
+
yield* logPending("Dependencies may have changed, reloading…");
|
|
126
|
+
if (pending.specDirty) {
|
|
127
|
+
const current = yield* codegenHandler.pipe(Effect.tap(({ functionPaths: nextFunctionPaths }) => Effect.gen(function* () {
|
|
128
|
+
yield* logFunctionPathDiff(yield* Ref.get(functionPathsRef), nextFunctionPaths);
|
|
129
|
+
yield* Ref.set(functionPathsRef, nextFunctionPaths);
|
|
130
|
+
})), catchAndLog);
|
|
131
|
+
if (Option.isNone(current)) return;
|
|
132
|
+
if (current.value.anyWritesHappened) yield* Effect.sleep(ECHO_COOLDOWN);
|
|
133
|
+
yield* drainUntilQuiescent(signal, COALESCE_QUIESCENCE, COALESCE_MAX_WAIT);
|
|
134
|
+
yield* Ref.set(pendingRef, pendingInit);
|
|
67
135
|
}
|
|
68
|
-
const specResult = yield* Effect.if(pending.specDirty, {
|
|
69
|
-
onTrue: () => loadSpec.pipe(Effect.andThen(Effect.fn(function* (spec) {
|
|
70
|
-
yield* Effect.logDebug("Spec loaded");
|
|
71
|
-
const previous = yield* Ref.get(functionPathsRef);
|
|
72
|
-
const path = yield* Path.Path;
|
|
73
|
-
const convexDirectory = yield* ConvexDirectory.get;
|
|
74
|
-
const current = make(spec);
|
|
75
|
-
const { functionsAdded, functionsRemoved, groupsRemoved, groupsAdded, groupsChanged } = diff(previous, current);
|
|
76
|
-
yield* removeGroups(groupsRemoved);
|
|
77
|
-
yield* Effect.forEach(groupsRemoved, (gp) => Effect.gen(function* () {
|
|
78
|
-
const relativeModulePath = yield* modulePath(gp);
|
|
79
|
-
yield* logFileChangeIndented("Removed", path.join(convexDirectory, relativeModulePath));
|
|
80
|
-
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemovedIndented);
|
|
81
|
-
}));
|
|
82
|
-
yield* writeGroups(spec, groupsAdded);
|
|
83
|
-
yield* Effect.forEach(groupsAdded, (gp) => Effect.gen(function* () {
|
|
84
|
-
const relativeModulePath = yield* modulePath(gp);
|
|
85
|
-
yield* logFileChangeIndented("Added", path.join(convexDirectory, relativeModulePath));
|
|
86
|
-
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAddedIndented);
|
|
87
|
-
}));
|
|
88
|
-
yield* writeGroups(spec, groupsChanged);
|
|
89
|
-
yield* Effect.forEach(groupsChanged, (gp) => Effect.gen(function* () {
|
|
90
|
-
const relativeModulePath = yield* modulePath(gp);
|
|
91
|
-
yield* logFileChangeIndented("Modified", path.join(convexDirectory, relativeModulePath));
|
|
92
|
-
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAddedIndented);
|
|
93
|
-
yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemovedIndented);
|
|
94
|
-
}));
|
|
95
|
-
yield* Ref.set(functionPathsRef, current);
|
|
96
|
-
return Option.some(void 0);
|
|
97
|
-
})), Effect.catchTag("SpecImportFailedError", () => logFailure("Spec import failed").pipe(Effect.as(Option.none()))), Effect.catchTag("SpecFileDoesNotExportSpecError", () => logFailure("Spec file does not default export a Convex spec").pipe(Effect.as(Option.none()))), Effect.catchTag("NodeSpecFileDoesNotExportSpecError", () => logFailure("Node spec file does not default export a Node spec").pipe(Effect.as(Option.none())))),
|
|
98
|
-
onFalse: () => Effect.succeed(Option.some(void 0))
|
|
99
|
-
});
|
|
100
136
|
const dirtyOptionalFiles = [
|
|
101
137
|
...pending.httpDirty ? [syncOptionalFile(generateHttp, "http.ts")] : [],
|
|
102
138
|
...pending.cronsDirty ? [syncOptionalFile(generateCrons, "crons.ts")] : [],
|
|
103
139
|
...pending.authDirty ? [syncOptionalFile(generateAuthConfig, "auth.config.ts")] : []
|
|
104
140
|
];
|
|
105
141
|
yield* Array.isNonEmptyReadonlyArray(dirtyOptionalFiles) ? Effect.all(dirtyOptionalFiles, { concurrency: "unbounded" }) : Effect.void;
|
|
106
|
-
yield*
|
|
107
|
-
onSome: () => logSuccess("Generated files are up-to-date"),
|
|
108
|
-
onNone: () => Effect.void
|
|
109
|
-
});
|
|
142
|
+
yield* logSuccess("Generated files are up-to-date");
|
|
110
143
|
}));
|
|
111
144
|
});
|
|
112
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Every file whose import graph codegen should react to. Each one becomes
|
|
147
|
+
* its own scoped esbuild watcher; the union of their watches gives us
|
|
148
|
+
* dependency-aware tracking of anything reachable from `confect/`,
|
|
149
|
+
* including files outside `confect/`.
|
|
150
|
+
*/
|
|
151
|
+
const discoverEntryPoints = Effect.gen(function* () {
|
|
113
152
|
const fs = yield* FileSystem.FileSystem;
|
|
114
153
|
const path = yield* Path.Path;
|
|
154
|
+
const projectRoot = yield* ProjectRoot.get;
|
|
115
155
|
const confectDirectory = yield* ConfectDirectory.get;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
const tryEntry = (relativePath, pendingKey) => Effect.gen(function* () {
|
|
157
|
+
const absolutePath = path.join(confectDirectory, relativePath);
|
|
158
|
+
if (!(yield* fs.exists(absolutePath))) return Option.none();
|
|
159
|
+
return Option.some({
|
|
160
|
+
absolutePath,
|
|
161
|
+
displayPath: path.relative(projectRoot, absolutePath),
|
|
162
|
+
pendingKey
|
|
163
|
+
});
|
|
124
164
|
});
|
|
165
|
+
const fixedEntryOptions = yield* Effect.all([
|
|
166
|
+
tryEntry(GENERATED_SPEC_PATH, "specDirty"),
|
|
167
|
+
tryEntry(GENERATED_NODE_SPEC_PATH, "specDirty"),
|
|
168
|
+
tryEntry("schema.ts", "specDirty"),
|
|
169
|
+
tryEntry("http.ts", "httpDirty"),
|
|
170
|
+
tryEntry("crons.ts", "cronsDirty"),
|
|
171
|
+
tryEntry("auth.ts", "authDirty")
|
|
172
|
+
]);
|
|
173
|
+
const implRelativePaths = yield* discoverLeafImplFiles;
|
|
174
|
+
const implEntryOptions = yield* Effect.forEach(implRelativePaths, (relativePath) => tryEntry(relativePath, "specDirty"));
|
|
175
|
+
return Array.getSomes([...fixedEntryOptions, ...implEntryOptions]);
|
|
125
176
|
});
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
setup(build) {
|
|
156
|
-
build.onEnd((result) => {
|
|
157
|
-
if (result.errors.length === 0) build._emit?.();
|
|
158
|
-
else Effect.runPromise(Effect.gen(function* () {
|
|
159
|
-
const formattedMessages = yield* Effect.promise(() => esbuild.formatMessages(result.errors, {
|
|
160
|
-
kind: "error",
|
|
161
|
-
color: true,
|
|
162
|
-
terminalWidth: 80
|
|
177
|
+
const esbuildOptions = (entry, signal, pendingRef, watcherErrorsRef) => {
|
|
178
|
+
const initialBuildSeenRef = Ref.unsafeMake(false);
|
|
179
|
+
return {
|
|
180
|
+
entryPoints: [entry.absolutePath],
|
|
181
|
+
bundle: true,
|
|
182
|
+
write: false,
|
|
183
|
+
metafile: true,
|
|
184
|
+
platform: "node",
|
|
185
|
+
format: "esm",
|
|
186
|
+
logLevel: "silent",
|
|
187
|
+
external: EXTERNAL_PACKAGES,
|
|
188
|
+
plugins: [absoluteExternalsPlugin, {
|
|
189
|
+
name: "notify-rebuild",
|
|
190
|
+
setup(build) {
|
|
191
|
+
build.onEnd((result) => {
|
|
192
|
+
Effect.runPromise(Effect.gen(function* () {
|
|
193
|
+
const isInitial = !(yield* Ref.getAndSet(initialBuildSeenRef, true));
|
|
194
|
+
yield* Ref.update(watcherErrorsRef, (current) => {
|
|
195
|
+
const next = new Map(current);
|
|
196
|
+
if (result.errors.length > 0) next.set(entry.absolutePath, result.errors);
|
|
197
|
+
else next.delete(entry.absolutePath);
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
200
|
+
if (isInitial && result.errors.length === 0) return;
|
|
201
|
+
yield* Ref.update(pendingRef, (p) => ({
|
|
202
|
+
...p,
|
|
203
|
+
[entry.pendingKey]: true
|
|
204
|
+
}));
|
|
205
|
+
yield* Queue.offer(signal, void 0);
|
|
163
206
|
}));
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}));
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}]
|
|
171
|
-
});
|
|
172
|
-
const createSpecWatcher = (entryPoint) => Stream.asyncPush((emit) => Effect.acquireRelease(Effect.promise(async () => {
|
|
173
|
-
const opts = esbuildOptions(entryPoint);
|
|
174
|
-
const plugin = opts.plugins[0];
|
|
175
|
-
const originalSetup = plugin.setup;
|
|
176
|
-
plugin.setup = (build) => {
|
|
177
|
-
build._emit = () => emit.single();
|
|
178
|
-
return originalSetup(build);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}]
|
|
179
210
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
211
|
+
};
|
|
212
|
+
const createEntryPointWatcher = (entry, signal, pendingRef, watcherErrorsRef) => Effect.acquireRelease(Effect.promise(async () => {
|
|
213
|
+
const ctx = await esbuild.context(esbuildOptions(entry, signal, pendingRef, watcherErrorsRef));
|
|
184
214
|
await ctx.watch();
|
|
185
215
|
return ctx;
|
|
186
|
-
}), (ctx) => Effect.
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
}), (ctx) => Effect.gen(function* () {
|
|
217
|
+
yield* Effect.promise(() => ctx.dispose());
|
|
218
|
+
yield* Ref.update(watcherErrorsRef, (current) => {
|
|
219
|
+
if (!current.has(entry.absolutePath)) return current;
|
|
220
|
+
const next = new Map(current);
|
|
221
|
+
next.delete(entry.absolutePath);
|
|
222
|
+
return next;
|
|
223
|
+
});
|
|
224
|
+
yield* Effect.logDebug(`esbuild watcher disposed: ${entry.displayPath}`);
|
|
225
|
+
}));
|
|
226
|
+
/**
|
|
227
|
+
* Holds one scoped esbuild watcher per entry point and reconciles the set
|
|
228
|
+
* whenever something offers to `restartQueue`. Adding or removing an entry
|
|
229
|
+
* point only spawns/disposes the affected watcher; unchanged entries keep
|
|
230
|
+
* their existing context, so a structural change doesn't churn watchers
|
|
231
|
+
* for unrelated files.
|
|
232
|
+
*/
|
|
233
|
+
const entryPointsWatcher = (signal, pendingRef, restartQueue, watcherErrorsRef) => Effect.gen(function* () {
|
|
234
|
+
const parentScope = yield* Effect.scope;
|
|
235
|
+
const scopesRef = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
236
|
+
const sync = Effect.gen(function* () {
|
|
237
|
+
const desired = yield* discoverEntryPoints;
|
|
238
|
+
const desiredByPath = new Map(desired.map((entryPoint) => [entryPoint.absolutePath, entryPoint]));
|
|
239
|
+
const current = yield* Ref.get(scopesRef);
|
|
240
|
+
yield* Effect.forEach(Array.fromIterable(current), ([absolutePath, childScope]) => desiredByPath.has(absolutePath) ? Effect.void : Scope.close(childScope, Exit.void).pipe(Effect.andThen(Ref.update(scopesRef, (scopes) => {
|
|
241
|
+
const updated = new Map(scopes);
|
|
242
|
+
updated.delete(absolutePath);
|
|
243
|
+
return updated;
|
|
244
|
+
}))));
|
|
245
|
+
yield* Effect.forEach(desired, (entry) => Effect.gen(function* () {
|
|
246
|
+
if ((yield* Ref.get(scopesRef)).has(entry.absolutePath)) return;
|
|
247
|
+
const childScope = yield* Scope.fork(parentScope, ExecutionStrategy.sequential);
|
|
248
|
+
yield* createEntryPointWatcher(entry, signal, pendingRef, watcherErrorsRef).pipe(Scope.extend(childScope));
|
|
249
|
+
yield* Ref.update(scopesRef, (scopes) => {
|
|
250
|
+
const updated = new Map(scopes);
|
|
251
|
+
updated.set(entry.absolutePath, childScope);
|
|
252
|
+
return updated;
|
|
253
|
+
});
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
256
|
+
yield* sync;
|
|
257
|
+
return yield* Effect.forever(Queue.take(restartQueue).pipe(Effect.andThen(sync)));
|
|
189
258
|
});
|
|
190
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Single recursive `fs.watch` on `confect/`. Flips the matching dirty flag
|
|
261
|
+
* for any change to an entry-point-shaped file (so codegen runs without
|
|
262
|
+
* waiting on a newly spawned esbuild watcher), and offers to
|
|
263
|
+
* `restartQueue` when an entry point is created or removed so the watcher
|
|
264
|
+
* manager picks up the new set.
|
|
265
|
+
*/
|
|
266
|
+
const confectStructureWatcher = (signal, pendingRef, restartQueue) => Effect.gen(function* () {
|
|
191
267
|
const fs = yield* FileSystem.FileSystem;
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
268
|
+
const path = yield* Path.Path;
|
|
269
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
270
|
+
yield* pipe(fs.watch(confectDirectory, { recursive: true }), Stream.debounce(Duration.millis(200)), Stream.runForEach((event) => handleConfectChange({
|
|
271
|
+
relativePath: path.relative(confectDirectory, event.path),
|
|
272
|
+
eventTag: event._tag,
|
|
273
|
+
signal,
|
|
274
|
+
pendingRef,
|
|
275
|
+
restartQueue
|
|
276
|
+
})));
|
|
277
|
+
});
|
|
278
|
+
const TOP_LEVEL_OPTIONAL_KEYS = new Map([
|
|
279
|
+
["http.ts", "httpDirty"],
|
|
280
|
+
["crons.ts", "cronsDirty"],
|
|
281
|
+
["auth.ts", "authDirty"]
|
|
282
|
+
]);
|
|
283
|
+
const flipDirtyAndSignal = (pendingRef, signal, key, restartQueue, restart) => pipe(Ref.update(pendingRef, (p) => ({
|
|
284
|
+
...p,
|
|
285
|
+
[key]: true
|
|
286
|
+
})), Effect.andThen(Queue.offer(signal, void 0)), Effect.andThen(restart ? Queue.offer(restartQueue, void 0) : Effect.void));
|
|
287
|
+
const handleConfectChange = ({ relativePath, eventTag, signal, pendingRef, restartQueue }) => {
|
|
288
|
+
if (relativePath.split(/[/\\]/).includes("_generated")) return Effect.void;
|
|
289
|
+
if (!relativePath.endsWith(".ts")) return Effect.void;
|
|
290
|
+
const isLifecycleChange = eventTag !== "Update";
|
|
291
|
+
const topLevelKey = TOP_LEVEL_OPTIONAL_KEYS.get(relativePath);
|
|
292
|
+
if (topLevelKey !== void 0) return flipDirtyAndSignal(pendingRef, signal, topLevelKey, restartQueue, isLifecycleChange);
|
|
293
|
+
if (relativePath === "schema.ts") return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, isLifecycleChange);
|
|
294
|
+
if (isLeafSpecPath(relativePath) || isLeafImplPath(relativePath)) return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, isLifecycleChange);
|
|
295
|
+
if (eventTag === "Create") return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, false);
|
|
296
|
+
return Effect.void;
|
|
211
297
|
};
|
|
212
|
-
const formatBuildErrors = (errors, formattedMessages) => pipe(formattedMessages, Array.map((message, i) => formatBuildError(errors[i], message)), Array.join(""), String.trimEnd);
|
|
213
|
-
var SpecFileDoesNotExportSpecError = class extends Schema.TaggedError()("SpecFileDoesNotExportSpecError", {}) {};
|
|
214
|
-
var NodeSpecFileDoesNotExportSpecError = class extends Schema.TaggedError()("NodeSpecFileDoesNotExportSpecError", {}) {};
|
|
215
|
-
var SpecImportFailedError = class extends Schema.TaggedError()("SpecImportFailedError", { error: Schema.Unknown }) {};
|
|
216
298
|
const syncOptionalFile = (generate, convexFile) => pipe(generate, Effect.andThen(Option.match({
|
|
217
299
|
onSome: ({ change, convexFilePath }) => Match.value(change).pipe(Match.when("Unchanged", () => Effect.void), Match.whenOr("Added", "Modified", (addedOrModified) => logFileChangeIndented(addedOrModified, convexFilePath)), Match.exhaustive),
|
|
218
300
|
onNone: () => Effect.gen(function* () {
|
|
@@ -226,34 +308,6 @@ const syncOptionalFile = (generate, convexFile) => pipe(generate, Effect.andThen
|
|
|
226
308
|
}
|
|
227
309
|
})
|
|
228
310
|
})));
|
|
229
|
-
const optionalConfectFiles = {
|
|
230
|
-
"http.ts": "httpDirty",
|
|
231
|
-
"crons.ts": "cronsDirty",
|
|
232
|
-
"auth.ts": "authDirty",
|
|
233
|
-
"nodeSpec.ts": "specDirty",
|
|
234
|
-
"nodeImpl.ts": "nodeImplDirty"
|
|
235
|
-
};
|
|
236
|
-
const confectDirectoryWatcher = (signal, pendingRef, specWatcherRestartQueue) => Effect.gen(function* () {
|
|
237
|
-
const fs = yield* FileSystem.FileSystem;
|
|
238
|
-
const path = yield* Path.Path;
|
|
239
|
-
const confectDirectory = yield* ConfectDirectory.get;
|
|
240
|
-
yield* pipe(fs.watch(confectDirectory), Stream.runForEach((event) => {
|
|
241
|
-
const basename = path.basename(event.path);
|
|
242
|
-
const pendingKey = optionalConfectFiles[basename];
|
|
243
|
-
if (pendingKey !== void 0) return pipe(pendingRef, Ref.update((pending) => {
|
|
244
|
-
const next = {
|
|
245
|
-
...pending,
|
|
246
|
-
[pendingKey]: true
|
|
247
|
-
};
|
|
248
|
-
if (basename === "nodeImpl.ts") return {
|
|
249
|
-
...next,
|
|
250
|
-
specDirty: true
|
|
251
|
-
};
|
|
252
|
-
return next;
|
|
253
|
-
}), Effect.andThen(Queue.offer(signal, void 0)), Effect.andThen(basename === "nodeSpec.ts" ? Queue.offer(specWatcherRestartQueue, void 0) : Effect.void));
|
|
254
|
-
return Effect.void;
|
|
255
|
-
}));
|
|
256
|
-
});
|
|
257
311
|
|
|
258
312
|
//#endregion
|
|
259
313
|
export { dev };
|