@gaganref/confect-cli 0.0.0-stage → 10.3.1-fork.2
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.
- package/CHANGELOG.md +1824 -0
- package/LICENSE +7 -0
- package/README.md +16 -2
- package/bin/confect.mjs +26 -0
- package/dist/Ansi.mjs +18 -0
- package/dist/Ansi.mjs.map +1 -0
- package/dist/BuildError.mjs +30 -0
- package/dist/BuildError.mjs.map +1 -0
- package/dist/Bundler.mjs +426 -0
- package/dist/Bundler.mjs.map +1 -0
- package/dist/CodeBlockWriter.mjs +55 -0
- package/dist/CodeBlockWriter.mjs.map +1 -0
- package/dist/CodegenError.mjs +194 -0
- package/dist/CodegenError.mjs.map +1 -0
- package/dist/ConfectDirectory.mjs +35 -0
- package/dist/ConfectDirectory.mjs.map +1 -0
- package/dist/ConvexConfig.mjs +198 -0
- package/dist/ConvexConfig.mjs.map +1 -0
- package/dist/ConvexDirectory.mjs +54 -0
- package/dist/ConvexDirectory.mjs.map +1 -0
- package/dist/Declaration.mjs +258 -0
- package/dist/Declaration.mjs.map +1 -0
- package/dist/DirectoryOptions.mjs +42 -0
- package/dist/DirectoryOptions.mjs.map +1 -0
- package/dist/DocName.mjs +25 -0
- package/dist/DocName.mjs.map +1 -0
- package/dist/EntryPoint.mjs +111 -0
- package/dist/EntryPoint.mjs.map +1 -0
- package/dist/Evaluation.mjs +117 -0
- package/dist/Evaluation.mjs.map +1 -0
- package/dist/EvaluationWorker.mjs +46 -0
- package/dist/EvaluationWorker.mjs.map +1 -0
- package/dist/FunctionModule.mjs +137 -0
- package/dist/FunctionModule.mjs.map +1 -0
- package/dist/FunctionPath.mjs +18 -0
- package/dist/FunctionPath.mjs.map +1 -0
- package/dist/FunctionPaths.mjs +45 -0
- package/dist/FunctionPaths.mjs.map +1 -0
- package/dist/GenerationSnapshot.mjs +183 -0
- package/dist/GenerationSnapshot.mjs.map +1 -0
- package/dist/GroupPath.mjs +51 -0
- package/dist/GroupPath.mjs.map +1 -0
- package/dist/GroupPaths.mjs +8 -0
- package/dist/GroupPaths.mjs.map +1 -0
- package/dist/ImplModulePath.mjs +8 -0
- package/dist/ImplModulePath.mjs.map +1 -0
- package/dist/LeafModule.mjs +285 -0
- package/dist/LeafModule.mjs.map +1 -0
- package/dist/ProjectRoot.mjs +38 -0
- package/dist/ProjectRoot.mjs.map +1 -0
- package/dist/RelativeModulePath.mjs +13 -0
- package/dist/RelativeModulePath.mjs.map +1 -0
- package/dist/Remove.mjs +24 -0
- package/dist/Remove.mjs.map +1 -0
- package/dist/RestoreWatcher.mjs +31 -0
- package/dist/RestoreWatcher.mjs.map +1 -0
- package/dist/Rollback.mjs +50 -0
- package/dist/Rollback.mjs.map +1 -0
- package/dist/SnapshotError.mjs +21 -0
- package/dist/SnapshotError.mjs.map +1 -0
- package/dist/SpecAssemblyNode.mjs +91 -0
- package/dist/SpecAssemblyNode.mjs.map +1 -0
- package/dist/SpecModulePath.mjs +10 -0
- package/dist/SpecModulePath.mjs.map +1 -0
- package/dist/SyncState.mjs +122 -0
- package/dist/SyncState.mjs.map +1 -0
- package/dist/TableModule.mjs +97 -0
- package/dist/TableModule.mjs.map +1 -0
- package/dist/TableModulePath.mjs +8 -0
- package/dist/TableModulePath.mjs.map +1 -0
- package/dist/WatchBuild.mjs +190 -0
- package/dist/WatchBuild.mjs.map +1 -0
- package/dist/WatchGraph.mjs +35 -0
- package/dist/WatchGraph.mjs.map +1 -0
- package/dist/WatchTopology.mjs +55 -0
- package/dist/WatchTopology.mjs.map +1 -0
- package/dist/Watcher.mjs +378 -0
- package/dist/Watcher.mjs.map +1 -0
- package/dist/cliApp.mjs +9 -0
- package/dist/cliApp.mjs.map +1 -0
- package/dist/confect/codegen.mjs +548 -0
- package/dist/confect/codegen.mjs.map +1 -0
- package/dist/confect/declaration.mjs +218 -0
- package/dist/confect/declaration.mjs.map +1 -0
- package/dist/confect/dev.mjs +182 -0
- package/dist/confect/dev.mjs.map +1 -0
- package/dist/confect.mjs +9 -0
- package/dist/confect.mjs.map +1 -0
- package/dist/index.mjs +19 -0
- package/dist/index.mjs.map +1 -0
- package/dist/log.mjs +107 -0
- package/dist/log.mjs.map +1 -0
- package/dist/package.mjs +6 -0
- package/dist/package.mjs.map +1 -0
- package/dist/templates.mjs +448 -0
- package/dist/templates.mjs.map +1 -0
- package/dist/utils.mjs +105 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +51 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { logFileRemoved, logWarn } from "./log.mjs";
|
|
2
|
+
import { ComponentNodeGroupError, logCodegenError } from "./CodegenError.mjs";
|
|
3
|
+
import { ConvexDirectory } from "./ConvexDirectory.mjs";
|
|
4
|
+
import { ConfectDirectory } from "./ConfectDirectory.mjs";
|
|
5
|
+
import { CONVEX_CONFIG_FILENAME } from "./ConvexConfig.mjs";
|
|
6
|
+
import { remove } from "./Remove.mjs";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as FileSystem from "effect/FileSystem";
|
|
9
|
+
import * as Path from "effect/Path";
|
|
10
|
+
import * as Schema from "effect/Schema";
|
|
11
|
+
//#region src/Rollback.ts
|
|
12
|
+
const isNodeGroupRejection = Schema.is(ComponentNodeGroupError);
|
|
13
|
+
/**
|
|
14
|
+
* Keep a component's previous generated output when generation rejects a Node
|
|
15
|
+
* group. Specs can import bootstrap output, so generation writes it before
|
|
16
|
+
* their runtimes are known. `duringRestore` pauses whatever watches the output
|
|
17
|
+
* while it is replaced.
|
|
18
|
+
*/
|
|
19
|
+
const preserveComponentOutput = (generation, duringRestore = Effect.void) => Effect.gen(function* () {
|
|
20
|
+
if ((yield* ConvexDirectory.target).kind === "app") return yield* generation;
|
|
21
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22
|
+
const path = yield* Path.Path;
|
|
23
|
+
const backupDirectory = yield* fs.makeTempDirectoryScoped();
|
|
24
|
+
const configPath = path.join(yield* ConvexDirectory.get, CONVEX_CONFIG_FILENAME);
|
|
25
|
+
const snapshots = yield* Effect.forEach([path.join(yield* ConfectDirectory.get, "_generated"), configPath], Effect.fnUntraced(function* (original, index) {
|
|
26
|
+
const backup = path.join(backupDirectory, String(index));
|
|
27
|
+
const existed = yield* fs.exists(original);
|
|
28
|
+
if (existed) yield* fs.copy(original, backup);
|
|
29
|
+
return {
|
|
30
|
+
original,
|
|
31
|
+
backup,
|
|
32
|
+
existed
|
|
33
|
+
};
|
|
34
|
+
}));
|
|
35
|
+
const restore = (rejection) => Effect.gen(function* () {
|
|
36
|
+
yield* duringRestore;
|
|
37
|
+
yield* Effect.forEach(snapshots, ({ original, backup, existed }) => Effect.gen(function* () {
|
|
38
|
+
if (existed && original === configPath && (yield* fs.exists(original)) && (yield* fs.readFileString(original)) === (yield* fs.readFileString(backup))) return;
|
|
39
|
+
if (yield* fs.exists(original)) yield* remove(original);
|
|
40
|
+
if (existed) yield* fs.copy(backup, original);
|
|
41
|
+
else yield* logFileRemoved(original);
|
|
42
|
+
}));
|
|
43
|
+
yield* logWarn("Restored generated output after rejecting the Node group.");
|
|
44
|
+
}).pipe(Effect.scoped, Effect.tapError(() => logCodegenError(rejection)));
|
|
45
|
+
return yield* generation.pipe(Effect.tapError((error) => isNodeGroupRejection(error) ? restore(error) : Effect.void));
|
|
46
|
+
}).pipe(Effect.scoped);
|
|
47
|
+
//#endregion
|
|
48
|
+
export { preserveComponentOutput };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=Rollback.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Rollback.mjs","names":[],"sources":["../src/Rollback.ts"],"sourcesContent":["import * as Effect from \"effect/Effect\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as Path from \"effect/Path\";\nimport * as Schema from \"effect/Schema\";\nimport type * as Scope from \"effect/Scope\";\nimport { ComponentNodeGroupError, logCodegenError } from \"./CodegenError\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\nimport { CONVEX_CONFIG_FILENAME } from \"./ConvexConfig\";\nimport { ConvexDirectory } from \"./ConvexDirectory\";\nimport { logFileRemoved, logWarn } from \"./log\";\nimport { remove } from \"./Remove\";\n\nconst isNodeGroupRejection = Schema.is(ComponentNodeGroupError);\n\n/**\n * Keep a component's previous generated output when generation rejects a Node\n * group. Specs can import bootstrap output, so generation writes it before\n * their runtimes are known. `duringRestore` pauses whatever watches the output\n * while it is replaced.\n */\nexport const preserveComponentOutput = <A, E, R>(\n generation: Effect.Effect<A, E, R>,\n duringRestore: Effect.Effect<void, never, Scope.Scope> = Effect.void,\n) =>\n Effect.gen(function* () {\n if ((yield* ConvexDirectory.target).kind === \"app\")\n return yield* generation;\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const backupDirectory = yield* fs.makeTempDirectoryScoped();\n const configPath = path.join(\n yield* ConvexDirectory.get,\n CONVEX_CONFIG_FILENAME,\n );\n const snapshots = yield* Effect.forEach(\n [path.join(yield* ConfectDirectory.get, \"_generated\"), configPath],\n Effect.fnUntraced(function* (original, index) {\n const backup = path.join(backupDirectory, String(index));\n const existed = yield* fs.exists(original);\n if (existed) yield* fs.copy(original, backup);\n return { original, backup, existed };\n }),\n );\n const restore = (rejection: ComponentNodeGroupError) =>\n Effect.gen(function* () {\n yield* duringRestore;\n yield* Effect.forEach(snapshots, ({ original, backup, existed }) =>\n Effect.gen(function* () {\n if (\n existed &&\n original === configPath &&\n (yield* fs.exists(original)) &&\n (yield* fs.readFileString(original)) ===\n (yield* fs.readFileString(backup))\n ) {\n return;\n }\n if (yield* fs.exists(original)) {\n yield* remove(original);\n }\n if (existed) yield* fs.copy(backup, original);\n else yield* logFileRemoved(original);\n }),\n );\n yield* logWarn(\n \"Restored generated output after rejecting the Node group.\",\n );\n }).pipe(\n Effect.scoped,\n Effect.tapError(() => logCodegenError(rejection)),\n );\n return yield* generation.pipe(\n Effect.tapError((error) =>\n isNodeGroupRejection(error) ? restore(error) : Effect.void,\n ),\n );\n }).pipe(Effect.scoped);\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,uBAAuB,OAAO,GAAG,uBAAuB;;;;;;;AAQ9D,MAAa,2BACX,YACA,gBAAyD,OAAO,SAEhE,OAAO,IAAI,aAAa;CACtB,KAAK,OAAO,gBAAgB,OAAA,CAAQ,SAAS,OAC3C,OAAO,OAAO;CAChB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,kBAAkB,OAAO,GAAG,wBAAwB;CAC1D,MAAM,aAAa,KAAK,KACtB,OAAO,gBAAgB,KACvB,sBACF;CACA,MAAM,YAAY,OAAO,OAAO,QAC9B,CAAC,KAAK,KAAK,OAAO,iBAAiB,KAAK,YAAY,GAAG,UAAU,GACjE,OAAO,WAAW,WAAW,UAAU,OAAO;EAC5C,MAAM,SAAS,KAAK,KAAK,iBAAiB,OAAO,KAAK,CAAC;EACvD,MAAM,UAAU,OAAO,GAAG,OAAO,QAAQ;EACzC,IAAI,SAAS,OAAO,GAAG,KAAK,UAAU,MAAM;EAC5C,OAAO;GAAE;GAAU;GAAQ;EAAQ;CACrC,CAAC,CACH;CACA,MAAM,WAAW,cACf,OAAO,IAAI,aAAa;EACtB,OAAO;EACP,OAAO,OAAO,QAAQ,YAAY,EAAE,UAAU,QAAQ,cACpD,OAAO,IAAI,aAAa;GACtB,IACE,WACA,aAAa,eACZ,OAAO,GAAG,OAAO,QAAQ,OACzB,OAAO,GAAG,eAAe,QAAQ,QAC/B,OAAO,GAAG,eAAe,MAAM,IAElC;GAEF,IAAI,OAAO,GAAG,OAAO,QAAQ,GAC3B,OAAO,OAAO,QAAQ;GAExB,IAAI,SAAS,OAAO,GAAG,KAAK,QAAQ,QAAQ;QACvC,OAAO,eAAe,QAAQ;EACrC,CAAC,CACH;EACA,OAAO,QACL,2DACF;CACF,CAAC,CAAC,CAAC,KACD,OAAO,QACP,OAAO,eAAe,gBAAgB,SAAS,CAAC,CAClD;CACF,OAAO,OAAO,WAAW,KACvB,OAAO,UAAU,UACf,qBAAqB,KAAK,IAAI,QAAQ,KAAK,IAAI,OAAO,IACxD,CACF;AACF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
//#region src/SnapshotError.ts
|
|
3
|
+
var SnapshotError = class extends Schema.TaggedError()("SnapshotError", {
|
|
4
|
+
reason: Schema.Literals([
|
|
5
|
+
"InvalidShape",
|
|
6
|
+
"NoncanonicalPath",
|
|
7
|
+
"DuplicateLeaf",
|
|
8
|
+
"NoncanonicalFunctionNames"
|
|
9
|
+
]),
|
|
10
|
+
relativePath: Schema.String,
|
|
11
|
+
pathSegments: Schema.Array(Schema.String),
|
|
12
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
13
|
+
}) {
|
|
14
|
+
get message() {
|
|
15
|
+
return `Invalid generation snapshot (${this.reason}) at ${this.relativePath || "<snapshot>"}`;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { SnapshotError };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=SnapshotError.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SnapshotError.mjs","names":[],"sources":["../src/SnapshotError.ts"],"sourcesContent":["import * as Schema from \"effect/Schema\";\n\nexport class SnapshotError extends Schema.TaggedError<SnapshotError>()(\n \"SnapshotError\",\n {\n reason: Schema.Literals([\n \"InvalidShape\",\n \"NoncanonicalPath\",\n \"DuplicateLeaf\",\n \"NoncanonicalFunctionNames\",\n ]),\n relativePath: Schema.String,\n pathSegments: Schema.Array(Schema.String),\n cause: Schema.optionalKey(Schema.Defect()),\n },\n) {\n override get message(): string {\n return `Invalid generation snapshot (${this.reason}) at ${this.relativePath || \"<snapshot>\"}`;\n }\n}\n"],"mappings":";;AAEA,IAAa,gBAAb,cAAmC,OAAO,YAA2B,CAAC,CACpE,iBACA;CACE,QAAQ,OAAO,SAAS;EACtB;EACA;EACA;EACA;CACF,CAAC;CACD,cAAc,OAAO;CACrB,cAAc,OAAO,MAAM,OAAO,MAAM;CACxC,OAAO,OAAO,YAAY,OAAO,OAAO,CAAC;AAC3C,CACF,CAAC,CAAC;CACA,IAAa,UAAkB;EAC7B,OAAO,gCAAgC,KAAK,OAAO,OAAO,KAAK,gBAAgB;CACjF;AACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CodeBlockWriter } from "./CodeBlockWriter.mjs";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Array from "effect/Array";
|
|
4
|
+
import { pipe } from "effect/Function";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Record from "effect/Record";
|
|
7
|
+
import * as Order from "effect/Order";
|
|
8
|
+
//#region src/SpecAssemblyNode.ts
|
|
9
|
+
const importBindingFromLeaf = (leaf) => ({
|
|
10
|
+
importPath: leaf.specImportPath,
|
|
11
|
+
exportName: leaf.exportName,
|
|
12
|
+
localName: Array.join(leaf.pathSegments, "_")
|
|
13
|
+
});
|
|
14
|
+
const assemblyNodesAtDepth = (leaves, depth) => pipe(Array.groupBy(leaves, (leaf) => leaf.pathSegments[depth]), Record.toEntries, Array.sortBy(Order.mapInput(Order.String, ([segment]) => segment)), Array.map(([segment, groupLeaves]) => {
|
|
15
|
+
const terminal = Array.findFirst(groupLeaves, (leaf) => leaf.pathSegments.length === depth + 1);
|
|
16
|
+
const descendants = Array.filter(groupLeaves, (leaf) => leaf.pathSegments.length > depth + 1);
|
|
17
|
+
return {
|
|
18
|
+
segment,
|
|
19
|
+
importBinding: Option.map(terminal, importBindingFromLeaf),
|
|
20
|
+
children: assemblyNodesAtDepth(descendants, depth + 1)
|
|
21
|
+
};
|
|
22
|
+
}));
|
|
23
|
+
const assemblyNodesFromLeaves = (leaves) => assemblyNodesAtDepth(leaves, 0);
|
|
24
|
+
const importBindingsForNode = (node) => pipe(node.children, Array.flatMap(importBindingsForNode), (childBindings) => Option.match(node.importBinding, {
|
|
25
|
+
onNone: () => childBindings,
|
|
26
|
+
onSome: (binding) => Array.prepend(childBindings, binding)
|
|
27
|
+
}));
|
|
28
|
+
const collectImportBindings = (nodes) => pipe(Array.flatMap(nodes, importBindingsForNode), (bindings) => Record.fromIterableBy(bindings, (binding) => binding.importPath), Record.toEntries, Array.map(([, binding]) => binding), Array.sortBy(Order.mapInput(Order.String, (binding) => binding.localName)));
|
|
29
|
+
const writeGroupEntry = Effect.fnUntraced(function* (cbw, node) {
|
|
30
|
+
yield* cbw.write("[");
|
|
31
|
+
yield* cbw.quote(node.segment);
|
|
32
|
+
yield* cbw.write("]: ");
|
|
33
|
+
yield* writeGroupAssembly(cbw, node);
|
|
34
|
+
yield* cbw.write(",\n");
|
|
35
|
+
});
|
|
36
|
+
const writeGroupAssembly = Effect.fnUntraced(function* (cbw, node) {
|
|
37
|
+
const group = Option.match(node.importBinding, {
|
|
38
|
+
onNone: () => "GroupSpec.make()",
|
|
39
|
+
onSome: (binding) => binding.localName
|
|
40
|
+
});
|
|
41
|
+
yield* Array.match(node.children, {
|
|
42
|
+
onEmpty: () => cbw.write(group),
|
|
43
|
+
onNonEmpty: (children) => Effect.gen(function* () {
|
|
44
|
+
yield* cbw.write(`GroupSpec.addGroupsAt(${group}, {\n`);
|
|
45
|
+
yield* cbw.indent(Effect.forEach(children, (child) => writeGroupEntry(cbw, child)));
|
|
46
|
+
yield* cbw.write("})");
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
const groupTypeExpr = (node) => Array.match(node.children, {
|
|
51
|
+
onEmpty: () => Option.match(node.importBinding, {
|
|
52
|
+
onNone: () => `GroupSpec.GroupSpec<"Convex", "${node.segment}", never, never>`,
|
|
53
|
+
onSome: (binding) => `typeof ${binding.localName}`
|
|
54
|
+
}),
|
|
55
|
+
onNonEmpty: (children) => {
|
|
56
|
+
const childUnion = pipe(children, Array.map(rootGroupTypeMember), Array.join(" | "));
|
|
57
|
+
return Option.match(node.importBinding, {
|
|
58
|
+
onNone: () => `GroupSpec.GroupSpec<"Convex", "${node.segment}", never, ${childUnion}>`,
|
|
59
|
+
onSome: (binding) => `GroupSpec.AddGroups<typeof ${binding.localName}, ${childUnion}>`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const rootGroupTypeMember = (node) => `GroupSpec.NamedAt<${groupTypeExpr(node)}, "${node.segment}">`;
|
|
64
|
+
const render = Effect.fnUntraced(function* (nodes) {
|
|
65
|
+
const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });
|
|
66
|
+
yield* cbw.writeLine(Array.match(nodes, {
|
|
67
|
+
onEmpty: () => `import { Spec } from "@confect/core";`,
|
|
68
|
+
onNonEmpty: () => `import { GroupSpec, Spec } from "@confect/core";`
|
|
69
|
+
}));
|
|
70
|
+
yield* Effect.forEach(collectImportBindings(nodes), (binding) => cbw.writeLine(`import ${binding.localName} from "${binding.importPath}";`));
|
|
71
|
+
yield* cbw.blankLine();
|
|
72
|
+
yield* cbw.write("const spec: ");
|
|
73
|
+
yield* Array.match(nodes, {
|
|
74
|
+
onEmpty: () => cbw.write("Spec.Spec"),
|
|
75
|
+
onNonEmpty: (nonEmptyNodes) => Effect.gen(function* () {
|
|
76
|
+
yield* cbw.write("Spec.Spec<{\n");
|
|
77
|
+
yield* cbw.indent(Effect.forEach(nonEmptyNodes, (node) => cbw.writeLine(`readonly ${node.segment}: ${rootGroupTypeMember(node)};`)));
|
|
78
|
+
yield* cbw.write("}>");
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
yield* cbw.write(" = Spec.fromGroups({\n");
|
|
82
|
+
yield* cbw.indent(Effect.forEach(nodes, (node) => writeGroupEntry(cbw, node)));
|
|
83
|
+
yield* cbw.writeLine("}) as typeof spec;");
|
|
84
|
+
yield* cbw.blankLine();
|
|
85
|
+
yield* cbw.writeLine(`export default spec;`);
|
|
86
|
+
return yield* cbw.toString();
|
|
87
|
+
});
|
|
88
|
+
//#endregion
|
|
89
|
+
export { assemblyNodesFromLeaves, collectImportBindings, render };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=SpecAssemblyNode.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpecAssemblyNode.mjs","names":[],"sources":["../src/SpecAssemblyNode.ts"],"sourcesContent":["import type { LeafModule } from \"./LeafModule\";\nimport { pipe } from \"effect/Function\";\nimport * as Array from \"effect/Array\";\nimport * as Effect from \"effect/Effect\";\nimport { CodeBlockWriter } from \"./CodeBlockWriter\";\nimport * as Option from \"effect/Option\";\nimport * as Order from \"effect/Order\";\nimport * as Record from \"effect/Record\";\n\nexport interface SpecImportBinding {\n readonly importPath: string;\n readonly exportName: string;\n readonly localName: string;\n}\n\nexport interface SpecAssemblyNode {\n readonly segment: string;\n readonly importBinding: Option.Option<SpecImportBinding>;\n readonly children: ReadonlyArray<SpecAssemblyNode>;\n}\n\nconst importBindingFromLeaf = (leaf: LeafModule): SpecImportBinding => ({\n importPath: leaf.specImportPath,\n exportName: leaf.exportName,\n localName: Array.join(leaf.pathSegments, \"_\"),\n});\n\nconst assemblyNodesAtDepth = (\n leaves: ReadonlyArray<LeafModule>,\n depth: number,\n): ReadonlyArray<SpecAssemblyNode> =>\n pipe(\n Array.groupBy(leaves, (leaf) => leaf.pathSegments[depth]!),\n Record.toEntries,\n Array.sortBy(Order.mapInput(Order.String, ([segment]) => segment)),\n Array.map(([segment, groupLeaves]) => {\n const terminal = Array.findFirst(\n groupLeaves,\n (leaf) => leaf.pathSegments.length === depth + 1,\n );\n const descendants = Array.filter(\n groupLeaves,\n (leaf) => leaf.pathSegments.length > depth + 1,\n );\n return {\n segment,\n importBinding: Option.map(terminal, importBindingFromLeaf),\n children: assemblyNodesAtDepth(descendants, depth + 1),\n };\n }),\n );\n\nexport const assemblyNodesFromLeaves = (\n leaves: ReadonlyArray<LeafModule>,\n): ReadonlyArray<SpecAssemblyNode> => assemblyNodesAtDepth(leaves, 0);\n\nconst importBindingsForNode = (\n node: SpecAssemblyNode,\n): ReadonlyArray<SpecImportBinding> =>\n pipe(node.children, Array.flatMap(importBindingsForNode), (childBindings) =>\n Option.match(node.importBinding, {\n onNone: () => childBindings,\n onSome: (binding) => Array.prepend(childBindings, binding),\n }),\n );\n\nexport const collectImportBindings = (\n nodes: ReadonlyArray<SpecAssemblyNode>,\n): ReadonlyArray<SpecImportBinding> =>\n pipe(\n Array.flatMap(nodes, importBindingsForNode),\n (bindings) =>\n Record.fromIterableBy(bindings, (binding) => binding.importPath),\n Record.toEntries,\n Array.map(([, binding]) => binding),\n Array.sortBy(Order.mapInput(Order.String, (binding) => binding.localName)),\n );\n\nconst writeGroupEntry = Effect.fnUntraced(function* (\n cbw: CodeBlockWriter,\n node: SpecAssemblyNode,\n): Effect.fn.Return<void> {\n yield* cbw.write(\"[\");\n yield* cbw.quote(node.segment);\n yield* cbw.write(\"]: \");\n yield* writeGroupAssembly(cbw, node);\n yield* cbw.write(\",\\n\");\n});\n\nconst writeGroupAssembly: (\n cbw: CodeBlockWriter,\n node: SpecAssemblyNode,\n) => Effect.Effect<void> = Effect.fnUntraced(function* (cbw, node) {\n const group = Option.match(node.importBinding, {\n onNone: () => \"GroupSpec.make()\",\n onSome: (binding) => binding.localName,\n });\n yield* Array.match(node.children, {\n onEmpty: () => cbw.write(group),\n onNonEmpty: (children) =>\n Effect.gen(function* () {\n yield* cbw.write(`GroupSpec.addGroupsAt(${group}, {\\n`);\n yield* cbw.indent(\n Effect.forEach(children, (child) => writeGroupEntry(cbw, child)),\n );\n yield* cbw.write(\"})\");\n }),\n });\n});\n\nconst groupTypeExpr = (node: SpecAssemblyNode): string =>\n Array.match(node.children, {\n onEmpty: () =>\n Option.match(node.importBinding, {\n onNone: () =>\n `GroupSpec.GroupSpec<\"Convex\", \"${node.segment}\", never, never>`,\n onSome: (binding) => `typeof ${binding.localName}`,\n }),\n onNonEmpty: (children) => {\n const childUnion = pipe(\n children,\n Array.map(rootGroupTypeMember),\n Array.join(\" | \"),\n );\n return Option.match(node.importBinding, {\n onNone: () =>\n `GroupSpec.GroupSpec<\"Convex\", \"${node.segment}\", never, ${childUnion}>`,\n onSome: (binding) =>\n `GroupSpec.AddGroups<typeof ${binding.localName}, ${childUnion}>`,\n });\n },\n });\n\nconst rootGroupTypeMember = (node: SpecAssemblyNode): string =>\n `GroupSpec.NamedAt<${groupTypeExpr(node)}, \"${node.segment}\">`;\n\nexport const render = Effect.fnUntraced(function* (\n nodes: ReadonlyArray<SpecAssemblyNode>,\n) {\n const cbw = new CodeBlockWriter({ indentNumberOfSpaces: 2 });\n\n yield* cbw.writeLine(\n Array.match(nodes, {\n onEmpty: () => `import { Spec } from \"@confect/core\";`,\n onNonEmpty: () => `import { GroupSpec, Spec } from \"@confect/core\";`,\n }),\n );\n\n yield* Effect.forEach(collectImportBindings(nodes), (binding) =>\n cbw.writeLine(`import ${binding.localName} from \"${binding.importPath}\";`),\n );\n\n yield* cbw.blankLine();\n\n yield* cbw.write(\"const spec: \");\n yield* Array.match(nodes, {\n onEmpty: () => cbw.write(\"Spec.Spec\"),\n onNonEmpty: (nonEmptyNodes) =>\n Effect.gen(function* () {\n yield* cbw.write(\"Spec.Spec<{\\n\");\n yield* cbw.indent(\n Effect.forEach(nonEmptyNodes, (node) =>\n cbw.writeLine(\n `readonly ${node.segment}: ${rootGroupTypeMember(node)};`,\n ),\n ),\n );\n yield* cbw.write(\"}>\");\n }),\n });\n yield* cbw.write(\" = Spec.fromGroups({\\n\");\n yield* cbw.indent(\n Effect.forEach(nodes, (node) => writeGroupEntry(cbw, node)),\n );\n yield* cbw.writeLine(\"}) as typeof spec;\");\n yield* cbw.blankLine();\n yield* cbw.writeLine(`export default spec;`);\n\n return yield* cbw.toString();\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { withSuffix } from "./RelativeModulePath.mjs";
|
|
2
|
+
import { ImplModulePath } from "./ImplModulePath.mjs";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
//#region src/SpecModulePath.ts
|
|
5
|
+
const SpecModulePath = withSuffix(".spec.ts").pipe(Schema.brand("@confect/cli/SpecModulePath"));
|
|
6
|
+
const toImpl = (path) => ImplModulePath.make(`${path.slice(0, -8)}.impl.ts`);
|
|
7
|
+
//#endregion
|
|
8
|
+
export { SpecModulePath, toImpl };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=SpecModulePath.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpecModulePath.mjs","names":["RelativeModulePath.withSuffix"],"sources":["../src/SpecModulePath.ts"],"sourcesContent":["import * as Schema from \"effect/Schema\";\nimport { ImplModulePath } from \"./ImplModulePath\";\nimport * as RelativeModulePath from \"./RelativeModulePath\";\n\nexport const SpecModulePath = RelativeModulePath.withSuffix(\".spec.ts\").pipe(\n Schema.brand(\"@confect/cli/SpecModulePath\"),\n);\n\nexport type SpecModulePath = typeof SpecModulePath.Type;\n\nexport const toImpl = (path: SpecModulePath): ImplModulePath =>\n ImplModulePath.make(`${path.slice(0, -\".spec.ts\".length)}.impl.ts`);\n\nexport const fromImpl = (path: ImplModulePath): SpecModulePath =>\n SpecModulePath.make(`${path.slice(0, -\".impl.ts\".length)}.spec.ts`);\n"],"mappings":";;;;AAIA,MAAa,iBAAiBA,WAA8B,UAAU,CAAC,CAAC,KACtE,OAAO,MAAM,6BAA6B,CAC5C;AAIA,MAAa,UAAU,SACrB,eAAe,KAAK,GAAG,KAAK,MAAM,GAAG,EAAkB,EAAE,SAAS"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as Array from "effect/Array";
|
|
2
|
+
import * as HashSet from "effect/HashSet";
|
|
3
|
+
import * as Match from "effect/Match";
|
|
4
|
+
import * as HashMap from "effect/HashMap";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
//#region src/SyncState.ts
|
|
7
|
+
const Work = Data.taggedEnum();
|
|
8
|
+
const empty = {
|
|
9
|
+
work: Work.None(),
|
|
10
|
+
optional: HashSet.empty()
|
|
11
|
+
};
|
|
12
|
+
const isDirty = (pending) => !Work.$is("None")(pending.work) || !HashSet.isEmpty(pending.optional);
|
|
13
|
+
const full = (pending) => ({
|
|
14
|
+
...pending,
|
|
15
|
+
work: Work.Full()
|
|
16
|
+
});
|
|
17
|
+
const reconcile = (pending) => ({
|
|
18
|
+
...pending,
|
|
19
|
+
work: Work.$is("None")(pending.work) ? Work.Reconcile() : pending.work
|
|
20
|
+
});
|
|
21
|
+
const add = (pending, entries, forceFull = false) => {
|
|
22
|
+
const next = Array.reduce(entries, {
|
|
23
|
+
needsFull: forceFull || Work.$is("Full")(pending.work),
|
|
24
|
+
paths: Work.$match(pending.work, {
|
|
25
|
+
None: () => HashSet.empty(),
|
|
26
|
+
Reconcile: () => HashSet.empty(),
|
|
27
|
+
Full: () => HashSet.empty(),
|
|
28
|
+
Impls: ({ paths }) => HashSet.fromIterable(paths),
|
|
29
|
+
Declarations: ({ paths }) => HashSet.fromIterable(paths)
|
|
30
|
+
}),
|
|
31
|
+
declarations: Work.$match(pending.work, {
|
|
32
|
+
None: () => HashMap.empty(),
|
|
33
|
+
Reconcile: () => HashMap.empty(),
|
|
34
|
+
Full: () => HashMap.empty(),
|
|
35
|
+
Impls: () => HashMap.empty(),
|
|
36
|
+
Declarations: ({ entries: declarations }) => HashMap.fromIterable(declarations.map((entry) => [`${entry._tag}:${entry.relativePath}`, entry]))
|
|
37
|
+
}),
|
|
38
|
+
optional: pending.optional
|
|
39
|
+
}, (state, entry) => Match.value(entry).pipe(Match.tag("Impl", ({ relativePath }) => ({
|
|
40
|
+
...state,
|
|
41
|
+
paths: HashSet.add(state.paths, relativePath)
|
|
42
|
+
})), Match.tag("Http", "Crons", "Auth", ({ _tag }) => ({
|
|
43
|
+
...state,
|
|
44
|
+
optional: HashSet.add(state.optional, _tag)
|
|
45
|
+
})), Match.tag("Spec", "Table", (declaration) => ({
|
|
46
|
+
...state,
|
|
47
|
+
declarations: HashMap.set(state.declarations, `${declaration._tag}:${declaration.relativePath}`, declaration)
|
|
48
|
+
})), Match.tag("Config", "Env", "App", "Callbacks", () => ({
|
|
49
|
+
...state,
|
|
50
|
+
needsFull: true
|
|
51
|
+
})), Match.exhaustive));
|
|
52
|
+
const impls = Array.fromIterable(next.paths);
|
|
53
|
+
const declarations = Array.fromIterable(HashMap.values(next.declarations));
|
|
54
|
+
return {
|
|
55
|
+
work: next.needsFull ? Work.Full() : Array.isReadonlyArrayNonEmpty(declarations) ? Work.Declarations({
|
|
56
|
+
entries: declarations,
|
|
57
|
+
paths: impls
|
|
58
|
+
}) : Array.isReadonlyArrayNonEmpty(impls) ? Work.Impls({ paths: impls }) : Work.$is("Reconcile")(pending.work) ? Work.Reconcile() : Work.None(),
|
|
59
|
+
optional: next.optional
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Full generation writes a declared backend's `convex/http.ts` and
|
|
64
|
+
* `auth.config.ts` and rejects a component's `auth.ts`, so those edits need it
|
|
65
|
+
* instead of the optional-file writers.
|
|
66
|
+
*/
|
|
67
|
+
const forBackend = (pending, backend) => {
|
|
68
|
+
const generated = HashSet.filter(pending.optional, (file) => Match.value(file).pipe(Match.when("Http", () => backend.declared), Match.when("Auth", () => backend.declared || backend.component), Match.when("Crons", () => false), Match.exhaustive));
|
|
69
|
+
if (HashSet.isEmpty(generated)) return pending;
|
|
70
|
+
return {
|
|
71
|
+
work: Work.Full(),
|
|
72
|
+
optional: backend.declared ? HashSet.difference(pending.optional, generated) : pending.optional
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const Phase = Data.taggedEnum();
|
|
76
|
+
const Outcome = Data.taggedEnum();
|
|
77
|
+
const transition = (outcome) => Outcome.$match(outcome, {
|
|
78
|
+
Succeeded: ({ snapshot }) => Phase.Incremental({
|
|
79
|
+
snapshot,
|
|
80
|
+
failedImpls: HashSet.empty()
|
|
81
|
+
}),
|
|
82
|
+
GenerationFailed: () => Phase.NeedsFullSync(),
|
|
83
|
+
ReconciliationFailed: () => Phase.NeedsFullSync(),
|
|
84
|
+
ValidationFailed: ({ snapshot, paths }) => Phase.Incremental({
|
|
85
|
+
snapshot,
|
|
86
|
+
failedImpls: HashSet.fromIterable(paths)
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
const shouldRetry = (phase, outcome) => Phase.$is("Bootstrapping")(phase) && Outcome.$is("GenerationFailed")(outcome);
|
|
90
|
+
const needsDeploymentNotification = (phase) => Phase.$match(phase, {
|
|
91
|
+
Bootstrapping: () => true,
|
|
92
|
+
NeedsFullSync: () => true,
|
|
93
|
+
Incremental: ({ failedImpls }) => !HashSet.isEmpty(failedImpls)
|
|
94
|
+
});
|
|
95
|
+
const implsToValidate = (phase, pending) => Array.fromIterable(HashSet.union(Phase.$match(phase, {
|
|
96
|
+
Bootstrapping: () => HashSet.empty(),
|
|
97
|
+
NeedsFullSync: () => HashSet.empty(),
|
|
98
|
+
Incremental: ({ failedImpls }) => failedImpls
|
|
99
|
+
}), Work.$match(pending.work, {
|
|
100
|
+
None: () => HashSet.empty(),
|
|
101
|
+
Reconcile: () => HashSet.empty(),
|
|
102
|
+
Full: () => HashSet.empty(),
|
|
103
|
+
Impls: ({ paths }) => HashSet.fromIterable(paths),
|
|
104
|
+
Declarations: ({ paths }) => HashSet.fromIterable(paths)
|
|
105
|
+
})));
|
|
106
|
+
const Operation = Data.taggedEnum();
|
|
107
|
+
const plan = (phase, pending) => Phase.$match(phase, {
|
|
108
|
+
Bootstrapping: () => Operation.Generate(),
|
|
109
|
+
NeedsFullSync: () => Operation.Generate(),
|
|
110
|
+
Incremental: (ready) => Match.value(pending.work).pipe(Match.tag("Full", () => Operation.Generate()), Match.tag("None", "Reconcile", "Impls", () => Operation.Validate({
|
|
111
|
+
snapshot: ready.snapshot,
|
|
112
|
+
paths: implsToValidate(ready, pending)
|
|
113
|
+
})), Match.tag("Declarations", ({ entries }) => Operation.Update({
|
|
114
|
+
snapshot: ready.snapshot,
|
|
115
|
+
declarations: entries,
|
|
116
|
+
paths: implsToValidate(ready, pending)
|
|
117
|
+
})), Match.exhaustive)
|
|
118
|
+
});
|
|
119
|
+
//#endregion
|
|
120
|
+
export { Operation, Outcome, Phase, Work, add, empty, forBackend, full, implsToValidate, isDirty, needsDeploymentNotification, plan, reconcile, shouldRetry, transition };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=SyncState.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncState.mjs","names":[],"sources":["../src/SyncState.ts"],"sourcesContent":["import * as Array from \"effect/Array\";\nimport * as Data from \"effect/Data\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as HashMap from \"effect/HashMap\";\nimport * as Match from \"effect/Match\";\nimport type { Declaration, Kind, OptionalFile } from \"./EntryPoint\";\nimport type { ImplModulePath } from \"./ImplModulePath\";\nimport type { GenerationSnapshot } from \"./GenerationSnapshot\";\n\nexport type Work = Data.TaggedEnum<{\n None: {};\n Reconcile: {};\n Full: {};\n Impls: { readonly paths: Array.NonEmptyReadonlyArray<ImplModulePath> };\n Declarations: {\n readonly entries: Array.NonEmptyReadonlyArray<Declaration>;\n readonly paths: ReadonlyArray<ImplModulePath>;\n };\n}>;\nexport const Work = Data.taggedEnum<Work>();\n\nexport interface Pending {\n readonly work: Work;\n readonly optional: HashSet.HashSet<OptionalFile>;\n}\n\nexport const empty: Pending = { work: Work.None(), optional: HashSet.empty() };\nexport const isDirty = (pending: Pending): boolean =>\n !Work.$is(\"None\")(pending.work) || !HashSet.isEmpty(pending.optional);\nexport const full = (pending: Pending): Pending => ({\n ...pending,\n work: Work.Full(),\n});\n\nexport const reconcile = (pending: Pending): Pending => ({\n ...pending,\n work: Work.$is(\"None\")(pending.work) ? Work.Reconcile() : pending.work,\n});\n\nexport const add = (\n pending: Pending,\n entries: ReadonlyArray<Kind>,\n forceFull = false,\n): Pending => {\n const next = Array.reduce(\n entries,\n {\n needsFull: forceFull || Work.$is(\"Full\")(pending.work),\n paths: Work.$match(pending.work, {\n None: () => HashSet.empty<ImplModulePath>(),\n Reconcile: () => HashSet.empty<ImplModulePath>(),\n Full: () => HashSet.empty<ImplModulePath>(),\n Impls: ({ paths }) => HashSet.fromIterable(paths),\n Declarations: ({ paths }) => HashSet.fromIterable(paths),\n }),\n declarations: Work.$match(pending.work, {\n None: () => HashMap.empty<string, Declaration>(),\n Reconcile: () => HashMap.empty<string, Declaration>(),\n Full: () => HashMap.empty<string, Declaration>(),\n Impls: () => HashMap.empty<string, Declaration>(),\n Declarations: ({ entries: declarations }) =>\n HashMap.fromIterable(\n declarations.map(\n (entry) =>\n [`${entry._tag}:${entry.relativePath}`, entry] as const,\n ),\n ),\n }),\n optional: pending.optional,\n },\n (state, entry) =>\n Match.value(entry).pipe(\n Match.tag(\"Impl\", ({ relativePath }) => ({\n ...state,\n paths: HashSet.add(state.paths, relativePath),\n })),\n Match.tag(\"Http\", \"Crons\", \"Auth\", ({ _tag }) => ({\n ...state,\n optional: HashSet.add(state.optional, _tag),\n })),\n Match.tag(\"Spec\", \"Table\", (declaration) => ({\n ...state,\n declarations: HashMap.set(\n state.declarations,\n `${declaration._tag}:${declaration.relativePath}`,\n declaration,\n ),\n })),\n Match.tag(\"Config\", \"Env\", \"App\", \"Callbacks\", () => ({\n ...state,\n needsFull: true,\n })),\n Match.exhaustive,\n ),\n );\n const impls = Array.fromIterable(next.paths);\n const declarations = Array.fromIterable(HashMap.values(next.declarations));\n return {\n work: next.needsFull\n ? Work.Full()\n : Array.isReadonlyArrayNonEmpty(declarations)\n ? Work.Declarations({ entries: declarations, paths: impls })\n : Array.isReadonlyArrayNonEmpty(impls)\n ? Work.Impls({ paths: impls })\n : Work.$is(\"Reconcile\")(pending.work)\n ? Work.Reconcile()\n : Work.None(),\n optional: next.optional,\n };\n};\n\nexport interface Backend {\n readonly declared: boolean;\n readonly component: boolean;\n}\n\n/**\n * Full generation writes a declared backend's `convex/http.ts` and\n * `auth.config.ts` and rejects a component's `auth.ts`, so those edits need it\n * instead of the optional-file writers.\n */\nexport const forBackend = (pending: Pending, backend: Backend): Pending => {\n const generated = HashSet.filter(pending.optional, (file) =>\n Match.value(file).pipe(\n Match.when(\"Http\", () => backend.declared),\n Match.when(\"Auth\", () => backend.declared || backend.component),\n Match.when(\"Crons\", () => false),\n Match.exhaustive,\n ),\n );\n if (HashSet.isEmpty(generated)) return pending;\n return {\n work: Work.Full(),\n optional: backend.declared\n ? HashSet.difference(pending.optional, generated)\n : pending.optional,\n };\n};\n\nexport type Phase = Data.TaggedEnum<{\n Bootstrapping: {};\n NeedsFullSync: {};\n Incremental: {\n readonly failedImpls: HashSet.HashSet<ImplModulePath>;\n readonly snapshot: GenerationSnapshot;\n };\n}>;\nexport const Phase = Data.taggedEnum<Phase>();\n\nexport type Outcome = Data.TaggedEnum<{\n Succeeded: { readonly snapshot: GenerationSnapshot };\n GenerationFailed: {};\n ReconciliationFailed: {};\n ValidationFailed: {\n readonly snapshot: GenerationSnapshot;\n readonly paths: Array.NonEmptyReadonlyArray<ImplModulePath>;\n };\n}>;\nexport const Outcome = Data.taggedEnum<Outcome>();\n\nexport const transition = (outcome: Outcome): Phase =>\n Outcome.$match(outcome, {\n Succeeded: ({ snapshot }) =>\n Phase.Incremental({\n snapshot,\n failedImpls: HashSet.empty(),\n }),\n GenerationFailed: () => Phase.NeedsFullSync(),\n ReconciliationFailed: () => Phase.NeedsFullSync(),\n ValidationFailed: ({ snapshot, paths }) =>\n Phase.Incremental({\n snapshot,\n failedImpls: HashSet.fromIterable(paths),\n }),\n });\n\nexport const shouldRetry = (phase: Phase, outcome: Outcome): boolean =>\n Phase.$is(\"Bootstrapping\")(phase) && Outcome.$is(\"GenerationFailed\")(outcome);\n\nexport const needsDeploymentNotification = (phase: Phase): boolean =>\n Phase.$match(phase, {\n Bootstrapping: () => true,\n NeedsFullSync: () => true,\n Incremental: ({ failedImpls }) => !HashSet.isEmpty(failedImpls),\n });\n\nexport const implsToValidate = (\n phase: Phase,\n pending: Pending,\n): ReadonlyArray<ImplModulePath> =>\n Array.fromIterable(\n HashSet.union(\n Phase.$match(phase, {\n Bootstrapping: () => HashSet.empty<ImplModulePath>(),\n NeedsFullSync: () => HashSet.empty<ImplModulePath>(),\n Incremental: ({ failedImpls }) => failedImpls,\n }),\n Work.$match(pending.work, {\n None: () => HashSet.empty<ImplModulePath>(),\n Reconcile: () => HashSet.empty<ImplModulePath>(),\n Full: () => HashSet.empty<ImplModulePath>(),\n Impls: ({ paths }) => HashSet.fromIterable(paths),\n Declarations: ({ paths }) => HashSet.fromIterable(paths),\n }),\n ),\n );\n\nexport type Operation = Data.TaggedEnum<{\n Generate: {};\n Validate: {\n readonly snapshot: GenerationSnapshot;\n readonly paths: ReadonlyArray<ImplModulePath>;\n };\n Update: {\n readonly snapshot: GenerationSnapshot;\n readonly declarations: Array.NonEmptyReadonlyArray<Declaration>;\n readonly paths: ReadonlyArray<ImplModulePath>;\n };\n}>;\nexport const Operation = Data.taggedEnum<Operation>();\n\nexport const plan = (phase: Phase, pending: Pending): Operation =>\n Phase.$match(phase, {\n Bootstrapping: () => Operation.Generate(),\n NeedsFullSync: () => Operation.Generate(),\n Incremental: (ready) =>\n Match.value(pending.work).pipe(\n Match.tag(\"Full\", () => Operation.Generate()),\n Match.tag(\"None\", \"Reconcile\", \"Impls\", () =>\n Operation.Validate({\n snapshot: ready.snapshot,\n paths: implsToValidate(ready, pending),\n }),\n ),\n Match.tag(\"Declarations\", ({ entries }) =>\n Operation.Update({\n snapshot: ready.snapshot,\n declarations: entries,\n paths: implsToValidate(ready, pending),\n }),\n ),\n Match.exhaustive,\n ),\n });\n"],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { fromBundlerError } from "./BuildError.mjs";
|
|
2
|
+
import { bundle, withBatch } from "./Bundler.mjs";
|
|
3
|
+
import { DuplicateTableNameError, InvalidTableDefaultExportError, InvalidTableFilenameError } from "./CodegenError.mjs";
|
|
4
|
+
import { ConfectDirectory } from "./ConfectDirectory.mjs";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import { Identifier } from "@confect/core";
|
|
7
|
+
import * as FileSystem from "effect/FileSystem";
|
|
8
|
+
import * as Path from "effect/Path";
|
|
9
|
+
import * as Array from "effect/Array";
|
|
10
|
+
import { pipe } from "effect/Function";
|
|
11
|
+
import * as Order from "effect/Order";
|
|
12
|
+
import * as Table from "@confect/core/Table";
|
|
13
|
+
//#region src/TableModule.ts
|
|
14
|
+
const TABLES_DIRNAME = "tables";
|
|
15
|
+
const tableNameFromRelativePath = Effect.fnUntraced(function* (relativePath) {
|
|
16
|
+
const { name } = (yield* Path.Path).parse(relativePath);
|
|
17
|
+
return name;
|
|
18
|
+
});
|
|
19
|
+
const discoverTableFiles = Effect.gen(function* () {
|
|
20
|
+
const fs = yield* FileSystem.FileSystem;
|
|
21
|
+
const path = yield* Path.Path;
|
|
22
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
23
|
+
const tablesDirectory = path.join(confectDirectory, TABLES_DIRNAME);
|
|
24
|
+
if (!(yield* fs.exists(tablesDirectory))) return [];
|
|
25
|
+
const allPaths = yield* fs.readDirectory(tablesDirectory, { recursive: true });
|
|
26
|
+
return pipe(allPaths, Array.filter((p) => p.endsWith(".ts") && !p.endsWith(".test.ts")), Array.map((p) => path.join(TABLES_DIRNAME, p)));
|
|
27
|
+
});
|
|
28
|
+
const byTableName = Order.mapInput(Order.String, (tableModule) => tableModule.tableName);
|
|
29
|
+
/**
|
|
30
|
+
* Discover every `confect/tables/**\/*.ts` module by listing the directory.
|
|
31
|
+
* Validates that each filename is a legal table identifier—the table name is
|
|
32
|
+
* derived from the file basename, so the filename must be a valid JavaScript
|
|
33
|
+
* identifier with no leading underscore (Convex reserves `_<name>` for system
|
|
34
|
+
* tables).
|
|
35
|
+
*
|
|
36
|
+
* This step does _not_ bundle the table modules. It runs early in the codegen
|
|
37
|
+
* pipeline so the `_generated/id.ts` constructor can be emitted _before_ any
|
|
38
|
+
* user-authored table is bundled (those modules import from `_generated/id.ts`
|
|
39
|
+
* for cross-table refs).
|
|
40
|
+
*
|
|
41
|
+
* A missing `confect/tables/` directory is allowed and produces an empty list.
|
|
42
|
+
*
|
|
43
|
+
* Fails with {@link InvalidTableFilenameError} if any filename is not a valid
|
|
44
|
+
* table identifier, or {@link DuplicateTableNameError} if two files resolve to
|
|
45
|
+
* the same table name (the directory is scanned recursively but names are
|
|
46
|
+
* derived from the basename alone, so `tables/a/notes.ts` and
|
|
47
|
+
* `tables/b/notes.ts` would collide).
|
|
48
|
+
*/
|
|
49
|
+
const discover = Effect.gen(function* () {
|
|
50
|
+
const relativePaths = yield* discoverTableFiles;
|
|
51
|
+
const tableModules = yield* Effect.forEach(relativePaths, Effect.fnUntraced(function* (relativePath) {
|
|
52
|
+
const tableName = yield* tableNameFromRelativePath(relativePath);
|
|
53
|
+
yield* Effect.try({
|
|
54
|
+
try: () => Identifier.validateConfectTableIdentifier(tableName),
|
|
55
|
+
catch: (e) => new InvalidTableFilenameError({
|
|
56
|
+
tablePath: relativePath,
|
|
57
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
relativePath,
|
|
62
|
+
tableName
|
|
63
|
+
};
|
|
64
|
+
}), { concurrency: "unbounded" });
|
|
65
|
+
const sorted = pipe(tableModules, Array.sortBy(byTableName));
|
|
66
|
+
const collisions = Object.entries(Array.groupBy(sorted, (tableModule) => tableModule.tableName)).filter(([, group]) => group.length > 1).map(([tableName, group]) => ({
|
|
67
|
+
tableName,
|
|
68
|
+
tablePaths: Array.map(group, (tableModule) => tableModule.relativePath)
|
|
69
|
+
}));
|
|
70
|
+
if (collisions.length > 0) return yield* new DuplicateTableNameError({ collisions });
|
|
71
|
+
return sorted;
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Bundle every discovered table module and verify that its default export is an
|
|
75
|
+
* {@link Table.UnnamedTable} (the result of `Table.make(...)` before a name has
|
|
76
|
+
* been bound). Fails with {@link InvalidTableDefaultExportError} if any
|
|
77
|
+
* module's default export is missing or has the wrong shape.
|
|
78
|
+
*
|
|
79
|
+
* Must run _after_ `_generated/id.ts` has been emitted, because user-authored
|
|
80
|
+
* table modules typically `import { Id } from "../_generated/id"` for
|
|
81
|
+
* cross-table references.
|
|
82
|
+
*/
|
|
83
|
+
const validate = Effect.fnUntraced(function* (tableModules) {
|
|
84
|
+
const path = yield* Path.Path;
|
|
85
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
86
|
+
const validateRelativePath = Effect.fnUntraced(function* (relativePath) {
|
|
87
|
+
const absolutePath = path.resolve(confectDirectory, relativePath);
|
|
88
|
+
const { module } = yield* bundle(absolutePath).pipe(Effect.mapError((error) => fromBundlerError(relativePath, error)));
|
|
89
|
+
if (!Table.isUnnamedTable(module.default)) return yield* new InvalidTableDefaultExportError({ tablePath: relativePath });
|
|
90
|
+
});
|
|
91
|
+
const relativePaths = tableModules.map(({ relativePath }) => relativePath);
|
|
92
|
+
yield* withBatch(relativePaths.map((relativePath) => path.resolve(confectDirectory, relativePath)), Effect.forEach(relativePaths, validateRelativePath, { concurrency: 4 }));
|
|
93
|
+
});
|
|
94
|
+
//#endregion
|
|
95
|
+
export { TABLES_DIRNAME, discover, discoverTableFiles, validate };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=TableModule.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableModule.mjs","names":["Bundler.bundle","Bundler.withBatch"],"sources":["../src/TableModule.ts"],"sourcesContent":["import { Identifier } from \"@confect/core\";\nimport * as Table from \"@confect/core/Table\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as Path from \"effect/Path\";\nimport { pipe } from \"effect/Function\";\nimport * as Array from \"effect/Array\";\nimport * as Effect from \"effect/Effect\";\nimport * as Order from \"effect/Order\";\nimport { fromBundlerError } from \"./BuildError\";\nimport * as Bundler from \"./Bundler\";\nimport {\n DuplicateTableNameError,\n InvalidTableDefaultExportError,\n InvalidTableFilenameError,\n} from \"./CodegenError\";\nimport { ConfectDirectory } from \"./ConfectDirectory\";\n\nexport const TABLES_DIRNAME = \"tables\";\n\n/**\n * Discovered metadata for a single user-authored table module under\n * `confect/tables/`.\n *\n * - `relativePath`—path from `confect/` to the file (e.g. `tables/notes.ts`).\n * - `tableName`—the file basename (e.g. `notes`). This is also the import binding\n * used in generated files, and the table name surfaced to Convex.\n */\nexport interface TableModule {\n readonly relativePath: string;\n readonly tableName: string;\n}\n\nconst tableNameFromRelativePath = Effect.fnUntraced(function* (\n relativePath: string,\n) {\n const path = yield* Path.Path;\n const { name } = path.parse(relativePath);\n return name;\n});\n\nexport const discoverTableFiles = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n const tablesDirectory = path.join(confectDirectory, TABLES_DIRNAME);\n\n if (!(yield* fs.exists(tablesDirectory))) {\n return [] as ReadonlyArray<string>;\n }\n\n const allPaths = yield* fs.readDirectory(tablesDirectory, {\n recursive: true,\n });\n\n return pipe(\n allPaths,\n Array.filter((p) => p.endsWith(\".ts\") && !p.endsWith(\".test.ts\")),\n Array.map((p) => path.join(TABLES_DIRNAME, p)),\n );\n});\n\nconst byTableName = Order.mapInput(\n Order.String,\n (tableModule: TableModule) => tableModule.tableName,\n);\n\n/**\n * Discover every `confect/tables/**\\/*.ts` module by listing the directory.\n * Validates that each filename is a legal table identifier—the table name is\n * derived from the file basename, so the filename must be a valid JavaScript\n * identifier with no leading underscore (Convex reserves `_<name>` for system\n * tables).\n *\n * This step does _not_ bundle the table modules. It runs early in the codegen\n * pipeline so the `_generated/id.ts` constructor can be emitted _before_ any\n * user-authored table is bundled (those modules import from `_generated/id.ts`\n * for cross-table refs).\n *\n * A missing `confect/tables/` directory is allowed and produces an empty list.\n *\n * Fails with {@link InvalidTableFilenameError} if any filename is not a valid\n * table identifier, or {@link DuplicateTableNameError} if two files resolve to\n * the same table name (the directory is scanned recursively but names are\n * derived from the basename alone, so `tables/a/notes.ts` and\n * `tables/b/notes.ts` would collide).\n */\nexport const discover = Effect.gen(function* () {\n const relativePaths = yield* discoverTableFiles;\n\n const tableModules = yield* Effect.forEach(\n relativePaths,\n Effect.fnUntraced(function* (relativePath: string) {\n const tableName = yield* tableNameFromRelativePath(relativePath);\n yield* Effect.try({\n try: () => Identifier.validateConfectTableIdentifier(tableName),\n catch: (e) =>\n new InvalidTableFilenameError({\n tablePath: relativePath,\n reason: e instanceof Error ? e.message : String(e),\n }),\n });\n return { relativePath, tableName } satisfies TableModule;\n }),\n { concurrency: \"unbounded\" },\n );\n\n const sorted = pipe(tableModules, Array.sortBy(byTableName));\n\n const collisions = Object.entries(\n Array.groupBy(sorted, (tableModule) => tableModule.tableName),\n )\n .filter(([, group]) => group.length > 1)\n .map(([tableName, group]) => ({\n tableName,\n tablePaths: Array.map(group, (tableModule) => tableModule.relativePath),\n }));\n\n if (collisions.length > 0) {\n return yield* new DuplicateTableNameError({ collisions });\n }\n\n return sorted;\n});\n\n/**\n * Bundle every discovered table module and verify that its default export is an\n * {@link Table.UnnamedTable} (the result of `Table.make(...)` before a name has\n * been bound). Fails with {@link InvalidTableDefaultExportError} if any\n * module's default export is missing or has the wrong shape.\n *\n * Must run _after_ `_generated/id.ts` has been emitted, because user-authored\n * table modules typically `import { Id } from \"../_generated/id\"` for\n * cross-table references.\n */\nexport const validate = Effect.fnUntraced(function* (\n tableModules: ReadonlyArray<TableModule>,\n) {\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const validateRelativePath = Effect.fnUntraced(function* (\n relativePath: string,\n ) {\n const absolutePath = path.resolve(confectDirectory, relativePath);\n const { module } = yield* Bundler.bundle(absolutePath).pipe(\n Effect.mapError((error) => fromBundlerError(relativePath, error)),\n );\n\n if (!Table.isUnnamedTable(module.default)) {\n return yield* new InvalidTableDefaultExportError({\n tablePath: relativePath,\n });\n }\n });\n\n const relativePaths = tableModules.map(({ relativePath }) => relativePath);\n yield* Bundler.withBatch(\n relativePaths.map((relativePath) =>\n path.resolve(confectDirectory, relativePath),\n ),\n Effect.forEach(relativePaths, validateRelativePath, { concurrency: 4 }),\n );\n});\n"],"mappings":";;;;;;;;;;;;;AAiBA,MAAa,iBAAiB;AAe9B,MAAM,4BAA4B,OAAO,WAAW,WAClD,cACA;CAEA,MAAM,EAAE,UAAS,OADG,KAAK,KAAA,CACH,MAAM,YAAY;CACxC,OAAO;AACT,CAAC;AAED,MAAa,qBAAqB,OAAO,IAAI,aAAa;CACxD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,mBAAmB,OAAO,iBAAiB;CACjD,MAAM,kBAAkB,KAAK,KAAK,kBAAkB,cAAc;CAElE,IAAI,EAAE,OAAO,GAAG,OAAO,eAAe,IACpC,OAAO,CAAC;CAGV,MAAM,WAAW,OAAO,GAAG,cAAc,iBAAiB,EACxD,WAAW,KACb,CAAC;CAED,OAAO,KACL,UACA,MAAM,QAAQ,MAAM,EAAE,SAAS,KAAK,KAAK,CAAC,EAAE,SAAS,UAAU,CAAC,GAChE,MAAM,KAAK,MAAM,KAAK,KAAK,gBAAgB,CAAC,CAAC,CAC/C;AACF,CAAC;AAED,MAAM,cAAc,MAAM,SACxB,MAAM,SACL,gBAA6B,YAAY,SAC5C;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,WAAW,OAAO,IAAI,aAAa;CAC9C,MAAM,gBAAgB,OAAO;CAE7B,MAAM,eAAe,OAAO,OAAO,QACjC,eACA,OAAO,WAAW,WAAW,cAAsB;EACjD,MAAM,YAAY,OAAO,0BAA0B,YAAY;EAC/D,OAAO,OAAO,IAAI;GAChB,WAAW,WAAW,+BAA+B,SAAS;GAC9D,QAAQ,MACN,IAAI,0BAA0B;IAC5B,WAAW;IACX,QAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACnD,CAAC;EACL,CAAC;EACD,OAAO;GAAE;GAAc;EAAU;CACnC,CAAC,GACD,EAAE,aAAa,YAAY,CAC7B;CAEA,MAAM,SAAS,KAAK,cAAc,MAAM,OAAO,WAAW,CAAC;CAE3D,MAAM,aAAa,OAAO,QACxB,MAAM,QAAQ,SAAS,gBAAgB,YAAY,SAAS,CAC9D,CAAC,CACE,QAAQ,GAAG,WAAW,MAAM,SAAS,CAAC,CAAC,CACvC,KAAK,CAAC,WAAW,YAAY;EAC5B;EACA,YAAY,MAAM,IAAI,QAAQ,gBAAgB,YAAY,YAAY;CACxE,EAAE;CAEJ,IAAI,WAAW,SAAS,GACtB,OAAO,OAAO,IAAI,wBAAwB,EAAE,WAAW,CAAC;CAG1D,OAAO;AACT,CAAC;;;;;;;;;;;AAYD,MAAa,WAAW,OAAO,WAAW,WACxC,cACA;CACA,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,mBAAmB,OAAO,iBAAiB;CAEjD,MAAM,uBAAuB,OAAO,WAAW,WAC7C,cACA;EACA,MAAM,eAAe,KAAK,QAAQ,kBAAkB,YAAY;EAChE,MAAM,EAAE,WAAW,OAAOA,OAAe,YAAY,CAAC,CAAC,KACrD,OAAO,UAAU,UAAU,iBAAiB,cAAc,KAAK,CAAC,CAClE;EAEA,IAAI,CAAC,MAAM,eAAe,OAAO,OAAO,GACtC,OAAO,OAAO,IAAI,+BAA+B,EAC/C,WAAW,aACb,CAAC;CAEL,CAAC;CAED,MAAM,gBAAgB,aAAa,KAAK,EAAE,mBAAmB,YAAY;CACzE,OAAOC,UACL,cAAc,KAAK,iBACjB,KAAK,QAAQ,kBAAkB,YAAY,CAC7C,GACA,OAAO,QAAQ,eAAe,sBAAsB,EAAE,aAAa,EAAE,CAAC,CACxE;AACF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { withSuffix } from "./RelativeModulePath.mjs";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
//#region src/TableModulePath.ts
|
|
4
|
+
const TableModulePath = withSuffix(".ts").check(Schema.makeFilter((value) => value.split(/[/\\]/)[0] === "tables")).pipe(Schema.brand("@confect/cli/TableModulePath"));
|
|
5
|
+
//#endregion
|
|
6
|
+
export { TableModulePath };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=TableModulePath.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableModulePath.mjs","names":["RelativeModulePath.withSuffix"],"sources":["../src/TableModulePath.ts"],"sourcesContent":["import * as Schema from \"effect/Schema\";\nimport * as RelativeModulePath from \"./RelativeModulePath\";\n\nexport const TableModulePath = RelativeModulePath.withSuffix(\".ts\")\n .check(Schema.makeFilter((value) => value.split(/[/\\\\]/)[0] === \"tables\"))\n .pipe(Schema.brand(\"@confect/cli/TableModulePath\"));\n\nexport type TableModulePath = typeof TableModulePath.Type;\n"],"mappings":";;;AAGA,MAAa,kBAAkBA,WAA8B,KAAK,CAAC,CAChE,MAAM,OAAO,YAAY,UAAU,MAAM,MAAM,OAAO,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,CACzE,KAAK,OAAO,MAAM,8BAA8B,CAAC"}
|