@confect/cli 9.0.0-next.0 → 9.0.0-next.10

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +351 -1
  2. package/dist/BuildError.mjs +9 -2
  3. package/dist/BuildError.mjs.map +1 -1
  4. package/dist/Bundler.mjs +67 -57
  5. package/dist/Bundler.mjs.map +1 -1
  6. package/dist/CodeBlockWriter.mjs +1 -1
  7. package/dist/CodeBlockWriter.mjs.map +1 -1
  8. package/dist/CodegenError.mjs +36 -21
  9. package/dist/CodegenError.mjs.map +1 -1
  10. package/dist/ConfectDirectory.mjs +5 -2
  11. package/dist/ConfectDirectory.mjs.map +1 -1
  12. package/dist/ConvexDirectory.mjs +6 -2
  13. package/dist/ConvexDirectory.mjs.map +1 -1
  14. package/dist/FunctionPath.mjs +1 -1
  15. package/dist/FunctionPath.mjs.map +1 -1
  16. package/dist/FunctionPaths.mjs +5 -1
  17. package/dist/FunctionPaths.mjs.map +1 -1
  18. package/dist/GroupPath.mjs +9 -2
  19. package/dist/GroupPath.mjs.map +1 -1
  20. package/dist/GroupPaths.mjs +1 -1
  21. package/dist/GroupPaths.mjs.map +1 -1
  22. package/dist/LeafModule.mjs +20 -24
  23. package/dist/LeafModule.mjs.map +1 -1
  24. package/dist/ProjectRoot.mjs +8 -3
  25. package/dist/ProjectRoot.mjs.map +1 -1
  26. package/dist/SpecAssemblyNode.mjs +9 -11
  27. package/dist/SpecAssemblyNode.mjs.map +1 -1
  28. package/dist/TableModule.mjs +94 -0
  29. package/dist/TableModule.mjs.map +1 -0
  30. package/dist/cliApp.mjs +1 -1
  31. package/dist/cliApp.mjs.map +1 -1
  32. package/dist/confect/codegen.mjs +272 -141
  33. package/dist/confect/codegen.mjs.map +1 -1
  34. package/dist/confect/dev.mjs +36 -17
  35. package/dist/confect/dev.mjs.map +1 -1
  36. package/dist/confect.mjs +2 -2
  37. package/dist/confect.mjs.map +1 -1
  38. package/dist/index.mjs +3 -2
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/log.mjs +9 -4
  41. package/dist/log.mjs.map +1 -1
  42. package/dist/package.mjs +1 -1
  43. package/dist/templates.mjs +139 -48
  44. package/dist/templates.mjs.map +1 -1
  45. package/dist/utils.mjs +42 -22
  46. package/dist/utils.mjs.map +1 -1
  47. package/package.json +33 -50
  48. package/dist/index.d.mts +0 -1
  49. package/src/BuildError.ts +0 -210
  50. package/src/Bundler.ts +0 -144
  51. package/src/CodeBlockWriter.ts +0 -65
  52. package/src/CodegenError.ts +0 -344
  53. package/src/ConfectDirectory.ts +0 -42
  54. package/src/ConvexDirectory.ts +0 -68
  55. package/src/FunctionPath.ts +0 -27
  56. package/src/FunctionPaths.ts +0 -103
  57. package/src/GroupPath.ts +0 -118
  58. package/src/GroupPaths.ts +0 -7
  59. package/src/LeafModule.ts +0 -313
  60. package/src/ProjectRoot.ts +0 -50
  61. package/src/SpecAssemblyNode.ts +0 -82
  62. package/src/cliApp.ts +0 -8
  63. package/src/confect/codegen.ts +0 -589
  64. package/src/confect/dev.ts +0 -749
  65. package/src/confect.ts +0 -19
  66. package/src/index.ts +0 -22
  67. package/src/log.ts +0 -104
  68. package/src/templates.ts +0 -477
  69. package/src/utils.ts +0 -429
package/src/GroupPaths.ts DELETED
@@ -1,7 +0,0 @@
1
- import { Schema } from "effect";
2
- import * as GroupPath from "./GroupPath";
3
-
4
- export const GroupPaths = Schema.HashSetFromSelf(GroupPath.GroupPath).pipe(
5
- Schema.brand("@confect/cli/GroupPaths"),
6
- );
7
- export type GroupPaths = typeof GroupPaths.Type;
package/src/LeafModule.ts DELETED
@@ -1,313 +0,0 @@
1
- import { GroupSpec, Registry } from "@confect/core";
2
- import * as GroupImpl from "@confect/server/GroupImpl";
3
- import { FileSystem, Path } from "@effect/platform";
4
- import type { Context } from "effect";
5
- import { Array, Effect, Layer, Option, Ref, String } from "effect";
6
- import { fromBundlerError } from "./BuildError";
7
- import * as Bundler from "./Bundler";
8
- import {
9
- ImplMissingDefaultLayerError,
10
- ImplMissingFunctionsError,
11
- ImplMissingSpecImportError,
12
- ImplNotFinalizedError,
13
- SpecMissingDefaultGroupSpecError,
14
- SpecRuntimeMismatchError,
15
- } from "./CodegenError";
16
- import { ConfectDirectory } from "./ConfectDirectory";
17
- import { removePathExtension } from "./utils";
18
-
19
- export interface LeafModule {
20
- readonly relativePath: string;
21
- readonly pathSegments: readonly [string, ...string[]];
22
- readonly groupPathDot: string;
23
- readonly registryGroupPathDot: string;
24
- readonly exportName: string;
25
- readonly runtime: "Convex" | "Node";
26
- readonly specImportPath: string;
27
- }
28
-
29
- export const SPEC_SUFFIX = ".spec.ts";
30
- export const IMPL_SUFFIX = ".impl.ts";
31
-
32
- const swapModuleSuffix = (
33
- relativePath: string,
34
- fromSuffix: string,
35
- toSuffix: string,
36
- ) =>
37
- Effect.gen(function* () {
38
- const path = yield* Path.Path;
39
- const { dir, name, ext } = path.parse(relativePath);
40
- if (ext !== ".ts" || !name.endsWith(fromSuffix.slice(0, -".ts".length))) {
41
- return relativePath;
42
- }
43
-
44
- const stem = name.slice(0, -fromSuffix.slice(0, -".ts".length).length);
45
- const nextName = `${stem}${toSuffix.slice(0, -".ts".length)}`;
46
- return dir.length > 0
47
- ? path.join(dir, `${nextName}${ext}`)
48
- : `${nextName}${ext}`;
49
- });
50
-
51
- export const isLeafSpecPath = (relativePath: string) =>
52
- relativePath.endsWith(SPEC_SUFFIX);
53
-
54
- export const isLeafImplPath = (relativePath: string) =>
55
- relativePath.endsWith(IMPL_SUFFIX);
56
-
57
- export const exportNameFromModulePath = (relativePath: string) =>
58
- Effect.gen(function* () {
59
- const path = yield* Path.Path;
60
- const { name, ext } = path.parse(relativePath);
61
- if (ext !== ".ts") {
62
- return name;
63
- }
64
- return name.endsWith(".spec") ? name.slice(0, -".spec".length) : name;
65
- });
66
-
67
- export const groupPathFromRelativeModulePath = (relativePath: string) =>
68
- Effect.gen(function* () {
69
- const path = yield* Path.Path;
70
- const { dir, name, ext } = path.parse(relativePath);
71
- const stem =
72
- ext === ".ts" && name.endsWith(".spec")
73
- ? name.slice(0, -".spec".length)
74
- : name;
75
- const dirSegments = Array.filter(
76
- String.split(dir, path.sep),
77
- String.isNonEmpty,
78
- );
79
- const pathSegments = Array.append(dirSegments, stem) as [
80
- string,
81
- ...string[],
82
- ];
83
- return {
84
- pathSegments,
85
- groupPathDot: Array.join(pathSegments, "."),
86
- };
87
- });
88
-
89
- export const specImportPathFromGenerated = (specRelativePath: string) =>
90
- Effect.gen(function* () {
91
- const withoutExt = yield* removePathExtension(specRelativePath);
92
- return `../${withoutExt}`;
93
- });
94
-
95
- export const specPathForImpl = (implRelativePath: string) =>
96
- swapModuleSuffix(implRelativePath, IMPL_SUFFIX, SPEC_SUFFIX);
97
-
98
- export const implPathForSpec = (specRelativePath: string) =>
99
- swapModuleSuffix(specRelativePath, SPEC_SUFFIX, IMPL_SUFFIX);
100
-
101
- export const isNodeLeafModule = (relativePath: string) =>
102
- relativePath.startsWith("node/") || relativePath.startsWith("node\\");
103
-
104
- export const toNodeRegistryLeaf = (leaf: LeafModule): LeafModule => ({
105
- ...leaf,
106
- pathSegments: [leaf.exportName],
107
- groupPathDot: leaf.exportName,
108
- });
109
-
110
- export const registeredFunctionsRelativePath = (leaf: LeafModule) =>
111
- Effect.gen(function* () {
112
- const path = yield* Path.Path;
113
- return (
114
- path.join(
115
- "registeredFunctions",
116
- ...leaf.pathSegments.slice(leaf.runtime === "Node" ? 1 : 0),
117
- ) + ".ts"
118
- );
119
- });
120
-
121
- export const discoverLeafSpecFiles = Effect.gen(function* () {
122
- const fs = yield* FileSystem.FileSystem;
123
- const path = yield* Path.Path;
124
- const confectDirectory = yield* ConfectDirectory.get;
125
-
126
- const excludedDirs = new Set(["_generated", "tables"]);
127
- const excludedFiles = new Set(["nodeSpec.ts", "spec.ts"]);
128
-
129
- const allPaths = yield* fs.readDirectory(confectDirectory, {
130
- recursive: true,
131
- });
132
-
133
- return Array.filter(allPaths, (relativePath) => {
134
- if (!isLeafSpecPath(relativePath)) {
135
- return false;
136
- }
137
-
138
- if (excludedFiles.has(relativePath)) {
139
- return false;
140
- }
141
-
142
- const segments = String.split(relativePath, path.sep);
143
- return !Array.some(segments, (segment) => excludedDirs.has(segment));
144
- });
145
- });
146
-
147
- export const discoverLeafImplFiles = Effect.gen(function* () {
148
- const fs = yield* FileSystem.FileSystem;
149
- const path = yield* Path.Path;
150
- const confectDirectory = yield* ConfectDirectory.get;
151
-
152
- const excludedDirs = new Set(["_generated", "tables"]);
153
-
154
- const allPaths = yield* fs.readDirectory(confectDirectory, {
155
- recursive: true,
156
- });
157
-
158
- return Array.filter(allPaths, (relativePath) => {
159
- if (!isLeafImplPath(relativePath)) {
160
- return false;
161
- }
162
-
163
- const segments = String.split(relativePath, path.sep);
164
- return !Array.some(segments, (segment) => excludedDirs.has(segment));
165
- });
166
- });
167
-
168
- export const toLeafModule = (specRelativePath: string) =>
169
- Effect.gen(function* () {
170
- const exportName = yield* exportNameFromModulePath(specRelativePath);
171
- const { pathSegments, groupPathDot } =
172
- yield* groupPathFromRelativeModulePath(specRelativePath);
173
- const specImportPath = yield* specImportPathFromGenerated(specRelativePath);
174
- const runtime = isNodeLeafModule(specRelativePath) ? "Node" : "Convex";
175
-
176
- return {
177
- relativePath: specRelativePath,
178
- pathSegments,
179
- groupPathDot,
180
- exportName,
181
- runtime,
182
- registryGroupPathDot: runtime === "Node" ? exportName : groupPathDot,
183
- specImportPath,
184
- } satisfies LeafModule;
185
- });
186
-
187
- const absoluteModulePath = (relativePath: string) =>
188
- Effect.gen(function* () {
189
- const confectDirectory = yield* ConfectDirectory.get;
190
- const path = yield* Path.Path;
191
- return path.resolve(confectDirectory, relativePath);
192
- });
193
-
194
- /**
195
- * Validate that the leaf's spec file default-exports a `GroupSpec` whose
196
- * runtime matches the leaf's location (`Convex` for files outside
197
- * `confect/node/`, `Node` for files inside it).
198
- */
199
- export const validateSpec = (leaf: LeafModule) =>
200
- Effect.gen(function* () {
201
- const absolutePath = yield* absoluteModulePath(leaf.relativePath);
202
- const { module } = yield* Bundler.bundle(absolutePath).pipe(
203
- Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)),
204
- );
205
-
206
- const groupSpec = module.default;
207
-
208
- if (!GroupSpec.isGroupSpec(groupSpec)) {
209
- return yield* new SpecMissingDefaultGroupSpecError({
210
- specPath: leaf.relativePath,
211
- });
212
- }
213
-
214
- if (groupSpec.runtime !== leaf.runtime) {
215
- return yield* new SpecRuntimeMismatchError({
216
- specPath: leaf.relativePath,
217
- expectedRuntime: leaf.runtime,
218
- actualRuntime: groupSpec.runtime,
219
- });
220
- }
221
- });
222
-
223
- /**
224
- * Walk the built `Context` for a `Finalized` `GroupImpl` service value. The
225
- * lookup is value-shaped (via `GroupImpl.isFinalizedGroupImpl`) so we don't
226
- * need to know the group's path up front to construct a typed tag for it.
227
- */
228
- const findFinalizedGroupImpl = <S>(
229
- context: Context.Context<S>,
230
- ): Option.Option<GroupImpl.AnyFinalized> =>
231
- Array.findFirst(context.unsafeMap.values(), GroupImpl.isFinalizedGroupImpl);
232
-
233
- /**
234
- * Build the impl layer with a fresh `Registry` so each validation is
235
- * isolated from prior validations' `FunctionImpl.make` writes. The CLI no
236
- * longer reads the registry directly — `GroupImpl.finalize` snapshots the
237
- * registered function names onto the produced `Finalized` `GroupImpl`
238
- * service value — but a fresh `Ref` is still required because the default
239
- * `Context.Reference` is cached globally and would otherwise accumulate
240
- * items across impls.
241
- */
242
- const buildImplLayer = (implLayer: Layer.Layer<unknown>) =>
243
- Effect.gen(function* () {
244
- const registry = Ref.unsafeMake<Registry.RegistryItems>({});
245
- return yield* Layer.build(
246
- implLayer as Layer.Layer<unknown, never, never>,
247
- ).pipe(Effect.provideService(Registry.Registry, registry));
248
- }).pipe(Effect.scoped);
249
-
250
- /**
251
- * Validate that the leaf's sibling impl file imports the spec, default-exports
252
- * a finalized `GroupImpl` layer, and provides a `FunctionImpl` for every
253
- * function declared by the spec.
254
- */
255
- export const validateImpl = (leaf: LeafModule) =>
256
- Effect.gen(function* () {
257
- const implRelativePath = yield* implPathForSpec(leaf.relativePath);
258
- const implAbsolutePath = yield* absoluteModulePath(implRelativePath);
259
- const specAbsolutePath = yield* absoluteModulePath(leaf.relativePath);
260
-
261
- const bundled = yield* Bundler.bundle(implAbsolutePath).pipe(
262
- Effect.mapError((error) => fromBundlerError(implRelativePath, error)),
263
- );
264
-
265
- if (
266
- !(yield* Bundler.directlyImports(
267
- bundled,
268
- implAbsolutePath,
269
- specAbsolutePath,
270
- ))
271
- ) {
272
- return yield* new ImplMissingSpecImportError({
273
- implPath: implRelativePath,
274
- expectedSpecPath: leaf.relativePath,
275
- });
276
- }
277
-
278
- if (!Layer.isLayer(bundled.module.default)) {
279
- return yield* new ImplMissingDefaultLayerError({
280
- implPath: implRelativePath,
281
- });
282
- }
283
-
284
- const { module: specModule } = yield* Bundler.bundle(specAbsolutePath).pipe(
285
- Effect.mapError((error) => fromBundlerError(leaf.relativePath, error)),
286
- );
287
- const groupSpec = specModule.default as GroupSpec.AnyWithProps;
288
- const expectedFunctionNames = Object.keys(groupSpec.functions);
289
-
290
- const context = yield* buildImplLayer(
291
- bundled.module.default as Layer.Layer<unknown>,
292
- );
293
- const finalizedGroupImpl = yield* Option.match(
294
- findFinalizedGroupImpl(context),
295
- {
296
- onNone: () => new ImplNotFinalizedError({ implPath: implRelativePath }),
297
- onSome: Effect.succeed,
298
- },
299
- );
300
-
301
- const registeredSet = new Set(finalizedGroupImpl.registeredFunctionNames);
302
- const missing = expectedFunctionNames.filter(
303
- (name) => !registeredSet.has(name),
304
- );
305
-
306
- if (missing.length > 0) {
307
- return yield* new ImplMissingFunctionsError({
308
- implPath: implRelativePath,
309
- groupPath: finalizedGroupImpl.groupPath,
310
- missingFunctionNames: missing,
311
- });
312
- }
313
- });
@@ -1,50 +0,0 @@
1
- import { FileSystem, Path } from "@effect/platform";
2
- import { NodeFileSystem } from "@effect/platform-node";
3
- import { Array, Effect, Option, Ref, Schema } from "effect";
4
-
5
- export class ProjectRoot extends Effect.Service<ProjectRoot>()(
6
- "@confect/cli/ProjectRoot",
7
- {
8
- effect: Effect.gen(function* () {
9
- const projectRoot = yield* findProjectRoot;
10
-
11
- const ref = yield* Ref.make<string>(projectRoot);
12
-
13
- return { get: Ref.get(ref) } as const;
14
- }),
15
- dependencies: [NodeFileSystem.layer],
16
- accessors: true,
17
- },
18
- ) {}
19
-
20
- export const findProjectRoot = Effect.gen(function* () {
21
- const fs = yield* FileSystem.FileSystem;
22
- const path = yield* Path.Path;
23
-
24
- const startDir = path.resolve(".");
25
- const root = path.parse(startDir).root;
26
-
27
- const directories = Array.unfold(startDir, (dir) =>
28
- dir === root
29
- ? Option.none()
30
- : Option.some([dir, path.dirname(dir)] as const),
31
- );
32
-
33
- const projectRoot = yield* Effect.findFirst(directories, (dir) =>
34
- fs.exists(path.join(dir, "package.json")),
35
- );
36
-
37
- return yield* Option.match(projectRoot, {
38
- onNone: () => Effect.fail(new ProjectRootNotFoundError()),
39
- onSome: Effect.succeed,
40
- });
41
- });
42
-
43
- export class ProjectRootNotFoundError extends Schema.TaggedError<ProjectRootNotFoundError>()(
44
- "ProjectRootNotFoundError",
45
- {},
46
- ) {
47
- override get message(): string {
48
- return "Could not find project root (no 'package.json' found)";
49
- }
50
- }
@@ -1,82 +0,0 @@
1
- import type { LeafModule } from "./LeafModule";
2
- import { Array, Option, Order, pipe, Record } from "effect";
3
-
4
- export interface SpecImportBinding {
5
- readonly importPath: string;
6
- readonly exportName: string;
7
- }
8
-
9
- export interface SpecAssemblyNode {
10
- readonly segment: string;
11
- readonly importBinding: Option.Option<SpecImportBinding>;
12
- readonly children: ReadonlyArray<SpecAssemblyNode>;
13
- }
14
-
15
- const importBindingFromLeaf = (leaf: LeafModule): SpecImportBinding => ({
16
- importPath: leaf.specImportPath,
17
- exportName: leaf.exportName,
18
- });
19
-
20
- const assemblyNodesAtDepth = (
21
- leaves: ReadonlyArray<LeafModule>,
22
- depth: number,
23
- ): ReadonlyArray<SpecAssemblyNode> =>
24
- pipe(
25
- Array.groupBy(leaves, (leaf) => leaf.pathSegments[depth]!),
26
- Record.toEntries,
27
- Array.sortBy(Order.mapInput(Order.string, ([segment]) => segment)),
28
- Array.map(([segment, groupLeaves]) => {
29
- const terminal = Array.findFirst(
30
- groupLeaves,
31
- (leaf) => leaf.pathSegments.length === depth + 1,
32
- );
33
- const descendants = Array.filter(
34
- groupLeaves,
35
- (leaf) => leaf.pathSegments.length > depth + 1,
36
- );
37
- return {
38
- segment,
39
- importBinding: Option.map(terminal, importBindingFromLeaf),
40
- children: assemblyNodesAtDepth(descendants, depth + 1),
41
- };
42
- }),
43
- );
44
-
45
- export const assemblyNodesFromLeaves = (
46
- leaves: ReadonlyArray<LeafModule>,
47
- ): ReadonlyArray<SpecAssemblyNode> => assemblyNodesAtDepth(leaves, 0);
48
-
49
- export const partitionByRuntime = (
50
- leaves: ReadonlyArray<LeafModule>,
51
- ): {
52
- readonly convex: ReadonlyArray<LeafModule>;
53
- readonly node: ReadonlyArray<LeafModule>;
54
- } => {
55
- const [node, convex] = Array.partition(
56
- leaves,
57
- (leaf) => leaf.runtime === "Convex",
58
- );
59
- return { convex, node };
60
- };
61
-
62
- const importBindingsForNode = (
63
- node: SpecAssemblyNode,
64
- ): ReadonlyArray<SpecImportBinding> =>
65
- pipe(node.children, Array.flatMap(importBindingsForNode), (childBindings) =>
66
- Option.match(node.importBinding, {
67
- onNone: () => childBindings,
68
- onSome: (binding) => Array.prepend(childBindings, binding),
69
- }),
70
- );
71
-
72
- export const collectImportBindings = (
73
- nodes: ReadonlyArray<SpecAssemblyNode>,
74
- ): ReadonlyArray<SpecImportBinding> =>
75
- pipe(
76
- Array.flatMap(nodes, importBindingsForNode),
77
- (bindings) =>
78
- Record.fromIterableBy(bindings, (binding) => binding.exportName),
79
- Record.toEntries,
80
- Array.sortBy(Order.mapInput(Order.string, ([exportName]) => exportName)),
81
- Array.map(([, binding]) => binding),
82
- );
package/src/cliApp.ts DELETED
@@ -1,8 +0,0 @@
1
- import { Command } from "@effect/cli";
2
- import packageJson from "../package.json" with { type: "json" };
3
- import { confect } from "./confect";
4
-
5
- export const cliApp = Command.run(confect, {
6
- name: "Confect",
7
- version: packageJson.version,
8
- });