@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.
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.mjs","names":["templates.api","templates.nodeApi","templates.schema","templates.services","templates.registeredFunctions","templates.nodeRegisteredFunctions","templates.refs"],"sources":["../../src/confect/codegen.ts"],"sourcesContent":["import { DatabaseSchema, Spec } from \"@confect/core\";\nimport { Command } from \"@effect/cli\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { Effect, Match, Option } from \"effect\";\nimport {\n logFileAdded,\n logFileModified,\n logFileRemoved,\n logPending,\n logSuccess,\n} from \"../log\";\nimport { ConfectDirectory } from \"../ConfectDirectory\";\nimport { ConvexDirectory } from \"../ConvexDirectory\";\nimport * as templates from \"../templates\";\nimport {\n bundleAndImport,\n generateAuthConfig,\n generateCrons,\n generateFunctions,\n generateHttp,\n removePathExtension,\n writeFileStringAndLog,\n} from \"../utils\";\n\nconst getNodeSpecPath = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n return path.join(confectDirectory, \"nodeSpec.ts\");\n});\n\nconst loadNodeSpec = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const nodeSpecPath = yield* getNodeSpecPath;\n\n if (!(yield* fs.exists(nodeSpecPath))) {\n return Option.none<Spec.AnyWithPropsWithRuntime<\"Node\">>();\n }\n\n const nodeSpecModule = yield* bundleAndImport(nodeSpecPath);\n const nodeSpec = nodeSpecModule.default;\n\n if (!Spec.isNodeSpec(nodeSpec)) {\n return yield* Effect.die(\"nodeSpec.ts does not export a valid Node Spec\");\n }\n\n return Option.some(nodeSpec);\n});\n\nexport const codegen = Command.make(\"codegen\", {}, () =>\n Effect.gen(function* () {\n yield* logPending(\"Performing initial sync…\");\n yield* codegenHandler;\n yield* logSuccess(\"Generated files are up-to-date\");\n }),\n).pipe(\n Command.withDescription(\n \"Generate `confect/_generated` files and the contents of the `convex` directory (except `tsconfig.json`)\",\n ),\n);\n\nexport const codegenHandler = Effect.gen(function* () {\n yield* generateConfectGeneratedDirectory;\n yield* Effect.all(\n [\n generateApi,\n generateRefs,\n generateRegisteredFunctions,\n generateNodeApi,\n generateNodeRegisteredFunctions,\n generateServices,\n ],\n { concurrency: \"unbounded\" },\n );\n const [functionPaths] = yield* Effect.all(\n [\n generateFunctionModules,\n generateSchema,\n logGenerated(generateHttp),\n logGenerated(generateCrons),\n logGenerated(generateAuthConfig),\n ],\n { concurrency: \"unbounded\" },\n );\n return functionPaths;\n});\n\nconst generateConfectGeneratedDirectory = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n if (!(yield* fs.exists(path.join(confectDirectory, \"_generated\")))) {\n yield* fs.makeDirectory(path.join(confectDirectory, \"_generated\"), {\n recursive: true,\n });\n yield* logFileAdded(path.join(confectDirectory, \"_generated\") + \"/\");\n }\n});\n\nconst generateApi = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const apiPath = path.join(confectDirectory, \"_generated\", \"api.ts\");\n const apiDir = path.dirname(apiPath);\n\n const schemaImportPath = yield* removePathExtension(\n path.relative(apiDir, path.join(confectDirectory, \"schema.ts\")),\n );\n\n const specImportPath = yield* removePathExtension(\n path.relative(apiDir, path.join(confectDirectory, \"spec.ts\")),\n );\n\n const apiContents = yield* templates.api({\n schemaImportPath,\n specImportPath,\n });\n\n yield* writeFileStringAndLog(apiPath, apiContents);\n});\n\nexport const generateNodeApi = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const nodeSpecPath = yield* getNodeSpecPath;\n const nodeApiPath = path.join(confectDirectory, \"_generated\", \"nodeApi.ts\");\n\n if (!(yield* fs.exists(nodeSpecPath))) {\n if (yield* fs.exists(nodeApiPath)) {\n yield* fs.remove(nodeApiPath);\n yield* logFileRemoved(nodeApiPath);\n }\n return;\n }\n\n const nodeApiDir = path.dirname(nodeApiPath);\n\n const schemaImportPath = yield* removePathExtension(\n path.relative(nodeApiDir, path.join(confectDirectory, \"schema.ts\")),\n );\n\n const nodeSpecImportPath = yield* removePathExtension(\n path.relative(nodeApiDir, nodeSpecPath),\n );\n\n const nodeApiContents = yield* templates.nodeApi({\n schemaImportPath,\n nodeSpecImportPath,\n });\n\n yield* writeFileStringAndLog(nodeApiPath, nodeApiContents);\n});\n\nconst generateFunctionModules = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const specPath = path.join(confectDirectory, \"spec.ts\");\n\n const specModule = yield* bundleAndImport(specPath);\n const spec = specModule.default;\n\n if (!Spec.isConvexSpec(spec)) {\n return yield* Effect.die(\"spec.ts does not export a valid Convex Spec\");\n }\n\n const nodeImplPath = path.join(confectDirectory, \"nodeImpl.ts\");\n const nodeImplExists = yield* fs.exists(nodeImplPath);\n const nodeSpecOption = yield* loadNodeSpec;\n\n const mergedSpec = Option.match(nodeSpecOption, {\n onNone: () => spec,\n onSome: (nodeSpec) => (nodeImplExists ? Spec.merge(spec, nodeSpec) : spec),\n });\n\n return yield* generateFunctions(mergedSpec);\n});\n\nconst generateSchema = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const confectSchemaPath = path.join(confectDirectory, \"schema.ts\");\n\n yield* bundleAndImport(confectSchemaPath).pipe(\n Effect.andThen((schemaModule) => {\n const defaultExport = schemaModule.default;\n\n return DatabaseSchema.isDatabaseSchema(defaultExport)\n ? Effect.succeed(defaultExport)\n : Effect.die(\"Invalid schema module\");\n }),\n );\n\n const convexSchemaPath = path.join(convexDirectory, \"schema.ts\");\n\n const relativeImportPath = path.relative(\n path.dirname(convexSchemaPath),\n confectSchemaPath,\n );\n const importPathWithoutExt = yield* removePathExtension(relativeImportPath);\n const schemaContents = yield* templates.schema({\n schemaImportPath: importPathWithoutExt,\n });\n\n yield* writeFileStringAndLog(convexSchemaPath, schemaContents);\n});\n\nconst generateServices = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const confectGeneratedDirectory = path.join(confectDirectory, \"_generated\");\n\n const servicesPath = path.join(confectGeneratedDirectory, \"services.ts\");\n const schemaImportPath = path.relative(\n path.dirname(servicesPath),\n path.join(confectDirectory, \"schema\"),\n );\n\n const servicesContentsString = yield* templates.services({\n schemaImportPath,\n });\n\n yield* writeFileStringAndLog(servicesPath, servicesContentsString);\n});\n\nconst generateRegisteredFunctions = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const confectGeneratedDirectory = path.join(confectDirectory, \"_generated\");\n\n const registeredFunctionsPath = path.join(\n confectGeneratedDirectory,\n \"registeredFunctions.ts\",\n );\n const implImportPath = yield* removePathExtension(\n path.relative(\n path.dirname(registeredFunctionsPath),\n path.join(confectDirectory, \"impl.ts\"),\n ),\n );\n\n const registeredFunctionsContents = yield* templates.registeredFunctions({\n implImportPath,\n });\n\n yield* writeFileStringAndLog(\n registeredFunctionsPath,\n registeredFunctionsContents,\n );\n});\n\nexport const generateNodeRegisteredFunctions = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const nodeImplPath = path.join(confectDirectory, \"nodeImpl.ts\");\n const nodeSpecPath = yield* getNodeSpecPath;\n const nodeRegisteredFunctionsPath = path.join(\n confectDirectory,\n \"_generated\",\n \"nodeRegisteredFunctions.ts\",\n );\n\n const nodeImplExists = yield* fs.exists(nodeImplPath);\n const nodeSpecExists = yield* fs.exists(nodeSpecPath);\n\n if (!nodeImplExists || !nodeSpecExists) {\n if (yield* fs.exists(nodeRegisteredFunctionsPath)) {\n yield* fs.remove(nodeRegisteredFunctionsPath);\n yield* logFileRemoved(nodeRegisteredFunctionsPath);\n }\n return;\n }\n\n const nodeImplImportPath = yield* removePathExtension(\n path.relative(path.dirname(nodeRegisteredFunctionsPath), nodeImplPath),\n );\n\n const nodeRegisteredFunctionsContents =\n yield* templates.nodeRegisteredFunctions({\n nodeImplImportPath,\n });\n\n yield* writeFileStringAndLog(\n nodeRegisteredFunctionsPath,\n nodeRegisteredFunctionsContents,\n );\n});\n\nconst generateRefs = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const confectGeneratedDirectory = path.join(confectDirectory, \"_generated\");\n const refsPath = path.join(confectGeneratedDirectory, \"refs.ts\");\n const refsDir = path.dirname(refsPath);\n\n const specImportPath = yield* removePathExtension(\n path.relative(refsDir, path.join(confectDirectory, \"spec.ts\")),\n );\n\n const nodeSpecPath = yield* getNodeSpecPath;\n const nodeSpecExists = yield* fs.exists(nodeSpecPath);\n const nodeSpecImportPath = nodeSpecExists\n ? yield* removePathExtension(path.relative(refsDir, nodeSpecPath))\n : null;\n\n const refsContents = yield* templates.refs({\n specImportPath,\n ...(nodeSpecImportPath === null ? {} : { nodeSpecImportPath }),\n });\n\n yield* writeFileStringAndLog(refsPath, refsContents);\n});\n\nconst logGenerated = (effect: typeof generateHttp) =>\n effect.pipe(\n Effect.tap(\n Option.match({\n onNone: () => Effect.void,\n onSome: ({ change, convexFilePath }) =>\n Match.value(change).pipe(\n Match.when(\"Added\", () => logFileAdded(convexFilePath)),\n Match.when(\"Modified\", () => logFileModified(convexFilePath)),\n Match.when(\"Unchanged\", () => Effect.void),\n Match.exhaustive,\n ),\n }),\n ),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"codegen.mjs","names":["CodegenError.tapAndLog","templates.assembledSpec","templates.registeredFunctionsForGroup","Bundler.bundle","FunctionPaths.make","templates.api","templates.nodeApi","templates.schema","templates.services","templates.refs"],"sources":["../../src/confect/codegen.ts"],"sourcesContent":["import { Spec } from \"@confect/core\";\nimport * as DatabaseSchema from \"@confect/server/DatabaseSchema\";\nimport { Command } from \"@effect/cli\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { Array, Effect, Either, HashSet, Match, Option, Ref } from \"effect\";\nimport { fromBundlerError } from \"../BuildError\";\nimport * as CodegenError from \"../CodegenError\";\nimport {\n MissingImplFileError,\n MissingSchemaFileError,\n MissingSpecFileError,\n SchemaInvalidDefaultExportError,\n} from \"../CodegenError\";\nimport { ConfectDirectory } from \"../ConfectDirectory\";\nimport { ConvexDirectory } from \"../ConvexDirectory\";\nimport * as FunctionPaths from \"../FunctionPaths\";\nimport {\n logFileAdded,\n logFileModified,\n logFileRemoved,\n logPending,\n logSuccess,\n} from \"../log\";\nimport {\n discoverLeafImplFiles,\n discoverLeafSpecFiles,\n implPathForSpec,\n registeredFunctionsRelativePath,\n specPathForImpl,\n toLeafModule,\n toNodeRegistryLeaf,\n validateImpl,\n validateSpec,\n type LeafModule,\n} from \"../LeafModule\";\nimport {\n assemblyNodesFromLeaves,\n partitionByRuntime,\n} from \"../SpecAssemblyNode\";\nimport * as templates from \"../templates\";\nimport * as Bundler from \"../Bundler\";\nimport {\n generateAuthConfig,\n generateCrons,\n generateFunctions,\n generateHttp,\n removePathExtension,\n removePathIfExists,\n toModuleImportPath,\n touchConvexSchema,\n writeFileStringAndLog,\n WriteTracker,\n} from \"../utils\";\n\nconst GENERATED_SPEC_PATH = \"_generated/spec.ts\";\nconst GENERATED_NODE_SPEC_PATH = \"_generated/nodeSpec.ts\";\n\nconst LEGACY_PATHS = [\n \"spec.ts\",\n \"nodeSpec.ts\",\n \"impl.ts\",\n \"nodeImpl.ts\",\n \"notesAndRandom.impl.ts\",\n \"groups.impl.ts\",\n \"_generated/registeredFunctions.ts\",\n \"_generated/nodeRegisteredFunctions.ts\",\n \"_generated/impl.ts\",\n \"_generated/nodeImpl.ts\",\n];\n\nexport const codegen = Command.make(\"codegen\", {}, () =>\n Effect.gen(function* () {\n yield* logPending(\"Performing initial sync…\");\n yield* codegenHandler.pipe(\n Effect.asVoid,\n Effect.tap(() => logSuccess(\"Generated files are up-to-date\")),\n CodegenError.tapAndLog,\n );\n }),\n).pipe(\n Command.withDescription(\n \"Generate `confect/_generated` files and the contents of the `convex` directory (except `convex.config.ts` and `tsconfig.json`)\",\n ),\n);\n\nexport const codegenHandler = Effect.gen(function* () {\n const tracker = yield* Ref.make(false);\n\n const functionPaths = yield* runCodegen.pipe(\n Effect.provideService(WriteTracker, tracker),\n );\n\n const anyWritesHappened = yield* Ref.get(tracker);\n return { functionPaths, anyWritesHappened };\n});\n\nconst runCodegen = Effect.gen(function* () {\n yield* generateConfectGeneratedDirectory;\n // Validate schema first so its missing-file / invalid-default-export\n // diagnostics surface ahead of impl bundling, which transitively depends\n // on schema via `_generated/api.ts` and would otherwise blow up with a\n // less actionable bundler error.\n yield* validateSchema;\n const leaves = yield* loadAndValidateLeafModules;\n yield* removeLegacyFiles;\n yield* generateAssembledSpecs(leaves);\n yield* validateImplModules(leaves);\n yield* generateGroupRegisteredFunctions(leaves);\n yield* removeObsoleteRegisteredFunctions(leaves);\n yield* Effect.all(\n [generateApi, generateRefs, generateNodeApi, generateServices],\n { concurrency: \"unbounded\" },\n );\n const [functionPaths] = yield* Effect.all(\n [\n generateFunctionModules,\n generateSchema,\n logGenerated(generateHttp),\n logGenerated(generateCrons),\n logGenerated(generateAuthConfig),\n ],\n { concurrency: \"unbounded\" },\n );\n yield* touchConvexSchema;\n return functionPaths;\n});\n\nconst generateConfectGeneratedDirectory = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n if (!(yield* fs.exists(path.join(confectDirectory, \"_generated\")))) {\n yield* fs.makeDirectory(path.join(confectDirectory, \"_generated\"), {\n recursive: true,\n });\n yield* logFileAdded(path.join(confectDirectory, \"_generated\") + \"/\");\n }\n});\n\nconst loadAndValidateLeafModules = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const specFiles = yield* discoverLeafSpecFiles;\n\n const leaves = yield* Effect.forEach(specFiles, (specRelativePath) =>\n Effect.gen(function* () {\n const leaf = yield* toLeafModule(specRelativePath);\n yield* validateSpec(leaf);\n\n const implRelativePath = yield* implPathForSpec(specRelativePath);\n const implAbsolutePath = path.join(confectDirectory, implRelativePath);\n if (!(yield* fs.exists(implAbsolutePath))) {\n return yield* new MissingImplFileError({\n specPath: specRelativePath,\n expectedImplPath: implRelativePath,\n });\n }\n\n return leaf;\n }),\n );\n\n yield* validateOrphanImpls(specFiles);\n\n return leaves;\n});\n\nconst validateOrphanImpls = (specFiles: ReadonlyArray<string>) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const implFiles = yield* discoverLeafImplFiles;\n const specPaths = new Set(specFiles);\n\n yield* Effect.forEach(implFiles, (implRelativePath) =>\n Effect.gen(function* () {\n const specRelativePath = yield* specPathForImpl(implRelativePath);\n if (specPaths.has(specRelativePath)) {\n return;\n }\n\n const specAbsolutePath = path.join(confectDirectory, specRelativePath);\n if (!(yield* fs.exists(specAbsolutePath))) {\n return yield* new MissingSpecFileError({\n implPath: implRelativePath,\n expectedSpecPath: specRelativePath,\n });\n }\n }),\n );\n });\n\nconst removeLegacyFiles = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n yield* Effect.forEach(LEGACY_PATHS, (relativePath) =>\n Effect.gen(function* () {\n const absolutePath = path.join(confectDirectory, relativePath);\n if (yield* fs.exists(absolutePath)) {\n yield* removePathIfExists(absolutePath);\n yield* logFileRemoved(absolutePath);\n }\n }),\n );\n});\n\nconst generateAssembledSpecs = (leaves: ReadonlyArray<LeafModule>) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const { convex, node } = partitionByRuntime(leaves);\n\n if (convex.length > 0) {\n const nodes = assemblyNodesFromLeaves(convex);\n const specContents = yield* templates.assembledSpec({\n nodes,\n runtime: \"Convex\",\n });\n yield* writeFileStringAndLog(\n path.join(confectDirectory, GENERATED_SPEC_PATH),\n specContents,\n );\n }\n\n if (node.length > 0) {\n const nodes = assemblyNodesFromLeaves(\n Array.map(node, toNodeRegistryLeaf),\n );\n const nodeSpecContents = yield* templates.assembledSpec({\n nodes,\n runtime: \"Node\",\n });\n yield* writeFileStringAndLog(\n path.join(confectDirectory, GENERATED_NODE_SPEC_PATH),\n nodeSpecContents,\n );\n }\n });\n\nconst validateImplModules = (leaves: ReadonlyArray<LeafModule>) =>\n Effect.forEach(leaves, validateImpl);\n\nconst generateGroupRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n yield* Effect.forEach(leaves, (leaf) =>\n Effect.gen(function* () {\n const registryRelativePath =\n yield* registeredFunctionsRelativePath(leaf);\n const registryPath = path.join(\n confectDirectory,\n \"_generated\",\n registryRelativePath,\n );\n const registryDir = path.dirname(registryPath);\n const fs = yield* FileSystem.FileSystem;\n if (!(yield* fs.exists(registryDir))) {\n yield* fs.makeDirectory(registryDir, { recursive: true });\n }\n\n const implRelativePath = yield* implPathForSpec(leaf.relativePath);\n const apiFileName = leaf.runtime === \"Node\" ? \"nodeApi.ts\" : \"api.ts\";\n const apiImportPath = yield* toModuleImportPath(\n path.relative(\n path.dirname(registryPath),\n path.join(confectDirectory, \"_generated\", apiFileName),\n ),\n );\n const implImportPath = yield* toModuleImportPath(\n path.relative(\n path.dirname(registryPath),\n path.join(confectDirectory, implRelativePath),\n ),\n );\n\n const contents = yield* templates.registeredFunctionsForGroup({\n apiImportPath,\n groupPathDot: leaf.registryGroupPathDot,\n implImportPath,\n layerExportName: leaf.exportName,\n useNode: leaf.runtime === \"Node\",\n });\n\n yield* writeFileStringAndLog(registryPath, contents);\n }),\n );\n });\n\nconst removeObsoleteRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const registryRoot = path.join(\n confectDirectory,\n \"_generated\",\n \"registeredFunctions\",\n );\n\n if (!(yield* fs.exists(registryRoot))) {\n return;\n }\n\n const expected = new Set(\n yield* Effect.forEach(leaves, (leaf) =>\n registeredFunctionsRelativePath(leaf),\n ),\n );\n\n const existing = yield* fs.readDirectory(registryRoot, { recursive: true });\n yield* Effect.forEach(existing, (relativePath) => {\n if (path.extname(relativePath) !== \".ts\") {\n return Effect.void;\n }\n const normalized = path.join(\"registeredFunctions\", relativePath);\n if (!expected.has(normalized)) {\n return Effect.gen(function* () {\n const absolutePath = path.join(registryRoot, relativePath);\n if (yield* fs.exists(absolutePath)) {\n yield* removePathIfExists(absolutePath);\n yield* logFileRemoved(absolutePath);\n }\n });\n }\n return Effect.void;\n });\n });\n\nconst getGeneratedSpecPath = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n return path.join(confectDirectory, GENERATED_SPEC_PATH);\n});\n\nconst getGeneratedNodeSpecPath = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n return path.join(confectDirectory, GENERATED_NODE_SPEC_PATH);\n});\n\nconst loadGeneratedSpec = Effect.gen(function* () {\n const specPath = yield* getGeneratedSpecPath;\n const { module: specModule } = yield* Bundler.bundle(specPath);\n const spec = specModule.default;\n\n if (!Spec.isConvexSpec(spec)) {\n return yield* Effect.dieMessage(\n \"_generated/spec.ts does not export a valid Convex Spec\",\n );\n }\n\n return spec;\n});\n\nconst loadGeneratedNodeSpec = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const nodeSpecPath = yield* getGeneratedNodeSpecPath;\n\n if (!(yield* fs.exists(nodeSpecPath))) {\n return Option.none<Spec.AnyWithPropsWithRuntime<\"Node\">>();\n }\n\n const { module: nodeSpecModule } = yield* Bundler.bundle(nodeSpecPath);\n const nodeSpec = nodeSpecModule.default;\n\n if (!Spec.isNodeSpec(nodeSpec)) {\n return yield* Effect.dieMessage(\n \"_generated/nodeSpec.ts does not export a valid Node Spec\",\n );\n }\n\n return Option.some(nodeSpec);\n});\n\nconst emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());\n\nexport const loadPreviousFunctionPaths = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const specPath = yield* getGeneratedSpecPath;\n\n if (!(yield* fs.exists(specPath))) {\n return emptyFunctionPaths;\n }\n\n const specEither = yield* loadGeneratedSpec.pipe(Effect.either);\n\n return yield* Either.match(specEither, {\n onLeft: () => Effect.succeed(emptyFunctionPaths),\n onRight: (spec) =>\n Effect.gen(function* () {\n const nodeSpecOption = yield* loadGeneratedNodeSpec;\n const mergedSpec = Option.match(nodeSpecOption, {\n onNone: () => spec,\n onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),\n });\n return FunctionPaths.make(mergedSpec);\n }),\n });\n});\n\nconst generateApi = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const apiPath = path.join(confectDirectory, \"_generated\", \"api.ts\");\n const apiDir = path.dirname(apiPath);\n\n const schemaImportPath = yield* toModuleImportPath(\n path.relative(apiDir, path.join(confectDirectory, \"schema.ts\")),\n );\n\n const specImportPath = yield* toModuleImportPath(\n path.relative(apiDir, path.join(confectDirectory, GENERATED_SPEC_PATH)),\n );\n\n const apiContents = yield* templates.api({\n schemaImportPath,\n specImportPath,\n });\n\n yield* writeFileStringAndLog(apiPath, apiContents);\n});\n\nexport const generateNodeApi = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const nodeSpecPath = yield* getGeneratedNodeSpecPath;\n const nodeApiPath = path.join(confectDirectory, \"_generated\", \"nodeApi.ts\");\n\n if (!(yield* fs.exists(nodeSpecPath))) {\n if (yield* fs.exists(nodeApiPath)) {\n yield* removePathIfExists(nodeApiPath);\n yield* logFileRemoved(nodeApiPath);\n }\n return;\n }\n\n const nodeApiDir = path.dirname(nodeApiPath);\n\n const schemaImportPath = yield* toModuleImportPath(\n path.relative(nodeApiDir, path.join(confectDirectory, \"schema.ts\")),\n );\n\n const nodeSpecImportPath = yield* toModuleImportPath(\n path.relative(nodeApiDir, nodeSpecPath),\n );\n\n const nodeApiContents = yield* templates.nodeApi({\n schemaImportPath,\n nodeSpecImportPath,\n });\n\n yield* writeFileStringAndLog(nodeApiPath, nodeApiContents);\n});\n\nconst generateFunctionModules = Effect.gen(function* () {\n const spec = yield* loadGeneratedSpec;\n const nodeSpecOption = yield* loadGeneratedNodeSpec;\n\n const mergedSpec = Option.match(nodeSpecOption, {\n onNone: () => spec,\n onSome: (nodeSpec) => Spec.merge(spec, nodeSpec),\n });\n\n return yield* generateFunctions(mergedSpec);\n});\n\nexport const validateSchema = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const confectSchemaPath = path.join(confectDirectory, \"schema.ts\");\n\n if (!(yield* fs.exists(confectSchemaPath))) {\n return yield* new MissingSchemaFileError({ schemaPath: \"schema.ts\" });\n }\n\n yield* Bundler.bundle(confectSchemaPath).pipe(\n Effect.mapError((error) => fromBundlerError(\"schema.ts\", error)),\n Effect.andThen(({ module: schemaModule }) => {\n const defaultExport = schemaModule.default;\n\n return DatabaseSchema.isDatabaseSchema(defaultExport)\n ? Effect.succeed(defaultExport)\n : Effect.fail(\n new SchemaInvalidDefaultExportError({\n schemaPath: \"schema.ts\",\n }),\n );\n }),\n );\n});\n\nconst generateSchema = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const convexDirectory = yield* ConvexDirectory.get;\n\n const confectSchemaPath = path.join(confectDirectory, \"schema.ts\");\n\n // `validateSchema` runs once at the top of `runCodegen`; no need to\n // bundle the schema again here.\n\n const convexSchemaPath = path.join(convexDirectory, \"schema.ts\");\n\n const relativeImportPath = path.relative(\n path.dirname(convexSchemaPath),\n confectSchemaPath,\n );\n const importPathWithoutExt = yield* removePathExtension(relativeImportPath);\n const schemaContents = yield* templates.schema({\n schemaImportPath: importPathWithoutExt,\n });\n\n yield* writeFileStringAndLog(convexSchemaPath, schemaContents);\n});\n\nconst generateServices = Effect.gen(function* () {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const confectGeneratedDirectory = path.join(confectDirectory, \"_generated\");\n\n const servicesPath = path.join(confectGeneratedDirectory, \"services.ts\");\n const schemaImportPath = path.relative(\n path.dirname(servicesPath),\n path.join(confectDirectory, \"schema\"),\n );\n\n const servicesContentsString = yield* templates.services({\n schemaImportPath,\n });\n\n yield* writeFileStringAndLog(servicesPath, servicesContentsString);\n});\n\nconst generateRefs = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const confectGeneratedDirectory = path.join(confectDirectory, \"_generated\");\n const refsPath = path.join(confectGeneratedDirectory, \"refs.ts\");\n const refsDir = path.dirname(refsPath);\n\n const specImportPath = yield* toModuleImportPath(\n path.relative(refsDir, path.join(confectDirectory, GENERATED_SPEC_PATH)),\n );\n\n const nodeSpecPath = yield* getGeneratedNodeSpecPath;\n const nodeSpecExists = yield* fs.exists(nodeSpecPath);\n const nodeSpecImportPath = nodeSpecExists\n ? Option.some(\n yield* toModuleImportPath(path.relative(refsDir, nodeSpecPath)),\n )\n : Option.none<string>();\n\n const refsContents = yield* templates.refs({\n specImportPath,\n nodeSpecImportPath,\n });\n\n yield* writeFileStringAndLog(refsPath, refsContents);\n});\n\nconst logGenerated = (effect: typeof generateHttp) =>\n effect.pipe(\n Effect.tap(\n Option.match({\n onNone: () => Effect.void,\n onSome: ({ change, convexFilePath }) =>\n Match.value(change).pipe(\n Match.when(\"Added\", () => logFileAdded(convexFilePath)),\n Match.when(\"Modified\", () => logFileModified(convexFilePath)),\n Match.when(\"Unchanged\", () => Effect.void),\n Match.exhaustive,\n ),\n }),\n ),\n );\n"],"mappings":"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"}
@@ -1,26 +1,35 @@
1
- import { modulePath, toString } from "../GroupPath.mjs";
2
1
  import { ProjectRoot } from "../ProjectRoot.mjs";
3
- import { logFailure, logPending, logSuccess } from "../log.mjs";
2
+ import { logFunctionAdded, logFunctionRemoved, logPending, logSuccess } from "../log.mjs";
3
+ import { logCoalescedBuildErrors } from "../BuildError.mjs";
4
+ import { catchAndLog } from "../CodegenError.mjs";
4
5
  import { ConvexDirectory } from "../ConvexDirectory.mjs";
5
6
  import { ConfectDirectory } from "../ConfectDirectory.mjs";
6
- import { diff, make } from "../FunctionPaths.mjs";
7
- import { EXTERNAL_PACKAGES, bundleAndImport, generateAuthConfig, generateCrons, generateHttp, removeGroups, writeGroups } from "../utils.mjs";
8
- import { codegenHandler, generateNodeApi, generateNodeRegisteredFunctions } from "./codegen.mjs";
9
- import { Array, Console, Deferred, Duration, Effect, Equal, HashSet, Match, Option, Queue, Ref, Schema, Stream, String, pipe } from "effect";
7
+ import { FunctionPaths, diff } from "../FunctionPaths.mjs";
8
+ import { EXTERNAL_PACKAGES, absoluteExternalsPlugin } from "../Bundler.mjs";
9
+ import { generateAuthConfig, generateCrons, generateHttp } from "../utils.mjs";
10
+ import { discoverLeafImplFiles, isLeafImplPath, isLeafSpecPath } from "../LeafModule.mjs";
11
+ import { codegenHandler, loadPreviousFunctionPaths } from "./codegen.mjs";
12
+ import { Array, Chunk, Clock, Console, Duration, Effect, Equal, ExecutionStrategy, Exit, HashSet, Match, Option, Order, Queue, Ref, Scope, Stream, String, pipe } from "effect";
10
13
  import { Command } from "@effect/cli";
11
- import { Spec } from "@confect/core";
12
14
  import { FileSystem, Path } from "@effect/platform";
13
15
  import { Ansi, AnsiDoc } from "@effect/printer-ansi";
14
16
  import * as esbuild from "esbuild";
15
17
 
16
18
  //#region src/confect/dev.ts
19
+ const GENERATED_SPEC_PATH = "_generated/spec.ts";
20
+ const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
21
+ const COALESCE_QUIESCENCE = Duration.millis(300);
22
+ const COALESCE_MAX_WAIT = Duration.seconds(5);
23
+ const ECHO_COOLDOWN = Duration.millis(500);
24
+ const emptyFunctionPaths = FunctionPaths.make(HashSet.empty());
17
25
  const pendingInit = {
18
26
  specDirty: false,
19
- nodeImplDirty: false,
20
27
  httpDirty: false,
21
28
  cronsDirty: false,
22
29
  authDirty: false
23
30
  };
31
+ const isPendingDirty = (p) => p.specDirty || p.httpDirty || p.cronsDirty || p.authDirty;
32
+ const emptyWatcherErrors = /* @__PURE__ */ new Map();
24
33
  const changeChar = (change) => Match.value(change).pipe(Match.when("Added", () => ({
25
34
  char: "+",
26
35
  color: Ansi.green
@@ -37,182 +46,255 @@ const logFileChangeIndented = (change, fullPath) => Effect.gen(function* () {
37
46
  const { char, color } = changeChar(change);
38
47
  yield* Console.log(pipe(AnsiDoc.char(char), AnsiDoc.annotate(color), AnsiDoc.catWithSpace(AnsiDoc.hcat([pipe(AnsiDoc.text(prefix), AnsiDoc.annotate(Ansi.blackBright)), pipe(AnsiDoc.text(suffix), AnsiDoc.annotate(color))])), AnsiDoc.render({ style: "pretty" })));
39
48
  });
40
- const logFunctionAddedIndented = (functionPath) => Console.log(pipe(AnsiDoc.text(" "), AnsiDoc.cat(pipe(AnsiDoc.char("+"), AnsiDoc.annotate(Ansi.green))), AnsiDoc.catWithSpace(AnsiDoc.hcat([pipe(AnsiDoc.text(toString(functionPath.groupPath) + "."), AnsiDoc.annotate(Ansi.blackBright)), pipe(AnsiDoc.text(functionPath.name), AnsiDoc.annotate(Ansi.green))])), AnsiDoc.render({ style: "pretty" })));
41
- const logFunctionRemovedIndented = (functionPath) => Console.log(pipe(AnsiDoc.text(" "), AnsiDoc.cat(pipe(AnsiDoc.char("-"), AnsiDoc.annotate(Ansi.red))), AnsiDoc.catWithSpace(AnsiDoc.hcat([pipe(AnsiDoc.text(toString(functionPath.groupPath) + "."), AnsiDoc.annotate(Ansi.blackBright)), pipe(AnsiDoc.text(functionPath.name), AnsiDoc.annotate(Ansi.red))])), AnsiDoc.render({ style: "pretty" })));
49
+ const logFunctionPathDiff = (previous, current) => Effect.gen(function* () {
50
+ const { functionsAdded, functionsRemoved, groupsRemoved, groupsAdded, groupsChanged } = diff(previous, current);
51
+ const logForGroups = (groupPaths, fnPaths, logFn) => Effect.forEach(groupPaths, (gp) => Effect.forEach(Array.fromIterable(HashSet.filter(fnPaths, (fp) => Equal.equals(fp.groupPath, gp))), logFn));
52
+ yield* logForGroups(groupsRemoved, functionsRemoved, logFunctionRemoved);
53
+ yield* logForGroups(groupsAdded, functionsAdded, logFunctionAdded);
54
+ yield* Effect.forEach(groupsChanged, (gp) => Effect.gen(function* () {
55
+ yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAdded);
56
+ yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemoved);
57
+ }));
58
+ });
42
59
  const dev = Command.make("dev", {}, () => Effect.gen(function* () {
43
60
  yield* logPending("Performing initial sync…");
44
- const initialFunctionPaths = yield* codegenHandler;
61
+ const previousFunctionPaths = yield* loadPreviousFunctionPaths;
62
+ const initialResult = yield* codegenHandler.pipe(Effect.tap(({ functionPaths }) => logFunctionPathDiff(previousFunctionPaths, functionPaths)), Effect.tap(() => logSuccess("Generated files are up-to-date")), catchAndLog);
63
+ const initialFunctionPaths = Option.match(initialResult, {
64
+ onNone: () => emptyFunctionPaths,
65
+ onSome: ({ functionPaths }) => functionPaths
66
+ });
45
67
  const pendingRef = yield* Ref.make(pendingInit);
46
68
  const signal = yield* Queue.sliding(1);
47
- const specWatcherRestartQueue = yield* Queue.sliding(1);
69
+ const restartQueue = yield* Queue.sliding(1);
70
+ const watcherErrorsRef = yield* Ref.make(emptyWatcherErrors);
48
71
  yield* Effect.all([
49
- specFileWatcher(signal, pendingRef, specWatcherRestartQueue),
50
- confectDirectoryWatcher(signal, pendingRef, specWatcherRestartQueue),
51
- syncLoop(signal, pendingRef, initialFunctionPaths)
72
+ Effect.scoped(entryPointsWatcher(signal, pendingRef, restartQueue, watcherErrorsRef)),
73
+ confectStructureWatcher(signal, pendingRef, restartQueue),
74
+ syncLoop(signal, pendingRef, initialFunctionPaths, watcherErrorsRef)
52
75
  ], { concurrency: "unbounded" });
53
76
  })).pipe(Command.withDescription("Start the Confect development server"));
54
- const syncLoop = (signal, pendingRef, initialFunctionPaths) => Effect.gen(function* () {
77
+ const esbuildMessageKey = (m) => `${m.location?.file ?? ""}:${m.location?.line ?? ""}:${m.location?.column ?? ""}:${m.text}`;
78
+ const allMessages = (errors) => pipe(Array.fromIterable(errors.values()), Array.flatten);
79
+ const dedupeWatcherErrors = (errors) => pipe(allMessages(errors), Array.dedupeWith((messageA, messageB) => esbuildMessageKey(messageA) === esbuildMessageKey(messageB)));
80
+ const watcherErrorsSignature = (errors) => pipe(allMessages(errors), Array.map(esbuildMessageKey), Array.dedupe, Array.sort(Order.string), Array.join("\n"));
81
+ /**
82
+ * Log any watcher errors that haven't already been logged at their
83
+ * current signature. Suppresses the per-watcher fanout that happens
84
+ * when one root cause (e.g. a missing import) breaks every entry
85
+ * point's build at the same source location.
86
+ */
87
+ const logChangedWatcherErrors = (watcherErrorsRef, lastLoggedSignatureRef) => Effect.gen(function* () {
88
+ const errors = yield* Ref.get(watcherErrorsRef);
89
+ const signature = watcherErrorsSignature(errors);
90
+ if (signature === (yield* Ref.get(lastLoggedSignatureRef))) return;
91
+ yield* Ref.set(lastLoggedSignatureRef, signature);
92
+ if (errors.size === 0) return;
93
+ yield* logCoalescedBuildErrors(dedupeWatcherErrors(errors));
94
+ });
95
+ /**
96
+ * Block until the signal queue has been quiet for `quiescence`. esbuild
97
+ * watchers' `onEnd` events for a single user edit can be spread across
98
+ * hundreds of milliseconds, so a fixed window misses late arrivals.
99
+ * Bounded by `maxWait` so pathological signal floods can't pin the
100
+ * loop forever.
101
+ */
102
+ const drainUntilQuiescent = (signal, quiescence, maxWait) => Effect.gen(function* () {
103
+ const start = yield* Clock.currentTimeMillis;
104
+ const maxMillis = Duration.toMillis(maxWait);
105
+ yield* Effect.iterate(true, {
106
+ while: (keepGoing) => keepGoing,
107
+ body: () => Effect.gen(function* () {
108
+ yield* Effect.sleep(quiescence);
109
+ const drained = yield* Queue.takeAll(signal);
110
+ if (Chunk.isEmpty(drained)) return false;
111
+ return (yield* Clock.currentTimeMillis) - start < maxMillis;
112
+ })
113
+ });
114
+ });
115
+ const syncLoop = (signal, pendingRef, initialFunctionPaths, watcherErrorsRef) => Effect.gen(function* () {
55
116
  const functionPathsRef = yield* Ref.make(initialFunctionPaths);
56
- const initialSyncDone = yield* Deferred.make();
117
+ const lastLoggedErrorsRef = yield* Ref.make("");
57
118
  return yield* Effect.forever(Effect.gen(function* () {
58
- yield* Effect.logDebug("Running sync loop...");
119
+ yield* Effect.logDebug("Running sync loop");
59
120
  yield* Queue.take(signal);
60
- const isDone = yield* Deferred.isDone(initialSyncDone);
61
- yield* Effect.when(logPending("Dependencies changed, reloading…"), () => isDone);
62
- yield* Deferred.succeed(initialSyncDone, void 0);
121
+ yield* drainUntilQuiescent(signal, COALESCE_QUIESCENCE, COALESCE_MAX_WAIT);
122
+ yield* logChangedWatcherErrors(watcherErrorsRef, lastLoggedErrorsRef);
63
123
  const pending = yield* Ref.getAndSet(pendingRef, pendingInit);
64
- if (pending.specDirty || pending.nodeImplDirty) {
65
- yield* generateNodeApi;
66
- yield* generateNodeRegisteredFunctions;
124
+ if (!isPendingDirty(pending)) return;
125
+ yield* logPending("Dependencies may have changed, reloading…");
126
+ if (pending.specDirty) {
127
+ const current = yield* codegenHandler.pipe(Effect.tap(({ functionPaths: nextFunctionPaths }) => Effect.gen(function* () {
128
+ yield* logFunctionPathDiff(yield* Ref.get(functionPathsRef), nextFunctionPaths);
129
+ yield* Ref.set(functionPathsRef, nextFunctionPaths);
130
+ })), catchAndLog);
131
+ if (Option.isNone(current)) return;
132
+ if (current.value.anyWritesHappened) yield* Effect.sleep(ECHO_COOLDOWN);
133
+ yield* drainUntilQuiescent(signal, COALESCE_QUIESCENCE, COALESCE_MAX_WAIT);
134
+ yield* Ref.set(pendingRef, pendingInit);
67
135
  }
68
- const specResult = yield* Effect.if(pending.specDirty, {
69
- onTrue: () => loadSpec.pipe(Effect.andThen(Effect.fn(function* (spec) {
70
- yield* Effect.logDebug("Spec loaded");
71
- const previous = yield* Ref.get(functionPathsRef);
72
- const path = yield* Path.Path;
73
- const convexDirectory = yield* ConvexDirectory.get;
74
- const current = make(spec);
75
- const { functionsAdded, functionsRemoved, groupsRemoved, groupsAdded, groupsChanged } = diff(previous, current);
76
- yield* removeGroups(groupsRemoved);
77
- yield* Effect.forEach(groupsRemoved, (gp) => Effect.gen(function* () {
78
- const relativeModulePath = yield* modulePath(gp);
79
- yield* logFileChangeIndented("Removed", path.join(convexDirectory, relativeModulePath));
80
- yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemovedIndented);
81
- }));
82
- yield* writeGroups(spec, groupsAdded);
83
- yield* Effect.forEach(groupsAdded, (gp) => Effect.gen(function* () {
84
- const relativeModulePath = yield* modulePath(gp);
85
- yield* logFileChangeIndented("Added", path.join(convexDirectory, relativeModulePath));
86
- yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAddedIndented);
87
- }));
88
- yield* writeGroups(spec, groupsChanged);
89
- yield* Effect.forEach(groupsChanged, (gp) => Effect.gen(function* () {
90
- const relativeModulePath = yield* modulePath(gp);
91
- yield* logFileChangeIndented("Modified", path.join(convexDirectory, relativeModulePath));
92
- yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsAdded, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionAddedIndented);
93
- yield* Effect.forEach(Array.fromIterable(HashSet.filter(functionsRemoved, (fp) => Equal.equals(fp.groupPath, gp))), logFunctionRemovedIndented);
94
- }));
95
- yield* Ref.set(functionPathsRef, current);
96
- return Option.some(void 0);
97
- })), Effect.catchTag("SpecImportFailedError", () => logFailure("Spec import failed").pipe(Effect.as(Option.none()))), Effect.catchTag("SpecFileDoesNotExportSpecError", () => logFailure("Spec file does not default export a Convex spec").pipe(Effect.as(Option.none()))), Effect.catchTag("NodeSpecFileDoesNotExportSpecError", () => logFailure("Node spec file does not default export a Node spec").pipe(Effect.as(Option.none())))),
98
- onFalse: () => Effect.succeed(Option.some(void 0))
99
- });
100
136
  const dirtyOptionalFiles = [
101
137
  ...pending.httpDirty ? [syncOptionalFile(generateHttp, "http.ts")] : [],
102
138
  ...pending.cronsDirty ? [syncOptionalFile(generateCrons, "crons.ts")] : [],
103
139
  ...pending.authDirty ? [syncOptionalFile(generateAuthConfig, "auth.config.ts")] : []
104
140
  ];
105
141
  yield* Array.isNonEmptyReadonlyArray(dirtyOptionalFiles) ? Effect.all(dirtyOptionalFiles, { concurrency: "unbounded" }) : Effect.void;
106
- yield* Option.match(specResult, {
107
- onSome: () => logSuccess("Generated files are up-to-date"),
108
- onNone: () => Effect.void
109
- });
142
+ yield* logSuccess("Generated files are up-to-date");
110
143
  }));
111
144
  });
112
- const loadSpec = Effect.gen(function* () {
145
+ /**
146
+ * Every file whose import graph codegen should react to. Each one becomes
147
+ * its own scoped esbuild watcher; the union of their watches gives us
148
+ * dependency-aware tracking of anything reachable from `confect/`,
149
+ * including files outside `confect/`.
150
+ */
151
+ const discoverEntryPoints = Effect.gen(function* () {
113
152
  const fs = yield* FileSystem.FileSystem;
114
153
  const path = yield* Path.Path;
154
+ const projectRoot = yield* ProjectRoot.get;
115
155
  const confectDirectory = yield* ConfectDirectory.get;
116
- const spec = (yield* bundleAndImport(yield* getSpecPath).pipe(Effect.mapError((error) => new SpecImportFailedError({ error })))).default;
117
- if (!Spec.isConvexSpec(spec)) return yield* new SpecFileDoesNotExportSpecError();
118
- const nodeImplPath = path.join(confectDirectory, "nodeImpl.ts");
119
- const nodeImplExists = yield* fs.exists(nodeImplPath);
120
- const nodeSpecOption = yield* loadNodeSpec;
121
- return Option.match(nodeSpecOption, {
122
- onNone: () => spec,
123
- onSome: (nodeSpec) => nodeImplExists ? Spec.merge(spec, nodeSpec) : spec
156
+ const tryEntry = (relativePath, pendingKey) => Effect.gen(function* () {
157
+ const absolutePath = path.join(confectDirectory, relativePath);
158
+ if (!(yield* fs.exists(absolutePath))) return Option.none();
159
+ return Option.some({
160
+ absolutePath,
161
+ displayPath: path.relative(projectRoot, absolutePath),
162
+ pendingKey
163
+ });
124
164
  });
165
+ const fixedEntryOptions = yield* Effect.all([
166
+ tryEntry(GENERATED_SPEC_PATH, "specDirty"),
167
+ tryEntry(GENERATED_NODE_SPEC_PATH, "specDirty"),
168
+ tryEntry("schema.ts", "specDirty"),
169
+ tryEntry("http.ts", "httpDirty"),
170
+ tryEntry("crons.ts", "cronsDirty"),
171
+ tryEntry("auth.ts", "authDirty")
172
+ ]);
173
+ const implRelativePaths = yield* discoverLeafImplFiles;
174
+ const implEntryOptions = yield* Effect.forEach(implRelativePaths, (relativePath) => tryEntry(relativePath, "specDirty"));
175
+ return Array.getSomes([...fixedEntryOptions, ...implEntryOptions]);
125
176
  });
126
- const getSpecPath = Effect.gen(function* () {
127
- const path = yield* Path.Path;
128
- const confectDirectory = yield* ConfectDirectory.get;
129
- return path.join(confectDirectory, "spec.ts");
130
- });
131
- const getNodeSpecPath = Effect.gen(function* () {
132
- const path = yield* Path.Path;
133
- const confectDirectory = yield* ConfectDirectory.get;
134
- return path.join(confectDirectory, "nodeSpec.ts");
135
- });
136
- const loadNodeSpec = Effect.gen(function* () {
137
- const fs = yield* FileSystem.FileSystem;
138
- const nodeSpecPath = yield* getNodeSpecPath;
139
- if (!(yield* fs.exists(nodeSpecPath))) return Option.none();
140
- const nodeSpec = (yield* bundleAndImport(nodeSpecPath).pipe(Effect.mapError((error) => new SpecImportFailedError({ error })))).default;
141
- if (!Spec.isNodeSpec(nodeSpec)) return yield* new NodeSpecFileDoesNotExportSpecError();
142
- return Option.some(nodeSpec);
143
- });
144
- const esbuildOptions = (entryPoint) => ({
145
- entryPoints: [entryPoint],
146
- bundle: true,
147
- write: false,
148
- metafile: true,
149
- platform: "node",
150
- format: "esm",
151
- logLevel: "silent",
152
- external: EXTERNAL_PACKAGES,
153
- plugins: [{
154
- name: "notify-rebuild",
155
- setup(build) {
156
- build.onEnd((result) => {
157
- if (result.errors.length === 0) build._emit?.();
158
- else Effect.runPromise(Effect.gen(function* () {
159
- const formattedMessages = yield* Effect.promise(() => esbuild.formatMessages(result.errors, {
160
- kind: "error",
161
- color: true,
162
- terminalWidth: 80
177
+ const esbuildOptions = (entry, signal, pendingRef, watcherErrorsRef) => {
178
+ const initialBuildSeenRef = Ref.unsafeMake(false);
179
+ return {
180
+ entryPoints: [entry.absolutePath],
181
+ bundle: true,
182
+ write: false,
183
+ metafile: true,
184
+ platform: "node",
185
+ format: "esm",
186
+ logLevel: "silent",
187
+ external: EXTERNAL_PACKAGES,
188
+ plugins: [absoluteExternalsPlugin, {
189
+ name: "notify-rebuild",
190
+ setup(build) {
191
+ build.onEnd((result) => {
192
+ Effect.runPromise(Effect.gen(function* () {
193
+ const isInitial = !(yield* Ref.getAndSet(initialBuildSeenRef, true));
194
+ yield* Ref.update(watcherErrorsRef, (current) => {
195
+ const next = new Map(current);
196
+ if (result.errors.length > 0) next.set(entry.absolutePath, result.errors);
197
+ else next.delete(entry.absolutePath);
198
+ return next;
199
+ });
200
+ if (isInitial && result.errors.length === 0) return;
201
+ yield* Ref.update(pendingRef, (p) => ({
202
+ ...p,
203
+ [entry.pendingKey]: true
204
+ }));
205
+ yield* Queue.offer(signal, void 0);
163
206
  }));
164
- const output = formatBuildErrors(result.errors, formattedMessages);
165
- yield* Console.error("\n" + output + "\n");
166
- yield* logFailure("Build errors found");
167
- }));
168
- });
169
- }
170
- }]
171
- });
172
- const createSpecWatcher = (entryPoint) => Stream.asyncPush((emit) => Effect.acquireRelease(Effect.promise(async () => {
173
- const opts = esbuildOptions(entryPoint);
174
- const plugin = opts.plugins[0];
175
- const originalSetup = plugin.setup;
176
- plugin.setup = (build) => {
177
- build._emit = () => emit.single();
178
- return originalSetup(build);
207
+ });
208
+ }
209
+ }]
179
210
  };
180
- const ctx = await esbuild.context({
181
- ...opts,
182
- plugins: [plugin]
183
- });
211
+ };
212
+ const createEntryPointWatcher = (entry, signal, pendingRef, watcherErrorsRef) => Effect.acquireRelease(Effect.promise(async () => {
213
+ const ctx = await esbuild.context(esbuildOptions(entry, signal, pendingRef, watcherErrorsRef));
184
214
  await ctx.watch();
185
215
  return ctx;
186
- }), (ctx) => Effect.promise(() => ctx.dispose()).pipe(Effect.tap(() => Effect.logDebug("esbuild watcher disposed")))), {
187
- bufferSize: 1,
188
- strategy: "sliding"
216
+ }), (ctx) => Effect.gen(function* () {
217
+ yield* Effect.promise(() => ctx.dispose());
218
+ yield* Ref.update(watcherErrorsRef, (current) => {
219
+ if (!current.has(entry.absolutePath)) return current;
220
+ const next = new Map(current);
221
+ next.delete(entry.absolutePath);
222
+ return next;
223
+ });
224
+ yield* Effect.logDebug(`esbuild watcher disposed: ${entry.displayPath}`);
225
+ }));
226
+ /**
227
+ * Holds one scoped esbuild watcher per entry point and reconciles the set
228
+ * whenever something offers to `restartQueue`. Adding or removing an entry
229
+ * point only spawns/disposes the affected watcher; unchanged entries keep
230
+ * their existing context, so a structural change doesn't churn watchers
231
+ * for unrelated files.
232
+ */
233
+ const entryPointsWatcher = (signal, pendingRef, restartQueue, watcherErrorsRef) => Effect.gen(function* () {
234
+ const parentScope = yield* Effect.scope;
235
+ const scopesRef = yield* Ref.make(/* @__PURE__ */ new Map());
236
+ const sync = Effect.gen(function* () {
237
+ const desired = yield* discoverEntryPoints;
238
+ const desiredByPath = new Map(desired.map((entryPoint) => [entryPoint.absolutePath, entryPoint]));
239
+ const current = yield* Ref.get(scopesRef);
240
+ yield* Effect.forEach(Array.fromIterable(current), ([absolutePath, childScope]) => desiredByPath.has(absolutePath) ? Effect.void : Scope.close(childScope, Exit.void).pipe(Effect.andThen(Ref.update(scopesRef, (scopes) => {
241
+ const updated = new Map(scopes);
242
+ updated.delete(absolutePath);
243
+ return updated;
244
+ }))));
245
+ yield* Effect.forEach(desired, (entry) => Effect.gen(function* () {
246
+ if ((yield* Ref.get(scopesRef)).has(entry.absolutePath)) return;
247
+ const childScope = yield* Scope.fork(parentScope, ExecutionStrategy.sequential);
248
+ yield* createEntryPointWatcher(entry, signal, pendingRef, watcherErrorsRef).pipe(Scope.extend(childScope));
249
+ yield* Ref.update(scopesRef, (scopes) => {
250
+ const updated = new Map(scopes);
251
+ updated.set(entry.absolutePath, childScope);
252
+ return updated;
253
+ });
254
+ }));
255
+ });
256
+ yield* sync;
257
+ return yield* Effect.forever(Queue.take(restartQueue).pipe(Effect.andThen(sync)));
189
258
  });
190
- const specFileWatcher = (signal, pendingRef, specWatcherRestartQueue) => Effect.forever(Effect.gen(function* () {
259
+ /**
260
+ * Single recursive `fs.watch` on `confect/`. Flips the matching dirty flag
261
+ * for any change to an entry-point-shaped file (so codegen runs without
262
+ * waiting on a newly spawned esbuild watcher), and offers to
263
+ * `restartQueue` when an entry point is created or removed so the watcher
264
+ * manager picks up the new set.
265
+ */
266
+ const confectStructureWatcher = (signal, pendingRef, restartQueue) => Effect.gen(function* () {
191
267
  const fs = yield* FileSystem.FileSystem;
192
- const specPath = yield* getSpecPath;
193
- const nodeSpecPath = yield* getNodeSpecPath;
194
- const nodeSpecExists = yield* fs.exists(nodeSpecPath);
195
- const specWatcher = createSpecWatcher(specPath);
196
- const nodeSpecWatcher = nodeSpecExists ? createSpecWatcher(nodeSpecPath) : Stream.empty;
197
- const specChanges = pipe(Stream.merge(specWatcher, nodeSpecWatcher), Stream.map(() => "change"));
198
- const restartStream = pipe(Stream.fromQueue(specWatcherRestartQueue), Stream.map(() => "restart"));
199
- yield* pipe(Stream.merge(specChanges, restartStream), Stream.debounce(Duration.millis(200)), Stream.takeUntil((event) => event === "restart"), Stream.runForEach((event) => event === "change" ? Ref.update(pendingRef, (pending) => ({
200
- ...pending,
201
- specDirty: true
202
- })).pipe(Effect.andThen(Queue.offer(signal, void 0))) : Effect.void));
203
- }));
204
- const formatBuildError = (error, formattedMessage) => {
205
- const lines = String.split(formattedMessage, "\n");
206
- const redErrorText = pipe(AnsiDoc.text(error?.text ?? ""), AnsiDoc.annotate(Ansi.red), AnsiDoc.render({ style: "pretty" }));
207
- return pipe(pipe(Array.findFirstIndex(lines, (l) => pipe(l, String.trim, String.isNonEmpty)), Option.match({
208
- onNone: () => lines,
209
- onSome: (index) => Array.modify(lines, index, () => redErrorText)
210
- })), Array.join("\n"));
268
+ const path = yield* Path.Path;
269
+ const confectDirectory = yield* ConfectDirectory.get;
270
+ yield* pipe(fs.watch(confectDirectory, { recursive: true }), Stream.debounce(Duration.millis(200)), Stream.runForEach((event) => handleConfectChange({
271
+ relativePath: path.relative(confectDirectory, event.path),
272
+ eventTag: event._tag,
273
+ signal,
274
+ pendingRef,
275
+ restartQueue
276
+ })));
277
+ });
278
+ const TOP_LEVEL_OPTIONAL_KEYS = new Map([
279
+ ["http.ts", "httpDirty"],
280
+ ["crons.ts", "cronsDirty"],
281
+ ["auth.ts", "authDirty"]
282
+ ]);
283
+ const flipDirtyAndSignal = (pendingRef, signal, key, restartQueue, restart) => pipe(Ref.update(pendingRef, (p) => ({
284
+ ...p,
285
+ [key]: true
286
+ })), Effect.andThen(Queue.offer(signal, void 0)), Effect.andThen(restart ? Queue.offer(restartQueue, void 0) : Effect.void));
287
+ const handleConfectChange = ({ relativePath, eventTag, signal, pendingRef, restartQueue }) => {
288
+ if (relativePath.split(/[/\\]/).includes("_generated")) return Effect.void;
289
+ if (!relativePath.endsWith(".ts")) return Effect.void;
290
+ const isLifecycleChange = eventTag !== "Update";
291
+ const topLevelKey = TOP_LEVEL_OPTIONAL_KEYS.get(relativePath);
292
+ if (topLevelKey !== void 0) return flipDirtyAndSignal(pendingRef, signal, topLevelKey, restartQueue, isLifecycleChange);
293
+ if (relativePath === "schema.ts") return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, isLifecycleChange);
294
+ if (isLeafSpecPath(relativePath) || isLeafImplPath(relativePath)) return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, isLifecycleChange);
295
+ if (eventTag === "Create") return flipDirtyAndSignal(pendingRef, signal, "specDirty", restartQueue, false);
296
+ return Effect.void;
211
297
  };
212
- const formatBuildErrors = (errors, formattedMessages) => pipe(formattedMessages, Array.map((message, i) => formatBuildError(errors[i], message)), Array.join(""), String.trimEnd);
213
- var SpecFileDoesNotExportSpecError = class extends Schema.TaggedError()("SpecFileDoesNotExportSpecError", {}) {};
214
- var NodeSpecFileDoesNotExportSpecError = class extends Schema.TaggedError()("NodeSpecFileDoesNotExportSpecError", {}) {};
215
- var SpecImportFailedError = class extends Schema.TaggedError()("SpecImportFailedError", { error: Schema.Unknown }) {};
216
298
  const syncOptionalFile = (generate, convexFile) => pipe(generate, Effect.andThen(Option.match({
217
299
  onSome: ({ change, convexFilePath }) => Match.value(change).pipe(Match.when("Unchanged", () => Effect.void), Match.whenOr("Added", "Modified", (addedOrModified) => logFileChangeIndented(addedOrModified, convexFilePath)), Match.exhaustive),
218
300
  onNone: () => Effect.gen(function* () {
@@ -226,34 +308,6 @@ const syncOptionalFile = (generate, convexFile) => pipe(generate, Effect.andThen
226
308
  }
227
309
  })
228
310
  })));
229
- const optionalConfectFiles = {
230
- "http.ts": "httpDirty",
231
- "crons.ts": "cronsDirty",
232
- "auth.ts": "authDirty",
233
- "nodeSpec.ts": "specDirty",
234
- "nodeImpl.ts": "nodeImplDirty"
235
- };
236
- const confectDirectoryWatcher = (signal, pendingRef, specWatcherRestartQueue) => Effect.gen(function* () {
237
- const fs = yield* FileSystem.FileSystem;
238
- const path = yield* Path.Path;
239
- const confectDirectory = yield* ConfectDirectory.get;
240
- yield* pipe(fs.watch(confectDirectory), Stream.runForEach((event) => {
241
- const basename = path.basename(event.path);
242
- const pendingKey = optionalConfectFiles[basename];
243
- if (pendingKey !== void 0) return pipe(pendingRef, Ref.update((pending) => {
244
- const next = {
245
- ...pending,
246
- [pendingKey]: true
247
- };
248
- if (basename === "nodeImpl.ts") return {
249
- ...next,
250
- specDirty: true
251
- };
252
- return next;
253
- }), Effect.andThen(Queue.offer(signal, void 0)), Effect.andThen(basename === "nodeSpec.ts" ? Queue.offer(specWatcherRestartQueue, void 0) : Effect.void));
254
- return Effect.void;
255
- }));
256
- });
257
311
 
258
312
  //#endregion
259
313
  export { dev };