@confect/cli 8.0.0 → 9.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.
package/dist/utils.mjs CHANGED
@@ -2,71 +2,46 @@ import { fromGroupModulePath, getGroupSpec, modulePath } from "./GroupPath.mjs";
2
2
  import { logFileAdded, logFileModified, logFileRemoved } from "./log.mjs";
3
3
  import { ConvexDirectory } from "./ConvexDirectory.mjs";
4
4
  import { ConfectDirectory } from "./ConfectDirectory.mjs";
5
- import { authConfig, crons, functions, http } from "./templates.mjs";
6
5
  import { GroupPaths } from "./GroupPaths.mjs";
7
6
  import { groupPaths, make } from "./FunctionPaths.mjs";
8
- import { createRequire } from "node:module";
9
- import { Array, Effect, HashSet, Option, Order, Record, String, pipe } from "effect";
7
+ import { authConfig, crons, functions, http } from "./templates.mjs";
8
+ import { Array, Context, Effect, HashSet, Option, Order, Record, Ref, String, pipe } from "effect";
10
9
  import { FileSystem, Path } from "@effect/platform";
11
- import { pathToFileURL } from "node:url";
12
- import * as esbuild from "esbuild";
13
10
 
14
11
  //#region src/utils.ts
12
+ /**
13
+ * Tracks whether the current codegen run wrote anything to disk. Set to
14
+ * `true` by every helper that actually overwrites a file (skipping the
15
+ * content-unchanged case). `codegenHandler` provides a fresh `Ref` per
16
+ * run and reads it back to report `anyWritesHappened`; callers outside a
17
+ * codegen pass hit the cached default `Ref` and need not interact with
18
+ * the tracker.
19
+ */
20
+ var WriteTracker = class extends Context.Reference()("@confect/cli/WriteTracker", { defaultValue: () => Ref.unsafeMake(false) }) {};
21
+ const markWritten = Effect.gen(function* () {
22
+ const tracker = yield* WriteTracker;
23
+ yield* Ref.set(tracker, true);
24
+ });
15
25
  const removePathExtension = (pathStr) => Effect.gen(function* () {
16
26
  const path = yield* Path.Path;
17
27
  return String.slice(0, -path.extname(pathStr).length)(pathStr);
18
28
  });
19
- const EXTERNAL_PACKAGES = [
20
- "@confect/core",
21
- "@confect/server",
22
- "effect",
23
- "@effect/*"
24
- ];
25
- const isExternalImport = (path) => EXTERNAL_PACKAGES.some((p) => {
26
- if (p.endsWith("/*")) return path.startsWith(p.slice(0, -1));
27
- return path === p || path.startsWith(p + "/");
28
- });
29
- const absoluteExternalsPlugin = {
30
- name: "absolute-externals",
31
- setup(build) {
32
- build.onResolve({ filter: /.*/ }, async (args) => {
33
- if (args.kind !== "import-statement" && args.kind !== "dynamic-import") return;
34
- if (!isExternalImport(args.path)) return;
35
- const parentFile = pathToFileURL(args.resolveDir + "/_").href;
36
- return {
37
- path: pathToFileURL(createRequire(parentFile).resolve(args.path)).href,
38
- external: true
39
- };
40
- });
41
- }
42
- };
43
- /**
44
- * Bundle a TypeScript entry point with esbuild and import the result via a
45
- * data URL. This handles extensionless `.ts` imports regardless of whether the
46
- * user's project sets `"type": "module"` in package.json.
47
- */
48
- const bundleAndImport = (entryPoint) => Effect.gen(function* () {
49
- const code = (yield* Effect.promise(() => esbuild.build({
50
- entryPoints: [entryPoint],
51
- bundle: true,
52
- write: false,
53
- platform: "node",
54
- format: "esm",
55
- logLevel: "silent",
56
- plugins: [absoluteExternalsPlugin]
57
- }))).outputFiles[0].text;
58
- const dataUrl = "data:text/javascript;base64," + Buffer.from(code).toString("base64");
59
- return yield* Effect.promise(() => import(dataUrl));
29
+ /** Ensures a relative path is a valid ESM/TS module specifier (e.g. `spec` → `./spec`). */
30
+ const toModuleImportPath = (relativePath) => Effect.gen(function* () {
31
+ const withoutExt = yield* removePathExtension(relativePath);
32
+ return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
60
33
  });
61
34
  const writeFileStringAndLog = (filePath, contents) => Effect.gen(function* () {
62
35
  const fs = yield* FileSystem.FileSystem;
63
36
  if (!(yield* fs.exists(filePath))) {
64
37
  yield* fs.writeFileString(filePath, contents);
38
+ yield* markWritten;
65
39
  yield* logFileAdded(filePath);
66
40
  return;
67
41
  }
68
42
  if ((yield* fs.readFileString(filePath)) !== contents) {
69
43
  yield* fs.writeFileString(filePath, contents);
44
+ yield* markWritten;
70
45
  yield* logFileModified(filePath);
71
46
  }
72
47
  });
@@ -83,42 +58,58 @@ const writeFileString = (filePath, contents) => Effect.gen(function* () {
83
58
  const fs = yield* FileSystem.FileSystem;
84
59
  if (!(yield* fs.exists(filePath))) {
85
60
  yield* fs.writeFileString(filePath, contents);
61
+ yield* markWritten;
86
62
  return "Added";
87
63
  }
88
64
  if ((yield* fs.readFileString(filePath)) !== contents) {
89
65
  yield* fs.writeFileString(filePath, contents);
66
+ yield* markWritten;
90
67
  return "Modified";
91
68
  }
92
69
  return "Unchanged";
93
70
  });
94
- const generateGroupModule = ({ groupPath, functionNames }) => Effect.gen(function* () {
71
+ const removePathIfExists = (filePath) => Effect.gen(function* () {
72
+ const fs = yield* FileSystem.FileSystem;
73
+ if (!(yield* fs.exists(filePath))) return;
74
+ yield* fs.remove(filePath).pipe(Effect.catchTag("SystemError", (error) => error.reason === "NotFound" ? Effect.void : Effect.fail(error)));
75
+ });
76
+ /**
77
+ * Bump the mtime of `convex/schema.ts` so the Convex CLI's chokidar watcher
78
+ * emits a `change` event after every successful Confect codegen run. Without
79
+ * this, a codegen that doesn't change any file content (for example,
80
+ * recovering from a transient broken state in `confect/`) leaves Convex
81
+ * stuck on its previous error because nothing it observes has changed.
82
+ */
83
+ const touchConvexSchema = Effect.gen(function* () {
84
+ const fs = yield* FileSystem.FileSystem;
85
+ const path = yield* Path.Path;
86
+ const convexDirectory = yield* ConvexDirectory.get;
87
+ const schemaPath = path.join(convexDirectory, "schema.ts");
88
+ if (!(yield* fs.exists(schemaPath))) return;
89
+ const now = /* @__PURE__ */ new Date();
90
+ yield* fs.utimes(schemaPath, now, now);
91
+ });
92
+ const generateGroupModule = ({ groupPath, functionNames, registeredFunctionsImportPath, useNode = false }) => Effect.gen(function* () {
95
93
  const fs = yield* FileSystem.FileSystem;
96
94
  const path = yield* Path.Path;
97
95
  const convexDirectory = yield* ConvexDirectory.get;
98
- const confectDirectory = yield* ConfectDirectory.get;
99
96
  const relativeModulePath = yield* modulePath(groupPath);
100
- const modulePath$1 = path.join(convexDirectory, relativeModulePath);
101
- const directoryPath = path.dirname(modulePath$1);
97
+ const modulePath$4 = path.join(convexDirectory, relativeModulePath);
98
+ const directoryPath = path.dirname(modulePath$4);
102
99
  if (!(yield* fs.exists(directoryPath))) yield* fs.makeDirectory(directoryPath, { recursive: true });
103
- const isNodeGroup = groupPath.pathSegments[0] === "node";
104
- const registeredFunctionsFileName = isNodeGroup ? "nodeRegisteredFunctions.ts" : "registeredFunctions.ts";
105
- const registeredFunctionsPath = path.join(confectDirectory, "_generated", registeredFunctionsFileName);
106
- const registeredFunctionsImportPath = yield* removePathExtension(path.relative(path.dirname(modulePath$1), registeredFunctionsPath));
107
- const registeredFunctionsVariableName = isNodeGroup ? "nodeRegisteredFunctions" : "registeredFunctions";
108
100
  const functionsContentsString = yield* functions({
109
- groupPath,
110
101
  functionNames,
111
102
  registeredFunctionsImportPath,
112
- registeredFunctionsVariableName,
113
- useNode: isNodeGroup,
114
- ...isNodeGroup ? { registeredFunctionsLookupPath: groupPath.pathSegments.slice(1) } : {}
103
+ useNode
115
104
  });
116
- if (!(yield* fs.exists(modulePath$1))) {
117
- yield* fs.writeFileString(modulePath$1, functionsContentsString);
105
+ if (!(yield* fs.exists(modulePath$4))) {
106
+ yield* fs.writeFileString(modulePath$4, functionsContentsString);
107
+ yield* markWritten;
118
108
  return "Added";
119
109
  }
120
- if ((yield* fs.readFileString(modulePath$1)) !== functionsContentsString) {
121
- yield* fs.writeFileString(modulePath$1, functionsContentsString);
110
+ if ((yield* fs.readFileString(modulePath$4)) !== functionsContentsString) {
111
+ yield* fs.writeFileString(modulePath$4, functionsContentsString);
112
+ yield* markWritten;
122
113
  return "Modified";
123
114
  }
124
115
  return "Unchanged";
@@ -134,18 +125,23 @@ const logGroupPaths = (groupPaths, logFn) => Effect.gen(function* () {
134
125
  const generateFunctions = (spec) => Effect.gen(function* () {
135
126
  const path = yield* Path.Path;
136
127
  const convexDirectory = yield* ConvexDirectory.get;
128
+ const confectDirectory = yield* ConfectDirectory.get;
137
129
  const groupPathsFromFs = yield* getGroupPathsFromFs;
138
130
  const functionPaths = make(spec);
139
131
  const groupPathsFromSpec = groupPaths(functionPaths);
140
132
  const overlappingGroupPaths = GroupPaths.make(HashSet.intersection(groupPathsFromFs, groupPathsFromSpec));
141
133
  yield* Effect.forEach(overlappingGroupPaths, (groupPath) => Effect.gen(function* () {
134
+ const functionNames = pipe((yield* getGroupSpec(spec, groupPath)).functions, Record.values, Array.sortBy(Order.mapInput(Order.string, (fn) => fn.name)), Array.map((fn) => fn.name));
135
+ const relativeModulePath = yield* modulePath(groupPath);
136
+ const modulePath$3 = path.join(convexDirectory, relativeModulePath);
137
+ const registrySegments = groupPath.pathSegments[0] === "node" ? groupPath.pathSegments.slice(1) : groupPath.pathSegments;
138
+ const registeredFunctionsPath = path.join(confectDirectory, "_generated", "registeredFunctions", ...registrySegments) + ".ts";
142
139
  if ((yield* generateGroupModule({
143
140
  groupPath,
144
- functionNames: pipe((yield* getGroupSpec(spec, groupPath)).functions, Record.values, Array.sortBy(Order.mapInput(Order.string, (fn) => fn.name)), Array.map((fn) => fn.name))
145
- })) === "Modified") {
146
- const relativeModulePath = yield* modulePath(groupPath);
147
- yield* logFileModified(path.join(convexDirectory, relativeModulePath));
148
- }
141
+ functionNames,
142
+ registeredFunctionsImportPath: yield* toModuleImportPath(path.relative(path.dirname(modulePath$3), registeredFunctionsPath)),
143
+ useNode: groupPath.pathSegments[0] === "node"
144
+ })) === "Modified") yield* logFileModified(modulePath$3);
149
145
  }));
150
146
  const extinctGroupPaths = GroupPaths.make(HashSet.difference(groupPathsFromFs, groupPathsFromSpec));
151
147
  yield* removeGroups(extinctGroupPaths);
@@ -169,23 +165,31 @@ const getGroupPathsFromFs = Effect.gen(function* () {
169
165
  return pipe(yield* pipe(yield* fs.readDirectory(convexDirectory, { recursive: true }), Array.filter((convexPath) => path.extname(convexPath) === ".ts" && !RESERVED_CONVEX_TS_FILE_NAMES.has(path.basename(convexPath)) && path.basename(path.dirname(convexPath)) !== "_generated"), Effect.forEach((groupModulePath) => fromGroupModulePath(groupModulePath))), HashSet.fromIterable, GroupPaths.make);
170
166
  });
171
167
  const removeGroups = (groupPaths) => Effect.gen(function* () {
172
- const fs = yield* FileSystem.FileSystem;
173
168
  const path = yield* Path.Path;
174
169
  const convexDirectory = yield* ConvexDirectory.get;
175
170
  yield* Effect.all(HashSet.map(groupPaths, (groupPath) => Effect.gen(function* () {
176
171
  const relativeModulePath = yield* modulePath(groupPath);
177
- const modulePath$2 = path.join(convexDirectory, relativeModulePath);
172
+ const modulePath$1 = path.join(convexDirectory, relativeModulePath);
178
173
  yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);
179
- yield* fs.remove(modulePath$2);
174
+ yield* removePathIfExists(modulePath$1);
180
175
  yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);
181
176
  })), { concurrency: "unbounded" });
182
177
  });
183
178
  const writeGroups = (spec, groupPaths) => Effect.forEach(groupPaths, (groupPath) => Effect.gen(function* () {
179
+ const path = yield* Path.Path;
180
+ const convexDirectory = yield* ConvexDirectory.get;
181
+ const confectDirectory = yield* ConfectDirectory.get;
184
182
  const functionNames = pipe((yield* getGroupSpec(spec, groupPath)).functions, Record.values, Array.sortBy(Order.mapInput(Order.string, (fn) => fn.name)), Array.map((fn) => fn.name));
183
+ const relativeModulePath = yield* modulePath(groupPath);
184
+ const modulePath$2 = path.join(convexDirectory, relativeModulePath);
185
+ const registeredFunctionsPath = path.join(confectDirectory, "_generated", "registeredFunctions", ...groupPath.pathSegments) + ".ts";
186
+ const registeredFunctionsImportPath = yield* toModuleImportPath(path.relative(path.dirname(modulePath$2), registeredFunctionsPath));
185
187
  yield* Effect.logDebug(`Generating group ${groupPath}...`);
186
188
  yield* generateGroupModule({
187
189
  groupPath,
188
- functionNames
190
+ functionNames,
191
+ registeredFunctionsImportPath,
192
+ useNode: groupPath.pathSegments[0] === "node"
189
193
  });
190
194
  yield* Effect.logDebug(`Group ${groupPath} generated`);
191
195
  }));
@@ -208,5 +212,5 @@ const generateCrons = generateOptionalFile("crons.ts", "crons.ts", (importPath)
208
212
  const generateAuthConfig = generateOptionalFile("auth.ts", "auth.config.ts", (importPath) => authConfig({ authImportPath: importPath }));
209
213
 
210
214
  //#endregion
211
- export { EXTERNAL_PACKAGES, bundleAndImport, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removeGroups, removePathExtension, writeFileStringAndLog, writeGroups };
215
+ export { WriteTracker, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removePathExtension, removePathIfExists, toModuleImportPath, touchConvexSchema, writeFileStringAndLog };
212
216
  //# sourceMappingURL=utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":["GroupPath.modulePath","modulePath","templates.functions","FunctionPaths.make","FunctionPaths.groupPaths","GroupPath.getGroupSpec","GroupPath.fromGroupModulePath","templates.http","templates.crons","templates.authConfig"],"sources":["../src/utils.ts"],"sourcesContent":["import type { FunctionSpec, Spec } from \"@confect/core\";\nimport { createRequire } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport type { PlatformError } from \"@effect/platform/Error\";\nimport {\n Array,\n Effect,\n HashSet,\n Option,\n Order,\n pipe,\n Record,\n String,\n} from \"effect\";\nimport * as esbuild from \"esbuild\";\nimport * as FunctionPaths from \"./FunctionPaths\";\nimport * as GroupPath from \"./GroupPath\";\nimport * as GroupPaths from \"./GroupPaths\";\nimport { logFileAdded, logFileModified, logFileRemoved } from \"./log\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\nimport { ConvexDirectory } from \"./ConvexDirectory\";\nimport * as templates from \"./templates\";\n\nexport const removePathExtension = (pathStr: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n\n return String.slice(0, -path.extname(pathStr).length)(pathStr);\n });\n\nexport const EXTERNAL_PACKAGES = [\n \"@confect/core\",\n \"@confect/server\",\n \"effect\",\n \"@effect/*\",\n];\n\nconst isExternalImport = (path: string) =>\n EXTERNAL_PACKAGES.some((p) => {\n if (p.endsWith(\"/*\")) {\n return path.startsWith(p.slice(0, -1));\n }\n return path === p || path.startsWith(p + \"/\");\n });\n\nconst absoluteExternalsPlugin: esbuild.Plugin = {\n name: \"absolute-externals\",\n setup(build) {\n build.onResolve({ filter: /.*/ }, async (args) => {\n if (args.kind !== \"import-statement\" && args.kind !== \"dynamic-import\")\n return;\n if (!isExternalImport(args.path)) return;\n // `import.meta.resolve`'s second argument is silently ignored in modern\n // Node, so resolution would always walk up from the CLI's bundled file\n // (`packages/cli/dist/utils.mjs`) instead of from the user's project.\n // Use `createRequire` keyed on the importing file's directory so we\n // resolve out of *their* `node_modules`. The synthetic filename is just\n // a CommonJS resolution anchor; the file does not need to exist.\n const parentFile = pathToFileURL(args.resolveDir + \"/_\").href;\n const require_ = createRequire(parentFile);\n const resolvedPath = require_.resolve(args.path);\n const resolved = pathToFileURL(resolvedPath).href;\n return { path: resolved, external: true };\n });\n },\n};\n\n/**\n * Bundle a TypeScript entry point with esbuild and import the result via a\n * data URL. This handles extensionless `.ts` imports regardless of whether the\n * user's project sets `\"type\": \"module\"` in package.json.\n */\nexport const bundleAndImport = (entryPoint: string) =>\n Effect.gen(function* () {\n const result = yield* Effect.promise(() =>\n esbuild.build({\n entryPoints: [entryPoint],\n bundle: true,\n write: false,\n platform: \"node\",\n format: \"esm\",\n logLevel: \"silent\",\n plugins: [absoluteExternalsPlugin],\n }),\n );\n const code = result.outputFiles[0]!.text;\n const dataUrl =\n \"data:text/javascript;base64,\" + Buffer.from(code).toString(\"base64\");\n return yield* Effect.promise(() => import(dataUrl));\n });\n\nexport const writeFileStringAndLog = (filePath: string, contents: string) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n if (!(yield* fs.exists(filePath))) {\n yield* fs.writeFileString(filePath, contents);\n yield* logFileAdded(filePath);\n return;\n }\n const existing = yield* fs.readFileString(filePath);\n if (existing !== contents) {\n yield* fs.writeFileString(filePath, contents);\n yield* logFileModified(filePath);\n }\n });\n\nexport const findProjectRoot = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n const startDir = path.resolve(\".\");\n const root = path.parse(startDir).root;\n\n const directories = Array.unfold(startDir, (dir) =>\n dir === root\n ? Option.none()\n : Option.some([dir, path.dirname(dir)] as const),\n );\n\n const projectRoot = yield* Effect.findFirst(directories, (dir) =>\n fs.exists(path.join(dir, \"package.json\")),\n );\n\n return Option.getOrElse(projectRoot, () => startDir);\n});\n\nexport type WriteChange = \"Added\" | \"Modified\" | \"Unchanged\";\n\nexport const writeFileString = (\n filePath: string,\n contents: string,\n): Effect.Effect<WriteChange, PlatformError, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n\n if (!(yield* fs.exists(filePath))) {\n yield* fs.writeFileString(filePath, contents);\n return \"Added\";\n }\n const existing = yield* fs.readFileString(filePath);\n if (existing !== contents) {\n yield* fs.writeFileString(filePath, contents);\n return \"Modified\";\n }\n return \"Unchanged\";\n });\n\nexport const generateGroupModule = ({\n groupPath,\n functionNames,\n}: {\n groupPath: GroupPath.GroupPath;\n functionNames: string[];\n}) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n\n const directoryPath = path.dirname(modulePath);\n if (!(yield* fs.exists(directoryPath))) {\n yield* fs.makeDirectory(directoryPath, { recursive: true });\n }\n\n const isNodeGroup = groupPath.pathSegments[0] === \"node\";\n const registeredFunctionsFileName = isNodeGroup\n ? \"nodeRegisteredFunctions.ts\"\n : \"registeredFunctions.ts\";\n const registeredFunctionsPath = path.join(\n confectDirectory,\n \"_generated\",\n registeredFunctionsFileName,\n );\n const registeredFunctionsImportPath = yield* removePathExtension(\n path.relative(path.dirname(modulePath), registeredFunctionsPath),\n );\n const registeredFunctionsVariableName = isNodeGroup\n ? \"nodeRegisteredFunctions\"\n : \"registeredFunctions\";\n\n const functionsContentsString = yield* templates.functions({\n groupPath,\n functionNames,\n registeredFunctionsImportPath,\n registeredFunctionsVariableName,\n useNode: isNodeGroup,\n ...(isNodeGroup\n ? {\n registeredFunctionsLookupPath: groupPath.pathSegments.slice(1),\n }\n : {}),\n });\n\n if (!(yield* fs.exists(modulePath))) {\n yield* fs.writeFileString(modulePath, functionsContentsString);\n return \"Added\" as const;\n }\n const existing = yield* fs.readFileString(modulePath);\n if (existing !== functionsContentsString) {\n yield* fs.writeFileString(modulePath, functionsContentsString);\n return \"Modified\" as const;\n }\n return \"Unchanged\" as const;\n });\n\nconst logGroupPaths = <R>(\n groupPaths: GroupPaths.GroupPaths,\n logFn: (fullPath: string) => Effect.Effect<void, never, R>,\n) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n yield* Effect.forEach(groupPaths, (gp) =>\n Effect.gen(function* () {\n const relativeModulePath = yield* GroupPath.modulePath(gp);\n yield* logFn(path.join(convexDirectory, relativeModulePath));\n }),\n );\n });\n\nexport const generateFunctions = (spec: Spec.AnyWithProps) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const groupPathsFromFs = yield* getGroupPathsFromFs;\n const functionPaths = FunctionPaths.make(spec);\n const groupPathsFromSpec = FunctionPaths.groupPaths(functionPaths);\n\n const overlappingGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.intersection(groupPathsFromFs, groupPathsFromSpec),\n );\n yield* Effect.forEach(overlappingGroupPaths, (groupPath) =>\n Effect.gen(function* () {\n const group = yield* GroupPath.getGroupSpec(spec, groupPath);\n const functionNames = pipe(\n group.functions,\n Record.values,\n Array.sortBy(\n Order.mapInput(\n Order.string,\n (fn: FunctionSpec.AnyWithProps) => fn.name,\n ),\n ),\n Array.map((fn) => fn.name),\n );\n const result = yield* generateGroupModule({ groupPath, functionNames });\n if (result === \"Modified\") {\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n yield* logFileModified(\n path.join(convexDirectory, relativeModulePath),\n );\n }\n }),\n );\n\n const extinctGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.difference(groupPathsFromFs, groupPathsFromSpec),\n );\n yield* removeGroups(extinctGroupPaths);\n yield* logGroupPaths(extinctGroupPaths, logFileRemoved);\n\n const newGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.difference(groupPathsFromSpec, groupPathsFromFs),\n );\n yield* writeGroups(spec, newGroupPaths);\n yield* logGroupPaths(newGroupPaths, logFileAdded);\n\n return functionPaths;\n });\n\nconst getGroupPathsFromFs = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const RESERVED_CONVEX_TS_FILE_NAMES = new Set([\n \"schema.ts\",\n \"http.ts\",\n \"crons.ts\",\n \"auth.config.ts\",\n \"convex.config.ts\",\n ]);\n\n const allConvexPaths = yield* fs.readDirectory(convexDirectory, {\n recursive: true,\n });\n const groupPathArray = yield* pipe(\n allConvexPaths,\n Array.filter(\n (convexPath) =>\n path.extname(convexPath) === \".ts\" &&\n !RESERVED_CONVEX_TS_FILE_NAMES.has(path.basename(convexPath)) &&\n path.basename(path.dirname(convexPath)) !== \"_generated\",\n ),\n Effect.forEach((groupModulePath) =>\n GroupPath.fromGroupModulePath(groupModulePath),\n ),\n );\n return pipe(groupPathArray, HashSet.fromIterable, GroupPaths.GroupPaths.make);\n});\n\nexport const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n yield* Effect.all(\n HashSet.map(groupPaths, (groupPath) =>\n Effect.gen(function* () {\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n\n yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);\n\n yield* fs.remove(modulePath);\n yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);\n }),\n ),\n { concurrency: \"unbounded\" },\n );\n });\n\nexport const writeGroups = (\n spec: Spec.AnyWithProps,\n groupPaths: GroupPaths.GroupPaths,\n) =>\n Effect.forEach(groupPaths, (groupPath) =>\n Effect.gen(function* () {\n const group = yield* GroupPath.getGroupSpec(spec, groupPath);\n\n const functionNames = pipe(\n group.functions,\n Record.values,\n Array.sortBy(\n Order.mapInput(\n Order.string,\n (fn: FunctionSpec.AnyWithProps) => fn.name,\n ),\n ),\n Array.map((fn) => fn.name),\n );\n\n yield* Effect.logDebug(`Generating group ${groupPath}...`);\n yield* generateGroupModule({\n groupPath,\n functionNames,\n });\n yield* Effect.logDebug(`Group ${groupPath} generated`);\n }),\n );\n\nconst generateOptionalFile = (\n confectFile: string,\n convexFile: string,\n generateContents: (importPath: string) => Effect.Effect<string>,\n) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const confectFilePath = path.join(confectDirectory, confectFile);\n\n if (!(yield* fs.exists(confectFilePath))) {\n return Option.none();\n }\n\n const convexFilePath = path.join(convexDirectory, convexFile);\n const relativeImportPath = path.relative(\n path.dirname(convexFilePath),\n confectFilePath,\n );\n const importPathWithoutExt = yield* removePathExtension(relativeImportPath);\n const contents = yield* generateContents(importPathWithoutExt);\n const change = yield* writeFileString(convexFilePath, contents);\n return Option.some({ change, convexFilePath });\n });\n\nexport const generateHttp = generateOptionalFile(\n \"http.ts\",\n \"http.ts\",\n (importPath) => templates.http({ httpImportPath: importPath }),\n);\n\nexport const generateCrons = generateOptionalFile(\n \"crons.ts\",\n \"crons.ts\",\n (importPath) => templates.crons({ cronsImportPath: importPath }),\n);\n\nexport const generateAuthConfig = generateOptionalFile(\n \"auth.ts\",\n \"auth.config.ts\",\n (importPath) => templates.authConfig({ authImportPath: importPath }),\n);\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","names":["GroupPath.modulePath","modulePath","templates.functions","FunctionPaths.make","FunctionPaths.groupPaths","GroupPath.getGroupSpec","GroupPath.fromGroupModulePath","templates.http","templates.crons","templates.authConfig"],"sources":["../src/utils.ts"],"sourcesContent":["import type { FunctionSpec, Spec } from \"@confect/core\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport type { PlatformError } from \"@effect/platform/Error\";\nimport {\n Array,\n Context,\n Effect,\n HashSet,\n Option,\n Order,\n pipe,\n Record,\n Ref,\n String,\n} from \"effect\";\nimport * as FunctionPaths from \"./FunctionPaths\";\nimport * as GroupPath from \"./GroupPath\";\nimport * as GroupPaths from \"./GroupPaths\";\nimport { logFileAdded, logFileModified, logFileRemoved } from \"./log\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\nimport { ConvexDirectory } from \"./ConvexDirectory\";\nimport * as templates from \"./templates\";\n\n/**\n * Tracks whether the current codegen run wrote anything to disk. Set to\n * `true` by every helper that actually overwrites a file (skipping the\n * content-unchanged case). `codegenHandler` provides a fresh `Ref` per\n * run and reads it back to report `anyWritesHappened`; callers outside a\n * codegen pass hit the cached default `Ref` and need not interact with\n * the tracker.\n */\nexport class WriteTracker extends Context.Reference<WriteTracker>()(\n \"@confect/cli/WriteTracker\",\n { defaultValue: () => Ref.unsafeMake(false) },\n) {}\n\nconst markWritten = Effect.gen(function* () {\n const tracker = yield* WriteTracker;\n yield* Ref.set(tracker, true);\n});\n\nexport const removePathExtension = (pathStr: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n\n return String.slice(0, -path.extname(pathStr).length)(pathStr);\n });\n\n/** Ensures a relative path is a valid ESM/TS module specifier (e.g. `spec` → `./spec`). */\nexport const toModuleImportPath = (relativePath: string) =>\n Effect.gen(function* () {\n const withoutExt = yield* removePathExtension(relativePath);\n return withoutExt.startsWith(\".\") ? withoutExt : `./${withoutExt}`;\n });\n\nexport const writeFileStringAndLog = (filePath: string, contents: string) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n if (!(yield* fs.exists(filePath))) {\n yield* fs.writeFileString(filePath, contents);\n yield* markWritten;\n yield* logFileAdded(filePath);\n return;\n }\n const existing = yield* fs.readFileString(filePath);\n if (existing !== contents) {\n yield* fs.writeFileString(filePath, contents);\n yield* markWritten;\n yield* logFileModified(filePath);\n }\n });\n\nexport const findProjectRoot = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n const startDir = path.resolve(\".\");\n const root = path.parse(startDir).root;\n\n const directories = Array.unfold(startDir, (dir) =>\n dir === root\n ? Option.none()\n : Option.some([dir, path.dirname(dir)] as const),\n );\n\n const projectRoot = yield* Effect.findFirst(directories, (dir) =>\n fs.exists(path.join(dir, \"package.json\")),\n );\n\n return Option.getOrElse(projectRoot, () => startDir);\n});\n\nexport type WriteChange = \"Added\" | \"Modified\" | \"Unchanged\";\n\nexport const writeFileString = (\n filePath: string,\n contents: string,\n): Effect.Effect<WriteChange, PlatformError, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n\n if (!(yield* fs.exists(filePath))) {\n yield* fs.writeFileString(filePath, contents);\n yield* markWritten;\n return \"Added\";\n }\n const existing = yield* fs.readFileString(filePath);\n if (existing !== contents) {\n yield* fs.writeFileString(filePath, contents);\n yield* markWritten;\n return \"Modified\";\n }\n return \"Unchanged\";\n });\n\nexport const removePathIfExists = (\n filePath: string,\n): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n\n if (!(yield* fs.exists(filePath))) {\n return;\n }\n\n yield* fs\n .remove(filePath)\n .pipe(\n Effect.catchTag(\"SystemError\", (error) =>\n error.reason === \"NotFound\" ? Effect.void : Effect.fail(error),\n ),\n );\n });\n\n/**\n * Bump the mtime of `convex/schema.ts` so the Convex CLI's chokidar watcher\n * emits a `change` event after every successful Confect codegen run. Without\n * this, a codegen that doesn't change any file content (for example,\n * recovering from a transient broken state in `confect/`) leaves Convex\n * stuck on its previous error because nothing it observes has changed.\n */\nexport const touchConvexSchema = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const schemaPath = path.join(convexDirectory, \"schema.ts\");\n\n if (!(yield* fs.exists(schemaPath))) {\n return;\n }\n\n const now = new Date();\n yield* fs.utimes(schemaPath, now, now);\n});\n\nexport const generateGroupModule = ({\n groupPath,\n functionNames,\n registeredFunctionsImportPath,\n useNode = false,\n}: {\n groupPath: GroupPath.GroupPath;\n functionNames: string[];\n registeredFunctionsImportPath: string;\n useNode?: boolean;\n}) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n\n const directoryPath = path.dirname(modulePath);\n if (!(yield* fs.exists(directoryPath))) {\n yield* fs.makeDirectory(directoryPath, { recursive: true });\n }\n\n const functionsContentsString = yield* templates.functions({\n functionNames,\n registeredFunctionsImportPath,\n useNode,\n });\n\n if (!(yield* fs.exists(modulePath))) {\n yield* fs.writeFileString(modulePath, functionsContentsString);\n yield* markWritten;\n return \"Added\" as const;\n }\n const existing = yield* fs.readFileString(modulePath);\n if (existing !== functionsContentsString) {\n yield* fs.writeFileString(modulePath, functionsContentsString);\n yield* markWritten;\n return \"Modified\" as const;\n }\n return \"Unchanged\" as const;\n });\n\nconst logGroupPaths = <R>(\n groupPaths: GroupPaths.GroupPaths,\n logFn: (fullPath: string) => Effect.Effect<void, never, R>,\n) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n yield* Effect.forEach(groupPaths, (gp) =>\n Effect.gen(function* () {\n const relativeModulePath = yield* GroupPath.modulePath(gp);\n yield* logFn(path.join(convexDirectory, relativeModulePath));\n }),\n );\n });\n\nexport const generateFunctions = (spec: Spec.AnyWithProps) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const groupPathsFromFs = yield* getGroupPathsFromFs;\n const functionPaths = FunctionPaths.make(spec);\n const groupPathsFromSpec = FunctionPaths.groupPaths(functionPaths);\n\n const overlappingGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.intersection(groupPathsFromFs, groupPathsFromSpec),\n );\n yield* Effect.forEach(overlappingGroupPaths, (groupPath) =>\n Effect.gen(function* () {\n const group = yield* GroupPath.getGroupSpec(spec, groupPath);\n const functionNames = pipe(\n group.functions,\n Record.values,\n Array.sortBy(\n Order.mapInput(\n Order.string,\n (fn: FunctionSpec.AnyWithProps) => fn.name,\n ),\n ),\n Array.map((fn) => fn.name),\n );\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n const registrySegments =\n groupPath.pathSegments[0] === \"node\"\n ? groupPath.pathSegments.slice(1)\n : groupPath.pathSegments;\n const registeredFunctionsPath =\n path.join(\n confectDirectory,\n \"_generated\",\n \"registeredFunctions\",\n ...registrySegments,\n ) + \".ts\";\n const registeredFunctionsImportPath = yield* toModuleImportPath(\n path.relative(path.dirname(modulePath), registeredFunctionsPath),\n );\n const result = yield* generateGroupModule({\n groupPath,\n functionNames,\n registeredFunctionsImportPath,\n useNode: groupPath.pathSegments[0] === \"node\",\n });\n if (result === \"Modified\") {\n yield* logFileModified(modulePath);\n }\n }),\n );\n\n const extinctGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.difference(groupPathsFromFs, groupPathsFromSpec),\n );\n yield* removeGroups(extinctGroupPaths);\n yield* logGroupPaths(extinctGroupPaths, logFileRemoved);\n\n const newGroupPaths = GroupPaths.GroupPaths.make(\n HashSet.difference(groupPathsFromSpec, groupPathsFromFs),\n );\n yield* writeGroups(spec, newGroupPaths);\n yield* logGroupPaths(newGroupPaths, logFileAdded);\n\n return functionPaths;\n });\n\nconst getGroupPathsFromFs = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const RESERVED_CONVEX_TS_FILE_NAMES = new Set([\n \"schema.ts\",\n \"http.ts\",\n \"crons.ts\",\n \"auth.config.ts\",\n \"convex.config.ts\",\n ]);\n\n const allConvexPaths = yield* fs.readDirectory(convexDirectory, {\n recursive: true,\n });\n const groupPathArray = yield* pipe(\n allConvexPaths,\n Array.filter(\n (convexPath) =>\n path.extname(convexPath) === \".ts\" &&\n !RESERVED_CONVEX_TS_FILE_NAMES.has(path.basename(convexPath)) &&\n path.basename(path.dirname(convexPath)) !== \"_generated\",\n ),\n Effect.forEach((groupModulePath) =>\n GroupPath.fromGroupModulePath(groupModulePath),\n ),\n );\n return pipe(groupPathArray, HashSet.fromIterable, GroupPaths.GroupPaths.make);\n});\n\nexport const removeGroups = (groupPaths: GroupPaths.GroupPaths) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n\n yield* Effect.all(\n HashSet.map(groupPaths, (groupPath) =>\n Effect.gen(function* () {\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n\n yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);\n\n yield* removePathIfExists(modulePath);\n yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);\n }),\n ),\n { concurrency: \"unbounded\" },\n );\n });\n\nexport const writeGroups = (\n spec: Spec.AnyWithProps,\n groupPaths: GroupPaths.GroupPaths,\n) =>\n Effect.forEach(groupPaths, (groupPath) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const confectDirectory = yield* ConfectDirectory.get;\n const group = yield* GroupPath.getGroupSpec(spec, groupPath);\n\n const functionNames = pipe(\n group.functions,\n Record.values,\n Array.sortBy(\n Order.mapInput(\n Order.string,\n (fn: FunctionSpec.AnyWithProps) => fn.name,\n ),\n ),\n Array.map((fn) => fn.name),\n );\n\n const relativeModulePath = yield* GroupPath.modulePath(groupPath);\n const modulePath = path.join(convexDirectory, relativeModulePath);\n const registeredFunctionsPath =\n path.join(\n confectDirectory,\n \"_generated\",\n \"registeredFunctions\",\n ...groupPath.pathSegments,\n ) + \".ts\";\n const registeredFunctionsImportPath = yield* toModuleImportPath(\n path.relative(path.dirname(modulePath), registeredFunctionsPath),\n );\n\n yield* Effect.logDebug(`Generating group ${groupPath}...`);\n yield* generateGroupModule({\n groupPath,\n functionNames,\n registeredFunctionsImportPath,\n useNode: groupPath.pathSegments[0] === \"node\",\n });\n yield* Effect.logDebug(`Group ${groupPath} generated`);\n }),\n );\n\nconst generateOptionalFile = (\n confectFile: string,\n convexFile: string,\n generateContents: (importPath: string) => Effect.Effect<string>,\n) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const confectFilePath = path.join(confectDirectory, confectFile);\n\n if (!(yield* fs.exists(confectFilePath))) {\n return Option.none();\n }\n\n const convexFilePath = path.join(convexDirectory, convexFile);\n const relativeImportPath = path.relative(\n path.dirname(convexFilePath),\n confectFilePath,\n );\n const importPathWithoutExt = yield* removePathExtension(relativeImportPath);\n const contents = yield* generateContents(importPathWithoutExt);\n const change = yield* writeFileString(convexFilePath, contents);\n return Option.some({ change, convexFilePath });\n });\n\nexport const generateHttp = generateOptionalFile(\n \"http.ts\",\n \"http.ts\",\n (importPath) => templates.http({ httpImportPath: importPath }),\n);\n\nexport const generateCrons = generateOptionalFile(\n \"crons.ts\",\n \"crons.ts\",\n (importPath) => templates.crons({ cronsImportPath: importPath }),\n);\n\nexport const generateAuthConfig = generateOptionalFile(\n \"auth.ts\",\n \"auth.config.ts\",\n (importPath) => templates.authConfig({ authImportPath: importPath }),\n);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@confect/cli",
3
- "version": "8.0.0",
3
+ "version": "9.0.0-next.0",
4
4
  "description": "Developer tooling for codegen and sync",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,8 @@
65
65
  },
66
66
  "peerDependencies": {
67
67
  "effect": "^3.21.2",
68
- "@confect/core": "^8.0.0"
68
+ "@confect/core": "^9.0.0-next.0",
69
+ "@confect/server": "^9.0.0-next.0"
69
70
  },
70
71
  "engines": {
71
72
  "node": ">=22",
@@ -76,7 +77,7 @@
76
77
  "types": "./dist/index.d.ts",
77
78
  "scripts": {
78
79
  "build": "tsdown --config-loader unrun",
79
- "dev": "tsdown --watch",
80
+ "dev": "tsdown --watch --config-loader unrun",
80
81
  "test": "vitest run",
81
82
  "typecheck": "tsc --noEmit --project tsconfig.json",
82
83
  "fix": "prettier --write . && eslint --fix . --max-warnings=0",
@@ -0,0 +1,210 @@
1
+ import { Ansi, AnsiDoc } from "@effect/printer-ansi";
2
+ import { Array, Effect, Match, Option, pipe, Schema, String } from "effect";
3
+ import * as esbuild from "esbuild";
4
+ import { formatPathDoc } from "./log";
5
+
6
+ // --- Variants ---
7
+
8
+ export class BundleFailedError extends Schema.TaggedError<BundleFailedError>()(
9
+ "BundleFailedError",
10
+ {
11
+ file: Schema.String,
12
+ errors: Schema.Array(Schema.Unknown),
13
+ },
14
+ ) {}
15
+
16
+ export class ImportFailedError extends Schema.TaggedError<ImportFailedError>()(
17
+ "ImportFailedError",
18
+ {
19
+ file: Schema.String,
20
+ cause: Schema.Unknown,
21
+ },
22
+ ) {}
23
+
24
+ export const BuildError = Schema.Union(BundleFailedError, ImportFailedError);
25
+ export type BuildError = typeof BuildError.Type;
26
+
27
+ export const isBuildError = (error: unknown): error is BuildError =>
28
+ Schema.is(BuildError)(error);
29
+
30
+ // --- Bundler adapter ---
31
+
32
+ /**
33
+ * Internal failure produced by the esbuild bundle/import pipeline. Always
34
+ * remapped to a {@link BuildError} (which carries enough context for the CLI
35
+ * to render it) before reaching a user-surface boundary.
36
+ */
37
+ export class BundlerError extends Schema.TaggedError<BundlerError>()(
38
+ "BundlerError",
39
+ {
40
+ cause: Schema.Unknown,
41
+ },
42
+ ) {}
43
+
44
+ const isEsbuildBuildFailure = (error: unknown): error is esbuild.BuildFailure =>
45
+ typeof error === "object" &&
46
+ error !== null &&
47
+ "errors" in error &&
48
+ globalThis.Array.isArray((error as esbuild.BuildFailure).errors);
49
+
50
+ export const fromBundlerError = (
51
+ file: string,
52
+ error: BundlerError,
53
+ ): BuildError =>
54
+ isEsbuildBuildFailure(error.cause)
55
+ ? new BundleFailedError({ file, errors: error.cause.errors })
56
+ : new ImportFailedError({ file, cause: error.cause });
57
+
58
+ // --- Rendering ---
59
+
60
+ const cross = pipe(AnsiDoc.char("✘"), AnsiDoc.annotate(Ansi.red));
61
+
62
+ const errorGutter = pipe(
63
+ AnsiDoc.char("│"),
64
+ AnsiDoc.annotate(Ansi.red),
65
+ AnsiDoc.render({ style: "pretty" }),
66
+ );
67
+
68
+ const withErrorGutterBlock = (output: string): string =>
69
+ pipe(
70
+ String.split(output, "\n"),
71
+ Array.map((line) =>
72
+ pipe(line, String.trim) === "" ? errorGutter : `${errorGutter} ${line}`,
73
+ ),
74
+ Array.join("\n"),
75
+ (guttered) => `${errorGutter}\n${guttered}\n${errorGutter}`,
76
+ );
77
+
78
+ const formatBuildMessage = (
79
+ error: esbuild.Message | undefined,
80
+ formattedMessage: string,
81
+ ): string => {
82
+ const lines = String.split(formattedMessage, "\n");
83
+ const redErrorText = pipe(
84
+ AnsiDoc.text(error?.text ?? ""),
85
+ AnsiDoc.annotate(Ansi.red),
86
+ AnsiDoc.render({ style: "pretty" }),
87
+ );
88
+ const replaced = pipe(
89
+ Array.findFirstIndex(lines, (l) => pipe(l, String.trim, String.isNonEmpty)),
90
+ Option.match({
91
+ onNone: () => lines,
92
+ onSome: (index) => Array.modify(lines, index, () => redErrorText),
93
+ }),
94
+ );
95
+ return pipe(replaced, Array.join("\n"));
96
+ };
97
+
98
+ /**
99
+ * Render a list of esbuild messages into a styled, gutter-prefixed block.
100
+ * Used by both {@link renderBundleFailedError} and the dev-mode esbuild
101
+ * watcher's `onEnd` hook (where the messages don't flow through the
102
+ * tagged-error pipeline).
103
+ */
104
+ export const formatEsbuildMessages = (
105
+ errors: readonly esbuild.Message[],
106
+ formattedMessages: readonly string[],
107
+ ): string =>
108
+ pipe(
109
+ formattedMessages,
110
+ Array.map((message, i) => formatBuildMessage(errors[i], message)),
111
+ Array.join(""),
112
+ String.trimEnd,
113
+ withErrorGutterBlock,
114
+ );
115
+
116
+ const renderImportFailedError = (error: ImportFailedError): AnsiDoc.AnsiDoc => {
117
+ const causeMessage =
118
+ error.cause instanceof Error
119
+ ? error.cause.message
120
+ : typeof error.cause === "string"
121
+ ? error.cause
122
+ : globalThis.String(error.cause);
123
+ const oneLineCause = pipe(
124
+ String.split(causeMessage, "\n"),
125
+ Array.findFirst((line) => pipe(line, String.trim, String.isNonEmpty)),
126
+ Option.map(String.trim),
127
+ Option.getOrElse(() => "unknown error"),
128
+ );
129
+ return pipe(
130
+ cross,
131
+ AnsiDoc.catWithSpace(
132
+ AnsiDoc.hcat([
133
+ AnsiDoc.text("Failed to load bundled module "),
134
+ formatPathDoc(error.file),
135
+ AnsiDoc.text(
136
+ `: ${oneLineCause}; check the file's top-level imports and side effects.`,
137
+ ),
138
+ ]),
139
+ ),
140
+ );
141
+ };
142
+
143
+ /**
144
+ * Render a {@link BundleFailedError} as a multi-line, ANSI-styled string:
145
+ * a one-line `✘ <file>: build errors` header followed by the
146
+ * gutter-prefixed esbuild diagnostic block. Multi-line is appropriate
147
+ * here because a single `BundleFailedError` carries an array of distinct
148
+ * esbuild messages.
149
+ */
150
+ const renderBundleFailedError = (error: BundleFailedError): string => {
151
+ const messages = error.errors as readonly esbuild.Message[];
152
+ const formatted = esbuild.formatMessagesSync(messages as esbuild.Message[], {
153
+ kind: "error",
154
+ color: true,
155
+ terminalWidth: 80,
156
+ });
157
+ const header = pipe(
158
+ cross,
159
+ AnsiDoc.catWithSpace(
160
+ AnsiDoc.hcat([formatPathDoc(error.file), AnsiDoc.text(": build errors")]),
161
+ ),
162
+ AnsiDoc.render({ style: "pretty" }),
163
+ );
164
+ return `${header}\n${formatEsbuildMessages(messages, formatted)}`;
165
+ };
166
+
167
+ /**
168
+ * Render any {@link BuildError} into a styled, ready-to-print string.
169
+ * `ImportFailedError` collapses to a single line; `BundleFailedError`
170
+ * expands to a header plus one diagnostic block per esbuild message.
171
+ */
172
+ export const renderBuildError = (error: BuildError): string =>
173
+ Match.value(error).pipe(
174
+ Match.tag("BundleFailedError", renderBundleFailedError),
175
+ Match.tag("ImportFailedError", (e) =>
176
+ pipe(renderImportFailedError(e), AnsiDoc.render({ style: "pretty" })),
177
+ ),
178
+ Match.exhaustive,
179
+ );
180
+
181
+ export const logBuildError = (error: BuildError) =>
182
+ Effect.sync(() => console.error(renderBuildError(error)));
183
+
184
+ /**
185
+ * Render a flat list of esbuild messages as a single error block with a
186
+ * generic `✘ Build errors` header. Used by dev-mode when multiple
187
+ * watchers surface the same underlying error and we want to log the
188
+ * coalesced set rather than one block per watcher.
189
+ */
190
+ const renderCoalescedBuildErrors = (
191
+ messages: readonly esbuild.Message[],
192
+ ): string => {
193
+ const formatted = esbuild.formatMessagesSync(messages as esbuild.Message[], {
194
+ kind: "error",
195
+ color: true,
196
+ terminalWidth: 80,
197
+ });
198
+ const header = pipe(
199
+ cross,
200
+ AnsiDoc.catWithSpace(AnsiDoc.text("Build errors")),
201
+ AnsiDoc.render({ style: "pretty" }),
202
+ );
203
+ return `${header}\n${formatEsbuildMessages(messages, formatted)}`;
204
+ };
205
+
206
+ export const logCoalescedBuildErrors = (messages: readonly esbuild.Message[]) =>
207
+ Effect.sync(() => {
208
+ if (messages.length === 0) return;
209
+ console.error(renderCoalescedBuildErrors(messages));
210
+ });