@confect/cli 9.0.0-next.1 → 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 +337 -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 +29 -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 +19 -26
  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 +203 -142
  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 +132 -45
  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 -376
  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 -317
  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 -710
  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 -482
  69. package/src/utils.ts +0 -429
package/package.json CHANGED
@@ -1,42 +1,14 @@
1
1
  {
2
2
  "name": "@confect/cli",
3
- "version": "9.0.0-next.1",
4
3
  "description": "Developer tooling for codegen and sync",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/rjdellecese/confect.git"
8
- },
9
- "bugs": {
10
- "url": "https://github.com/rjdellecese/confect/issues"
11
- },
12
- "homepage": "https://confect.dev",
13
- "sideEffects": false,
14
- "type": "module",
4
+ "version": "9.0.0-next.10",
5
+ "author": "RJ Dellecese",
15
6
  "bin": {
16
7
  "confect": "./dist/index.mjs"
17
8
  },
18
- "files": [
19
- "CHANGELOG.md",
20
- "LICENSE",
21
- "README.md",
22
- "dist",
23
- "package.json",
24
- "src"
25
- ],
26
- "exports": {
27
- ".": {
28
- "types": "./dist/index.d.ts",
29
- "default": "./dist/index.js"
30
- },
31
- "./package.json": "./package.json"
9
+ "bugs": {
10
+ "url": "https://github.com/rjdellecese/confect/issues"
32
11
  },
33
- "keywords": [
34
- "effect",
35
- "convex",
36
- "cli"
37
- ],
38
- "author": "RJ Dellecese",
39
- "license": "ISC",
40
12
  "dependencies": {
41
13
  "@effect/cli": "^0.75.1",
42
14
  "@effect/platform": "0.96.1",
@@ -44,45 +16,56 @@
44
16
  "@effect/platform-node-shared": "0.59.0",
45
17
  "@effect/printer": "^0.49.0",
46
18
  "@effect/printer-ansi": "^0.49.0",
19
+ "bundle-require": "^5.1.0",
47
20
  "code-block-writer": "^13.0.3",
48
21
  "esbuild": "^0.27.3"
49
22
  },
50
23
  "devDependencies": {
51
24
  "@effect/language-service": "0.86.1",
52
25
  "@effect/vitest": "0.29.0",
53
- "@eslint/js": "10.0.1",
54
26
  "@types/node": "25.3.3",
55
27
  "@vitest/coverage-v8": "3.2.4",
56
28
  "effect": "3.21.2",
57
- "eslint": "10.0.2",
58
- "prettier": "3.8.1",
59
29
  "tsdown": "0.20.3",
60
30
  "typescript": "5.9.3",
61
- "typescript-eslint": "8.56.1",
62
31
  "vite": "7.3.1",
63
32
  "vite-tsconfig-paths": "6.1.1",
64
33
  "vitest": "3.2.4"
65
34
  },
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "exports": {
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "CHANGELOG.md",
43
+ "LICENSE",
44
+ "README.md",
45
+ "dist",
46
+ "package.json"
47
+ ],
48
+ "homepage": "https://confect.dev",
49
+ "keywords": [
50
+ "cli",
51
+ "convex",
52
+ "effect"
53
+ ],
54
+ "license": "ISC",
66
55
  "peerDependencies": {
67
56
  "effect": "^3.21.2",
68
- "@confect/core": "^9.0.0-next.1",
69
- "@confect/server": "^9.0.0-next.1"
57
+ "@confect/core": "^9.0.0-next.10",
58
+ "@confect/server": "^9.0.0-next.10"
70
59
  },
71
- "engines": {
72
- "node": ">=22",
73
- "pnpm": ">=10"
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/rjdellecese/confect.git"
74
63
  },
75
- "main": "./dist/index.js",
76
- "module": "./dist/index.js",
77
- "types": "./dist/index.d.ts",
64
+ "type": "module",
78
65
  "scripts": {
79
- "build": "tsdown --config-loader unrun",
66
+ "clean": "rm -rf dist coverage node_modules",
80
67
  "dev": "tsdown --watch --config-loader unrun",
81
68
  "test": "vitest run",
82
- "typecheck": "tsc --noEmit --project tsconfig.json",
83
- "fix": "prettier --write . && eslint --fix . --max-warnings=0",
84
- "format": "prettier --check .",
85
- "lint": "eslint . --max-warnings=0",
86
- "clean": "rm -rf dist coverage node_modules"
69
+ "typecheck": "tsc --noEmit --project tsconfig.json"
87
70
  }
88
71
  }
package/dist/index.d.mts DELETED
@@ -1 +0,0 @@
1
- export { };
package/src/BuildError.ts DELETED
@@ -1,210 +0,0 @@
1
- import { Ansi, AnsiDoc } from "@effect/printer-ansi";
2
- import { Array, Effect, Match, Option, pipe, Schema, String } from "effect";
3
- import * as esbuild from "esbuild";
4
- import { formatPathDoc } from "./log";
5
-
6
- // --- Variants ---
7
-
8
- export class BundleFailedError extends Schema.TaggedError<BundleFailedError>()(
9
- "BundleFailedError",
10
- {
11
- file: Schema.String,
12
- errors: Schema.Array(Schema.Unknown),
13
- },
14
- ) {}
15
-
16
- export class ImportFailedError extends Schema.TaggedError<ImportFailedError>()(
17
- "ImportFailedError",
18
- {
19
- file: Schema.String,
20
- cause: Schema.Unknown,
21
- },
22
- ) {}
23
-
24
- export const BuildError = Schema.Union(BundleFailedError, ImportFailedError);
25
- export type BuildError = typeof BuildError.Type;
26
-
27
- export const isBuildError = (error: unknown): error is BuildError =>
28
- Schema.is(BuildError)(error);
29
-
30
- // --- Bundler adapter ---
31
-
32
- /**
33
- * Internal failure produced by the esbuild bundle/import pipeline. Always
34
- * remapped to a {@link BuildError} (which carries enough context for the CLI
35
- * to render it) before reaching a user-surface boundary.
36
- */
37
- export class BundlerError extends Schema.TaggedError<BundlerError>()(
38
- "BundlerError",
39
- {
40
- cause: Schema.Unknown,
41
- },
42
- ) {}
43
-
44
- const isEsbuildBuildFailure = (error: unknown): error is esbuild.BuildFailure =>
45
- typeof error === "object" &&
46
- error !== null &&
47
- "errors" in error &&
48
- globalThis.Array.isArray((error as esbuild.BuildFailure).errors);
49
-
50
- export const fromBundlerError = (
51
- file: string,
52
- error: BundlerError,
53
- ): BuildError =>
54
- isEsbuildBuildFailure(error.cause)
55
- ? new BundleFailedError({ file, errors: error.cause.errors })
56
- : new ImportFailedError({ file, cause: error.cause });
57
-
58
- // --- Rendering ---
59
-
60
- const cross = pipe(AnsiDoc.char("✘"), AnsiDoc.annotate(Ansi.red));
61
-
62
- const errorGutter = pipe(
63
- AnsiDoc.char("│"),
64
- AnsiDoc.annotate(Ansi.red),
65
- AnsiDoc.render({ style: "pretty" }),
66
- );
67
-
68
- const withErrorGutterBlock = (output: string): string =>
69
- pipe(
70
- String.split(output, "\n"),
71
- Array.map((line) =>
72
- pipe(line, String.trim) === "" ? errorGutter : `${errorGutter} ${line}`,
73
- ),
74
- Array.join("\n"),
75
- (guttered) => `${errorGutter}\n${guttered}\n${errorGutter}`,
76
- );
77
-
78
- const formatBuildMessage = (
79
- error: esbuild.Message | undefined,
80
- formattedMessage: string,
81
- ): string => {
82
- const lines = String.split(formattedMessage, "\n");
83
- const redErrorText = pipe(
84
- AnsiDoc.text(error?.text ?? ""),
85
- AnsiDoc.annotate(Ansi.red),
86
- AnsiDoc.render({ style: "pretty" }),
87
- );
88
- const replaced = pipe(
89
- Array.findFirstIndex(lines, (l) => pipe(l, String.trim, String.isNonEmpty)),
90
- Option.match({
91
- onNone: () => lines,
92
- onSome: (index) => Array.modify(lines, index, () => redErrorText),
93
- }),
94
- );
95
- return pipe(replaced, Array.join("\n"));
96
- };
97
-
98
- /**
99
- * Render a list of esbuild messages into a styled, gutter-prefixed block.
100
- * Used by both {@link renderBundleFailedError} and the dev-mode esbuild
101
- * watcher's `onEnd` hook (where the messages don't flow through the
102
- * tagged-error pipeline).
103
- */
104
- export const formatEsbuildMessages = (
105
- errors: readonly esbuild.Message[],
106
- formattedMessages: readonly string[],
107
- ): string =>
108
- pipe(
109
- formattedMessages,
110
- Array.map((message, i) => formatBuildMessage(errors[i], message)),
111
- Array.join(""),
112
- String.trimEnd,
113
- withErrorGutterBlock,
114
- );
115
-
116
- const renderImportFailedError = (error: ImportFailedError): AnsiDoc.AnsiDoc => {
117
- const causeMessage =
118
- error.cause instanceof Error
119
- ? error.cause.message
120
- : typeof error.cause === "string"
121
- ? error.cause
122
- : globalThis.String(error.cause);
123
- const oneLineCause = pipe(
124
- String.split(causeMessage, "\n"),
125
- Array.findFirst((line) => pipe(line, String.trim, String.isNonEmpty)),
126
- Option.map(String.trim),
127
- Option.getOrElse(() => "unknown error"),
128
- );
129
- return pipe(
130
- cross,
131
- AnsiDoc.catWithSpace(
132
- AnsiDoc.hcat([
133
- AnsiDoc.text("Failed to load bundled module "),
134
- formatPathDoc(error.file),
135
- AnsiDoc.text(
136
- `: ${oneLineCause}; check the file's top-level imports and side effects.`,
137
- ),
138
- ]),
139
- ),
140
- );
141
- };
142
-
143
- /**
144
- * Render a {@link BundleFailedError} as a multi-line, ANSI-styled string:
145
- * a one-line `✘ <file>: build errors` header followed by the
146
- * gutter-prefixed esbuild diagnostic block. Multi-line is appropriate
147
- * here because a single `BundleFailedError` carries an array of distinct
148
- * esbuild messages.
149
- */
150
- const renderBundleFailedError = (error: BundleFailedError): string => {
151
- const messages = error.errors as readonly esbuild.Message[];
152
- const formatted = esbuild.formatMessagesSync(messages as esbuild.Message[], {
153
- kind: "error",
154
- color: true,
155
- terminalWidth: 80,
156
- });
157
- const header = pipe(
158
- cross,
159
- AnsiDoc.catWithSpace(
160
- AnsiDoc.hcat([formatPathDoc(error.file), AnsiDoc.text(": build errors")]),
161
- ),
162
- AnsiDoc.render({ style: "pretty" }),
163
- );
164
- return `${header}\n${formatEsbuildMessages(messages, formatted)}`;
165
- };
166
-
167
- /**
168
- * Render any {@link BuildError} into a styled, ready-to-print string.
169
- * `ImportFailedError` collapses to a single line; `BundleFailedError`
170
- * expands to a header plus one diagnostic block per esbuild message.
171
- */
172
- export const renderBuildError = (error: BuildError): string =>
173
- Match.value(error).pipe(
174
- Match.tag("BundleFailedError", renderBundleFailedError),
175
- Match.tag("ImportFailedError", (e) =>
176
- pipe(renderImportFailedError(e), AnsiDoc.render({ style: "pretty" })),
177
- ),
178
- Match.exhaustive,
179
- );
180
-
181
- export const logBuildError = (error: BuildError) =>
182
- Effect.sync(() => console.error(renderBuildError(error)));
183
-
184
- /**
185
- * Render a flat list of esbuild messages as a single error block with a
186
- * generic `✘ Build errors` header. Used by dev-mode when multiple
187
- * watchers surface the same underlying error and we want to log the
188
- * coalesced set rather than one block per watcher.
189
- */
190
- const renderCoalescedBuildErrors = (
191
- messages: readonly esbuild.Message[],
192
- ): string => {
193
- const formatted = esbuild.formatMessagesSync(messages as esbuild.Message[], {
194
- kind: "error",
195
- color: true,
196
- terminalWidth: 80,
197
- });
198
- const header = pipe(
199
- cross,
200
- AnsiDoc.catWithSpace(AnsiDoc.text("Build errors")),
201
- AnsiDoc.render({ style: "pretty" }),
202
- );
203
- return `${header}\n${formatEsbuildMessages(messages, formatted)}`;
204
- };
205
-
206
- export const logCoalescedBuildErrors = (messages: readonly esbuild.Message[]) =>
207
- Effect.sync(() => {
208
- if (messages.length === 0) return;
209
- console.error(renderCoalescedBuildErrors(messages));
210
- });
package/src/Bundler.ts DELETED
@@ -1,144 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { pathToFileURL } from "node:url";
3
- import { Path } from "@effect/platform";
4
- import { Array, Effect, Option, pipe } from "effect";
5
- import * as esbuild from "esbuild";
6
- import { BundlerError } from "./BuildError";
7
-
8
- export interface Bundled {
9
- readonly module: any;
10
- readonly metafile: esbuild.Metafile;
11
- }
12
-
13
- export const EXTERNAL_PACKAGES = [
14
- "@confect/core",
15
- "@confect/server",
16
- "effect",
17
- "@effect/*",
18
- ];
19
-
20
- const isExternalImport = (path: string) =>
21
- EXTERNAL_PACKAGES.some((p) => {
22
- if (p.endsWith("/*")) {
23
- return path.startsWith(p.slice(0, -1));
24
- }
25
- return path === p || path.startsWith(p + "/");
26
- });
27
-
28
- export const absoluteExternalsPlugin: esbuild.Plugin = {
29
- name: "absolute-externals",
30
- setup(build) {
31
- build.onResolve({ filter: /.*/ }, async (args) => {
32
- if (args.kind !== "import-statement" && args.kind !== "dynamic-import")
33
- return;
34
- if (!isExternalImport(args.path)) return;
35
- // `import.meta.resolve`'s second argument is silently ignored in modern
36
- // Node, so resolution would always walk up from the CLI's bundled file
37
- // (`packages/cli/dist/utils.mjs`) instead of from the user's project.
38
- // Use `createRequire` keyed on the importing file's directory so we
39
- // resolve out of *their* `node_modules`. The synthetic filename is just
40
- // a CommonJS resolution anchor; the file does not need to exist.
41
- const parentFile = pathToFileURL(args.resolveDir + "/_").href;
42
- const require_ = createRequire(parentFile);
43
- const resolvedPath = require_.resolve(args.path);
44
- const resolved = pathToFileURL(resolvedPath).href;
45
- return { path: resolved, external: true };
46
- });
47
- },
48
- };
49
-
50
- const buildEntry = (entryPoint: string) =>
51
- Effect.tryPromise({
52
- try: () =>
53
- esbuild.build({
54
- entryPoints: [entryPoint],
55
- bundle: true,
56
- write: false,
57
- platform: "node",
58
- format: "esm",
59
- logLevel: "silent",
60
- metafile: true,
61
- plugins: [absoluteExternalsPlugin],
62
- }),
63
- catch: (cause) => new BundlerError({ cause }),
64
- });
65
-
66
- const importBundledModule = (result: esbuild.BuildResult) => {
67
- const code = result.outputFiles![0]!.text;
68
- const dataUrl =
69
- "data:text/javascript;base64," + Buffer.from(code).toString("base64");
70
- return import(dataUrl);
71
- };
72
-
73
- /**
74
- * Bundle a TypeScript entry point with esbuild and import the result via a
75
- * data URL. This handles extensionless `.ts` imports regardless of whether
76
- * the user's project sets `"type": "module"` in package.json. The returned
77
- * pair carries both the imported module and the esbuild metafile so callers
78
- * can inspect the import graph (see {@link directlyImports}).
79
- */
80
- export const bundle = (
81
- entryPoint: string,
82
- ): Effect.Effect<Bundled, BundlerError> =>
83
- Effect.gen(function* () {
84
- const result = yield* buildEntry(entryPoint);
85
- const module = yield* Effect.tryPromise({
86
- try: () => importBundledModule(result),
87
- catch: (cause) => new BundlerError({ cause }),
88
- });
89
- if (!result.metafile) {
90
- return yield* Effect.dieMessage("esbuild metafile missing");
91
- }
92
- return { module, metafile: result.metafile };
93
- });
94
-
95
- const findMetafileInputKey = (
96
- metafile: esbuild.Metafile,
97
- absolutePath: string,
98
- ) =>
99
- Effect.gen(function* () {
100
- const path = yield* Path.Path;
101
- const resolved = path.resolve(absolutePath);
102
- return Array.findFirst(
103
- Object.keys(metafile.inputs),
104
- (key) => path.resolve(key) === resolved,
105
- );
106
- });
107
-
108
- /**
109
- * Returns `true` when the module bundled from `sourceAbsolutePath` declares a
110
- * direct import of `targetAbsolutePath` (according to the bundle's esbuild
111
- * metafile). Returns `false` if either path is missing from the metafile.
112
- */
113
- export const directlyImports = (
114
- bundled: Bundled,
115
- sourceAbsolutePath: string,
116
- targetAbsolutePath: string,
117
- ) =>
118
- Effect.gen(function* () {
119
- const path = yield* Path.Path;
120
- const sourceKey = yield* findMetafileInputKey(
121
- bundled.metafile,
122
- sourceAbsolutePath,
123
- );
124
- const targetKey = yield* findMetafileInputKey(
125
- bundled.metafile,
126
- targetAbsolutePath,
127
- );
128
-
129
- return pipe(
130
- Option.all([sourceKey, targetKey]),
131
- Option.flatMap(([sourceKey_, targetKey_]) =>
132
- Option.fromNullable(bundled.metafile.inputs[sourceKey_]).pipe(
133
- Option.map((sourceInput) => {
134
- const targetResolved = path.resolve(targetKey_);
135
- return sourceInput.imports.some(
136
- (importedFile) =>
137
- path.resolve(importedFile.path) === targetResolved,
138
- );
139
- }),
140
- ),
141
- ),
142
- Option.getOrElse(() => false),
143
- );
144
- });
@@ -1,65 +0,0 @@
1
- import type { Options as CodeBlockWriterOptions } from "code-block-writer";
2
- import CodeBlockWriter_ from "code-block-writer";
3
- import { Effect } from "effect";
4
-
5
- export class CodeBlockWriter {
6
- private readonly writer: CodeBlockWriter_;
7
-
8
- constructor(opts?: Partial<CodeBlockWriterOptions>) {
9
- this.writer = new CodeBlockWriter_(opts);
10
- }
11
-
12
- indent<E = never, R = never>(
13
- effect: Effect.Effect<void, E, R>,
14
- ): Effect.Effect<void, E, R> {
15
- return Effect.gen(this, function* () {
16
- const indentationLevel = this.writer.getIndentationLevel();
17
- this.writer.setIndentationLevel(indentationLevel + 1);
18
- yield* effect;
19
- this.writer.setIndentationLevel(indentationLevel);
20
- });
21
- }
22
-
23
- writeLine<E = never, R = never>(line: string): Effect.Effect<void, E, R> {
24
- return Effect.sync(() => {
25
- this.writer.writeLine(line);
26
- });
27
- }
28
-
29
- write<E = never, R = never>(text: string): Effect.Effect<void, E, R> {
30
- return Effect.sync(() => {
31
- this.writer.write(text);
32
- });
33
- }
34
-
35
- quote<E = never, R = never>(text: string): Effect.Effect<void, E, R> {
36
- return Effect.sync(() => {
37
- this.writer.quote(text);
38
- });
39
- }
40
-
41
- conditionalWriteLine<E = never, R = never>(
42
- condition: boolean,
43
- text: string,
44
- ): Effect.Effect<void, E, R> {
45
- return Effect.sync(() => {
46
- this.writer.conditionalWriteLine(condition, text);
47
- });
48
- }
49
-
50
- newLine<E = never, R = never>(): Effect.Effect<void, E, R> {
51
- return Effect.sync(() => {
52
- this.writer.newLine();
53
- });
54
- }
55
-
56
- blankLine<E = never, R = never>(): Effect.Effect<void, E, R> {
57
- return Effect.sync(() => {
58
- this.writer.blankLine();
59
- });
60
- }
61
-
62
- toString<E = never, R = never>(): Effect.Effect<string, E, R> {
63
- return Effect.sync(() => this.writer.toString());
64
- }
65
- }