@confect/cli 9.0.0-next.5 → 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.
@@ -49,8 +49,14 @@ import { ProjectRoot } from "../ProjectRoot";
49
49
  import { generateAuthConfig, generateCrons, generateHttp } from "../utils";
50
50
  import { codegenHandler, loadPreviousFunctionPaths } from "./codegen";
51
51
 
52
- const GENERATED_SPEC_PATH = "_generated/spec.ts";
53
- const GENERATED_NODE_SPEC_PATH = "_generated/nodeSpec.ts";
52
+ const GENERATED_DIRNAME = "_generated";
53
+
54
+ const GENERATED_SPEC_PATH = Effect.andThen(Path.Path, (path) =>
55
+ path.join(GENERATED_DIRNAME, "spec.ts"),
56
+ );
57
+ const GENERATED_NODE_SPEC_PATH = Effect.andThen(Path.Path, (path) =>
58
+ path.join(GENERATED_DIRNAME, "nodeSpec.ts"),
59
+ );
54
60
 
55
61
  // Quiescence window: the sync loop waits this long for further signals
56
62
  // after each batch. One user edit fires `onEnd` on every esbuild
@@ -99,7 +105,7 @@ const changeChar = (change: "Added" | "Removed" | "Modified") =>
99
105
  Match.value(change).pipe(
100
106
  Match.when("Added", () => ({ char: "+", color: Ansi.green })),
101
107
  Match.when("Removed", () => ({ char: "-", color: Ansi.red })),
102
- Match.when("Modified", () => ({ char: "~", color: Ansi.yellow })),
108
+ Match.when("Modified", () => ({ char: "~", color: Ansi.magenta })),
103
109
  Match.exhaustive,
104
110
  );
105
111
 
@@ -414,10 +420,18 @@ const discoverEntryPoints = Effect.gen(function* () {
414
420
  });
415
421
  });
416
422
 
423
+ const generatedSpecPath = yield* GENERATED_SPEC_PATH;
424
+ const generatedNodeSpecPath = yield* GENERATED_NODE_SPEC_PATH;
425
+
417
426
  const fixedEntryOptions = yield* Effect.all([
418
- tryEntry(GENERATED_SPEC_PATH, "specDirty"),
419
- tryEntry(GENERATED_NODE_SPEC_PATH, "specDirty"),
420
- tryEntry("schema.ts", "specDirty"),
427
+ tryEntry(generatedSpecPath, "specDirty"),
428
+ tryEntry(generatedNodeSpecPath, "specDirty"),
429
+ // `confect/schema.ts` is no longer user-authored; the runtime
430
+ // `DatabaseSchema` lives at `_generated/schema.ts` (codegen-written,
431
+ // so not an entry point — wiring it through esbuild would form a
432
+ // codegen→write→onEnd→codegen loop). Updates to `confect/tables/*.ts`
433
+ // still reach this dev loop via the impl entry points' import graphs;
434
+ // brand-new tables are caught by the Create-event safety net below.
421
435
  tryEntry("http.ts", "httpDirty"),
422
436
  tryEntry("crons.ts", "cronsDirty"),
423
437
  tryEntry("auth.ts", "authDirty"),
@@ -703,6 +717,10 @@ const handleConfectChange = ({
703
717
  );
704
718
  }
705
719
 
720
+ // A stray `confect/schema.ts` (now codegen-owned at
721
+ // `_generated/schema.ts`) shouldn't exist; flagging it here ensures the
722
+ // next codegen pass surfaces the migration error
723
+ // (`LegacySchemaFileError`) instead of silently ignoring the file.
706
724
  if (relativePath === "schema.ts") {
707
725
  return flipDirtyAndSignal(
708
726
  pendingRef,
@@ -723,7 +741,7 @@ const handleConfectChange = ({
723
741
  );
724
742
  }
725
743
 
726
- // Any other `.ts` under `confect/` (helpers like `tables/Notes.ts`).
744
+ // Any other `.ts` under `confect/` (helpers like `tables/notes.ts`).
727
745
  // Updates to such files are handled by the esbuild watcher for whichever
728
746
  // entry point imports them — its onEnd flips the right dirty flag.
729
747
  // Creates are our safety net: when a previously-missing import is added,
package/src/log.ts CHANGED
@@ -57,7 +57,7 @@ export const logFileAdded = logFile("+", Ansi.green);
57
57
 
58
58
  export const logFileRemoved = logFile("-", Ansi.red);
59
59
 
60
- export const logFileModified = logFile("~", Ansi.yellow);
60
+ export const logFileModified = logFile("~", Ansi.magenta);
61
61
 
62
62
  // --- Function subline logs ---
63
63
 
@@ -101,4 +101,6 @@ export const logSuccess = logStatus("✔︎", Ansi.green);
101
101
 
102
102
  export const logFailure = logStatus("✘", Ansi.red);
103
103
 
104
- export const logPending = logStatus("⭘", Ansi.yellow);
104
+ export const logPending = logStatus("⭘", Ansi.cyan);
105
+
106
+ export const logWarn = logStatus("⚠", Ansi.yellow);
package/src/templates.ts CHANGED
@@ -2,7 +2,6 @@ import { Array, Effect, Option } from "effect";
2
2
  import { CodeBlockWriter } from "./CodeBlockWriter";
3
3
  import {
4
4
  collectImportBindings,
5
- collectLeafPaths,
6
5
  type SpecAssemblyNode,
7
6
  } from "./SpecAssemblyNode";
8
7
 
@@ -36,15 +35,197 @@ export const functions = ({
36
35
  return yield* cbw.toString();
37
36
  });
38
37
 
39
- export const schema = ({ schemaImportPath }: { schemaImportPath: string }) =>
38
+ /**
39
+ * Emit `convex/schema.ts` as a one-line re-export of the codegen-emitted
40
+ * deploy schema in `confect/_generated/convexSchema.ts`. Deploy-time
41
+ * consumers (the Convex CLI, `convex-test`) keep reading
42
+ * `convex/schema.ts`; the runtime `DatabaseSchema` in
43
+ * `confect/_generated/schema.ts` is untouched by this file.
44
+ */
45
+ export const schema = ({
46
+ convexSchemaImportPath,
47
+ }: {
48
+ convexSchemaImportPath: string;
49
+ }) =>
40
50
  Effect.gen(function* () {
41
51
  const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
42
52
 
43
- yield* cbw.writeLine(`import schemaDefinition from "${schemaImportPath}";`);
44
- yield* cbw.newLine();
45
53
  yield* cbw.writeLine(
46
- `export default schemaDefinition.convexSchemaDefinition;`,
54
+ `export { default } from "${convexSchemaImportPath}";`,
55
+ );
56
+
57
+ return yield* cbw.toString();
58
+ });
59
+
60
+ interface TableModuleBinding {
61
+ readonly importPath: string;
62
+ readonly tableName: string;
63
+ }
64
+
65
+ /**
66
+ * Emit `confect/_generated/schema.ts` — the runtime `DatabaseSchema` used
67
+ * by impls and the per-group registries (and downstream by per-function
68
+ * bundles for codec lookup). Every table wrapper at
69
+ * `confect/_generated/tables/<name>.ts` is imported statically and
70
+ * registered as a value entry on the `DatabaseSchema.make({...})` call.
71
+ * Per-table laziness lives inside each `Table`: its `Fields`, `Doc`, and
72
+ * `tableDefinition` are lazy memoised getters that only evaluate the
73
+ * user-supplied field-schema callback on first access, so unused tables in
74
+ * a function bundle never pay schema-construction cost despite the
75
+ * static import.
76
+ *
77
+ * The `DatabaseSchema` import is aliased to `$DatabaseSchema` because each
78
+ * table is imported under its own (filename-derived) name; a table named
79
+ * `DatabaseSchema` would otherwise collide with the library import and emit
80
+ * a duplicate-binding file. The leading `$` makes the alias collision-proof:
81
+ * `validateConfectTableIdentifier` requires names to match
82
+ * `/^[a-zA-Z][a-zA-Z0-9_]*$/`, which forbids `$`, so no valid table import
83
+ * can ever shadow it.
84
+ */
85
+ export const runtimeSchema = ({
86
+ tableModules,
87
+ }: {
88
+ tableModules: ReadonlyArray<TableModuleBinding>;
89
+ }) =>
90
+ Effect.gen(function* () {
91
+ const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
92
+
93
+ yield* cbw.writeLine(
94
+ `import { DatabaseSchema as $DatabaseSchema } from "@confect/server";`,
95
+ );
96
+
97
+ if (tableModules.length > 0) {
98
+ yield* cbw.blankLine();
99
+ yield* Effect.forEach(tableModules, ({ tableName, importPath }) =>
100
+ cbw.writeLine(`import ${tableName} from "${importPath}";`),
101
+ );
102
+ }
103
+
104
+ yield* cbw.blankLine();
105
+
106
+ if (tableModules.length === 0) {
107
+ yield* cbw.writeLine(`export default $DatabaseSchema.make({});`);
108
+ } else {
109
+ yield* cbw.writeLine(`export default $DatabaseSchema.make({`);
110
+ yield* cbw.indent(
111
+ Effect.gen(function* () {
112
+ for (const { tableName } of tableModules) {
113
+ yield* cbw.writeLine(`${tableName},`);
114
+ }
115
+ }),
116
+ );
117
+ yield* cbw.writeLine(`});`);
118
+ }
119
+
120
+ return yield* cbw.toString();
121
+ });
122
+
123
+ /**
124
+ * Emit `confect/_generated/convexSchema.ts` — the Convex deploy-time
125
+ * `SchemaDefinition`. Imports every table from its generated wrapper at
126
+ * `_generated/tables/<name>` and calls `defineSchema({...})` exactly once.
127
+ * The file deliberately avoids any `@confect/server` import so that the
128
+ * deploy artifact's import graph stays decoupled from the runtime
129
+ * `DatabaseSchema` machinery.
130
+ *
131
+ * The `defineSchema` import is aliased to `$defineSchema` because each table
132
+ * is imported under its own (filename-derived) name; a table named
133
+ * `defineSchema` would otherwise collide with the library import and emit a
134
+ * duplicate-binding file. The leading `$` makes the alias collision-proof:
135
+ * `validateConfectTableIdentifier` requires names to match
136
+ * `/^[a-zA-Z][a-zA-Z0-9_]*$/`, which forbids `$`, so no valid table import
137
+ * can ever shadow it.
138
+ */
139
+ export const convexSchema = ({
140
+ tableModules,
141
+ }: {
142
+ tableModules: ReadonlyArray<TableModuleBinding>;
143
+ }) =>
144
+ Effect.gen(function* () {
145
+ const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
146
+
147
+ yield* cbw.writeLine(
148
+ `import { defineSchema as $defineSchema } from "convex/server";`,
149
+ );
150
+
151
+ if (tableModules.length > 0) {
152
+ yield* cbw.blankLine();
153
+ yield* Effect.forEach(tableModules, ({ tableName, importPath }) =>
154
+ cbw.writeLine(`import ${tableName} from "${importPath}";`),
155
+ );
156
+ }
157
+
158
+ yield* cbw.blankLine();
159
+
160
+ if (tableModules.length === 0) {
161
+ yield* cbw.writeLine(`export default $defineSchema({});`);
162
+ } else {
163
+ yield* cbw.writeLine(`export default $defineSchema({`);
164
+ yield* cbw.indent(
165
+ Effect.gen(function* () {
166
+ for (const { tableName } of tableModules) {
167
+ yield* cbw.writeLine(`${tableName}: ${tableName}.tableDefinition,`);
168
+ }
169
+ }),
170
+ );
171
+ yield* cbw.writeLine(`});`);
172
+ }
173
+
174
+ return yield* cbw.toString();
175
+ });
176
+
177
+ /**
178
+ * Emit `confect/_generated/id.ts` — a type-constrained `Id` constructor and
179
+ * a `TableNames` union derived from the user's `confect/tables/*.ts`
180
+ * filenames. User-authored table modules import `Id` from this file to
181
+ * declare cross-table id references without typing the destination name as
182
+ * a free string (and without ever importing each other transitively).
183
+ *
184
+ * When the table directory is empty the `TableNames` union resolves to
185
+ * `never`, which still lets the file typecheck against an empty workspace.
186
+ */
187
+ export const id = ({ tableNames }: { tableNames: ReadonlyArray<string> }) =>
188
+ Effect.gen(function* () {
189
+ const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
190
+
191
+ yield* cbw.writeLine(`import { GenericId } from "@confect/core";`);
192
+ yield* cbw.blankLine();
193
+
194
+ const union =
195
+ tableNames.length === 0
196
+ ? "never"
197
+ : tableNames.map((n) => `"${n}"`).join(" | ");
198
+ yield* cbw.writeLine(`export type TableNames = ${union};`);
199
+ yield* cbw.blankLine();
200
+
201
+ yield* cbw.writeLine(
202
+ `export const Id = <const TableName extends TableNames>(`,
47
203
  );
204
+ yield* cbw.indent(cbw.writeLine(`tableName: TableName,`));
205
+ yield* cbw.writeLine(`) => GenericId.GenericId(tableName);`);
206
+
207
+ return yield* cbw.toString();
208
+ });
209
+
210
+ /**
211
+ * Emit `confect/_generated/tables/<tableName>.ts` — a two-line wrapper that
212
+ * imports the user-authored `UnnamedTable` and binds the file basename to
213
+ * it, producing the fully-named `Table` value that downstream consumers
214
+ * (schema, specs, impls) read.
215
+ */
216
+ export const tableWrapper = ({
217
+ tableName,
218
+ unnamedImportPath,
219
+ }: {
220
+ tableName: string;
221
+ unnamedImportPath: string;
222
+ }) =>
223
+ Effect.gen(function* () {
224
+ const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
225
+
226
+ yield* cbw.writeLine(`import unnamed from "${unnamedImportPath}";`);
227
+ yield* cbw.blankLine();
228
+ yield* cbw.writeLine(`export default unnamed("${tableName}");`);
48
229
 
49
230
  return yield* cbw.toString();
50
231
  });
@@ -110,54 +291,15 @@ export const refs = ({
110
291
  return yield* cbw.toString();
111
292
  });
112
293
 
113
- export const api = ({
294
+ export const registeredFunctionsForGroup = ({
114
295
  schemaImportPath,
115
296
  specImportPath,
116
- }: {
117
- schemaImportPath: string;
118
- specImportPath: string;
119
- }) =>
120
- Effect.gen(function* () {
121
- const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
122
-
123
- yield* cbw.writeLine(`import { Api } from "@confect/server";`);
124
- yield* cbw.writeLine(`import schema from "${schemaImportPath}";`);
125
- yield* cbw.writeLine(`import spec from "${specImportPath}";`);
126
- yield* cbw.blankLine();
127
- yield* cbw.writeLine(`export default Api.make(schema, spec);`);
128
-
129
- return yield* cbw.toString();
130
- });
131
-
132
- export const nodeApi = ({
133
- schemaImportPath,
134
- nodeSpecImportPath,
135
- }: {
136
- schemaImportPath: string;
137
- nodeSpecImportPath: string;
138
- }) =>
139
- Effect.gen(function* () {
140
- const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
141
-
142
- yield* cbw.writeLine(`import { Api } from "@confect/server";`);
143
- yield* cbw.blankLine();
144
- yield* cbw.writeLine(`import schema from "${schemaImportPath}";`);
145
- yield* cbw.writeLine(`import nodeSpec from "${nodeSpecImportPath}";`);
146
- yield* cbw.blankLine();
147
- yield* cbw.writeLine(`export default Api.make(schema, nodeSpec);`);
148
-
149
- return yield* cbw.toString();
150
- });
151
-
152
- export const registeredFunctionsForGroup = ({
153
- apiImportPath,
154
- groupPathDot,
155
297
  implImportPath,
156
298
  layerExportName,
157
299
  useNode = false,
158
300
  }: {
159
- apiImportPath: string;
160
- groupPathDot: string;
301
+ schemaImportPath: string;
302
+ specImportPath: string;
161
303
  implImportPath: string;
162
304
  layerExportName: string;
163
305
  useNode?: boolean;
@@ -178,14 +320,20 @@ export const registeredFunctionsForGroup = ({
178
320
  );
179
321
  }
180
322
 
181
- yield* cbw.writeLine(`import api from "${apiImportPath}";`);
323
+ yield* cbw.writeLine(`import databaseSchema from "${schemaImportPath}";`);
182
324
  yield* cbw.writeLine(`import ${layerExportName} from "${implImportPath}";`);
183
325
  yield* cbw.blankLine();
184
- const quotedGroupPath = `"${groupPathDot.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
326
+ // The group's own leaf spec is referenced type-only (`typeof import(...)`),
327
+ // so the spec module is erased at transpile time and never enters the
328
+ // per-function bundle; only `databaseSchema` and the impl are runtime
329
+ // imports. Typing from the leaf spec (not the project-wide assembled spec)
330
+ // keeps the registry's type dependent solely on its own group.
331
+ const specType = `typeof import("${specImportPath}")["default"]`;
332
+ const makeFn = useNode
333
+ ? "RegisteredNodeFunction.make"
334
+ : "RegisteredConvexFunction.make";
185
335
  yield* cbw.writeLine(
186
- useNode
187
- ? `export default RegisteredFunctions.buildForGroup(api, ${quotedGroupPath}, ${layerExportName}, RegisteredNodeFunction.make);`
188
- : `export default RegisteredFunctions.buildForGroup(api, ${quotedGroupPath}, ${layerExportName}, RegisteredConvexFunction.make);`,
336
+ `export default RegisteredFunctions.buildForGroup<${specType}>(databaseSchema, ${layerExportName}, ${makeFn});`,
189
337
  );
190
338
 
191
339
  return yield* cbw.toString();
@@ -473,13 +621,6 @@ export const assembledSpec = ({
473
621
  runtime === "Convex" ? "GroupSpec.makeAt" : "GroupSpec.makeNodeAt";
474
622
 
475
623
  yield* cbw.write(`export default ${specFactory}`);
476
- yield* Effect.forEach(collectLeafPaths(nodes), (leaf) =>
477
- Effect.gen(function* () {
478
- yield* cbw.write(`.addPath(${leaf.binding.localName}, `);
479
- yield* cbw.quote(leaf.dotPath);
480
- yield* cbw.write(")");
481
- }),
482
- );
483
624
  yield* Effect.forEach(nodes, (node) =>
484
625
  writeRootAddAt(cbw, node, groupFactory),
485
626
  );