@confect/cli 9.0.0-next.4 → 9.0.0-next.6

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.
@@ -1 +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). Returns the validated\n * `GroupSpec` so callers can avoid re-bundling for later inspection (e.g.\n * parent/child name-collision checks at codegen time).\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 return groupSpec;\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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"}
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). Returns the validated\n * `GroupSpec` so callers can avoid re-bundling for later inspection (e.g.\n * parent/child name-collision checks at codegen time).\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 return groupSpec;\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: leaf.groupPathDot,\n missingFunctionNames: missing,\n });\n }\n });\n"],"mappings":"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"}
@@ -28,25 +28,7 @@ const importBindingsForNode = (node) => pipe(node.children, Array.flatMap(import
28
28
  onSome: (binding) => Array.prepend(childBindings, binding)
29
29
  }));
30
30
  const collectImportBindings = (nodes) => pipe(Array.flatMap(nodes, importBindingsForNode), (bindings) => Record.fromIterableBy(bindings, (binding) => binding.importPath), Record.toEntries, Array.map(([, binding]) => binding), Array.sortBy(Order.mapInput(Order.string, (binding) => binding.localName)));
31
- const leafPathsForNode = (node, ancestorSegments) => {
32
- const segments = [...ancestorSegments, node.segment];
33
- const childPaths = Array.flatMap(node.children, (child) => leafPathsForNode(child, segments));
34
- return Option.match(node.importBinding, {
35
- onNone: () => childPaths,
36
- onSome: (binding) => Array.prepend(childPaths, {
37
- binding,
38
- dotPath: segments.join(".")
39
- })
40
- });
41
- };
42
- /**
43
- * Walk the assembly tree and produce one entry per leaf spec, pairing its
44
- * import binding with the full dot-path codegen will register via
45
- * `Spec.addPath`. Ordering matches `collectImportBindings` (sorted by the
46
- * binding's local name) so the generated file is stable across runs.
47
- */
48
- const collectLeafPaths = (nodes) => pipe(Array.flatMap(nodes, (node) => leafPathsForNode(node, [])), Array.sortBy(Order.mapInput(Order.string, (entry) => entry.binding.localName)));
49
31
 
50
32
  //#endregion
51
- export { assemblyNodesFromLeaves, collectImportBindings, collectLeafPaths, partitionByRuntime };
33
+ export { assemblyNodesFromLeaves, collectImportBindings, partitionByRuntime };
52
34
  //# sourceMappingURL=SpecAssemblyNode.mjs.map
@@ -1 +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 readonly localName: 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 localName: leaf.pathSegments.join(\"_\"),\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.importPath),\n Record.toEntries,\n Array.map(([, binding]) => binding),\n Array.sortBy(Order.mapInput(Order.string, (binding) => binding.localName)),\n );\n\nexport interface SpecLeafPath {\n readonly binding: SpecImportBinding;\n readonly dotPath: string;\n}\n\nconst leafPathsForNode = (\n node: SpecAssemblyNode,\n ancestorSegments: ReadonlyArray<string>,\n): ReadonlyArray<SpecLeafPath> => {\n const segments = [...ancestorSegments, node.segment];\n const childPaths = Array.flatMap(node.children, (child) =>\n leafPathsForNode(child, segments),\n );\n return Option.match(node.importBinding, {\n onNone: () => childPaths,\n onSome: (binding) =>\n Array.prepend(childPaths, { binding, dotPath: segments.join(\".\") }),\n });\n};\n\n/**\n * Walk the assembly tree and produce one entry per leaf spec, pairing its\n * import binding with the full dot-path codegen will register via\n * `Spec.addPath`. Ordering matches `collectImportBindings` (sorted by the\n * binding's local name) so the generated file is stable across runs.\n */\nexport const collectLeafPaths = (\n nodes: ReadonlyArray<SpecAssemblyNode>,\n): ReadonlyArray<SpecLeafPath> =>\n pipe(\n Array.flatMap(nodes, (node) => leafPathsForNode(node, [])),\n Array.sortBy(\n Order.mapInput(Order.string, (entry) => entry.binding.localName),\n ),\n );\n"],"mappings":";;;AAeA,MAAM,yBAAyB,UAAyC;CACtE,YAAY,KAAK;CACjB,YAAY,KAAK;CACjB,WAAW,KAAK,aAAa,KAAK,IAAI;CACvC;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,KAAK,GAAG,aAAa,QAAQ,EACnC,MAAM,OAAO,MAAM,SAAS,MAAM,SAAS,YAAY,QAAQ,UAAU,CAAC,CAC3E;AAOH,MAAM,oBACJ,MACA,qBACgC;CAChC,MAAM,WAAW,CAAC,GAAG,kBAAkB,KAAK,QAAQ;CACpD,MAAM,aAAa,MAAM,QAAQ,KAAK,WAAW,UAC/C,iBAAiB,OAAO,SAAS,CAClC;AACD,QAAO,OAAO,MAAM,KAAK,eAAe;EACtC,cAAc;EACd,SAAS,YACP,MAAM,QAAQ,YAAY;GAAE;GAAS,SAAS,SAAS,KAAK,IAAI;GAAE,CAAC;EACtE,CAAC;;;;;;;;AASJ,MAAa,oBACX,UAEA,KACE,MAAM,QAAQ,QAAQ,SAAS,iBAAiB,MAAM,EAAE,CAAC,CAAC,EAC1D,MAAM,OACJ,MAAM,SAAS,MAAM,SAAS,UAAU,MAAM,QAAQ,UAAU,CACjE,CACF"}
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 readonly localName: 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 localName: leaf.pathSegments.join(\"_\"),\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.importPath),\n Record.toEntries,\n Array.map(([, binding]) => binding),\n Array.sortBy(Order.mapInput(Order.string, (binding) => binding.localName)),\n );\n"],"mappings":";;;AAeA,MAAM,yBAAyB,UAAyC;CACtE,YAAY,KAAK;CACjB,YAAY,KAAK;CACjB,WAAW,KAAK,aAAa,KAAK,IAAI;CACvC;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,KAAK,GAAG,aAAa,QAAQ,EACnC,MAAM,OAAO,MAAM,SAAS,MAAM,SAAS,YAAY,QAAQ,UAAU,CAAC,CAC3E"}
@@ -0,0 +1,90 @@
1
+ import { fromBundlerError } from "./BuildError.mjs";
2
+ import { bundle } from "./Bundler.mjs";
3
+ import { DuplicateTableNameError, InvalidTableDefaultExportError, InvalidTableFilenameError } from "./CodegenError.mjs";
4
+ import { ConfectDirectory } from "./ConfectDirectory.mjs";
5
+ import { Array, Effect, Order, pipe } from "effect";
6
+ import { Identifier } from "@confect/core";
7
+ import { FileSystem, Path } from "@effect/platform";
8
+ import * as Table from "@confect/server/Table";
9
+
10
+ //#region src/TableModule.ts
11
+ const TABLES_DIRNAME = "tables";
12
+ const tableNameFromRelativePath = (relativePath) => Effect.gen(function* () {
13
+ const { name } = (yield* Path.Path).parse(relativePath);
14
+ return name;
15
+ });
16
+ const listTableFiles = Effect.gen(function* () {
17
+ const fs = yield* FileSystem.FileSystem;
18
+ const path = yield* Path.Path;
19
+ const confectDirectory = yield* ConfectDirectory.get;
20
+ const tablesDirectory = path.join(confectDirectory, TABLES_DIRNAME);
21
+ if (!(yield* fs.exists(tablesDirectory))) return [];
22
+ return pipe(yield* fs.readDirectory(tablesDirectory, { recursive: true }), Array.filter((p) => p.endsWith(".ts") && !p.endsWith(".test.ts")), Array.map((p) => path.join(TABLES_DIRNAME, p)));
23
+ });
24
+ const byTableName = Order.mapInput(Order.string, (tableModule) => tableModule.tableName);
25
+ /**
26
+ * Discover every `confect/tables/**\/*.ts` module by listing the directory.
27
+ * Validates that each filename is a legal table identifier — the table name is
28
+ * derived from the file basename, so the filename must be a valid JavaScript
29
+ * identifier with no leading underscore (Convex reserves `_<name>` for system
30
+ * tables).
31
+ *
32
+ * This step does *not* bundle the table modules. It runs early in the codegen
33
+ * pipeline so the `_generated/id.ts` constructor can be emitted *before* any
34
+ * user-authored table is bundled (those modules import from `_generated/id.ts`
35
+ * for cross-table refs).
36
+ *
37
+ * A missing `confect/tables/` directory is allowed and produces an empty list.
38
+ *
39
+ * Fails with {@link InvalidTableFilenameError} if any filename is not a valid
40
+ * table identifier, or {@link DuplicateTableNameError} if two files resolve to
41
+ * the same table name (the directory is scanned recursively but names are
42
+ * derived from the basename alone, so `tables/a/notes.ts` and
43
+ * `tables/b/notes.ts` would collide).
44
+ */
45
+ const discover = Effect.gen(function* () {
46
+ const relativePaths = yield* listTableFiles;
47
+ const sorted = pipe(yield* Effect.forEach(relativePaths, (relativePath) => Effect.gen(function* () {
48
+ const tableName = yield* tableNameFromRelativePath(relativePath);
49
+ yield* Effect.try({
50
+ try: () => Identifier.validateConfectTableIdentifier(tableName),
51
+ catch: (e) => new InvalidTableFilenameError({
52
+ tablePath: relativePath,
53
+ reason: e instanceof Error ? e.message : String(e)
54
+ })
55
+ });
56
+ return {
57
+ relativePath,
58
+ tableName
59
+ };
60
+ }), { concurrency: "unbounded" }), Array.sortBy(byTableName));
61
+ const collisions = Object.entries(Array.groupBy(sorted, (tableModule) => tableModule.tableName)).filter(([, group]) => group.length > 1).map(([tableName, group]) => ({
62
+ tableName,
63
+ tablePaths: Array.map(group, (tableModule) => tableModule.relativePath)
64
+ }));
65
+ if (collisions.length > 0) return yield* new DuplicateTableNameError({ collisions });
66
+ return sorted;
67
+ });
68
+ /**
69
+ * Bundle every discovered table module and verify that its default export is
70
+ * an {@link Table.UnnamedTable} (the result of `Table.make(...)` before a
71
+ * name has been bound). Fails with {@link InvalidTableDefaultExportError} if
72
+ * any module's default export is missing or has the wrong shape.
73
+ *
74
+ * Must run *after* `_generated/id.ts` has been emitted, because user-authored
75
+ * table modules typically `import { Id } from "../_generated/id"` for
76
+ * cross-table references.
77
+ */
78
+ const validate = (tableModules) => Effect.gen(function* () {
79
+ const path = yield* Path.Path;
80
+ const confectDirectory = yield* ConfectDirectory.get;
81
+ yield* Effect.forEach(tableModules, ({ relativePath }) => Effect.gen(function* () {
82
+ const absolutePath = path.resolve(confectDirectory, relativePath);
83
+ const { module } = yield* bundle(absolutePath).pipe(Effect.mapError((error) => fromBundlerError(relativePath, error)));
84
+ if (!Table.isUnnamedTable(module.default)) return yield* new InvalidTableDefaultExportError({ tablePath: relativePath });
85
+ }), { concurrency: "unbounded" });
86
+ });
87
+
88
+ //#endregion
89
+ export { TABLES_DIRNAME, discover, validate };
90
+ //# sourceMappingURL=TableModule.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TableModule.mjs","names":["Bundler.bundle"],"sources":["../src/TableModule.ts"],"sourcesContent":["import { Identifier } from \"@confect/core\";\nimport * as Table from \"@confect/server/Table\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { Array, Effect, Order, pipe } from \"effect\";\nimport { fromBundlerError } from \"./BuildError\";\nimport * as Bundler from \"./Bundler\";\nimport {\n DuplicateTableNameError,\n InvalidTableDefaultExportError,\n InvalidTableFilenameError,\n} from \"./CodegenError\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\n\nexport const TABLES_DIRNAME = \"tables\";\n\n/**\n * Discovered metadata for a single user-authored table module under\n * `confect/tables/`.\n *\n * - `relativePath` — path from `confect/` to the file (e.g. `tables/notes.ts`).\n * - `tableName` — the file basename (e.g. `notes`). This is also the import\n * binding used in generated files, and the table name surfaced to Convex.\n */\nexport interface TableModule {\n readonly relativePath: string;\n readonly tableName: string;\n}\n\nconst tableNameFromRelativePath = (relativePath: string) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const { name } = path.parse(relativePath);\n return name;\n });\n\nconst listTableFiles = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const tablesDirectory = path.join(confectDirectory, TABLES_DIRNAME);\n\n if (!(yield* fs.exists(tablesDirectory))) {\n return [] as ReadonlyArray<string>;\n }\n\n const allPaths = yield* fs.readDirectory(tablesDirectory, {\n recursive: true,\n });\n\n return pipe(\n allPaths,\n Array.filter((p) => p.endsWith(\".ts\") && !p.endsWith(\".test.ts\")),\n Array.map((p) => path.join(TABLES_DIRNAME, p)),\n );\n});\n\nconst byTableName = Order.mapInput(\n Order.string,\n (tableModule: TableModule) => tableModule.tableName,\n);\n\n/**\n * Discover every `confect/tables/**\\/*.ts` module by listing the directory.\n * Validates that each filename is a legal table identifier — the table name is\n * derived from the file basename, so the filename must be a valid JavaScript\n * identifier with no leading underscore (Convex reserves `_<name>` for system\n * tables).\n *\n * This step does *not* bundle the table modules. It runs early in the codegen\n * pipeline so the `_generated/id.ts` constructor can be emitted *before* any\n * user-authored table is bundled (those modules import from `_generated/id.ts`\n * for cross-table refs).\n *\n * A missing `confect/tables/` directory is allowed and produces an empty list.\n *\n * Fails with {@link InvalidTableFilenameError} if any filename is not a valid\n * table identifier, or {@link DuplicateTableNameError} if two files resolve to\n * the same table name (the directory is scanned recursively but names are\n * derived from the basename alone, so `tables/a/notes.ts` and\n * `tables/b/notes.ts` would collide).\n */\nexport const discover = Effect.gen(function* () {\n const relativePaths = yield* listTableFiles;\n\n const tableModules = yield* Effect.forEach(\n relativePaths,\n (relativePath) =>\n Effect.gen(function* () {\n const tableName = yield* tableNameFromRelativePath(relativePath);\n yield* Effect.try({\n try: () => Identifier.validateConfectTableIdentifier(tableName),\n catch: (e) =>\n new InvalidTableFilenameError({\n tablePath: relativePath,\n reason: e instanceof Error ? e.message : String(e),\n }),\n });\n return { relativePath, tableName } satisfies TableModule;\n }),\n { concurrency: \"unbounded\" },\n );\n\n const sorted = pipe(tableModules, Array.sortBy(byTableName));\n\n const collisions = Object.entries(\n Array.groupBy(sorted, (tableModule) => tableModule.tableName),\n )\n .filter(([, group]) => group.length > 1)\n .map(([tableName, group]) => ({\n tableName,\n tablePaths: Array.map(group, (tableModule) => tableModule.relativePath),\n }));\n\n if (collisions.length > 0) {\n return yield* new DuplicateTableNameError({ collisions });\n }\n\n return sorted;\n});\n\n/**\n * Bundle every discovered table module and verify that its default export is\n * an {@link Table.UnnamedTable} (the result of `Table.make(...)` before a\n * name has been bound). Fails with {@link InvalidTableDefaultExportError} if\n * any module's default export is missing or has the wrong shape.\n *\n * Must run *after* `_generated/id.ts` has been emitted, because user-authored\n * table modules typically `import { Id } from \"../_generated/id\"` for\n * cross-table references.\n */\nexport const validate = (tableModules: ReadonlyArray<TableModule>) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n yield* Effect.forEach(\n tableModules,\n ({ relativePath }) =>\n Effect.gen(function* () {\n const absolutePath = path.resolve(confectDirectory, relativePath);\n const { module } = yield* Bundler.bundle(absolutePath).pipe(\n Effect.mapError((error) => fromBundlerError(relativePath, error)),\n );\n\n if (!Table.isUnnamedTable(module.default)) {\n return yield* new InvalidTableDefaultExportError({\n tablePath: relativePath,\n });\n }\n }),\n { concurrency: \"unbounded\" },\n );\n });\n"],"mappings":";;;;;;;;;;AAaA,MAAa,iBAAiB;AAe9B,MAAM,6BAA6B,iBACjC,OAAO,IAAI,aAAa;CAEtB,MAAM,EAAE,UADK,OAAO,KAAK,MACH,MAAM,aAAa;AACzC,QAAO;EACP;AAEJ,MAAM,iBAAiB,OAAO,IAAI,aAAa;CAC7C,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,mBAAmB,OAAO,iBAAiB;CACjD,MAAM,kBAAkB,KAAK,KAAK,kBAAkB,eAAe;AAEnE,KAAI,EAAE,OAAO,GAAG,OAAO,gBAAgB,EACrC,QAAO,EAAE;AAOX,QAAO,KAJU,OAAO,GAAG,cAAc,iBAAiB,EACxD,WAAW,MACZ,CAAC,EAIA,MAAM,QAAQ,MAAM,EAAE,SAAS,MAAM,IAAI,CAAC,EAAE,SAAS,WAAW,CAAC,EACjE,MAAM,KAAK,MAAM,KAAK,KAAK,gBAAgB,EAAE,CAAC,CAC/C;EACD;AAEF,MAAM,cAAc,MAAM,SACxB,MAAM,SACL,gBAA6B,YAAY,UAC3C;;;;;;;;;;;;;;;;;;;;;AAsBD,MAAa,WAAW,OAAO,IAAI,aAAa;CAC9C,MAAM,gBAAgB,OAAO;CAoB7B,MAAM,SAAS,KAlBM,OAAO,OAAO,QACjC,gBACC,iBACC,OAAO,IAAI,aAAa;EACtB,MAAM,YAAY,OAAO,0BAA0B,aAAa;AAChE,SAAO,OAAO,IAAI;GAChB,WAAW,WAAW,+BAA+B,UAAU;GAC/D,QAAQ,MACN,IAAI,0BAA0B;IAC5B,WAAW;IACX,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACnD,CAAC;GACL,CAAC;AACF,SAAO;GAAE;GAAc;GAAW;GAClC,EACJ,EAAE,aAAa,aAAa,CAC7B,EAEiC,MAAM,OAAO,YAAY,CAAC;CAE5D,MAAM,aAAa,OAAO,QACxB,MAAM,QAAQ,SAAS,gBAAgB,YAAY,UAAU,CAC9D,CACE,QAAQ,GAAG,WAAW,MAAM,SAAS,EAAE,CACvC,KAAK,CAAC,WAAW,YAAY;EAC5B;EACA,YAAY,MAAM,IAAI,QAAQ,gBAAgB,YAAY,aAAa;EACxE,EAAE;AAEL,KAAI,WAAW,SAAS,EACtB,QAAO,OAAO,IAAI,wBAAwB,EAAE,YAAY,CAAC;AAG3D,QAAO;EACP;;;;;;;;;;;AAYF,MAAa,YAAY,iBACvB,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,mBAAmB,OAAO,iBAAiB;AAEjD,QAAO,OAAO,QACZ,eACC,EAAE,mBACD,OAAO,IAAI,aAAa;EACtB,MAAM,eAAe,KAAK,QAAQ,kBAAkB,aAAa;EACjE,MAAM,EAAE,WAAW,OAAOA,OAAe,aAAa,CAAC,KACrD,OAAO,UAAU,UAAU,iBAAiB,cAAc,MAAM,CAAC,CAClE;AAED,MAAI,CAAC,MAAM,eAAe,OAAO,QAAQ,CACvC,QAAO,OAAO,IAAI,+BAA+B,EAC/C,WAAW,cACZ,CAAC;GAEJ,EACJ,EAAE,aAAa,aAAa,CAC7B;EACD"}
@@ -1,35 +1,40 @@
1
- import { logFileAdded, logFileModified, logFileRemoved, logPending, logSuccess } from "../log.mjs";
2
- import { fromBundlerError } from "../BuildError.mjs";
3
- import { MissingImplFileError, MissingSchemaFileError, MissingSpecFileError, ParentChildNameCollisionError, SchemaInvalidDefaultExportError, tapAndLog } from "../CodegenError.mjs";
1
+ import { logFileAdded, logFileModified, logFileRemoved, logPending, logSuccess, logWarn } from "../log.mjs";
2
+ import { bundle } from "../Bundler.mjs";
3
+ import { LegacySchemaFileError, MissingImplFileError, MissingSpecFileError, ParentChildNameCollisionError, tapAndLog } from "../CodegenError.mjs";
4
4
  import { ConvexDirectory } from "../ConvexDirectory.mjs";
5
5
  import { ConfectDirectory } from "../ConfectDirectory.mjs";
6
6
  import { FunctionPaths, make } from "../FunctionPaths.mjs";
7
- import { bundle } from "../Bundler.mjs";
8
7
  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";
8
+ import { assembledSpec, convexSchema, id, refs, registeredFunctionsForGroup, runtimeSchema, schema, services, tableWrapper } from "../templates.mjs";
9
+ import { WriteTracker, generateAuthConfig, generateCrons, generateFunctions, generateHttp, removePathIfExists, toModuleImportPath, touchConvexSchema, writeFileStringAndLog } from "../utils.mjs";
11
10
  import { discoverLeafImplFiles, discoverLeafSpecFiles, implPathForSpec, registeredFunctionsRelativePath, specPathForImpl, toLeafModule, toNodeRegistryLeaf, validateImpl, validateSpec } from "../LeafModule.mjs";
11
+ import { TABLES_DIRNAME, discover, validate } from "../TableModule.mjs";
12
12
  import { Array, Effect, Either, HashSet, Match, Option, Ref } from "effect";
13
13
  import { Command } from "@effect/cli";
14
14
  import { Spec } from "@confect/core";
15
- import * as DatabaseSchema from "@confect/server/DatabaseSchema";
16
15
  import { FileSystem, Path } from "@effect/platform";
17
16
 
18
17
  //#region src/confect/codegen.ts
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
- ];
18
+ const GENERATED_DIRNAME = "_generated";
19
+ const GENERATED_SPEC_PATH = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "spec.ts"));
20
+ const GENERATED_NODE_SPEC_PATH = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "nodeSpec.ts"));
21
+ const GENERATED_SCHEMA_PATH = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "schema.ts"));
22
+ const GENERATED_CONVEX_SCHEMA_PATH = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "convexSchema.ts"));
23
+ const GENERATED_ID_PATH = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "id.ts"));
24
+ const GENERATED_TABLES_DIRNAME = Effect.andThen(Path.Path, (path) => path.join(GENERATED_DIRNAME, "tables"));
25
+ const LEGACY_PATHS = Effect.gen(function* () {
26
+ const path = yield* Path.Path;
27
+ return [
28
+ "spec.ts",
29
+ "nodeSpec.ts",
30
+ "impl.ts",
31
+ "nodeImpl.ts",
32
+ path.join(GENERATED_DIRNAME, "registeredFunctions.ts"),
33
+ path.join(GENERATED_DIRNAME, "nodeRegisteredFunctions.ts"),
34
+ path.join(GENERATED_DIRNAME, "impl.ts"),
35
+ path.join(GENERATED_DIRNAME, "nodeImpl.ts")
36
+ ];
37
+ });
33
38
  const codegen = Command.make("codegen", {}, () => Effect.gen(function* () {
34
39
  yield* logPending("Performing initial sync…");
35
40
  yield* codegenHandler.pipe(Effect.asVoid, Effect.tap(() => logSuccess("Generated files are up-to-date")), tapAndLog);
@@ -43,23 +48,31 @@ const codegenHandler = Effect.gen(function* () {
43
48
  });
44
49
  const runCodegen = Effect.gen(function* () {
45
50
  yield* generateConfectGeneratedDirectory;
46
- yield* validateSchema;
51
+ yield* rejectLegacySchemaFile;
52
+ const tableModules = yield* discover;
53
+ yield* warnIfNoTables(tableModules);
54
+ yield* generateIdConstructor(tableModules);
55
+ yield* validate(tableModules);
56
+ yield* generateTableWrappers(tableModules);
57
+ yield* removeObsoleteTableWrappers(tableModules);
58
+ yield* generateRuntimeSchema(tableModules);
47
59
  const { leaves, groupSpecsByRelativePath } = yield* loadAndValidateLeafModules;
48
60
  yield* removeLegacyFiles;
49
61
  yield* validateNoParentChildNameCollisions(leaves, groupSpecsByRelativePath);
50
62
  yield* generateAssembledSpecs(leaves);
51
- yield* validateImplModules(leaves);
52
- yield* generateGroupRegisteredFunctions(leaves);
53
- yield* removeObsoleteRegisteredFunctions(leaves);
54
63
  yield* Effect.all([
55
- generateApi,
64
+ removeGeneratedApi,
56
65
  generateRefs,
57
- generateNodeApi,
58
- generateServices
66
+ removeGeneratedNodeApi,
67
+ generateServices,
68
+ generateConvexSchema(tableModules)
59
69
  ], { concurrency: "unbounded" });
70
+ yield* validateImplModules(leaves);
71
+ yield* generateGroupRegisteredFunctions(leaves);
72
+ yield* removeObsoleteRegisteredFunctions(leaves);
60
73
  const [functionPaths] = yield* Effect.all([
61
74
  generateFunctionModules,
62
- generateSchema,
75
+ generateConvexSchemaReexport,
63
76
  logGenerated(generateHttp),
64
77
  logGenerated(generateCrons),
65
78
  logGenerated(generateAuthConfig)
@@ -184,7 +197,8 @@ const removeLegacyFiles = Effect.gen(function* () {
184
197
  const fs = yield* FileSystem.FileSystem;
185
198
  const path = yield* Path.Path;
186
199
  const confectDirectory = yield* ConfectDirectory.get;
187
- yield* Effect.forEach(LEGACY_PATHS, (relativePath) => Effect.gen(function* () {
200
+ const legacyPaths = yield* LEGACY_PATHS;
201
+ yield* Effect.forEach(legacyPaths, (relativePath) => Effect.gen(function* () {
188
202
  const absolutePath = path.join(confectDirectory, relativePath);
189
203
  if (yield* fs.exists(absolutePath)) {
190
204
  yield* removePathIfExists(absolutePath);
@@ -195,6 +209,8 @@ const removeLegacyFiles = Effect.gen(function* () {
195
209
  const generateAssembledSpecs = (leaves) => Effect.gen(function* () {
196
210
  const path = yield* Path.Path;
197
211
  const confectDirectory = yield* ConfectDirectory.get;
212
+ const generatedSpecPath = yield* GENERATED_SPEC_PATH;
213
+ const generatedNodeSpecPath = yield* GENERATED_NODE_SPEC_PATH;
198
214
  const { convex, node } = partitionByRuntime(leaves);
199
215
  if (convex.length > 0) {
200
216
  const nodes = assemblyNodesFromLeaves(convex);
@@ -202,7 +218,7 @@ const generateAssembledSpecs = (leaves) => Effect.gen(function* () {
202
218
  nodes,
203
219
  runtime: "Convex"
204
220
  });
205
- yield* writeFileStringAndLog(path.join(confectDirectory, GENERATED_SPEC_PATH), specContents);
221
+ yield* writeFileStringAndLog(path.join(confectDirectory, generatedSpecPath), specContents);
206
222
  }
207
223
  if (node.length > 0) {
208
224
  const nodes = assemblyNodesFromLeaves(Array.map(node, toNodeRegistryLeaf));
@@ -210,7 +226,7 @@ const generateAssembledSpecs = (leaves) => Effect.gen(function* () {
210
226
  nodes,
211
227
  runtime: "Node"
212
228
  });
213
- yield* writeFileStringAndLog(path.join(confectDirectory, GENERATED_NODE_SPEC_PATH), nodeSpecContents);
229
+ yield* writeFileStringAndLog(path.join(confectDirectory, generatedNodeSpecPath), nodeSpecContents);
214
230
  }
215
231
  });
216
232
  const validateImplModules = (leaves) => Effect.forEach(leaves, validateImpl);
@@ -224,12 +240,12 @@ const generateGroupRegisteredFunctions = (leaves) => Effect.gen(function* () {
224
240
  const fs = yield* FileSystem.FileSystem;
225
241
  if (!(yield* fs.exists(registryDir))) yield* fs.makeDirectory(registryDir, { recursive: true });
226
242
  const implRelativePath = yield* implPathForSpec(leaf.relativePath);
227
- const apiFileName = leaf.runtime === "Node" ? "nodeApi.ts" : "api.ts";
228
- const apiImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, "_generated", apiFileName)));
243
+ const schemaImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, "_generated", "schema.ts")));
244
+ const specImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, leaf.relativePath)));
229
245
  const implImportPath = yield* toModuleImportPath(path.relative(path.dirname(registryPath), path.join(confectDirectory, implRelativePath)));
230
246
  yield* writeFileStringAndLog(registryPath, yield* registeredFunctionsForGroup({
231
- apiImportPath,
232
- groupPathDot: leaf.registryGroupPathDot,
247
+ schemaImportPath,
248
+ specImportPath,
233
249
  implImportPath,
234
250
  layerExportName: leaf.exportName,
235
251
  useNode: leaf.runtime === "Node"
@@ -260,12 +276,14 @@ const removeObsoleteRegisteredFunctions = (leaves) => Effect.gen(function* () {
260
276
  const getGeneratedSpecPath = Effect.gen(function* () {
261
277
  const path = yield* Path.Path;
262
278
  const confectDirectory = yield* ConfectDirectory.get;
263
- return path.join(confectDirectory, GENERATED_SPEC_PATH);
279
+ const generatedSpecPath = yield* GENERATED_SPEC_PATH;
280
+ return path.join(confectDirectory, generatedSpecPath);
264
281
  });
265
282
  const getGeneratedNodeSpecPath = Effect.gen(function* () {
266
283
  const path = yield* Path.Path;
267
284
  const confectDirectory = yield* ConfectDirectory.get;
268
- return path.join(confectDirectory, GENERATED_NODE_SPEC_PATH);
285
+ const generatedNodeSpecPath = yield* GENERATED_NODE_SPEC_PATH;
286
+ return path.join(confectDirectory, generatedNodeSpecPath);
269
287
  });
270
288
  const loadGeneratedSpec = Effect.gen(function* () {
271
289
  const specPath = yield* getGeneratedSpecPath;
@@ -301,39 +319,22 @@ const loadPreviousFunctionPaths = Effect.gen(function* () {
301
319
  })
302
320
  });
303
321
  });
304
- const generateApi = Effect.gen(function* () {
305
- const path = yield* Path.Path;
306
- const confectDirectory = yield* ConfectDirectory.get;
307
- const apiPath = path.join(confectDirectory, "_generated", "api.ts");
308
- const apiDir = path.dirname(apiPath);
309
- const schemaImportPath = yield* toModuleImportPath(path.relative(apiDir, path.join(confectDirectory, "schema.ts")));
310
- const specImportPath = yield* toModuleImportPath(path.relative(apiDir, path.join(confectDirectory, GENERATED_SPEC_PATH)));
311
- yield* writeFileStringAndLog(apiPath, yield* api({
312
- schemaImportPath,
313
- specImportPath
314
- }));
315
- });
316
- const generateNodeApi = Effect.gen(function* () {
322
+ /**
323
+ * Remove a now-obsolete `_generated/<name>.ts` if present (and log it), for
324
+ * projects upgrading from a version that still emitted it.
325
+ */
326
+ const removeObsoleteGeneratedFile = (fileName) => Effect.gen(function* () {
317
327
  const fs = yield* FileSystem.FileSystem;
318
328
  const path = yield* Path.Path;
319
329
  const confectDirectory = yield* ConfectDirectory.get;
320
- const nodeSpecPath = yield* getGeneratedNodeSpecPath;
321
- const nodeApiPath = path.join(confectDirectory, "_generated", "nodeApi.ts");
322
- if (!(yield* fs.exists(nodeSpecPath))) {
323
- if (yield* fs.exists(nodeApiPath)) {
324
- yield* removePathIfExists(nodeApiPath);
325
- yield* logFileRemoved(nodeApiPath);
326
- }
327
- return;
330
+ const filePath = path.join(confectDirectory, "_generated", fileName);
331
+ if (yield* fs.exists(filePath)) {
332
+ yield* removePathIfExists(filePath);
333
+ yield* logFileRemoved(filePath);
328
334
  }
329
- const nodeApiDir = path.dirname(nodeApiPath);
330
- const schemaImportPath = yield* toModuleImportPath(path.relative(nodeApiDir, path.join(confectDirectory, "schema.ts")));
331
- const nodeSpecImportPath = yield* toModuleImportPath(path.relative(nodeApiDir, nodeSpecPath));
332
- yield* writeFileStringAndLog(nodeApiPath, yield* nodeApi({
333
- schemaImportPath,
334
- nodeSpecImportPath
335
- }));
336
335
  });
336
+ const removeGeneratedApi = removeObsoleteGeneratedFile("api.ts");
337
+ const removeGeneratedNodeApi = removeObsoleteGeneratedFile("nodeApi.ts");
337
338
  const generateFunctionModules = Effect.gen(function* () {
338
339
  const spec = yield* loadGeneratedSpec;
339
340
  const nodeSpecOption = yield* loadGeneratedNodeSpec;
@@ -342,32 +343,127 @@ const generateFunctionModules = Effect.gen(function* () {
342
343
  onSome: (nodeSpec) => Spec.merge(spec, nodeSpec)
343
344
  }));
344
345
  });
345
- const validateSchema = Effect.gen(function* () {
346
+ /**
347
+ * The user-authored `confect/schema.ts` is no longer supported: codegen now
348
+ * owns both `_generated/schema.ts` (runtime) and `_generated/convexSchema.ts`
349
+ * (deploy), derived from a single scan of `confect/tables/*.ts`. Detect a
350
+ * stray file and fail with a clear migration message — leaving it in place
351
+ * would silently shadow the codegen-owned `_generated/schema.ts` /
352
+ * `_generated/convexSchema.ts`.
353
+ */
354
+ const rejectLegacySchemaFile = Effect.gen(function* () {
346
355
  const fs = yield* FileSystem.FileSystem;
347
356
  const path = yield* Path.Path;
348
357
  const confectDirectory = yield* ConfectDirectory.get;
349
- const confectSchemaPath = path.join(confectDirectory, "schema.ts");
350
- if (!(yield* fs.exists(confectSchemaPath))) return yield* new MissingSchemaFileError({ schemaPath: "schema.ts" });
351
- yield* bundle(confectSchemaPath).pipe(Effect.mapError((error) => fromBundlerError("schema.ts", error)), Effect.andThen(({ module: schemaModule }) => {
352
- const defaultExport = schemaModule.default;
353
- return DatabaseSchema.isDatabaseSchema(defaultExport) ? Effect.succeed(defaultExport) : Effect.fail(new SchemaInvalidDefaultExportError({ schemaPath: "schema.ts" }));
358
+ const legacyPath = path.join(confectDirectory, "schema.ts");
359
+ if (yield* fs.exists(legacyPath)) return yield* new LegacySchemaFileError({ schemaPath: "schema.ts" });
360
+ });
361
+ /**
362
+ * Surface a yellow `⚠` warning when codegen sees no tables — either the
363
+ * `confect/tables/` directory is missing or it contains no `.ts` files.
364
+ * Generation still succeeds (emitting an empty `DatabaseSchema` and
365
+ * `defineSchema({})`), since action-only / table-free Confect backends
366
+ * are legal — but the warning catches the much more common case of a
367
+ * typoed directory or files placed under the wrong root.
368
+ */
369
+ const warnIfNoTables = (tableModules) => tableModules.length === 0 ? logWarn(`No tables discovered in \`confect/${TABLES_DIRNAME}/\`. Generating an empty schema; add a \`Table.make(...)\` module under that directory unless this backend is intentionally tables-free.`) : Effect.void;
370
+ const tableModuleBindings = (tableModules, generatedFilePath) => Effect.gen(function* () {
371
+ const path = yield* Path.Path;
372
+ const confectDirectory = yield* ConfectDirectory.get;
373
+ const generatedDir = path.dirname(generatedFilePath);
374
+ const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
375
+ return yield* Effect.forEach(tableModules, (tm) => Effect.gen(function* () {
376
+ const wrapperAbsolutePath = path.join(confectDirectory, generatedTablesDirname, `${tm.tableName}.ts`);
377
+ return {
378
+ importPath: yield* toModuleImportPath(path.relative(generatedDir, wrapperAbsolutePath)),
379
+ tableName: tm.tableName
380
+ };
354
381
  }));
355
382
  });
356
- const generateSchema = Effect.gen(function* () {
383
+ const generateIdConstructor = (tableModules) => Effect.gen(function* () {
384
+ const path = yield* Path.Path;
385
+ const confectDirectory = yield* ConfectDirectory.get;
386
+ const generatedIdPath = yield* GENERATED_ID_PATH;
387
+ const idPath = path.join(confectDirectory, generatedIdPath);
388
+ const tableNames = tableModules.map((tm) => tm.tableName);
389
+ yield* writeFileStringAndLog(idPath, yield* id({ tableNames }));
390
+ });
391
+ const generateTableWrappers = (tableModules) => Effect.gen(function* () {
392
+ const fs = yield* FileSystem.FileSystem;
393
+ const path = yield* Path.Path;
394
+ const confectDirectory = yield* ConfectDirectory.get;
395
+ const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
396
+ const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
397
+ if (!(yield* fs.exists(wrappersDir))) yield* fs.makeDirectory(wrappersDir, { recursive: true });
398
+ yield* Effect.forEach(tableModules, (tm) => Effect.gen(function* () {
399
+ const wrapperPath = path.join(confectDirectory, generatedTablesDirname, `${tm.tableName}.ts`);
400
+ const unnamedAbsolutePath = path.join(confectDirectory, tm.relativePath);
401
+ const unnamedImportPath = yield* toModuleImportPath(path.relative(path.dirname(wrapperPath), unnamedAbsolutePath));
402
+ yield* writeFileStringAndLog(wrapperPath, yield* tableWrapper({
403
+ tableName: tm.tableName,
404
+ unnamedImportPath
405
+ }));
406
+ }), { concurrency: "unbounded" });
407
+ });
408
+ /**
409
+ * Remove any stale `_generated/tables/*.ts` wrapper whose source table
410
+ * has been deleted or renamed. Mirrors `removeObsoleteRegisteredFunctions`
411
+ * for the wrapper directory.
412
+ */
413
+ const removeObsoleteTableWrappers = (tableModules) => Effect.gen(function* () {
414
+ const fs = yield* FileSystem.FileSystem;
415
+ const path = yield* Path.Path;
416
+ const confectDirectory = yield* ConfectDirectory.get;
417
+ const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
418
+ const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
419
+ if (!(yield* fs.exists(wrappersDir))) return;
420
+ const expected = new Set(tableModules.map((tm) => `${tm.tableName}.ts`));
421
+ const existing = yield* fs.readDirectory(wrappersDir, { recursive: true });
422
+ yield* Effect.forEach(existing, (entry) => {
423
+ if (path.extname(entry) !== ".ts") return Effect.void;
424
+ if (!expected.has(entry)) return Effect.gen(function* () {
425
+ const absolutePath = path.join(wrappersDir, entry);
426
+ if (yield* fs.exists(absolutePath)) {
427
+ yield* removePathIfExists(absolutePath);
428
+ yield* logFileRemoved(absolutePath);
429
+ }
430
+ });
431
+ return Effect.void;
432
+ });
433
+ });
434
+ const generateRuntimeSchema = (tableModules) => Effect.gen(function* () {
435
+ const path = yield* Path.Path;
436
+ const confectDirectory = yield* ConfectDirectory.get;
437
+ const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
438
+ const schemaPath = path.join(confectDirectory, generatedSchemaPath);
439
+ const bindings = yield* tableModuleBindings(tableModules, schemaPath);
440
+ yield* writeFileStringAndLog(schemaPath, yield* runtimeSchema({ tableModules: bindings }));
441
+ });
442
+ const generateConvexSchema = (tableModules) => Effect.gen(function* () {
443
+ const path = yield* Path.Path;
444
+ const confectDirectory = yield* ConfectDirectory.get;
445
+ const generatedConvexSchemaPath = yield* GENERATED_CONVEX_SCHEMA_PATH;
446
+ const convexSchemaPath = path.join(confectDirectory, generatedConvexSchemaPath);
447
+ const bindings = yield* tableModuleBindings(tableModules, convexSchemaPath);
448
+ yield* writeFileStringAndLog(convexSchemaPath, yield* convexSchema({ tableModules: bindings }));
449
+ });
450
+ const generateConvexSchemaReexport = Effect.gen(function* () {
357
451
  const path = yield* Path.Path;
358
452
  const confectDirectory = yield* ConfectDirectory.get;
359
453
  const convexDirectory = yield* ConvexDirectory.get;
360
- const confectSchemaPath = path.join(confectDirectory, "schema.ts");
454
+ const generatedConvexSchemaRelativePath = yield* GENERATED_CONVEX_SCHEMA_PATH;
361
455
  const convexSchemaPath = path.join(convexDirectory, "schema.ts");
362
- const importPathWithoutExt = yield* removePathExtension(path.relative(path.dirname(convexSchemaPath), confectSchemaPath));
363
- yield* writeFileStringAndLog(convexSchemaPath, yield* schema({ schemaImportPath: importPathWithoutExt }));
456
+ const generatedConvexSchemaPath = path.join(confectDirectory, generatedConvexSchemaRelativePath);
457
+ const convexSchemaImportPath = yield* toModuleImportPath(path.relative(path.dirname(convexSchemaPath), generatedConvexSchemaPath));
458
+ yield* writeFileStringAndLog(convexSchemaPath, yield* schema({ convexSchemaImportPath }));
364
459
  });
365
460
  const generateServices = Effect.gen(function* () {
366
461
  const path = yield* Path.Path;
367
462
  const confectDirectory = yield* ConfectDirectory.get;
368
463
  const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
369
464
  const servicesPath = path.join(confectGeneratedDirectory, "services.ts");
370
- const schemaImportPath = path.relative(path.dirname(servicesPath), path.join(confectDirectory, "schema"));
465
+ const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
466
+ const schemaImportPath = yield* toModuleImportPath(path.relative(path.dirname(servicesPath), path.join(confectDirectory, generatedSchemaPath)));
371
467
  yield* writeFileStringAndLog(servicesPath, yield* services({ schemaImportPath }));
372
468
  });
373
469
  const generateRefs = Effect.gen(function* () {
@@ -377,7 +473,8 @@ const generateRefs = Effect.gen(function* () {
377
473
  const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
378
474
  const refsPath = path.join(confectGeneratedDirectory, "refs.ts");
379
475
  const refsDir = path.dirname(refsPath);
380
- const specImportPath = yield* toModuleImportPath(path.relative(refsDir, path.join(confectDirectory, GENERATED_SPEC_PATH)));
476
+ const generatedSpecPath = yield* GENERATED_SPEC_PATH;
477
+ const specImportPath = yield* toModuleImportPath(path.relative(refsDir, path.join(confectDirectory, generatedSpecPath)));
381
478
  const nodeSpecPath = yield* getGeneratedNodeSpecPath;
382
479
  const nodeSpecImportPath = (yield* fs.exists(nodeSpecPath)) ? Option.some(yield* toModuleImportPath(path.relative(refsDir, nodeSpecPath))) : Option.none();
383
480
  yield* writeFileStringAndLog(refsPath, yield* refs({