@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.
@@ -0,0 +1,167 @@
1
+ import { fromBundlerError } from "./BuildError.mjs";
2
+ import { ImplMissingDefaultLayerError, ImplMissingFunctionsError, ImplMissingSpecImportError, ImplNotFinalizedError, SpecMissingDefaultGroupSpecError, SpecRuntimeMismatchError } from "./CodegenError.mjs";
3
+ import { ConfectDirectory } from "./ConfectDirectory.mjs";
4
+ import { bundle, directlyImports } from "./Bundler.mjs";
5
+ import { removePathExtension } from "./utils.mjs";
6
+ import { Array, Effect, Layer, Option, Ref, String } from "effect";
7
+ import { GroupSpec, Registry } from "@confect/core";
8
+ import { FileSystem, Path } from "@effect/platform";
9
+ import * as GroupImpl from "@confect/server/GroupImpl";
10
+
11
+ //#region src/LeafModule.ts
12
+ const SPEC_SUFFIX = ".spec.ts";
13
+ const IMPL_SUFFIX = ".impl.ts";
14
+ const swapModuleSuffix = (relativePath, fromSuffix, toSuffix) => Effect.gen(function* () {
15
+ const path = yield* Path.Path;
16
+ const { dir, name, ext } = path.parse(relativePath);
17
+ if (ext !== ".ts" || !name.endsWith(fromSuffix.slice(0, -3))) return relativePath;
18
+ const nextName = `${name.slice(0, -fromSuffix.slice(0, -3).length)}${toSuffix.slice(0, -3)}`;
19
+ return dir.length > 0 ? path.join(dir, `${nextName}${ext}`) : `${nextName}${ext}`;
20
+ });
21
+ const isLeafSpecPath = (relativePath) => relativePath.endsWith(SPEC_SUFFIX);
22
+ const isLeafImplPath = (relativePath) => relativePath.endsWith(IMPL_SUFFIX);
23
+ const exportNameFromModulePath = (relativePath) => Effect.gen(function* () {
24
+ const { name, ext } = (yield* Path.Path).parse(relativePath);
25
+ if (ext !== ".ts") return name;
26
+ return name.endsWith(".spec") ? name.slice(0, -5) : name;
27
+ });
28
+ const groupPathFromRelativeModulePath = (relativePath) => Effect.gen(function* () {
29
+ const path = yield* Path.Path;
30
+ const { dir, name, ext } = path.parse(relativePath);
31
+ const stem = ext === ".ts" && name.endsWith(".spec") ? name.slice(0, -5) : name;
32
+ const dirSegments = Array.filter(String.split(dir, path.sep), String.isNonEmpty);
33
+ const pathSegments = Array.append(dirSegments, stem);
34
+ return {
35
+ pathSegments,
36
+ groupPathDot: Array.join(pathSegments, ".")
37
+ };
38
+ });
39
+ const specImportPathFromGenerated = (specRelativePath) => Effect.gen(function* () {
40
+ return `../${yield* removePathExtension(specRelativePath)}`;
41
+ });
42
+ const specPathForImpl = (implRelativePath) => swapModuleSuffix(implRelativePath, IMPL_SUFFIX, SPEC_SUFFIX);
43
+ const implPathForSpec = (specRelativePath) => swapModuleSuffix(specRelativePath, SPEC_SUFFIX, IMPL_SUFFIX);
44
+ const isNodeLeafModule = (relativePath) => relativePath.startsWith("node/") || relativePath.startsWith("node\\");
45
+ const toNodeRegistryLeaf = (leaf) => ({
46
+ ...leaf,
47
+ pathSegments: [leaf.exportName],
48
+ groupPathDot: leaf.exportName
49
+ });
50
+ const registeredFunctionsRelativePath = (leaf) => Effect.gen(function* () {
51
+ return (yield* Path.Path).join("registeredFunctions", ...leaf.pathSegments.slice(leaf.runtime === "Node" ? 1 : 0)) + ".ts";
52
+ });
53
+ const discoverLeafSpecFiles = Effect.gen(function* () {
54
+ const fs = yield* FileSystem.FileSystem;
55
+ const path = yield* Path.Path;
56
+ const confectDirectory = yield* ConfectDirectory.get;
57
+ const excludedDirs = new Set(["_generated", "tables"]);
58
+ const excludedFiles = new Set(["nodeSpec.ts", "spec.ts"]);
59
+ const allPaths = yield* fs.readDirectory(confectDirectory, { recursive: true });
60
+ return Array.filter(allPaths, (relativePath) => {
61
+ if (!isLeafSpecPath(relativePath)) return false;
62
+ if (excludedFiles.has(relativePath)) return false;
63
+ const segments = String.split(relativePath, path.sep);
64
+ return !Array.some(segments, (segment) => excludedDirs.has(segment));
65
+ });
66
+ });
67
+ const discoverLeafImplFiles = Effect.gen(function* () {
68
+ const fs = yield* FileSystem.FileSystem;
69
+ const path = yield* Path.Path;
70
+ const confectDirectory = yield* ConfectDirectory.get;
71
+ const excludedDirs = new Set(["_generated", "tables"]);
72
+ const allPaths = yield* fs.readDirectory(confectDirectory, { recursive: true });
73
+ return Array.filter(allPaths, (relativePath) => {
74
+ if (!isLeafImplPath(relativePath)) return false;
75
+ const segments = String.split(relativePath, path.sep);
76
+ return !Array.some(segments, (segment) => excludedDirs.has(segment));
77
+ });
78
+ });
79
+ const toLeafModule = (specRelativePath) => Effect.gen(function* () {
80
+ const exportName = yield* exportNameFromModulePath(specRelativePath);
81
+ const { pathSegments, groupPathDot } = yield* groupPathFromRelativeModulePath(specRelativePath);
82
+ const specImportPath = yield* specImportPathFromGenerated(specRelativePath);
83
+ const runtime = isNodeLeafModule(specRelativePath) ? "Node" : "Convex";
84
+ return {
85
+ relativePath: specRelativePath,
86
+ pathSegments,
87
+ groupPathDot,
88
+ exportName,
89
+ runtime,
90
+ registryGroupPathDot: runtime === "Node" ? exportName : groupPathDot,
91
+ specImportPath
92
+ };
93
+ });
94
+ const absoluteModulePath = (relativePath) => Effect.gen(function* () {
95
+ const confectDirectory = yield* ConfectDirectory.get;
96
+ return (yield* Path.Path).resolve(confectDirectory, relativePath);
97
+ });
98
+ /**
99
+ * Validate that the leaf's spec file default-exports a `GroupSpec` whose
100
+ * runtime matches the leaf's location (`Convex` for files outside
101
+ * `confect/node/`, `Node` for files inside it).
102
+ */
103
+ const validateSpec = (leaf) => Effect.gen(function* () {
104
+ const absolutePath = yield* absoluteModulePath(leaf.relativePath);
105
+ const { module } = yield* bundle(absolutePath).pipe(Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)));
106
+ const groupSpec = module.default;
107
+ if (!GroupSpec.isGroupSpec(groupSpec)) return yield* new SpecMissingDefaultGroupSpecError({ specPath: leaf.relativePath });
108
+ if (groupSpec.runtime !== leaf.runtime) return yield* new SpecRuntimeMismatchError({
109
+ specPath: leaf.relativePath,
110
+ expectedRuntime: leaf.runtime,
111
+ actualRuntime: groupSpec.runtime
112
+ });
113
+ });
114
+ /**
115
+ * Walk the built `Context` for a `Finalized` `GroupImpl` service value. The
116
+ * lookup is value-shaped (via `GroupImpl.isFinalizedGroupImpl`) so we don't
117
+ * need to know the group's path up front to construct a typed tag for it.
118
+ */
119
+ const findFinalizedGroupImpl = (context) => Array.findFirst(context.unsafeMap.values(), GroupImpl.isFinalizedGroupImpl);
120
+ /**
121
+ * Build the impl layer with a fresh `Registry` so each validation is
122
+ * isolated from prior validations' `FunctionImpl.make` writes. The CLI no
123
+ * longer reads the registry directly — `GroupImpl.finalize` snapshots the
124
+ * registered function names onto the produced `Finalized` `GroupImpl`
125
+ * service value — but a fresh `Ref` is still required because the default
126
+ * `Context.Reference` is cached globally and would otherwise accumulate
127
+ * items across impls.
128
+ */
129
+ const buildImplLayer = (implLayer) => Effect.gen(function* () {
130
+ const registry = Ref.unsafeMake({});
131
+ return yield* Layer.build(implLayer).pipe(Effect.provideService(Registry.Registry, registry));
132
+ }).pipe(Effect.scoped);
133
+ /**
134
+ * Validate that the leaf's sibling impl file imports the spec, default-exports
135
+ * a finalized `GroupImpl` layer, and provides a `FunctionImpl` for every
136
+ * function declared by the spec.
137
+ */
138
+ const validateImpl = (leaf) => Effect.gen(function* () {
139
+ const implRelativePath = yield* implPathForSpec(leaf.relativePath);
140
+ const implAbsolutePath = yield* absoluteModulePath(implRelativePath);
141
+ const specAbsolutePath = yield* absoluteModulePath(leaf.relativePath);
142
+ const bundled = yield* bundle(implAbsolutePath).pipe(Effect.mapError((error) => fromBundlerError(implRelativePath, error)));
143
+ if (!(yield* directlyImports(bundled, implAbsolutePath, specAbsolutePath))) return yield* new ImplMissingSpecImportError({
144
+ implPath: implRelativePath,
145
+ expectedSpecPath: leaf.relativePath
146
+ });
147
+ if (!Layer.isLayer(bundled.module.default)) return yield* new ImplMissingDefaultLayerError({ implPath: implRelativePath });
148
+ const { module: specModule } = yield* bundle(specAbsolutePath).pipe(Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)));
149
+ const groupSpec = specModule.default;
150
+ const expectedFunctionNames = Object.keys(groupSpec.functions);
151
+ const context = yield* buildImplLayer(bundled.module.default);
152
+ const finalizedGroupImpl = yield* Option.match(findFinalizedGroupImpl(context), {
153
+ onNone: () => new ImplNotFinalizedError({ implPath: implRelativePath }),
154
+ onSome: Effect.succeed
155
+ });
156
+ const registeredSet = new Set(finalizedGroupImpl.registeredFunctionNames);
157
+ const missing = expectedFunctionNames.filter((name) => !registeredSet.has(name));
158
+ if (missing.length > 0) return yield* new ImplMissingFunctionsError({
159
+ implPath: implRelativePath,
160
+ groupPath: finalizedGroupImpl.groupPath,
161
+ missingFunctionNames: missing
162
+ });
163
+ });
164
+
165
+ //#endregion
166
+ export { discoverLeafImplFiles, discoverLeafSpecFiles, implPathForSpec, isLeafImplPath, isLeafSpecPath, registeredFunctionsRelativePath, specPathForImpl, toLeafModule, toNodeRegistryLeaf, validateImpl, validateSpec };
167
+ //# sourceMappingURL=LeafModule.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LeafModule.mjs","names":["Bundler.bundle","Bundler.directlyImports"],"sources":["../src/LeafModule.ts"],"sourcesContent":["import { GroupSpec, Registry } from \"@confect/core\";\nimport * as GroupImpl from \"@confect/server/GroupImpl\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport type { Context } from \"effect\";\nimport { Array, Effect, Layer, Option, Ref, String } from \"effect\";\nimport { fromBundlerError } from \"./BuildError\";\nimport * as Bundler from \"./Bundler\";\nimport {\n ImplMissingDefaultLayerError,\n ImplMissingFunctionsError,\n ImplMissingSpecImportError,\n ImplNotFinalizedError,\n SpecMissingDefaultGroupSpecError,\n SpecRuntimeMismatchError,\n} from \"./CodegenError\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\nimport { removePathExtension } from \"./utils\";\n\nexport interface LeafModule {\n readonly relativePath: string;\n readonly pathSegments: readonly [string, ...string[]];\n readonly groupPathDot: string;\n readonly registryGroupPathDot: string;\n readonly exportName: string;\n readonly runtime: \"Convex\" | \"Node\";\n readonly specImportPath: string;\n}\n\nexport const SPEC_SUFFIX = \".spec.ts\";\nexport const IMPL_SUFFIX = \".impl.ts\";\n\nconst swapModuleSuffix = (\n relativePath: string,\n fromSuffix: string,\n toSuffix: string,\n) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const { dir, name, ext } = path.parse(relativePath);\n if (ext !== \".ts\" || !name.endsWith(fromSuffix.slice(0, -\".ts\".length))) {\n return relativePath;\n }\n\n const stem = name.slice(0, -fromSuffix.slice(0, -\".ts\".length).length);\n const nextName = `${stem}${toSuffix.slice(0, -\".ts\".length)}`;\n return dir.length > 0\n ? path.join(dir, `${nextName}${ext}`)\n : `${nextName}${ext}`;\n });\n\nexport const isLeafSpecPath = (relativePath: string) =>\n relativePath.endsWith(SPEC_SUFFIX);\n\nexport const isLeafImplPath = (relativePath: string) =>\n relativePath.endsWith(IMPL_SUFFIX);\n\nexport const exportNameFromModulePath = (relativePath: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const { name, ext } = path.parse(relativePath);\n if (ext !== \".ts\") {\n return name;\n }\n return name.endsWith(\".spec\") ? name.slice(0, -\".spec\".length) : name;\n });\n\nexport const groupPathFromRelativeModulePath = (relativePath: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const { dir, name, ext } = path.parse(relativePath);\n const stem =\n ext === \".ts\" && name.endsWith(\".spec\")\n ? name.slice(0, -\".spec\".length)\n : name;\n const dirSegments = Array.filter(\n String.split(dir, path.sep),\n String.isNonEmpty,\n );\n const pathSegments = Array.append(dirSegments, stem) as [\n string,\n ...string[],\n ];\n return {\n pathSegments,\n groupPathDot: Array.join(pathSegments, \".\"),\n };\n });\n\nexport const specImportPathFromGenerated = (specRelativePath: string) =>\n Effect.gen(function* () {\n const withoutExt = yield* removePathExtension(specRelativePath);\n return `../${withoutExt}`;\n });\n\nexport const specPathForImpl = (implRelativePath: string) =>\n swapModuleSuffix(implRelativePath, IMPL_SUFFIX, SPEC_SUFFIX);\n\nexport const implPathForSpec = (specRelativePath: string) =>\n swapModuleSuffix(specRelativePath, SPEC_SUFFIX, IMPL_SUFFIX);\n\nexport const isNodeLeafModule = (relativePath: string) =>\n relativePath.startsWith(\"node/\") || relativePath.startsWith(\"node\\\\\");\n\nexport const toNodeRegistryLeaf = (leaf: LeafModule): LeafModule => ({\n ...leaf,\n pathSegments: [leaf.exportName],\n groupPathDot: leaf.exportName,\n});\n\nexport const registeredFunctionsRelativePath = (leaf: LeafModule) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n return (\n path.join(\n \"registeredFunctions\",\n ...leaf.pathSegments.slice(leaf.runtime === \"Node\" ? 1 : 0),\n ) + \".ts\"\n );\n });\n\nexport const discoverLeafSpecFiles = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const excludedDirs = new Set([\"_generated\", \"tables\"]);\n const excludedFiles = new Set([\"nodeSpec.ts\", \"spec.ts\"]);\n\n const allPaths = yield* fs.readDirectory(confectDirectory, {\n recursive: true,\n });\n\n return Array.filter(allPaths, (relativePath) => {\n if (!isLeafSpecPath(relativePath)) {\n return false;\n }\n\n if (excludedFiles.has(relativePath)) {\n return false;\n }\n\n const segments = String.split(relativePath, path.sep);\n return !Array.some(segments, (segment) => excludedDirs.has(segment));\n });\n});\n\nexport const discoverLeafImplFiles = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const excludedDirs = new Set([\"_generated\", \"tables\"]);\n\n const allPaths = yield* fs.readDirectory(confectDirectory, {\n recursive: true,\n });\n\n return Array.filter(allPaths, (relativePath) => {\n if (!isLeafImplPath(relativePath)) {\n return false;\n }\n\n const segments = String.split(relativePath, path.sep);\n return !Array.some(segments, (segment) => excludedDirs.has(segment));\n });\n});\n\nexport const toLeafModule = (specRelativePath: string) =>\n Effect.gen(function* () {\n const exportName = yield* exportNameFromModulePath(specRelativePath);\n const { pathSegments, groupPathDot } =\n yield* groupPathFromRelativeModulePath(specRelativePath);\n const specImportPath = yield* specImportPathFromGenerated(specRelativePath);\n const runtime = isNodeLeafModule(specRelativePath) ? \"Node\" : \"Convex\";\n\n return {\n relativePath: specRelativePath,\n pathSegments,\n groupPathDot,\n exportName,\n runtime,\n registryGroupPathDot: runtime === \"Node\" ? exportName : groupPathDot,\n specImportPath,\n } satisfies LeafModule;\n });\n\nconst absoluteModulePath = (relativePath: string) =>\n Effect.gen(function* () {\n const confectDirectory = yield* ConfectDirectory.get;\n const path = yield* Path.Path;\n return path.resolve(confectDirectory, relativePath);\n });\n\n/**\n * Validate that the leaf's spec file default-exports a `GroupSpec` whose\n * runtime matches the leaf's location (`Convex` for files outside\n * `confect/node/`, `Node` for files inside it).\n */\nexport const validateSpec = (leaf: LeafModule) =>\n Effect.gen(function* () {\n const absolutePath = yield* absoluteModulePath(leaf.relativePath);\n const { module } = yield* Bundler.bundle(absolutePath).pipe(\n Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)),\n );\n\n const groupSpec = module.default;\n\n if (!GroupSpec.isGroupSpec(groupSpec)) {\n return yield* new SpecMissingDefaultGroupSpecError({\n specPath: leaf.relativePath,\n });\n }\n\n if (groupSpec.runtime !== leaf.runtime) {\n return yield* new SpecRuntimeMismatchError({\n specPath: leaf.relativePath,\n expectedRuntime: leaf.runtime,\n actualRuntime: groupSpec.runtime,\n });\n }\n });\n\n/**\n * Walk the built `Context` for a `Finalized` `GroupImpl` service value. The\n * lookup is value-shaped (via `GroupImpl.isFinalizedGroupImpl`) so we don't\n * need to know the group's path up front to construct a typed tag for it.\n */\nconst findFinalizedGroupImpl = <S>(\n context: Context.Context<S>,\n): Option.Option<GroupImpl.AnyFinalized> =>\n Array.findFirst(context.unsafeMap.values(), GroupImpl.isFinalizedGroupImpl);\n\n/**\n * Build the impl layer with a fresh `Registry` so each validation is\n * isolated from prior validations' `FunctionImpl.make` writes. The CLI no\n * longer reads the registry directly — `GroupImpl.finalize` snapshots the\n * registered function names onto the produced `Finalized` `GroupImpl`\n * service value — but a fresh `Ref` is still required because the default\n * `Context.Reference` is cached globally and would otherwise accumulate\n * items across impls.\n */\nconst buildImplLayer = (implLayer: Layer.Layer<unknown>) =>\n Effect.gen(function* () {\n const registry = Ref.unsafeMake<Registry.RegistryItems>({});\n return yield* Layer.build(\n implLayer as Layer.Layer<unknown, never, never>,\n ).pipe(Effect.provideService(Registry.Registry, registry));\n }).pipe(Effect.scoped);\n\n/**\n * Validate that the leaf's sibling impl file imports the spec, default-exports\n * a finalized `GroupImpl` layer, and provides a `FunctionImpl` for every\n * function declared by the spec.\n */\nexport const validateImpl = (leaf: LeafModule) =>\n Effect.gen(function* () {\n const implRelativePath = yield* implPathForSpec(leaf.relativePath);\n const implAbsolutePath = yield* absoluteModulePath(implRelativePath);\n const specAbsolutePath = yield* absoluteModulePath(leaf.relativePath);\n\n const bundled = yield* Bundler.bundle(implAbsolutePath).pipe(\n Effect.mapError((error) => fromBundlerError(implRelativePath, error)),\n );\n\n if (\n !(yield* Bundler.directlyImports(\n bundled,\n implAbsolutePath,\n specAbsolutePath,\n ))\n ) {\n return yield* new ImplMissingSpecImportError({\n implPath: implRelativePath,\n expectedSpecPath: leaf.relativePath,\n });\n }\n\n if (!Layer.isLayer(bundled.module.default)) {\n return yield* new ImplMissingDefaultLayerError({\n implPath: implRelativePath,\n });\n }\n\n const { module: specModule } = yield* Bundler.bundle(specAbsolutePath).pipe(\n Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)),\n );\n const groupSpec = specModule.default as GroupSpec.AnyWithProps;\n const expectedFunctionNames = Object.keys(groupSpec.functions);\n\n const context = yield* buildImplLayer(\n bundled.module.default as Layer.Layer<unknown>,\n );\n const finalizedGroupImpl = yield* Option.match(\n findFinalizedGroupImpl(context),\n {\n onNone: () => new ImplNotFinalizedError({ implPath: implRelativePath }),\n onSome: Effect.succeed,\n },\n );\n\n const registeredSet = new Set(finalizedGroupImpl.registeredFunctionNames);\n const missing = expectedFunctionNames.filter(\n (name) => !registeredSet.has(name),\n );\n\n if (missing.length > 0) {\n return yield* new ImplMissingFunctionsError({\n implPath: implRelativePath,\n groupPath: finalizedGroupImpl.groupPath,\n missingFunctionNames: missing,\n });\n }\n });\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { Array, Option, Order, Record, pipe } from "effect";
2
+
3
+ //#region src/SpecAssemblyNode.ts
4
+ const importBindingFromLeaf = (leaf) => ({
5
+ importPath: leaf.specImportPath,
6
+ exportName: leaf.exportName
7
+ });
8
+ const assemblyNodesAtDepth = (leaves, depth) => pipe(Array.groupBy(leaves, (leaf) => leaf.pathSegments[depth]), Record.toEntries, Array.sortBy(Order.mapInput(Order.string, ([segment]) => segment)), Array.map(([segment, groupLeaves]) => {
9
+ const terminal = Array.findFirst(groupLeaves, (leaf) => leaf.pathSegments.length === depth + 1);
10
+ const descendants = Array.filter(groupLeaves, (leaf) => leaf.pathSegments.length > depth + 1);
11
+ return {
12
+ segment,
13
+ importBinding: Option.map(terminal, importBindingFromLeaf),
14
+ children: assemblyNodesAtDepth(descendants, depth + 1)
15
+ };
16
+ }));
17
+ const assemblyNodesFromLeaves = (leaves) => assemblyNodesAtDepth(leaves, 0);
18
+ const partitionByRuntime = (leaves) => {
19
+ const [node, convex] = Array.partition(leaves, (leaf) => leaf.runtime === "Convex");
20
+ return {
21
+ convex,
22
+ node
23
+ };
24
+ };
25
+ const importBindingsForNode = (node) => pipe(node.children, Array.flatMap(importBindingsForNode), (childBindings) => Option.match(node.importBinding, {
26
+ onNone: () => childBindings,
27
+ onSome: (binding) => Array.prepend(childBindings, binding)
28
+ }));
29
+ const collectImportBindings = (nodes) => pipe(Array.flatMap(nodes, importBindingsForNode), (bindings) => Record.fromIterableBy(bindings, (binding) => binding.exportName), Record.toEntries, Array.sortBy(Order.mapInput(Order.string, ([exportName]) => exportName)), Array.map(([, binding]) => binding));
30
+
31
+ //#endregion
32
+ export { assemblyNodesFromLeaves, collectImportBindings, partitionByRuntime };
33
+ //# sourceMappingURL=SpecAssemblyNode.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpecAssemblyNode.mjs","names":[],"sources":["../src/SpecAssemblyNode.ts"],"sourcesContent":["import type { LeafModule } from \"./LeafModule\";\nimport { Array, Option, Order, pipe, Record } from \"effect\";\n\nexport interface SpecImportBinding {\n readonly importPath: string;\n readonly exportName: string;\n}\n\nexport interface SpecAssemblyNode {\n readonly segment: string;\n readonly importBinding: Option.Option<SpecImportBinding>;\n readonly children: ReadonlyArray<SpecAssemblyNode>;\n}\n\nconst importBindingFromLeaf = (leaf: LeafModule): SpecImportBinding => ({\n importPath: leaf.specImportPath,\n exportName: leaf.exportName,\n});\n\nconst assemblyNodesAtDepth = (\n leaves: ReadonlyArray<LeafModule>,\n depth: number,\n): ReadonlyArray<SpecAssemblyNode> =>\n pipe(\n Array.groupBy(leaves, (leaf) => leaf.pathSegments[depth]!),\n Record.toEntries,\n Array.sortBy(Order.mapInput(Order.string, ([segment]) => segment)),\n Array.map(([segment, groupLeaves]) => {\n const terminal = Array.findFirst(\n groupLeaves,\n (leaf) => leaf.pathSegments.length === depth + 1,\n );\n const descendants = Array.filter(\n groupLeaves,\n (leaf) => leaf.pathSegments.length > depth + 1,\n );\n return {\n segment,\n importBinding: Option.map(terminal, importBindingFromLeaf),\n children: assemblyNodesAtDepth(descendants, depth + 1),\n };\n }),\n );\n\nexport const assemblyNodesFromLeaves = (\n leaves: ReadonlyArray<LeafModule>,\n): ReadonlyArray<SpecAssemblyNode> => assemblyNodesAtDepth(leaves, 0);\n\nexport const partitionByRuntime = (\n leaves: ReadonlyArray<LeafModule>,\n): {\n readonly convex: ReadonlyArray<LeafModule>;\n readonly node: ReadonlyArray<LeafModule>;\n} => {\n const [node, convex] = Array.partition(\n leaves,\n (leaf) => leaf.runtime === \"Convex\",\n );\n return { convex, node };\n};\n\nconst importBindingsForNode = (\n node: SpecAssemblyNode,\n): ReadonlyArray<SpecImportBinding> =>\n pipe(node.children, Array.flatMap(importBindingsForNode), (childBindings) =>\n Option.match(node.importBinding, {\n onNone: () => childBindings,\n onSome: (binding) => Array.prepend(childBindings, binding),\n }),\n );\n\nexport const collectImportBindings = (\n nodes: ReadonlyArray<SpecAssemblyNode>,\n): ReadonlyArray<SpecImportBinding> =>\n pipe(\n Array.flatMap(nodes, importBindingsForNode),\n (bindings) =>\n Record.fromIterableBy(bindings, (binding) => binding.exportName),\n Record.toEntries,\n Array.sortBy(Order.mapInput(Order.string, ([exportName]) => exportName)),\n Array.map(([, binding]) => binding),\n );\n"],"mappings":";;;AAcA,MAAM,yBAAyB,UAAyC;CACtE,YAAY,KAAK;CACjB,YAAY,KAAK;CAClB;AAED,MAAM,wBACJ,QACA,UAEA,KACE,MAAM,QAAQ,SAAS,SAAS,KAAK,aAAa,OAAQ,EAC1D,OAAO,WACP,MAAM,OAAO,MAAM,SAAS,MAAM,SAAS,CAAC,aAAa,QAAQ,CAAC,EAClE,MAAM,KAAK,CAAC,SAAS,iBAAiB;CACpC,MAAM,WAAW,MAAM,UACrB,cACC,SAAS,KAAK,aAAa,WAAW,QAAQ,EAChD;CACD,MAAM,cAAc,MAAM,OACxB,cACC,SAAS,KAAK,aAAa,SAAS,QAAQ,EAC9C;AACD,QAAO;EACL;EACA,eAAe,OAAO,IAAI,UAAU,sBAAsB;EAC1D,UAAU,qBAAqB,aAAa,QAAQ,EAAE;EACvD;EACD,CACH;AAEH,MAAa,2BACX,WACoC,qBAAqB,QAAQ,EAAE;AAErE,MAAa,sBACX,WAIG;CACH,MAAM,CAAC,MAAM,UAAU,MAAM,UAC3B,SACC,SAAS,KAAK,YAAY,SAC5B;AACD,QAAO;EAAE;EAAQ;EAAM;;AAGzB,MAAM,yBACJ,SAEA,KAAK,KAAK,UAAU,MAAM,QAAQ,sBAAsB,GAAG,kBACzD,OAAO,MAAM,KAAK,eAAe;CAC/B,cAAc;CACd,SAAS,YAAY,MAAM,QAAQ,eAAe,QAAQ;CAC3D,CAAC,CACH;AAEH,MAAa,yBACX,UAEA,KACE,MAAM,QAAQ,OAAO,sBAAsB,GAC1C,aACC,OAAO,eAAe,WAAW,YAAY,QAAQ,WAAW,EAClE,OAAO,WACP,MAAM,OAAO,MAAM,SAAS,MAAM,SAAS,CAAC,gBAAgB,WAAW,CAAC,EACxE,MAAM,KAAK,GAAG,aAAa,QAAQ,CACpC"}
@@ -1,40 +1,59 @@
1
1
  import { logFileAdded, logFileModified, logFileRemoved, logPending, logSuccess } from "../log.mjs";
2
+ import { fromBundlerError } from "../BuildError.mjs";
3
+ import { MissingImplFileError, MissingSchemaFileError, MissingSpecFileError, SchemaInvalidDefaultExportError, tapAndLog } from "../CodegenError.mjs";
2
4
  import { ConvexDirectory } from "../ConvexDirectory.mjs";
3
5
  import { ConfectDirectory } from "../ConfectDirectory.mjs";
4
- import { api, nodeApi, nodeRegisteredFunctions, refs, registeredFunctions, schema, services } from "../templates.mjs";
5
- import { bundleAndImport, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removePathExtension, writeFileStringAndLog } from "../utils.mjs";
6
- import { Effect, Match, Option } from "effect";
6
+ import { FunctionPaths, make } from "../FunctionPaths.mjs";
7
+ import { bundle } from "../Bundler.mjs";
8
+ import { assemblyNodesFromLeaves, partitionByRuntime } from "../SpecAssemblyNode.mjs";
9
+ import { api, assembledSpec, nodeApi, refs, registeredFunctionsForGroup, schema, services } from "../templates.mjs";
10
+ import { WriteTracker, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removePathExtension, removePathIfExists, toModuleImportPath, touchConvexSchema, writeFileStringAndLog } from "../utils.mjs";
11
+ import { discoverLeafImplFiles, discoverLeafSpecFiles, implPathForSpec, registeredFunctionsRelativePath, specPathForImpl, toLeafModule, toNodeRegistryLeaf, validateImpl, validateSpec } from "../LeafModule.mjs";
12
+ import { Array, Effect, Either, HashSet, Match, Option, Ref } from "effect";
7
13
  import { Command } from "@effect/cli";
8
- import { DatabaseSchema, Spec } from "@confect/core";
14
+ import { Spec } from "@confect/core";
15
+ import * as DatabaseSchema from "@confect/server/DatabaseSchema";
9
16
  import { FileSystem, Path } from "@effect/platform";
10
17
 
11
18
  //#region src/confect/codegen.ts
12
- const getNodeSpecPath = Effect.gen(function* () {
13
- const path = yield* Path.Path;
14
- const confectDirectory = yield* ConfectDirectory.get;
15
- return path.join(confectDirectory, "nodeSpec.ts");
16
- });
17
- const loadNodeSpec = Effect.gen(function* () {
18
- const fs = yield* FileSystem.FileSystem;
19
- const nodeSpecPath = yield* getNodeSpecPath;
20
- if (!(yield* fs.exists(nodeSpecPath))) return Option.none();
21
- const nodeSpec = (yield* bundleAndImport(nodeSpecPath)).default;
22
- if (!Spec.isNodeSpec(nodeSpec)) return yield* Effect.die("nodeSpec.ts does not export a valid Node Spec");
23
- return Option.some(nodeSpec);
24
- });
19
+ const GENERATED_SPEC_PATH = "_generated/spec.ts";
20
+ const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
21
+ const LEGACY_PATHS = [
22
+ "spec.ts",
23
+ "nodeSpec.ts",
24
+ "impl.ts",
25
+ "nodeImpl.ts",
26
+ "notesAndRandom.impl.ts",
27
+ "groups.impl.ts",
28
+ "_generated/registeredFunctions.ts",
29
+ "_generated/nodeRegisteredFunctions.ts",
30
+ "_generated/impl.ts",
31
+ "_generated/nodeImpl.ts"
32
+ ];
25
33
  const codegen = Command.make("codegen", {}, () => Effect.gen(function* () {
26
34
  yield* logPending("Performing initial sync…");
27
- yield* codegenHandler;
28
- yield* logSuccess("Generated files are up-to-date");
29
- })).pipe(Command.withDescription("Generate `confect/_generated` files and the contents of the `convex` directory (except `tsconfig.json`)"));
35
+ yield* codegenHandler.pipe(Effect.asVoid, Effect.tap(() => logSuccess("Generated files are up-to-date")), tapAndLog);
36
+ })).pipe(Command.withDescription("Generate `confect/_generated` files and the contents of the `convex` directory (except `convex.config.ts` and `tsconfig.json`)"));
30
37
  const codegenHandler = Effect.gen(function* () {
38
+ const tracker = yield* Ref.make(false);
39
+ return {
40
+ functionPaths: yield* runCodegen.pipe(Effect.provideService(WriteTracker, tracker)),
41
+ anyWritesHappened: yield* Ref.get(tracker)
42
+ };
43
+ });
44
+ const runCodegen = Effect.gen(function* () {
31
45
  yield* generateConfectGeneratedDirectory;
46
+ yield* validateSchema;
47
+ const leaves = yield* loadAndValidateLeafModules;
48
+ yield* removeLegacyFiles;
49
+ yield* generateAssembledSpecs(leaves);
50
+ yield* validateImplModules(leaves);
51
+ yield* generateGroupRegisteredFunctions(leaves);
52
+ yield* removeObsoleteRegisteredFunctions(leaves);
32
53
  yield* Effect.all([
33
54
  generateApi,
34
55
  generateRefs,
35
- generateRegisteredFunctions,
36
56
  generateNodeApi,
37
- generateNodeRegisteredFunctions,
38
57
  generateServices
39
58
  ], { concurrency: "unbounded" });
40
59
  const [functionPaths] = yield* Effect.all([
@@ -44,6 +63,7 @@ const codegenHandler = Effect.gen(function* () {
44
63
  logGenerated(generateCrons),
45
64
  logGenerated(generateAuthConfig)
46
65
  ], { concurrency: "unbounded" });
66
+ yield* touchConvexSchema;
47
67
  return functionPaths;
48
68
  });
49
69
  const generateConfectGeneratedDirectory = Effect.gen(function* () {
@@ -55,13 +75,169 @@ const generateConfectGeneratedDirectory = Effect.gen(function* () {
55
75
  yield* logFileAdded(path.join(confectDirectory, "_generated") + "/");
56
76
  }
57
77
  });
78
+ const loadAndValidateLeafModules = Effect.gen(function* () {
79
+ const fs = yield* FileSystem.FileSystem;
80
+ const path = yield* Path.Path;
81
+ const confectDirectory = yield* ConfectDirectory.get;
82
+ const specFiles = yield* discoverLeafSpecFiles;
83
+ const leaves = yield* Effect.forEach(specFiles, (specRelativePath) => Effect.gen(function* () {
84
+ const leaf = yield* toLeafModule(specRelativePath);
85
+ yield* validateSpec(leaf);
86
+ const implRelativePath = yield* implPathForSpec(specRelativePath);
87
+ const implAbsolutePath = path.join(confectDirectory, implRelativePath);
88
+ if (!(yield* fs.exists(implAbsolutePath))) return yield* new MissingImplFileError({
89
+ specPath: specRelativePath,
90
+ expectedImplPath: implRelativePath
91
+ });
92
+ return leaf;
93
+ }));
94
+ yield* validateOrphanImpls(specFiles);
95
+ return leaves;
96
+ });
97
+ const validateOrphanImpls = (specFiles) => Effect.gen(function* () {
98
+ const fs = yield* FileSystem.FileSystem;
99
+ const path = yield* Path.Path;
100
+ const confectDirectory = yield* ConfectDirectory.get;
101
+ const implFiles = yield* discoverLeafImplFiles;
102
+ const specPaths = new Set(specFiles);
103
+ yield* Effect.forEach(implFiles, (implRelativePath) => Effect.gen(function* () {
104
+ const specRelativePath = yield* specPathForImpl(implRelativePath);
105
+ if (specPaths.has(specRelativePath)) return;
106
+ const specAbsolutePath = path.join(confectDirectory, specRelativePath);
107
+ if (!(yield* fs.exists(specAbsolutePath))) return yield* new MissingSpecFileError({
108
+ implPath: implRelativePath,
109
+ expectedSpecPath: specRelativePath
110
+ });
111
+ }));
112
+ });
113
+ const removeLegacyFiles = Effect.gen(function* () {
114
+ const fs = yield* FileSystem.FileSystem;
115
+ const path = yield* Path.Path;
116
+ const confectDirectory = yield* ConfectDirectory.get;
117
+ yield* Effect.forEach(LEGACY_PATHS, (relativePath) => Effect.gen(function* () {
118
+ const absolutePath = path.join(confectDirectory, relativePath);
119
+ if (yield* fs.exists(absolutePath)) {
120
+ yield* removePathIfExists(absolutePath);
121
+ yield* logFileRemoved(absolutePath);
122
+ }
123
+ }));
124
+ });
125
+ const generateAssembledSpecs = (leaves) => Effect.gen(function* () {
126
+ const path = yield* Path.Path;
127
+ const confectDirectory = yield* ConfectDirectory.get;
128
+ const { convex, node } = partitionByRuntime(leaves);
129
+ if (convex.length > 0) {
130
+ const nodes = assemblyNodesFromLeaves(convex);
131
+ const specContents = yield* assembledSpec({
132
+ nodes,
133
+ runtime: "Convex"
134
+ });
135
+ yield* writeFileStringAndLog(path.join(confectDirectory, GENERATED_SPEC_PATH), specContents);
136
+ }
137
+ if (node.length > 0) {
138
+ const nodes = assemblyNodesFromLeaves(Array.map(node, toNodeRegistryLeaf));
139
+ const nodeSpecContents = yield* assembledSpec({
140
+ nodes,
141
+ runtime: "Node"
142
+ });
143
+ yield* writeFileStringAndLog(path.join(confectDirectory, GENERATED_NODE_SPEC_PATH), nodeSpecContents);
144
+ }
145
+ });
146
+ const validateImplModules = (leaves) => Effect.forEach(leaves, validateImpl);
147
+ const generateGroupRegisteredFunctions = (leaves) => Effect.gen(function* () {
148
+ const path = yield* Path.Path;
149
+ const confectDirectory = yield* ConfectDirectory.get;
150
+ yield* Effect.forEach(leaves, (leaf) => Effect.gen(function* () {
151
+ const registryRelativePath = yield* registeredFunctionsRelativePath(leaf);
152
+ const registryPath = path.join(confectDirectory, "_generated", registryRelativePath);
153
+ const registryDir = path.dirname(registryPath);
154
+ const fs = yield* FileSystem.FileSystem;
155
+ if (!(yield* fs.exists(registryDir))) yield* fs.makeDirectory(registryDir, { recursive: true });
156
+ const implRelativePath = yield* implPathForSpec(leaf.relativePath);
157
+ const apiFileName = leaf.runtime === "Node" ? "nodeApi.ts" : "api.ts";
158
+ const apiImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, "_generated", apiFileName)));
159
+ const implImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, implRelativePath)));
160
+ yield* writeFileStringAndLog(registryPath, yield* registeredFunctionsForGroup({
161
+ apiImportPath,
162
+ groupPathDot: leaf.registryGroupPathDot,
163
+ implImportPath,
164
+ layerExportName: leaf.exportName,
165
+ useNode: leaf.runtime === "Node"
166
+ }));
167
+ }));
168
+ });
169
+ const removeObsoleteRegisteredFunctions = (leaves) => Effect.gen(function* () {
170
+ const fs = yield* FileSystem.FileSystem;
171
+ const path = yield* Path.Path;
172
+ const confectDirectory = yield* ConfectDirectory.get;
173
+ const registryRoot = path.join(confectDirectory, "_generated", "registeredFunctions");
174
+ if (!(yield* fs.exists(registryRoot))) return;
175
+ const expected = new Set(yield* Effect.forEach(leaves, (leaf) => registeredFunctionsRelativePath(leaf)));
176
+ const existing = yield* fs.readDirectory(registryRoot, { recursive: true });
177
+ yield* Effect.forEach(existing, (relativePath) => {
178
+ if (path.extname(relativePath) !== ".ts") return Effect.void;
179
+ const normalized = path.join("registeredFunctions", relativePath);
180
+ if (!expected.has(normalized)) return Effect.gen(function* () {
181
+ const absolutePath = path.join(registryRoot, relativePath);
182
+ if (yield* fs.exists(absolutePath)) {
183
+ yield* removePathIfExists(absolutePath);
184
+ yield* logFileRemoved(absolutePath);
185
+ }
186
+ });
187
+ return Effect.void;
188
+ });
189
+ });
190
+ const getGeneratedSpecPath = Effect.gen(function* () {
191
+ const path = yield* Path.Path;
192
+ const confectDirectory = yield* ConfectDirectory.get;
193
+ return path.join(confectDirectory, GENERATED_SPEC_PATH);
194
+ });
195
+ const getGeneratedNodeSpecPath = Effect.gen(function* () {
196
+ const path = yield* Path.Path;
197
+ const confectDirectory = yield* ConfectDirectory.get;
198
+ return path.join(confectDirectory, GENERATED_NODE_SPEC_PATH);
199
+ });
200
+ const loadGeneratedSpec = Effect.gen(function* () {
201
+ const specPath = yield* getGeneratedSpecPath;
202
+ const { module: specModule } = yield* bundle(specPath);
203
+ const spec = specModule.default;
204
+ if (!Spec.isConvexSpec(spec)) return yield* Effect.dieMessage("_generated/spec.ts does not export a valid Convex Spec");
205
+ return spec;
206
+ });
207
+ const loadGeneratedNodeSpec = Effect.gen(function* () {
208
+ const fs = yield* FileSystem.FileSystem;
209
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
210
+ if (!(yield* fs.exists(nodeSpecPath))) return Option.none();
211
+ const { module: nodeSpecModule } = yield* bundle(nodeSpecPath);
212
+ const nodeSpec = nodeSpecModule.default;
213
+ if (!Spec.isNodeSpec(nodeSpec)) return yield* Effect.dieMessage("_generated/nodeSpec.ts does not export a valid Node Spec");
214
+ return Option.some(nodeSpec);
215
+ });
216
+ const emptyFunctionPaths = FunctionPaths.make(HashSet.empty());
217
+ const loadPreviousFunctionPaths = Effect.gen(function* () {
218
+ const fs = yield* FileSystem.FileSystem;
219
+ const specPath = yield* getGeneratedSpecPath;
220
+ if (!(yield* fs.exists(specPath))) return emptyFunctionPaths;
221
+ const specEither = yield* loadGeneratedSpec.pipe(Effect.either);
222
+ return yield* Either.match(specEither, {
223
+ onLeft: () => Effect.succeed(emptyFunctionPaths),
224
+ onRight: (spec) => Effect.gen(function* () {
225
+ const nodeSpecOption = yield* loadGeneratedNodeSpec;
226
+ const mergedSpec = Option.match(nodeSpecOption, {
227
+ onNone: () => spec,
228
+ onSome: (nodeSpec) => Spec.merge(spec, nodeSpec)
229
+ });
230
+ return make(mergedSpec);
231
+ })
232
+ });
233
+ });
58
234
  const generateApi = Effect.gen(function* () {
59
235
  const path = yield* Path.Path;
60
236
  const confectDirectory = yield* ConfectDirectory.get;
61
237
  const apiPath = path.join(confectDirectory, "_generated", "api.ts");
62
238
  const apiDir = path.dirname(apiPath);
63
- const schemaImportPath = yield* removePathExtension(path.relative(apiDir, path.join(confectDirectory, "schema.ts")));
64
- const specImportPath = yield* removePathExtension(path.relative(apiDir, path.join(confectDirectory, "spec.ts")));
239
+ const schemaImportPath = yield* toModuleImportPath(path.relative(apiDir, path.join(confectDirectory, "schema.ts")));
240
+ const specImportPath = yield* toModuleImportPath(path.relative(apiDir, path.join(confectDirectory, GENERATED_SPEC_PATH)));
65
241
  yield* writeFileStringAndLog(apiPath, yield* api({
66
242
  schemaImportPath,
67
243
  specImportPath
@@ -71,35 +247,40 @@ const generateNodeApi = Effect.gen(function* () {
71
247
  const fs = yield* FileSystem.FileSystem;
72
248
  const path = yield* Path.Path;
73
249
  const confectDirectory = yield* ConfectDirectory.get;
74
- const nodeSpecPath = yield* getNodeSpecPath;
250
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
75
251
  const nodeApiPath = path.join(confectDirectory, "_generated", "nodeApi.ts");
76
252
  if (!(yield* fs.exists(nodeSpecPath))) {
77
253
  if (yield* fs.exists(nodeApiPath)) {
78
- yield* fs.remove(nodeApiPath);
254
+ yield* removePathIfExists(nodeApiPath);
79
255
  yield* logFileRemoved(nodeApiPath);
80
256
  }
81
257
  return;
82
258
  }
83
259
  const nodeApiDir = path.dirname(nodeApiPath);
84
- const schemaImportPath = yield* removePathExtension(path.relative(nodeApiDir, path.join(confectDirectory, "schema.ts")));
85
- const nodeSpecImportPath = yield* removePathExtension(path.relative(nodeApiDir, nodeSpecPath));
260
+ const schemaImportPath = yield* toModuleImportPath(path.relative(nodeApiDir, path.join(confectDirectory, "schema.ts")));
261
+ const nodeSpecImportPath = yield* toModuleImportPath(path.relative(nodeApiDir, nodeSpecPath));
86
262
  yield* writeFileStringAndLog(nodeApiPath, yield* nodeApi({
87
263
  schemaImportPath,
88
264
  nodeSpecImportPath
89
265
  }));
90
266
  });
91
267
  const generateFunctionModules = Effect.gen(function* () {
268
+ const spec = yield* loadGeneratedSpec;
269
+ const nodeSpecOption = yield* loadGeneratedNodeSpec;
270
+ return yield* generateFunctions(Option.match(nodeSpecOption, {
271
+ onNone: () => spec,
272
+ onSome: (nodeSpec) => Spec.merge(spec, nodeSpec)
273
+ }));
274
+ });
275
+ const validateSchema = Effect.gen(function* () {
92
276
  const fs = yield* FileSystem.FileSystem;
93
277
  const path = yield* Path.Path;
94
278
  const confectDirectory = yield* ConfectDirectory.get;
95
- const spec = (yield* bundleAndImport(path.join(confectDirectory, "spec.ts"))).default;
96
- if (!Spec.isConvexSpec(spec)) return yield* Effect.die("spec.ts does not export a valid Convex Spec");
97
- const nodeImplPath = path.join(confectDirectory, "nodeImpl.ts");
98
- const nodeImplExists = yield* fs.exists(nodeImplPath);
99
- const nodeSpecOption = yield* loadNodeSpec;
100
- return yield* generateFunctions(Option.match(nodeSpecOption, {
101
- onNone: () => spec,
102
- onSome: (nodeSpec) => nodeImplExists ? Spec.merge(spec, nodeSpec) : spec
279
+ const confectSchemaPath = path.join(confectDirectory, "schema.ts");
280
+ if (!(yield* fs.exists(confectSchemaPath))) return yield* new MissingSchemaFileError({ schemaPath: "schema.ts" });
281
+ yield* bundle(confectSchemaPath).pipe(Effect.mapError((error) => fromBundlerError("schema.ts", error)), Effect.andThen(({ module: schemaModule }) => {
282
+ const defaultExport = schemaModule.default;
283
+ return DatabaseSchema.isDatabaseSchema(defaultExport) ? Effect.succeed(defaultExport) : Effect.fail(new SchemaInvalidDefaultExportError({ schemaPath: "schema.ts" }));
103
284
  }));
104
285
  });
105
286
  const generateSchema = Effect.gen(function* () {
@@ -107,10 +288,6 @@ const generateSchema = Effect.gen(function* () {
107
288
  const confectDirectory = yield* ConfectDirectory.get;
108
289
  const convexDirectory = yield* ConvexDirectory.get;
109
290
  const confectSchemaPath = path.join(confectDirectory, "schema.ts");
110
- yield* bundleAndImport(confectSchemaPath).pipe(Effect.andThen((schemaModule) => {
111
- const defaultExport = schemaModule.default;
112
- return DatabaseSchema.isDatabaseSchema(defaultExport) ? Effect.succeed(defaultExport) : Effect.die("Invalid schema module");
113
- }));
114
291
  const convexSchemaPath = path.join(convexDirectory, "schema.ts");
115
292
  const importPathWithoutExt = yield* removePathExtension(path.relative(path.dirname(convexSchemaPath), confectSchemaPath));
116
293
  yield* writeFileStringAndLog(convexSchemaPath, yield* schema({ schemaImportPath: importPathWithoutExt }));
@@ -123,33 +300,6 @@ const generateServices = Effect.gen(function* () {
123
300
  const schemaImportPath = path.relative(path.dirname(servicesPath), path.join(confectDirectory, "schema"));
124
301
  yield* writeFileStringAndLog(servicesPath, yield* services({ schemaImportPath }));
125
302
  });
126
- const generateRegisteredFunctions = Effect.gen(function* () {
127
- const path = yield* Path.Path;
128
- const confectDirectory = yield* ConfectDirectory.get;
129
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
130
- const registeredFunctionsPath = path.join(confectGeneratedDirectory, "registeredFunctions.ts");
131
- const implImportPath = yield* removePathExtension(path.relative(path.dirname(registeredFunctionsPath), path.join(confectDirectory, "impl.ts")));
132
- yield* writeFileStringAndLog(registeredFunctionsPath, yield* registeredFunctions({ implImportPath }));
133
- });
134
- const generateNodeRegisteredFunctions = Effect.gen(function* () {
135
- const fs = yield* FileSystem.FileSystem;
136
- const path = yield* Path.Path;
137
- const confectDirectory = yield* ConfectDirectory.get;
138
- const nodeImplPath = path.join(confectDirectory, "nodeImpl.ts");
139
- const nodeSpecPath = yield* getNodeSpecPath;
140
- const nodeRegisteredFunctionsPath = path.join(confectDirectory, "_generated", "nodeRegisteredFunctions.ts");
141
- const nodeImplExists = yield* fs.exists(nodeImplPath);
142
- const nodeSpecExists = yield* fs.exists(nodeSpecPath);
143
- if (!nodeImplExists || !nodeSpecExists) {
144
- if (yield* fs.exists(nodeRegisteredFunctionsPath)) {
145
- yield* fs.remove(nodeRegisteredFunctionsPath);
146
- yield* logFileRemoved(nodeRegisteredFunctionsPath);
147
- }
148
- return;
149
- }
150
- const nodeImplImportPath = yield* removePathExtension(path.relative(path.dirname(nodeRegisteredFunctionsPath), nodeImplPath));
151
- yield* writeFileStringAndLog(nodeRegisteredFunctionsPath, yield* nodeRegisteredFunctions({ nodeImplImportPath }));
152
- });
153
303
  const generateRefs = Effect.gen(function* () {
154
304
  const fs = yield* FileSystem.FileSystem;
155
305
  const path = yield* Path.Path;
@@ -157,12 +307,12 @@ const generateRefs = Effect.gen(function* () {
157
307
  const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
158
308
  const refsPath = path.join(confectGeneratedDirectory, "refs.ts");
159
309
  const refsDir = path.dirname(refsPath);
160
- const specImportPath = yield* removePathExtension(path.relative(refsDir, path.join(confectDirectory, "spec.ts")));
161
- const nodeSpecPath = yield* getNodeSpecPath;
162
- const nodeSpecImportPath = (yield* fs.exists(nodeSpecPath)) ? yield* removePathExtension(path.relative(refsDir, nodeSpecPath)) : null;
310
+ const specImportPath = yield* toModuleImportPath(path.relative(refsDir, path.join(confectDirectory, GENERATED_SPEC_PATH)));
311
+ const nodeSpecPath = yield* getGeneratedNodeSpecPath;
312
+ const nodeSpecImportPath = (yield* fs.exists(nodeSpecPath)) ? Option.some(yield* toModuleImportPath(path.relative(refsDir, nodeSpecPath))) : Option.none();
163
313
  yield* writeFileStringAndLog(refsPath, yield* refs({
164
314
  specImportPath,
165
- ...nodeSpecImportPath === null ? {} : { nodeSpecImportPath }
315
+ nodeSpecImportPath
166
316
  }));
167
317
  });
168
318
  const logGenerated = (effect) => effect.pipe(Effect.tap(Option.match({
@@ -171,5 +321,5 @@ const logGenerated = (effect) => effect.pipe(Effect.tap(Option.match({
171
321
  })));
172
322
 
173
323
  //#endregion
174
- export { codegen, codegenHandler, generateNodeApi, generateNodeRegisteredFunctions };
324
+ export { codegen, codegenHandler, loadPreviousFunctionPaths };
175
325
  //# sourceMappingURL=codegen.mjs.map