@confect/cli 7.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,68 +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 { 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";
9
9
  import { FileSystem, Path } from "@effect/platform";
10
- import * as esbuild from "esbuild";
11
10
 
12
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
+ });
13
25
  const removePathExtension = (pathStr) => Effect.gen(function* () {
14
26
  const path = yield* Path.Path;
15
27
  return String.slice(0, -path.extname(pathStr).length)(pathStr);
16
28
  });
17
- const EXTERNAL_PACKAGES = [
18
- "@confect/core",
19
- "@confect/server",
20
- "effect",
21
- "@effect/*"
22
- ];
23
- const isExternalImport = (path) => EXTERNAL_PACKAGES.some((p) => {
24
- if (p.endsWith("/*")) return path.startsWith(p.slice(0, -1));
25
- return path === p || path.startsWith(p + "/");
26
- });
27
- const absoluteExternalsPlugin = {
28
- name: "absolute-externals",
29
- setup(build) {
30
- build.onResolve({ filter: /.*/ }, async (args) => {
31
- if (args.kind !== "import-statement" && args.kind !== "dynamic-import") return;
32
- if (!isExternalImport(args.path)) return;
33
- return {
34
- path: import.meta.resolve(args.path, "file://" + args.resolveDir + "/"),
35
- external: true
36
- };
37
- });
38
- }
39
- };
40
- /**
41
- * Bundle a TypeScript entry point with esbuild and import the result via a
42
- * data URL. This handles extensionless `.ts` imports regardless of whether the
43
- * user's project sets `"type": "module"` in package.json.
44
- */
45
- const bundleAndImport = (entryPoint) => Effect.gen(function* () {
46
- const code = (yield* Effect.promise(() => esbuild.build({
47
- entryPoints: [entryPoint],
48
- bundle: true,
49
- write: false,
50
- platform: "node",
51
- format: "esm",
52
- logLevel: "silent",
53
- plugins: [absoluteExternalsPlugin]
54
- }))).outputFiles[0].text;
55
- const dataUrl = "data:text/javascript;base64," + Buffer.from(code).toString("base64");
56
- 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}`;
57
33
  });
58
34
  const writeFileStringAndLog = (filePath, contents) => Effect.gen(function* () {
59
35
  const fs = yield* FileSystem.FileSystem;
60
36
  if (!(yield* fs.exists(filePath))) {
61
37
  yield* fs.writeFileString(filePath, contents);
38
+ yield* markWritten;
62
39
  yield* logFileAdded(filePath);
63
40
  return;
64
41
  }
65
42
  if ((yield* fs.readFileString(filePath)) !== contents) {
66
43
  yield* fs.writeFileString(filePath, contents);
44
+ yield* markWritten;
67
45
  yield* logFileModified(filePath);
68
46
  }
69
47
  });
@@ -80,42 +58,58 @@ const writeFileString = (filePath, contents) => Effect.gen(function* () {
80
58
  const fs = yield* FileSystem.FileSystem;
81
59
  if (!(yield* fs.exists(filePath))) {
82
60
  yield* fs.writeFileString(filePath, contents);
61
+ yield* markWritten;
83
62
  return "Added";
84
63
  }
85
64
  if ((yield* fs.readFileString(filePath)) !== contents) {
86
65
  yield* fs.writeFileString(filePath, contents);
66
+ yield* markWritten;
87
67
  return "Modified";
88
68
  }
89
69
  return "Unchanged";
90
70
  });
91
- 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* () {
92
93
  const fs = yield* FileSystem.FileSystem;
93
94
  const path = yield* Path.Path;
94
95
  const convexDirectory = yield* ConvexDirectory.get;
95
- const confectDirectory = yield* ConfectDirectory.get;
96
96
  const relativeModulePath = yield* modulePath(groupPath);
97
- const modulePath$2 = path.join(convexDirectory, relativeModulePath);
98
- const directoryPath = path.dirname(modulePath$2);
97
+ const modulePath$4 = path.join(convexDirectory, relativeModulePath);
98
+ const directoryPath = path.dirname(modulePath$4);
99
99
  if (!(yield* fs.exists(directoryPath))) yield* fs.makeDirectory(directoryPath, { recursive: true });
100
- const isNodeGroup = groupPath.pathSegments[0] === "node";
101
- const registeredFunctionsFileName = isNodeGroup ? "nodeRegisteredFunctions.ts" : "registeredFunctions.ts";
102
- const registeredFunctionsPath = path.join(confectDirectory, "_generated", registeredFunctionsFileName);
103
- const registeredFunctionsImportPath = yield* removePathExtension(path.relative(path.dirname(modulePath$2), registeredFunctionsPath));
104
- const registeredFunctionsVariableName = isNodeGroup ? "nodeRegisteredFunctions" : "registeredFunctions";
105
100
  const functionsContentsString = yield* functions({
106
- groupPath,
107
101
  functionNames,
108
102
  registeredFunctionsImportPath,
109
- registeredFunctionsVariableName,
110
- useNode: isNodeGroup,
111
- ...isNodeGroup ? { registeredFunctionsLookupPath: groupPath.pathSegments.slice(1) } : {}
103
+ useNode
112
104
  });
113
- if (!(yield* fs.exists(modulePath$2))) {
114
- yield* fs.writeFileString(modulePath$2, functionsContentsString);
105
+ if (!(yield* fs.exists(modulePath$4))) {
106
+ yield* fs.writeFileString(modulePath$4, functionsContentsString);
107
+ yield* markWritten;
115
108
  return "Added";
116
109
  }
117
- if ((yield* fs.readFileString(modulePath$2)) !== functionsContentsString) {
118
- yield* fs.writeFileString(modulePath$2, functionsContentsString);
110
+ if ((yield* fs.readFileString(modulePath$4)) !== functionsContentsString) {
111
+ yield* fs.writeFileString(modulePath$4, functionsContentsString);
112
+ yield* markWritten;
119
113
  return "Modified";
120
114
  }
121
115
  return "Unchanged";
@@ -131,18 +125,23 @@ const logGroupPaths = (groupPaths, logFn) => Effect.gen(function* () {
131
125
  const generateFunctions = (spec) => Effect.gen(function* () {
132
126
  const path = yield* Path.Path;
133
127
  const convexDirectory = yield* ConvexDirectory.get;
128
+ const confectDirectory = yield* ConfectDirectory.get;
134
129
  const groupPathsFromFs = yield* getGroupPathsFromFs;
135
130
  const functionPaths = make(spec);
136
131
  const groupPathsFromSpec = groupPaths(functionPaths);
137
132
  const overlappingGroupPaths = GroupPaths.make(HashSet.intersection(groupPathsFromFs, groupPathsFromSpec));
138
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";
139
139
  if ((yield* generateGroupModule({
140
140
  groupPath,
141
- functionNames: pipe((yield* getGroupSpec(spec, groupPath)).functions, Record.values, Array.sortBy(Order.mapInput(Order.string, (fn) => fn.name)), Array.map((fn) => fn.name))
142
- })) === "Modified") {
143
- const relativeModulePath = yield* modulePath(groupPath);
144
- yield* logFileModified(path.join(convexDirectory, relativeModulePath));
145
- }
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);
146
145
  }));
147
146
  const extinctGroupPaths = GroupPaths.make(HashSet.difference(groupPathsFromFs, groupPathsFromSpec));
148
147
  yield* removeGroups(extinctGroupPaths);
@@ -166,23 +165,31 @@ const getGroupPathsFromFs = Effect.gen(function* () {
166
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);
167
166
  });
168
167
  const removeGroups = (groupPaths) => Effect.gen(function* () {
169
- const fs = yield* FileSystem.FileSystem;
170
168
  const path = yield* Path.Path;
171
169
  const convexDirectory = yield* ConvexDirectory.get;
172
170
  yield* Effect.all(HashSet.map(groupPaths, (groupPath) => Effect.gen(function* () {
173
171
  const relativeModulePath = yield* modulePath(groupPath);
174
172
  const modulePath$1 = path.join(convexDirectory, relativeModulePath);
175
173
  yield* Effect.logDebug(`Removing group '${relativeModulePath}'...`);
176
- yield* fs.remove(modulePath$1);
174
+ yield* removePathIfExists(modulePath$1);
177
175
  yield* Effect.logDebug(`Group '${relativeModulePath}' removed`);
178
176
  })), { concurrency: "unbounded" });
179
177
  });
180
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;
181
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));
182
187
  yield* Effect.logDebug(`Generating group ${groupPath}...`);
183
188
  yield* generateGroupModule({
184
189
  groupPath,
185
- functionNames
190
+ functionNames,
191
+ registeredFunctionsImportPath,
192
+ useNode: groupPath.pathSegments[0] === "node"
186
193
  });
187
194
  yield* Effect.logDebug(`Group ${groupPath} generated`);
188
195
  }));
@@ -205,5 +212,5 @@ const generateCrons = generateOptionalFile("crons.ts", "crons.ts", (importPath)
205
212
  const generateAuthConfig = generateOptionalFile("auth.ts", "auth.config.ts", (importPath) => authConfig({ authImportPath: importPath }));
206
213
 
207
214
  //#endregion
208
- export { EXTERNAL_PACKAGES, bundleAndImport, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removeGroups, removePathExtension, writeFileStringAndLog, writeGroups };
215
+ export { WriteTracker, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removePathExtension, removePathIfExists, toModuleImportPath, touchConvexSchema, writeFileStringAndLog };
209
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 { 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 const resolved = import.meta.resolve(\n args.path,\n \"file://\" + args.resolveDir + \"/\",\n );\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":";;;;;;;;;;;;;;;;;;;AA+BA,IAAa,eAAb,cAAkC,QAAQ,WAAyB,CACjE,6BACA,EAAE,oBAAoB,IAAI,WAAW,MAAM,EAAE,CAC9C,CAAC;AAEF,MAAM,cAAc,OAAO,IAAI,aAAa;CAC1C,MAAM,UAAU,OAAO;AACvB,QAAO,IAAI,IAAI,SAAS,KAAK;EAC7B;AAEF,MAAa,uBAAuB,YAClC,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;AAEzB,QAAO,OAAO,MAAM,GAAG,CAAC,KAAK,QAAQ,QAAQ,CAAC,OAAO,CAAC,QAAQ;EAC9D;;AAGJ,MAAa,sBAAsB,iBACjC,OAAO,IAAI,aAAa;CACtB,MAAM,aAAa,OAAO,oBAAoB,aAAa;AAC3D,QAAO,WAAW,WAAW,IAAI,GAAG,aAAa,KAAK;EACtD;AAEJ,MAAa,yBAAyB,UAAkB,aACtD,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;AAC7B,KAAI,EAAE,OAAO,GAAG,OAAO,SAAS,GAAG;AACjC,SAAO,GAAG,gBAAgB,UAAU,SAAS;AAC7C,SAAO;AACP,SAAO,aAAa,SAAS;AAC7B;;AAGF,MADiB,OAAO,GAAG,eAAe,SAAS,MAClC,UAAU;AACzB,SAAO,GAAG,gBAAgB,UAAU,SAAS;AAC7C,SAAO;AACP,SAAO,gBAAgB,SAAS;;EAElC;AAEJ,MAAa,kBAAkB,OAAO,IAAI,aAAa;CACrD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CAEzB,MAAM,WAAW,KAAK,QAAQ,IAAI;CAClC,MAAM,OAAO,KAAK,MAAM,SAAS,CAAC;CAElC,MAAM,cAAc,MAAM,OAAO,WAAW,QAC1C,QAAQ,OACJ,OAAO,MAAM,GACb,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAU,CACnD;CAED,MAAM,cAAc,OAAO,OAAO,UAAU,cAAc,QACxD,GAAG,OAAO,KAAK,KAAK,KAAK,eAAe,CAAC,CAC1C;AAED,QAAO,OAAO,UAAU,mBAAmB,SAAS;EACpD;AAIF,MAAa,mBACX,UACA,aAEA,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;AAE7B,KAAI,EAAE,OAAO,GAAG,OAAO,SAAS,GAAG;AACjC,SAAO,GAAG,gBAAgB,UAAU,SAAS;AAC7C,SAAO;AACP,SAAO;;AAGT,MADiB,OAAO,GAAG,eAAe,SAAS,MAClC,UAAU;AACzB,SAAO,GAAG,gBAAgB,UAAU,SAAS;AAC7C,SAAO;AACP,SAAO;;AAET,QAAO;EACP;AAEJ,MAAa,sBACX,aAEA,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;AAE7B,KAAI,EAAE,OAAO,GAAG,OAAO,SAAS,EAC9B;AAGF,QAAO,GACJ,OAAO,SAAS,CAChB,KACC,OAAO,SAAS,gBAAgB,UAC9B,MAAM,WAAW,aAAa,OAAO,OAAO,OAAO,KAAK,MAAM,CAC/D,CACF;EACH;;;;;;;;AASJ,MAAa,oBAAoB,OAAO,IAAI,aAAa;CACvD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;CAC/C,MAAM,aAAa,KAAK,KAAK,iBAAiB,YAAY;AAE1D,KAAI,EAAE,OAAO,GAAG,OAAO,WAAW,EAChC;CAGF,MAAM,sBAAM,IAAI,MAAM;AACtB,QAAO,GAAG,OAAO,YAAY,KAAK,IAAI;EACtC;AAEF,MAAa,uBAAuB,EAClC,WACA,eACA,+BACA,UAAU,YAOV,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;CAE/C,MAAM,qBAAqB,OAAOA,WAAqB,UAAU;CACjE,MAAMC,eAAa,KAAK,KAAK,iBAAiB,mBAAmB;CAEjE,MAAM,gBAAgB,KAAK,QAAQA,aAAW;AAC9C,KAAI,EAAE,OAAO,GAAG,OAAO,cAAc,EACnC,QAAO,GAAG,cAAc,eAAe,EAAE,WAAW,MAAM,CAAC;CAG7D,MAAM,0BAA0B,OAAOC,UAAoB;EACzD;EACA;EACA;EACD,CAAC;AAEF,KAAI,EAAE,OAAO,GAAG,OAAOD,aAAW,GAAG;AACnC,SAAO,GAAG,gBAAgBA,cAAY,wBAAwB;AAC9D,SAAO;AACP,SAAO;;AAGT,MADiB,OAAO,GAAG,eAAeA,aAAW,MACpC,yBAAyB;AACxC,SAAO,GAAG,gBAAgBA,cAAY,wBAAwB;AAC9D,SAAO;AACP,SAAO;;AAET,QAAO;EACP;AAEJ,MAAM,iBACJ,YACA,UAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;AAE/C,QAAO,OAAO,QAAQ,aAAa,OACjC,OAAO,IAAI,aAAa;EACtB,MAAM,qBAAqB,OAAOD,WAAqB,GAAG;AAC1D,SAAO,MAAM,KAAK,KAAK,iBAAiB,mBAAmB,CAAC;GAC5D,CACH;EACD;AAEJ,MAAa,qBAAqB,SAChC,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;CAC/C,MAAM,mBAAmB,OAAO,iBAAiB;CAEjD,MAAM,mBAAmB,OAAO;CAChC,MAAM,gBAAgBG,KAAmB,KAAK;CAC9C,MAAM,qBAAqBC,WAAyB,cAAc;CAElE,MAAM,mCAA8C,KAClD,QAAQ,aAAa,kBAAkB,mBAAmB,CAC3D;AACD,QAAO,OAAO,QAAQ,wBAAwB,cAC5C,OAAO,IAAI,aAAa;EAEtB,MAAM,gBAAgB,MADR,OAAOC,aAAuB,MAAM,UAAU,EAEpD,WACN,OAAO,QACP,MAAM,OACJ,MAAM,SACJ,MAAM,SACL,OAAkC,GAAG,KACvC,CACF,EACD,MAAM,KAAK,OAAO,GAAG,KAAK,CAC3B;EACD,MAAM,qBAAqB,OAAOL,WAAqB,UAAU;EACjE,MAAMC,eAAa,KAAK,KAAK,iBAAiB,mBAAmB;EACjE,MAAM,mBACJ,UAAU,aAAa,OAAO,SAC1B,UAAU,aAAa,MAAM,EAAE,GAC/B,UAAU;EAChB,MAAM,0BACJ,KAAK,KACH,kBACA,cACA,uBACA,GAAG,iBACJ,GAAG;AAUN,OANe,OAAO,oBAAoB;GACxC;GACA;GACA,+BANoC,OAAO,mBAC3C,KAAK,SAAS,KAAK,QAAQA,aAAW,EAAE,wBAAwB,CACjE;GAKC,SAAS,UAAU,aAAa,OAAO;GACxC,CAAC,MACa,WACb,QAAO,gBAAgBA,aAAW;GAEpC,CACH;CAED,MAAM,+BAA0C,KAC9C,QAAQ,WAAW,kBAAkB,mBAAmB,CACzD;AACD,QAAO,aAAa,kBAAkB;AACtC,QAAO,cAAc,mBAAmB,eAAe;CAEvD,MAAM,2BAAsC,KAC1C,QAAQ,WAAW,oBAAoB,iBAAiB,CACzD;AACD,QAAO,YAAY,MAAM,cAAc;AACvC,QAAO,cAAc,eAAe,aAAa;AAEjD,QAAO;EACP;AAEJ,MAAM,sBAAsB,OAAO,IAAI,aAAa;CAClD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;CAE/C,MAAM,gCAAgC,IAAI,IAAI;EAC5C;EACA;EACA;EACA;EACA;EACD,CAAC;AAiBF,QAAO,KAZgB,OAAO,KAHP,OAAO,GAAG,cAAc,iBAAiB,EAC9D,WAAW,MACZ,CAAC,EAGA,MAAM,QACH,eACC,KAAK,QAAQ,WAAW,KAAK,SAC7B,CAAC,8BAA8B,IAAI,KAAK,SAAS,WAAW,CAAC,IAC7D,KAAK,SAAS,KAAK,QAAQ,WAAW,CAAC,KAAK,aAC/C,EACD,OAAO,SAAS,oBACdK,oBAA8B,gBAAgB,CAC/C,CACF,EAC2B,QAAQ,yBAAoC,KAAK;EAC7E;AAEF,MAAa,gBAAgB,eAC3B,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;AAE/C,QAAO,OAAO,IACZ,QAAQ,IAAI,aAAa,cACvB,OAAO,IAAI,aAAa;EACtB,MAAM,qBAAqB,OAAON,WAAqB,UAAU;EACjE,MAAMC,eAAa,KAAK,KAAK,iBAAiB,mBAAmB;AAEjE,SAAO,OAAO,SAAS,mBAAmB,mBAAmB,MAAM;AAEnE,SAAO,mBAAmBA,aAAW;AACrC,SAAO,OAAO,SAAS,UAAU,mBAAmB,WAAW;GAC/D,CACH,EACD,EAAE,aAAa,aAAa,CAC7B;EACD;AAEJ,MAAa,eACX,MACA,eAEA,OAAO,QAAQ,aAAa,cAC1B,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,gBAAgB;CAC/C,MAAM,mBAAmB,OAAO,iBAAiB;CAGjD,MAAM,gBAAgB,MAFR,OAAOI,aAAuB,MAAM,UAAU,EAGpD,WACN,OAAO,QACP,MAAM,OACJ,MAAM,SACJ,MAAM,SACL,OAAkC,GAAG,KACvC,CACF,EACD,MAAM,KAAK,OAAO,GAAG,KAAK,CAC3B;CAED,MAAM,qBAAqB,OAAOL,WAAqB,UAAU;CACjE,MAAMC,eAAa,KAAK,KAAK,iBAAiB,mBAAmB;CACjE,MAAM,0BACJ,KAAK,KACH,kBACA,cACA,uBACA,GAAG,UAAU,aACd,GAAG;CACN,MAAM,gCAAgC,OAAO,mBAC3C,KAAK,SAAS,KAAK,QAAQA,aAAW,EAAE,wBAAwB,CACjE;AAED,QAAO,OAAO,SAAS,oBAAoB,UAAU,KAAK;AAC1D,QAAO,oBAAoB;EACzB;EACA;EACA;EACA,SAAS,UAAU,aAAa,OAAO;EACxC,CAAC;AACF,QAAO,OAAO,SAAS,SAAS,UAAU,YAAY;EACtD,CACH;AAEH,MAAM,wBACJ,aACA,YACA,qBAEA,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,mBAAmB,OAAO,iBAAiB;CACjD,MAAM,kBAAkB,OAAO,gBAAgB;CAE/C,MAAM,kBAAkB,KAAK,KAAK,kBAAkB,YAAY;AAEhE,KAAI,EAAE,OAAO,GAAG,OAAO,gBAAgB,EACrC,QAAO,OAAO,MAAM;CAGtB,MAAM,iBAAiB,KAAK,KAAK,iBAAiB,WAAW;CAO7D,MAAM,SAAS,OAAO,gBAAgB,gBADrB,OAAO,iBADK,OAAO,oBAJT,KAAK,SAC9B,KAAK,QAAQ,eAAe,EAC5B,gBACD,CAC0E,CACb,CACC;AAC/D,QAAO,OAAO,KAAK;EAAE;EAAQ;EAAgB,CAAC;EAC9C;AAEJ,MAAa,eAAe,qBAC1B,WACA,YACC,eAAeM,KAAe,EAAE,gBAAgB,YAAY,CAAC,CAC/D;AAED,MAAa,gBAAgB,qBAC3B,YACA,aACC,eAAeC,MAAgB,EAAE,iBAAiB,YAAY,CAAC,CACjE;AAED,MAAa,qBAAqB,qBAChC,WACA,mBACC,eAAeC,WAAqB,EAAE,gBAAgB,YAAY,CAAC,CACrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@confect/cli",
3
- "version": "7.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",
@@ -38,22 +38,22 @@
38
38
  "author": "RJ Dellecese",
39
39
  "license": "ISC",
40
40
  "dependencies": {
41
- "@effect/cli": "^0.73.2",
42
- "@effect/platform-node": "0.104.1",
43
- "@effect/platform-node-shared": "0.57.1",
44
- "@effect/printer": "^0.47.0",
45
- "@effect/printer-ansi": "^0.47.0",
41
+ "@effect/cli": "^0.75.1",
42
+ "@effect/platform": "0.96.1",
43
+ "@effect/platform-node": "0.106.0",
44
+ "@effect/platform-node-shared": "0.59.0",
45
+ "@effect/printer": "^0.49.0",
46
+ "@effect/printer-ansi": "^0.49.0",
46
47
  "code-block-writer": "^13.0.3",
47
48
  "esbuild": "^0.27.3"
48
49
  },
49
50
  "devDependencies": {
50
- "@effect/language-service": "0.77.0",
51
- "@effect/platform": "0.94.5",
52
- "@effect/vitest": "0.27.0",
51
+ "@effect/language-service": "0.86.1",
52
+ "@effect/vitest": "0.29.0",
53
53
  "@eslint/js": "10.0.1",
54
54
  "@types/node": "25.3.3",
55
55
  "@vitest/coverage-v8": "3.2.4",
56
- "effect": "3.19.19",
56
+ "effect": "3.21.2",
57
57
  "eslint": "10.0.2",
58
58
  "prettier": "3.8.1",
59
59
  "tsdown": "0.20.3",
@@ -64,10 +64,9 @@
64
64
  "vitest": "3.2.4"
65
65
  },
66
66
  "peerDependencies": {
67
- "@effect/platform": "^0.94.5",
68
- "effect": "^3.19.16",
69
- "@confect/core": "7.0.0",
70
- "@confect/server": "7.0.0"
67
+ "effect": "^3.21.2",
68
+ "@confect/core": "^9.0.0-next.0",
69
+ "@confect/server": "^9.0.0-next.0"
71
70
  },
72
71
  "engines": {
73
72
  "node": ">=22",
@@ -78,7 +77,7 @@
78
77
  "types": "./dist/index.d.ts",
79
78
  "scripts": {
80
79
  "build": "tsdown --config-loader unrun",
81
- "dev": "tsdown --watch",
80
+ "dev": "tsdown --watch --config-loader unrun",
82
81
  "test": "vitest run",
83
82
  "typecheck": "tsc --noEmit --project tsconfig.json",
84
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
+ });