@confect/cli 9.0.0-next.3 → 9.0.0-next.4
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 +21 -0
- package/dist/Bundler.mjs +34 -15
- package/dist/Bundler.mjs.map +1 -1
- package/dist/confect/dev.mjs +1 -2
- package/dist/confect/dev.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +3 -3
- package/src/Bundler.ts +47 -26
- package/src/confect/dev.ts +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @confect/cli
|
|
2
2
|
|
|
3
|
+
## 9.0.0-next.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 46e17f7: Externalize every bare-specifier dependency during codegen bundling instead of inlining third-party packages.
|
|
8
|
+
|
|
9
|
+
### Why
|
|
10
|
+
|
|
11
|
+
Since `9.0.0-next.0`, codegen bundles each `*.impl.ts` with esbuild so it can load the default-exported `Layer` and read the snapshotted function names from a `Finalized` `GroupImpl`. The bundler only marked `@confect/core`, `@confect/server`, `effect`, and `@effect/*` as external — every other dependency, including all of `node_modules` and every `node:*` built-in reached through inlined CJS source, was bundled into the output. With `format: "esm"`, esbuild rewrites any CJS `require(...)` in that inlined source to a runtime shim that throws `Dynamic require of "<id>" is not supported`, so any impl importing a third-party package (`sharp`, `luxon`, `@clerk/backend`, `jsonwebtoken`, `openai`, etc.) failed during `validateImpl`.
|
|
12
|
+
|
|
13
|
+
### What changed
|
|
14
|
+
- `@confect/cli/Bundler`'s `absoluteExternalsPlugin` now externalizes every bare specifier (anything not starting with `./`, `../`, or `/`), resolving each to an absolute file URL via the user's `node_modules`. `node:*` built-ins pass through unchanged.
|
|
15
|
+
- The `EXTERNAL_PACKAGES` allow-list is removed; relative imports continue to be bundled so the user's own source is still transpiled together.
|
|
16
|
+
- `@confect/cli/confect/dev` drops the redundant `external: EXTERNAL_PACKAGES` esbuild option — the plugin handles externalization for both codegen and dev-mode watchers.
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
This restores `confect codegen` for impls that import any non-`@confect/*` / `effect` / `@effect/*` library, fixing the regression introduced in `9.0.0-next.0`.
|
|
21
|
+
- @confect/core@9.0.0-next.4
|
|
22
|
+
- @confect/server@9.0.0-next.4
|
|
23
|
+
|
|
3
24
|
## 9.0.0-next.3
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/Bundler.mjs
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
import { BundlerError } from "./BuildError.mjs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import { Array, Effect, Option, pipe } from "effect";
|
|
4
3
|
import { Path } from "@effect/platform";
|
|
5
4
|
import * as esbuild from "esbuild";
|
|
6
5
|
import { pathToFileURL } from "node:url";
|
|
7
6
|
|
|
8
7
|
//#region src/Bundler.ts
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
const isRelativeOrAbsolutePath = (importPath) => importPath.startsWith("./") || importPath.startsWith("../") || importPath.startsWith("/");
|
|
9
|
+
const PLUGIN_DATA_SKIP = Symbol("absolute-externals.skip");
|
|
10
|
+
/**
|
|
11
|
+
* Mark every bare-specifier import as external and rewrite it to an absolute
|
|
12
|
+
* `file://` URL. Resolution is delegated to esbuild's own resolver via
|
|
13
|
+
* `build.resolve(...)`, which honors each package's `exports` map (preferring
|
|
14
|
+
* the `import` condition under `format: "esm"`) and falls back to
|
|
15
|
+
* `module`/`main` exactly the way Node's ESM resolution algorithm does.
|
|
16
|
+
*
|
|
17
|
+
* Bundles produced with this plugin are loaded via a data URL `import(...)`
|
|
18
|
+
* (see {@link bundle}); rewriting bare externals to absolute file URLs is what
|
|
19
|
+
* makes them resolvable at runtime, since a data URL has no parent file from
|
|
20
|
+
* which a bare specifier could be resolved.
|
|
21
|
+
*
|
|
22
|
+
* Relative/absolute-path imports are left to esbuild to bundle as usual, and
|
|
23
|
+
* `node:*` built-ins are passed through unchanged.
|
|
24
|
+
*/
|
|
19
25
|
const absoluteExternalsPlugin = {
|
|
20
26
|
name: "absolute-externals",
|
|
21
27
|
setup(build) {
|
|
22
28
|
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
29
|
+
if (args.pluginData?.[PLUGIN_DATA_SKIP]) return;
|
|
23
30
|
if (args.kind !== "import-statement" && args.kind !== "dynamic-import") return;
|
|
24
|
-
if (
|
|
25
|
-
|
|
31
|
+
if (isRelativeOrAbsolutePath(args.path)) return;
|
|
32
|
+
if (args.path.startsWith("node:")) return {
|
|
33
|
+
path: args.path,
|
|
34
|
+
external: true
|
|
35
|
+
};
|
|
36
|
+
const resolved = await build.resolve(args.path, {
|
|
37
|
+
kind: args.kind,
|
|
38
|
+
resolveDir: args.resolveDir,
|
|
39
|
+
pluginData: { [PLUGIN_DATA_SKIP]: true }
|
|
40
|
+
});
|
|
41
|
+
if (resolved.errors.length > 0) return {
|
|
42
|
+
errors: resolved.errors,
|
|
43
|
+
warnings: resolved.warnings
|
|
44
|
+
};
|
|
26
45
|
return {
|
|
27
|
-
path: pathToFileURL(
|
|
46
|
+
path: pathToFileURL(resolved.path).href,
|
|
28
47
|
external: true
|
|
29
48
|
};
|
|
30
49
|
});
|
|
@@ -87,5 +106,5 @@ const directlyImports = (bundled, sourceAbsolutePath, targetAbsolutePath) => Eff
|
|
|
87
106
|
});
|
|
88
107
|
|
|
89
108
|
//#endregion
|
|
90
|
-
export {
|
|
109
|
+
export { absoluteExternalsPlugin, bundle, directlyImports };
|
|
91
110
|
//# sourceMappingURL=Bundler.mjs.map
|
package/dist/Bundler.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Bundler.mjs","names":[],"sources":["../src/Bundler.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Bundler.mjs","names":[],"sources":["../src/Bundler.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { Path } from \"@effect/platform\";\nimport { Array, Effect, Option, pipe } from \"effect\";\nimport * as esbuild from \"esbuild\";\nimport { BundlerError } from \"./BuildError\";\n\nexport interface Bundled {\n readonly module: any;\n readonly metafile: esbuild.Metafile;\n}\n\nconst isRelativeOrAbsolutePath = (importPath: string) =>\n importPath.startsWith(\"./\") ||\n importPath.startsWith(\"../\") ||\n importPath.startsWith(\"/\");\n\n// Recursion guard for `absoluteExternalsPlugin`. When the plugin asks esbuild\n// to resolve a bare specifier via `build.resolve(...)`, esbuild invokes every\n// registered `onResolve` hook again for that same specifier — including this\n// one. The flag (carried through the recursive call via `pluginData`) tells\n// the recursive invocation to skip rewriting and fall through to esbuild's\n// built-in resolver, which is what we wanted from `build.resolve` in the\n// first place.\nconst PLUGIN_DATA_SKIP = Symbol(\"absolute-externals.skip\");\n\n/**\n * Mark every bare-specifier import as external and rewrite it to an absolute\n * `file://` URL. Resolution is delegated to esbuild's own resolver via\n * `build.resolve(...)`, which honors each package's `exports` map (preferring\n * the `import` condition under `format: \"esm\"`) and falls back to\n * `module`/`main` exactly the way Node's ESM resolution algorithm does.\n *\n * Bundles produced with this plugin are loaded via a data URL `import(...)`\n * (see {@link bundle}); rewriting bare externals to absolute file URLs is what\n * makes them resolvable at runtime, since a data URL has no parent file from\n * which a bare specifier could be resolved.\n *\n * Relative/absolute-path imports are left to esbuild to bundle as usual, and\n * `node:*` built-ins are passed through unchanged.\n */\nexport const absoluteExternalsPlugin: esbuild.Plugin = {\n name: \"absolute-externals\",\n setup(build) {\n build.onResolve({ filter: /.*/ }, async (args) => {\n if (args.pluginData?.[PLUGIN_DATA_SKIP]) return;\n if (args.kind !== \"import-statement\" && args.kind !== \"dynamic-import\")\n return;\n if (isRelativeOrAbsolutePath(args.path)) return;\n if (args.path.startsWith(\"node:\")) {\n return { path: args.path, external: true };\n }\n\n const resolved = await build.resolve(args.path, {\n kind: args.kind,\n resolveDir: args.resolveDir,\n pluginData: { [PLUGIN_DATA_SKIP]: true },\n });\n\n if (resolved.errors.length > 0) {\n return { errors: resolved.errors, warnings: resolved.warnings };\n }\n\n return {\n path: pathToFileURL(resolved.path).href,\n external: true,\n };\n });\n },\n};\n\nconst buildEntry = (entryPoint: string) =>\n Effect.tryPromise({\n try: () =>\n esbuild.build({\n entryPoints: [entryPoint],\n bundle: true,\n write: false,\n platform: \"node\",\n format: \"esm\",\n logLevel: \"silent\",\n metafile: true,\n plugins: [absoluteExternalsPlugin],\n }),\n catch: (cause) => new BundlerError({ cause }),\n });\n\nconst importBundledModule = (result: esbuild.BuildResult) => {\n const code = result.outputFiles![0]!.text;\n const dataUrl =\n \"data:text/javascript;base64,\" + Buffer.from(code).toString(\"base64\");\n return import(dataUrl);\n};\n\n/**\n * Bundle a TypeScript entry point with esbuild and import the result via a\n * data URL. This handles extensionless `.ts` imports regardless of whether\n * the user's project sets `\"type\": \"module\"` in package.json. The returned\n * pair carries both the imported module and the esbuild metafile so callers\n * can inspect the import graph (see {@link directlyImports}).\n */\nexport const bundle = (\n entryPoint: string,\n): Effect.Effect<Bundled, BundlerError> =>\n Effect.gen(function* () {\n const result = yield* buildEntry(entryPoint);\n const module = yield* Effect.tryPromise({\n try: () => importBundledModule(result),\n catch: (cause) => new BundlerError({ cause }),\n });\n if (!result.metafile) {\n return yield* Effect.dieMessage(\"esbuild metafile missing\");\n }\n return { module, metafile: result.metafile };\n });\n\nconst findMetafileInputKey = (\n metafile: esbuild.Metafile,\n absolutePath: string,\n) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const resolved = path.resolve(absolutePath);\n return Array.findFirst(\n Object.keys(metafile.inputs),\n (key) => path.resolve(key) === resolved,\n );\n });\n\n/**\n * Returns `true` when the module bundled from `sourceAbsolutePath` declares a\n * direct import of `targetAbsolutePath` (according to the bundle's esbuild\n * metafile). Returns `false` if either path is missing from the metafile.\n */\nexport const directlyImports = (\n bundled: Bundled,\n sourceAbsolutePath: string,\n targetAbsolutePath: string,\n) =>\n Effect.gen(function* () {\n const path = yield* Path.Path;\n const sourceKey = yield* findMetafileInputKey(\n bundled.metafile,\n sourceAbsolutePath,\n );\n const targetKey = yield* findMetafileInputKey(\n bundled.metafile,\n targetAbsolutePath,\n );\n\n return pipe(\n Option.all([sourceKey, targetKey]),\n Option.flatMap(([sourceKey_, targetKey_]) =>\n Option.fromNullable(bundled.metafile.inputs[sourceKey_]).pipe(\n Option.map((sourceInput) => {\n const targetResolved = path.resolve(targetKey_);\n return sourceInput.imports.some(\n (importedFile) =>\n path.resolve(importedFile.path) === targetResolved,\n );\n }),\n ),\n ),\n Option.getOrElse(() => false),\n );\n });\n"],"mappings":";;;;;;;AAWA,MAAM,4BAA4B,eAChC,WAAW,WAAW,KAAK,IAC3B,WAAW,WAAW,MAAM,IAC5B,WAAW,WAAW,IAAI;AAS5B,MAAM,mBAAmB,OAAO,0BAA0B;;;;;;;;;;;;;;;;AAiB1D,MAAa,0BAA0C;CACrD,MAAM;CACN,MAAM,OAAO;AACX,QAAM,UAAU,EAAE,QAAQ,MAAM,EAAE,OAAO,SAAS;AAChD,OAAI,KAAK,aAAa,kBAAmB;AACzC,OAAI,KAAK,SAAS,sBAAsB,KAAK,SAAS,iBACpD;AACF,OAAI,yBAAyB,KAAK,KAAK,CAAE;AACzC,OAAI,KAAK,KAAK,WAAW,QAAQ,CAC/B,QAAO;IAAE,MAAM,KAAK;IAAM,UAAU;IAAM;GAG5C,MAAM,WAAW,MAAM,MAAM,QAAQ,KAAK,MAAM;IAC9C,MAAM,KAAK;IACX,YAAY,KAAK;IACjB,YAAY,GAAG,mBAAmB,MAAM;IACzC,CAAC;AAEF,OAAI,SAAS,OAAO,SAAS,EAC3B,QAAO;IAAE,QAAQ,SAAS;IAAQ,UAAU,SAAS;IAAU;AAGjE,UAAO;IACL,MAAM,cAAc,SAAS,KAAK,CAAC;IACnC,UAAU;IACX;IACD;;CAEL;AAED,MAAM,cAAc,eAClB,OAAO,WAAW;CAChB,WACE,QAAQ,MAAM;EACZ,aAAa,CAAC,WAAW;EACzB,QAAQ;EACR,OAAO;EACP,UAAU;EACV,QAAQ;EACR,UAAU;EACV,UAAU;EACV,SAAS,CAAC,wBAAwB;EACnC,CAAC;CACJ,QAAQ,UAAU,IAAI,aAAa,EAAE,OAAO,CAAC;CAC9C,CAAC;AAEJ,MAAM,uBAAuB,WAAgC;CAC3D,MAAM,OAAO,OAAO,YAAa,GAAI;AAGrC,QAAO,OADL,iCAAiC,OAAO,KAAK,KAAK,CAAC,SAAS,SAAS;;;;;;;;;AAWzE,MAAa,UACX,eAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,WAAW,WAAW;CAC5C,MAAM,SAAS,OAAO,OAAO,WAAW;EACtC,WAAW,oBAAoB,OAAO;EACtC,QAAQ,UAAU,IAAI,aAAa,EAAE,OAAO,CAAC;EAC9C,CAAC;AACF,KAAI,CAAC,OAAO,SACV,QAAO,OAAO,OAAO,WAAW,2BAA2B;AAE7D,QAAO;EAAE;EAAQ,UAAU,OAAO;EAAU;EAC5C;AAEJ,MAAM,wBACJ,UACA,iBAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,WAAW,KAAK,QAAQ,aAAa;AAC3C,QAAO,MAAM,UACX,OAAO,KAAK,SAAS,OAAO,GAC3B,QAAQ,KAAK,QAAQ,IAAI,KAAK,SAChC;EACD;;;;;;AAOJ,MAAa,mBACX,SACA,oBACA,uBAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,YAAY,OAAO,qBACvB,QAAQ,UACR,mBACD;CACD,MAAM,YAAY,OAAO,qBACvB,QAAQ,UACR,mBACD;AAED,QAAO,KACL,OAAO,IAAI,CAAC,WAAW,UAAU,CAAC,EAClC,OAAO,SAAS,CAAC,YAAY,gBAC3B,OAAO,aAAa,QAAQ,SAAS,OAAO,YAAY,CAAC,KACvD,OAAO,KAAK,gBAAgB;EAC1B,MAAM,iBAAiB,KAAK,QAAQ,WAAW;AAC/C,SAAO,YAAY,QAAQ,MACxB,iBACC,KAAK,QAAQ,aAAa,KAAK,KAAK,eACvC;GACD,CACH,CACF,EACD,OAAO,gBAAgB,MAAM,CAC9B;EACD"}
|
package/dist/confect/dev.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { catchAndLog } from "../CodegenError.mjs";
|
|
|
5
5
|
import { ConvexDirectory } from "../ConvexDirectory.mjs";
|
|
6
6
|
import { ConfectDirectory } from "../ConfectDirectory.mjs";
|
|
7
7
|
import { FunctionPaths, diff } from "../FunctionPaths.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { absoluteExternalsPlugin } from "../Bundler.mjs";
|
|
9
9
|
import { generateAuthConfig, generateCrons, generateHttp } from "../utils.mjs";
|
|
10
10
|
import { discoverLeafImplFiles, isLeafImplPath, isLeafSpecPath } from "../LeafModule.mjs";
|
|
11
11
|
import { codegenHandler, loadPreviousFunctionPaths } from "./codegen.mjs";
|
|
@@ -184,7 +184,6 @@ const esbuildOptions = (entry, signal, pendingRef, watcherErrorsRef) => {
|
|
|
184
184
|
platform: "node",
|
|
185
185
|
format: "esm",
|
|
186
186
|
logLevel: "silent",
|
|
187
|
-
external: EXTERNAL_PACKAGES,
|
|
188
187
|
plugins: [absoluteExternalsPlugin, {
|
|
189
188
|
name: "notify-rebuild",
|
|
190
189
|
setup(build) {
|
package/dist/confect/dev.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.mjs","names":["FunctionPaths.diff","CodegenError.catchAndLog"],"sources":["../../src/confect/dev.ts"],"sourcesContent":["import { Command } from \"@effect/cli\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { Ansi, AnsiDoc } from \"@effect/printer-ansi\";\nimport {\n Array,\n Chunk,\n Clock,\n Console,\n Duration,\n Effect,\n Equal,\n ExecutionStrategy,\n Exit,\n HashSet,\n Match,\n Option,\n Order,\n pipe,\n Queue,\n Ref,\n Scope,\n Stream,\n String,\n} from \"effect\";\nimport * as esbuild from \"esbuild\";\nimport { logCoalescedBuildErrors } from \"../BuildError\";\nimport { absoluteExternalsPlugin, EXTERNAL_PACKAGES } from \"../Bundler\";\nimport * as CodegenError from \"../CodegenError\";\nimport { ConfectDirectory } from \"../ConfectDirectory\";\nimport { ConvexDirectory } from \"../ConvexDirectory\";\nimport * as FunctionPaths from \"../FunctionPaths\";\nimport type * as GroupPaths from \"../GroupPaths\";\nimport {\n discoverLeafImplFiles,\n isLeafImplPath,\n isLeafSpecPath,\n} from \"../LeafModule\";\nimport {\n logFunctionAdded,\n logFunctionRemoved,\n logPending,\n logSuccess,\n} from \"../log\";\nimport { ProjectRoot } from \"../ProjectRoot\";\nimport { generateAuthConfig, generateCrons, generateHttp } from \"../utils\";\nimport { codegenHandler, loadPreviousFunctionPaths } from \"./codegen\";\n\nconst GENERATED_SPEC_PATH = \"_generated/spec.ts\";\nconst GENERATED_NODE_SPEC_PATH = \"_generated/nodeSpec.ts\";\n\n// Quiescence window: the sync loop waits this long for further signals\n// after each batch. One user edit fires `onEnd` on every esbuild\n// watcher that touches the file, and rebuild times vary across entry\n// points so the onEnds can be spread over hundreds of milliseconds.\n// The drain keeps extending its wait (bounded by `COALESCE_MAX_WAIT`)\n// until no new signals arrive within the window, collapsing the whole\n// burst into a single codegen cycle.\nconst COALESCE_QUIESCENCE = Duration.millis(300);\n\n// Upper bound on `drainUntilQuiescent` so a pathological infinite\n// signal stream can't pin the sync loop forever.\nconst COALESCE_MAX_WAIT = Duration.seconds(5);\n\n// How long to wait for esbuild watchers to react to codegen's own\n// writes (e.g. an updated `_generated/spec.ts`). Added on top of the\n// quiescence drain when codegen reported writes happened.\nconst ECHO_COOLDOWN = Duration.millis(500);\n\nconst emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());\n\ntype Pending = {\n readonly specDirty: boolean;\n readonly httpDirty: boolean;\n readonly cronsDirty: boolean;\n readonly authDirty: boolean;\n};\n\ntype PendingKey = keyof Pending;\n\nconst pendingInit: Pending = {\n specDirty: false,\n httpDirty: false,\n cronsDirty: false,\n authDirty: false,\n};\n\nconst isPendingDirty = (p: Pending): boolean =>\n p.specDirty || p.httpDirty || p.cronsDirty || p.authDirty;\n\ntype WatcherErrors = ReadonlyMap<string, readonly esbuild.Message[]>;\n\nconst emptyWatcherErrors: WatcherErrors = new Map();\n\nconst changeChar = (change: \"Added\" | \"Removed\" | \"Modified\") =>\n Match.value(change).pipe(\n Match.when(\"Added\", () => ({ char: \"+\", color: Ansi.green })),\n Match.when(\"Removed\", () => ({ char: \"-\", color: Ansi.red })),\n Match.when(\"Modified\", () => ({ char: \"~\", color: Ansi.yellow })),\n Match.exhaustive,\n );\n\nconst logFileChangeIndented = (\n change: \"Added\" | \"Removed\" | \"Modified\",\n fullPath: string,\n) =>\n Effect.gen(function* () {\n const projectRoot = yield* ProjectRoot.get;\n const path = yield* Path.Path;\n\n const prefix = projectRoot + path.sep;\n const suffix = pipe(fullPath, String.startsWith(prefix))\n ? pipe(fullPath, String.slice(prefix.length))\n : fullPath;\n\n const { char, color } = changeChar(change);\n\n yield* Console.log(\n pipe(\n AnsiDoc.char(char),\n AnsiDoc.annotate(color),\n AnsiDoc.catWithSpace(\n AnsiDoc.hcat([\n pipe(AnsiDoc.text(prefix), AnsiDoc.annotate(Ansi.blackBright)),\n pipe(AnsiDoc.text(suffix), AnsiDoc.annotate(color)),\n ]),\n ),\n AnsiDoc.render({ style: \"pretty\" }),\n ),\n );\n });\n\nconst logFunctionPathDiff = (\n previous: FunctionPaths.FunctionPaths,\n current: FunctionPaths.FunctionPaths,\n) =>\n Effect.gen(function* () {\n const {\n functionsAdded,\n functionsRemoved,\n groupsRemoved,\n groupsAdded,\n groupsChanged,\n } = FunctionPaths.diff(previous, current);\n\n const logForGroups = (\n groupPaths: GroupPaths.GroupPaths,\n fnPaths: FunctionPaths.FunctionPaths,\n logFn: typeof logFunctionAdded,\n ) =>\n Effect.forEach(groupPaths, (gp) =>\n Effect.forEach(\n Array.fromIterable(\n HashSet.filter(fnPaths, (fp) => Equal.equals(fp.groupPath, gp)),\n ),\n logFn,\n ),\n );\n\n yield* logForGroups(groupsRemoved, functionsRemoved, logFunctionRemoved);\n yield* logForGroups(groupsAdded, functionsAdded, logFunctionAdded);\n yield* Effect.forEach(groupsChanged, (gp) =>\n Effect.gen(function* () {\n yield* Effect.forEach(\n Array.fromIterable(\n HashSet.filter(functionsAdded, (fp) =>\n Equal.equals(fp.groupPath, gp),\n ),\n ),\n logFunctionAdded,\n );\n yield* Effect.forEach(\n Array.fromIterable(\n HashSet.filter(functionsRemoved, (fp) =>\n Equal.equals(fp.groupPath, gp),\n ),\n ),\n logFunctionRemoved,\n );\n }),\n );\n });\n\nexport const dev = Command.make(\"dev\", {}, () =>\n Effect.gen(function* () {\n yield* logPending(\"Performing initial sync…\");\n const previousFunctionPaths = yield* loadPreviousFunctionPaths;\n const initialResult = yield* codegenHandler.pipe(\n Effect.tap(({ functionPaths }) =>\n logFunctionPathDiff(previousFunctionPaths, functionPaths),\n ),\n Effect.tap(() => logSuccess(\"Generated files are up-to-date\")),\n CodegenError.catchAndLog,\n );\n const initialFunctionPaths = Option.match(initialResult, {\n onNone: () => emptyFunctionPaths,\n onSome: ({ functionPaths }) => functionPaths,\n });\n\n const pendingRef = yield* Ref.make<Pending>(pendingInit);\n const signal = yield* Queue.sliding<void>(1);\n const restartQueue = yield* Queue.sliding<void>(1);\n const watcherErrorsRef = yield* Ref.make<WatcherErrors>(emptyWatcherErrors);\n\n yield* Effect.all(\n [\n Effect.scoped(\n entryPointsWatcher(\n signal,\n pendingRef,\n restartQueue,\n watcherErrorsRef,\n ),\n ),\n confectStructureWatcher(signal, pendingRef, restartQueue),\n syncLoop(signal, pendingRef, initialFunctionPaths, watcherErrorsRef),\n ],\n { concurrency: \"unbounded\" },\n );\n }),\n).pipe(Command.withDescription(\"Start the Confect development server\"));\n\nconst esbuildMessageKey = (m: esbuild.Message): string =>\n `${m.location?.file ?? \"\"}:${m.location?.line ?? \"\"}:${m.location?.column ?? \"\"}:${m.text}`;\n\nconst allMessages = (errors: WatcherErrors): ReadonlyArray<esbuild.Message> =>\n pipe(Array.fromIterable(errors.values()), Array.flatten);\n\nconst dedupeWatcherErrors = (\n errors: WatcherErrors,\n): ReadonlyArray<esbuild.Message> =>\n pipe(\n allMessages(errors),\n Array.dedupeWith(\n (messageA, messageB) =>\n esbuildMessageKey(messageA) === esbuildMessageKey(messageB),\n ),\n );\n\nconst watcherErrorsSignature = (errors: WatcherErrors): string =>\n pipe(\n allMessages(errors),\n Array.map(esbuildMessageKey),\n Array.dedupe,\n Array.sort(Order.string),\n Array.join(\"\\n\"),\n );\n\n/**\n * Log any watcher errors that haven't already been logged at their\n * current signature. Suppresses the per-watcher fanout that happens\n * when one root cause (e.g. a missing import) breaks every entry\n * point's build at the same source location.\n */\nconst logChangedWatcherErrors = (\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n lastLoggedSignatureRef: Ref.Ref<string>,\n) =>\n Effect.gen(function* () {\n const errors = yield* Ref.get(watcherErrorsRef);\n const signature = watcherErrorsSignature(errors);\n const previous = yield* Ref.get(lastLoggedSignatureRef);\n if (signature === previous) return;\n yield* Ref.set(lastLoggedSignatureRef, signature);\n if (errors.size === 0) return;\n yield* logCoalescedBuildErrors(dedupeWatcherErrors(errors));\n });\n\n/**\n * Block until the signal queue has been quiet for `quiescence`. esbuild\n * watchers' `onEnd` events for a single user edit can be spread across\n * hundreds of milliseconds, so a fixed window misses late arrivals.\n * Bounded by `maxWait` so pathological signal floods can't pin the\n * loop forever.\n */\nconst drainUntilQuiescent = (\n signal: Queue.Queue<void>,\n quiescence: Duration.Duration,\n maxWait: Duration.Duration,\n) =>\n Effect.gen(function* () {\n const start = yield* Clock.currentTimeMillis;\n const maxMillis = Duration.toMillis(maxWait);\n yield* Effect.iterate(true as boolean, {\n while: (keepGoing) => keepGoing,\n body: () =>\n Effect.gen(function* () {\n yield* Effect.sleep(quiescence);\n const drained = yield* Queue.takeAll(signal);\n if (Chunk.isEmpty(drained)) return false;\n const now = yield* Clock.currentTimeMillis;\n return now - start < maxMillis;\n }),\n });\n });\n\nconst syncLoop = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n initialFunctionPaths: FunctionPaths.FunctionPaths,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.gen(function* () {\n const functionPathsRef = yield* Ref.make(initialFunctionPaths);\n const lastLoggedErrorsRef = yield* Ref.make<string>(\"\");\n\n return yield* Effect.forever(\n Effect.gen(function* () {\n yield* Effect.logDebug(\"Running sync loop…\");\n // Wait for the first signal of a burst, then keep absorbing\n // follow-up signals from other watchers' onEnds until the queue\n // stays quiet for `COALESCE_QUIESCENCE`.\n yield* Queue.take(signal);\n yield* drainUntilQuiescent(\n signal,\n COALESCE_QUIESCENCE,\n COALESCE_MAX_WAIT,\n );\n\n yield* logChangedWatcherErrors(watcherErrorsRef, lastLoggedErrorsRef);\n\n const pending = yield* Ref.getAndSet(pendingRef, pendingInit);\n\n if (!isPendingDirty(pending)) {\n // No-op signal (e.g. a late echo after a previous cycle\n // already drained). Stay silent.\n return;\n }\n\n yield* logPending(\"Dependencies may have changed, reloading…\");\n\n if (pending.specDirty) {\n const current = yield* codegenHandler.pipe(\n Effect.tap(({ functionPaths: nextFunctionPaths }) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(functionPathsRef);\n yield* logFunctionPathDiff(previous, nextFunctionPaths);\n yield* Ref.set(functionPathsRef, nextFunctionPaths);\n }),\n ),\n CodegenError.catchAndLog,\n );\n if (Option.isNone(current)) {\n return;\n }\n // Drain any stragglers from this cycle's burst (slow watchers\n // whose onEnd fired after the first quiescence) plus, when\n // codegen wrote, the echo signals esbuild emits in response\n // to our writes. Reset `pendingRef` so those drained signals\n // don't carry a dirty flag into the next cycle.\n if (current.value.anyWritesHappened) {\n yield* Effect.sleep(ECHO_COOLDOWN);\n }\n yield* drainUntilQuiescent(\n signal,\n COALESCE_QUIESCENCE,\n COALESCE_MAX_WAIT,\n );\n yield* Ref.set(pendingRef, pendingInit);\n }\n\n const dirtyOptionalFiles = [\n ...(pending.httpDirty\n ? [syncOptionalFile(generateHttp, \"http.ts\")]\n : []),\n ...(pending.cronsDirty\n ? [syncOptionalFile(generateCrons, \"crons.ts\")]\n : []),\n ...(pending.authDirty\n ? [syncOptionalFile(generateAuthConfig, \"auth.config.ts\")]\n : []),\n ];\n\n yield* Array.isNonEmptyReadonlyArray(dirtyOptionalFiles)\n ? Effect.all(dirtyOptionalFiles, { concurrency: \"unbounded\" })\n : Effect.void;\n\n yield* logSuccess(\"Generated files are up-to-date\");\n }),\n );\n });\n\ninterface EntryPoint {\n readonly absolutePath: string;\n readonly displayPath: string;\n readonly pendingKey: PendingKey;\n}\n\n/**\n * Every file whose import graph codegen should react to. Each one becomes\n * its own scoped esbuild watcher; the union of their watches gives us\n * dependency-aware tracking of anything reachable from `confect/`,\n * including files outside `confect/`.\n */\nconst discoverEntryPoints = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const projectRoot = yield* ProjectRoot.get;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const tryEntry = (relativePath: string, pendingKey: PendingKey) =>\n Effect.gen(function* () {\n const absolutePath = path.join(confectDirectory, relativePath);\n if (!(yield* fs.exists(absolutePath))) {\n return Option.none<EntryPoint>();\n }\n return Option.some<EntryPoint>({\n absolutePath,\n displayPath: path.relative(projectRoot, absolutePath),\n pendingKey,\n });\n });\n\n const fixedEntryOptions = yield* Effect.all([\n tryEntry(GENERATED_SPEC_PATH, \"specDirty\"),\n tryEntry(GENERATED_NODE_SPEC_PATH, \"specDirty\"),\n tryEntry(\"schema.ts\", \"specDirty\"),\n tryEntry(\"http.ts\", \"httpDirty\"),\n tryEntry(\"crons.ts\", \"cronsDirty\"),\n tryEntry(\"auth.ts\", \"authDirty\"),\n ]);\n\n const implRelativePaths = yield* discoverLeafImplFiles;\n const implEntryOptions = yield* Effect.forEach(\n implRelativePaths,\n (relativePath) => tryEntry(relativePath, \"specDirty\"),\n );\n\n return Array.getSomes([...fixedEntryOptions, ...implEntryOptions]);\n});\n\nconst esbuildOptions = (\n entry: EntryPoint,\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) => {\n // First `onEnd` fires when esbuild finishes the watcher's initial\n // build. At startup that's an echo of the just-completed initial\n // codegen pass; for a watcher spawned mid-session (e.g. a newly\n // added impl) it's an echo of the codegen run that triggered the\n // restart. Either way, the entry's contents were already accounted\n // for, so we record any errors but don't flip dirty or push a\n // signal — only genuine subsequent rebuilds should do that.\n const initialBuildSeenRef = Ref.unsafeMake(false);\n return {\n entryPoints: [entry.absolutePath],\n bundle: true,\n write: false,\n metafile: true,\n platform: \"node\" as const,\n format: \"esm\" as const,\n logLevel: \"silent\" as const,\n external: EXTERNAL_PACKAGES,\n plugins: [\n absoluteExternalsPlugin,\n {\n name: \"notify-rebuild\",\n setup(build: esbuild.PluginBuild) {\n build.onEnd((result) => {\n Effect.runPromise(\n Effect.gen(function* () {\n const wasInitial = yield* Ref.getAndSet(\n initialBuildSeenRef,\n true,\n );\n const isInitial = !wasInitial;\n yield* Ref.update(watcherErrorsRef, (current) => {\n const next = new Map(current);\n if (result.errors.length > 0) {\n next.set(entry.absolutePath, result.errors);\n } else {\n next.delete(entry.absolutePath);\n }\n return next;\n });\n if (isInitial && result.errors.length === 0) return;\n yield* Ref.update(pendingRef, (p) => ({\n ...p,\n [entry.pendingKey]: true,\n }));\n yield* Queue.offer(signal, undefined);\n }),\n );\n });\n },\n },\n ],\n };\n};\n\nconst createEntryPointWatcher = (\n entry: EntryPoint,\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.acquireRelease(\n Effect.promise(async () => {\n const ctx = await esbuild.context(\n esbuildOptions(entry, signal, pendingRef, watcherErrorsRef),\n );\n await ctx.watch();\n return ctx;\n }),\n (ctx) =>\n Effect.gen(function* () {\n yield* Effect.promise(() => ctx.dispose());\n // Clear any errors recorded by this watcher so a disposed\n // watcher can't leave stale errors visible to the sync loop.\n yield* Ref.update(watcherErrorsRef, (current) => {\n if (!current.has(entry.absolutePath)) return current;\n const next = new Map(current);\n next.delete(entry.absolutePath);\n return next;\n });\n yield* Effect.logDebug(\n `esbuild watcher disposed: ${entry.displayPath}`,\n );\n }),\n );\n\n/**\n * Holds one scoped esbuild watcher per entry point and reconciles the set\n * whenever something offers to `restartQueue`. Adding or removing an entry\n * point only spawns/disposes the affected watcher; unchanged entries keep\n * their existing context, so a structural change doesn't churn watchers\n * for unrelated files.\n */\nconst entryPointsWatcher = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n restartQueue: Queue.Queue<void>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.gen(function* () {\n const parentScope = yield* Effect.scope;\n const scopesRef = yield* Ref.make(new Map<string, Scope.CloseableScope>());\n\n const sync = Effect.gen(function* () {\n const desired = yield* discoverEntryPoints;\n const desiredByPath = new Map(\n desired.map((entryPoint) => [entryPoint.absolutePath, entryPoint]),\n );\n const current = yield* Ref.get(scopesRef);\n\n yield* Effect.forEach(\n Array.fromIterable(current),\n ([absolutePath, childScope]) =>\n desiredByPath.has(absolutePath)\n ? Effect.void\n : Scope.close(childScope, Exit.void).pipe(\n Effect.andThen(\n Ref.update(scopesRef, (scopes) => {\n const updated = new Map(scopes);\n updated.delete(absolutePath);\n return updated;\n }),\n ),\n ),\n );\n\n yield* Effect.forEach(desired, (entry) =>\n Effect.gen(function* () {\n const existing = yield* Ref.get(scopesRef);\n if (existing.has(entry.absolutePath)) return;\n\n const childScope = yield* Scope.fork(\n parentScope,\n ExecutionStrategy.sequential,\n );\n yield* createEntryPointWatcher(\n entry,\n signal,\n pendingRef,\n watcherErrorsRef,\n ).pipe(Scope.extend(childScope));\n yield* Ref.update(scopesRef, (scopes) => {\n const updated = new Map(scopes);\n updated.set(entry.absolutePath, childScope);\n return updated;\n });\n }),\n );\n });\n\n yield* sync;\n\n return yield* Effect.forever(\n Queue.take(restartQueue).pipe(Effect.andThen(sync)),\n );\n });\n\n/**\n * Single recursive `fs.watch` on `confect/`. Flips the matching dirty flag\n * for any change to an entry-point-shaped file (so codegen runs without\n * waiting on a newly spawned esbuild watcher), and offers to\n * `restartQueue` when an entry point is created or removed so the watcher\n * manager picks up the new set.\n */\nconst confectStructureWatcher = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n restartQueue: Queue.Queue<void>,\n) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n yield* pipe(\n fs.watch(confectDirectory, { recursive: true }),\n Stream.debounce(Duration.millis(200)),\n Stream.runForEach((event) =>\n handleConfectChange({\n relativePath: path.relative(confectDirectory, event.path),\n eventTag: event._tag,\n signal,\n pendingRef,\n restartQueue,\n }),\n ),\n );\n });\n\nconst TOP_LEVEL_OPTIONAL_KEYS: ReadonlyMap<string, PendingKey> = new Map([\n [\"http.ts\", \"httpDirty\"],\n [\"crons.ts\", \"cronsDirty\"],\n [\"auth.ts\", \"authDirty\"],\n]);\n\nconst flipDirtyAndSignal = (\n pendingRef: Ref.Ref<Pending>,\n signal: Queue.Queue<void>,\n key: PendingKey,\n restartQueue: Queue.Queue<void>,\n restart: boolean,\n) =>\n pipe(\n Ref.update(pendingRef, (p) => ({ ...p, [key]: true })),\n Effect.andThen(Queue.offer(signal, undefined)),\n Effect.andThen(\n restart ? Queue.offer(restartQueue, undefined) : Effect.void,\n ),\n );\n\nconst handleConfectChange = ({\n relativePath,\n eventTag,\n signal,\n pendingRef,\n restartQueue,\n}: {\n relativePath: string;\n eventTag: \"Create\" | \"Update\" | \"Remove\";\n signal: Queue.Queue<void>;\n pendingRef: Ref.Ref<Pending>;\n restartQueue: Queue.Queue<void>;\n}) => {\n // _generated/ files are written by codegen itself; reacting to them here\n // would form a loop. The esbuild watchers track the generated specs as\n // entry points, so changes there flow back through `notify-rebuild`.\n if (relativePath.split(/[/\\\\]/).includes(\"_generated\")) {\n return Effect.void;\n }\n\n if (!relativePath.endsWith(\".ts\")) {\n return Effect.void;\n }\n\n const isLifecycleChange = eventTag !== \"Update\";\n\n const topLevelKey = TOP_LEVEL_OPTIONAL_KEYS.get(relativePath);\n if (topLevelKey !== undefined) {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n topLevelKey,\n restartQueue,\n isLifecycleChange,\n );\n }\n\n if (relativePath === \"schema.ts\") {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n isLifecycleChange,\n );\n }\n\n if (isLeafSpecPath(relativePath) || isLeafImplPath(relativePath)) {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n isLifecycleChange,\n );\n }\n\n // Any other `.ts` under `confect/` (helpers like `tables/Notes.ts`).\n // Updates to such files are handled by the esbuild watcher for whichever\n // entry point imports them — its onEnd flips the right dirty flag.\n // Creates are our safety net: when a previously-missing import is added,\n // esbuild may not have its parent directory on a poll path, so we\n // re-run codegen on Create here.\n if (eventTag === \"Create\") {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n false,\n );\n }\n\n return Effect.void;\n};\n\nconst syncOptionalFile = (generate: typeof generateHttp, convexFile: string) =>\n pipe(\n generate,\n Effect.andThen(\n Option.match({\n onSome: ({ change, convexFilePath }) =>\n Match.value(change).pipe(\n Match.when(\"Unchanged\", () => Effect.void),\n Match.whenOr(\"Added\", \"Modified\", (addedOrModified) =>\n logFileChangeIndented(addedOrModified, convexFilePath),\n ),\n Match.exhaustive,\n ),\n onNone: () =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const convexFilePath = path.join(convexDirectory, convexFile);\n\n if (yield* fs.exists(convexFilePath)) {\n yield* fs.remove(convexFilePath);\n yield* logFileChangeIndented(\"Removed\", convexFilePath);\n }\n }),\n }),\n ),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dev.mjs","names":["FunctionPaths.diff","CodegenError.catchAndLog"],"sources":["../../src/confect/dev.ts"],"sourcesContent":["import { Command } from \"@effect/cli\";\nimport { FileSystem, Path } from \"@effect/platform\";\nimport { Ansi, AnsiDoc } from \"@effect/printer-ansi\";\nimport {\n Array,\n Chunk,\n Clock,\n Console,\n Duration,\n Effect,\n Equal,\n ExecutionStrategy,\n Exit,\n HashSet,\n Match,\n Option,\n Order,\n pipe,\n Queue,\n Ref,\n Scope,\n Stream,\n String,\n} from \"effect\";\nimport * as esbuild from \"esbuild\";\nimport { logCoalescedBuildErrors } from \"../BuildError\";\nimport { absoluteExternalsPlugin } from \"../Bundler\";\nimport * as CodegenError from \"../CodegenError\";\nimport { ConfectDirectory } from \"../ConfectDirectory\";\nimport { ConvexDirectory } from \"../ConvexDirectory\";\nimport * as FunctionPaths from \"../FunctionPaths\";\nimport type * as GroupPaths from \"../GroupPaths\";\nimport {\n discoverLeafImplFiles,\n isLeafImplPath,\n isLeafSpecPath,\n} from \"../LeafModule\";\nimport {\n logFunctionAdded,\n logFunctionRemoved,\n logPending,\n logSuccess,\n} from \"../log\";\nimport { ProjectRoot } from \"../ProjectRoot\";\nimport { generateAuthConfig, generateCrons, generateHttp } from \"../utils\";\nimport { codegenHandler, loadPreviousFunctionPaths } from \"./codegen\";\n\nconst GENERATED_SPEC_PATH = \"_generated/spec.ts\";\nconst GENERATED_NODE_SPEC_PATH = \"_generated/nodeSpec.ts\";\n\n// Quiescence window: the sync loop waits this long for further signals\n// after each batch. One user edit fires `onEnd` on every esbuild\n// watcher that touches the file, and rebuild times vary across entry\n// points so the onEnds can be spread over hundreds of milliseconds.\n// The drain keeps extending its wait (bounded by `COALESCE_MAX_WAIT`)\n// until no new signals arrive within the window, collapsing the whole\n// burst into a single codegen cycle.\nconst COALESCE_QUIESCENCE = Duration.millis(300);\n\n// Upper bound on `drainUntilQuiescent` so a pathological infinite\n// signal stream can't pin the sync loop forever.\nconst COALESCE_MAX_WAIT = Duration.seconds(5);\n\n// How long to wait for esbuild watchers to react to codegen's own\n// writes (e.g. an updated `_generated/spec.ts`). Added on top of the\n// quiescence drain when codegen reported writes happened.\nconst ECHO_COOLDOWN = Duration.millis(500);\n\nconst emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());\n\ntype Pending = {\n readonly specDirty: boolean;\n readonly httpDirty: boolean;\n readonly cronsDirty: boolean;\n readonly authDirty: boolean;\n};\n\ntype PendingKey = keyof Pending;\n\nconst pendingInit: Pending = {\n specDirty: false,\n httpDirty: false,\n cronsDirty: false,\n authDirty: false,\n};\n\nconst isPendingDirty = (p: Pending): boolean =>\n p.specDirty || p.httpDirty || p.cronsDirty || p.authDirty;\n\ntype WatcherErrors = ReadonlyMap<string, readonly esbuild.Message[]>;\n\nconst emptyWatcherErrors: WatcherErrors = new Map();\n\nconst changeChar = (change: \"Added\" | \"Removed\" | \"Modified\") =>\n Match.value(change).pipe(\n Match.when(\"Added\", () => ({ char: \"+\", color: Ansi.green })),\n Match.when(\"Removed\", () => ({ char: \"-\", color: Ansi.red })),\n Match.when(\"Modified\", () => ({ char: \"~\", color: Ansi.yellow })),\n Match.exhaustive,\n );\n\nconst logFileChangeIndented = (\n change: \"Added\" | \"Removed\" | \"Modified\",\n fullPath: string,\n) =>\n Effect.gen(function* () {\n const projectRoot = yield* ProjectRoot.get;\n const path = yield* Path.Path;\n\n const prefix = projectRoot + path.sep;\n const suffix = pipe(fullPath, String.startsWith(prefix))\n ? pipe(fullPath, String.slice(prefix.length))\n : fullPath;\n\n const { char, color } = changeChar(change);\n\n yield* Console.log(\n pipe(\n AnsiDoc.char(char),\n AnsiDoc.annotate(color),\n AnsiDoc.catWithSpace(\n AnsiDoc.hcat([\n pipe(AnsiDoc.text(prefix), AnsiDoc.annotate(Ansi.blackBright)),\n pipe(AnsiDoc.text(suffix), AnsiDoc.annotate(color)),\n ]),\n ),\n AnsiDoc.render({ style: \"pretty\" }),\n ),\n );\n });\n\nconst logFunctionPathDiff = (\n previous: FunctionPaths.FunctionPaths,\n current: FunctionPaths.FunctionPaths,\n) =>\n Effect.gen(function* () {\n const {\n functionsAdded,\n functionsRemoved,\n groupsRemoved,\n groupsAdded,\n groupsChanged,\n } = FunctionPaths.diff(previous, current);\n\n const logForGroups = (\n groupPaths: GroupPaths.GroupPaths,\n fnPaths: FunctionPaths.FunctionPaths,\n logFn: typeof logFunctionAdded,\n ) =>\n Effect.forEach(groupPaths, (gp) =>\n Effect.forEach(\n Array.fromIterable(\n HashSet.filter(fnPaths, (fp) => Equal.equals(fp.groupPath, gp)),\n ),\n logFn,\n ),\n );\n\n yield* logForGroups(groupsRemoved, functionsRemoved, logFunctionRemoved);\n yield* logForGroups(groupsAdded, functionsAdded, logFunctionAdded);\n yield* Effect.forEach(groupsChanged, (gp) =>\n Effect.gen(function* () {\n yield* Effect.forEach(\n Array.fromIterable(\n HashSet.filter(functionsAdded, (fp) =>\n Equal.equals(fp.groupPath, gp),\n ),\n ),\n logFunctionAdded,\n );\n yield* Effect.forEach(\n Array.fromIterable(\n HashSet.filter(functionsRemoved, (fp) =>\n Equal.equals(fp.groupPath, gp),\n ),\n ),\n logFunctionRemoved,\n );\n }),\n );\n });\n\nexport const dev = Command.make(\"dev\", {}, () =>\n Effect.gen(function* () {\n yield* logPending(\"Performing initial sync…\");\n const previousFunctionPaths = yield* loadPreviousFunctionPaths;\n const initialResult = yield* codegenHandler.pipe(\n Effect.tap(({ functionPaths }) =>\n logFunctionPathDiff(previousFunctionPaths, functionPaths),\n ),\n Effect.tap(() => logSuccess(\"Generated files are up-to-date\")),\n CodegenError.catchAndLog,\n );\n const initialFunctionPaths = Option.match(initialResult, {\n onNone: () => emptyFunctionPaths,\n onSome: ({ functionPaths }) => functionPaths,\n });\n\n const pendingRef = yield* Ref.make<Pending>(pendingInit);\n const signal = yield* Queue.sliding<void>(1);\n const restartQueue = yield* Queue.sliding<void>(1);\n const watcherErrorsRef = yield* Ref.make<WatcherErrors>(emptyWatcherErrors);\n\n yield* Effect.all(\n [\n Effect.scoped(\n entryPointsWatcher(\n signal,\n pendingRef,\n restartQueue,\n watcherErrorsRef,\n ),\n ),\n confectStructureWatcher(signal, pendingRef, restartQueue),\n syncLoop(signal, pendingRef, initialFunctionPaths, watcherErrorsRef),\n ],\n { concurrency: \"unbounded\" },\n );\n }),\n).pipe(Command.withDescription(\"Start the Confect development server\"));\n\nconst esbuildMessageKey = (m: esbuild.Message): string =>\n `${m.location?.file ?? \"\"}:${m.location?.line ?? \"\"}:${m.location?.column ?? \"\"}:${m.text}`;\n\nconst allMessages = (errors: WatcherErrors): ReadonlyArray<esbuild.Message> =>\n pipe(Array.fromIterable(errors.values()), Array.flatten);\n\nconst dedupeWatcherErrors = (\n errors: WatcherErrors,\n): ReadonlyArray<esbuild.Message> =>\n pipe(\n allMessages(errors),\n Array.dedupeWith(\n (messageA, messageB) =>\n esbuildMessageKey(messageA) === esbuildMessageKey(messageB),\n ),\n );\n\nconst watcherErrorsSignature = (errors: WatcherErrors): string =>\n pipe(\n allMessages(errors),\n Array.map(esbuildMessageKey),\n Array.dedupe,\n Array.sort(Order.string),\n Array.join(\"\\n\"),\n );\n\n/**\n * Log any watcher errors that haven't already been logged at their\n * current signature. Suppresses the per-watcher fanout that happens\n * when one root cause (e.g. a missing import) breaks every entry\n * point's build at the same source location.\n */\nconst logChangedWatcherErrors = (\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n lastLoggedSignatureRef: Ref.Ref<string>,\n) =>\n Effect.gen(function* () {\n const errors = yield* Ref.get(watcherErrorsRef);\n const signature = watcherErrorsSignature(errors);\n const previous = yield* Ref.get(lastLoggedSignatureRef);\n if (signature === previous) return;\n yield* Ref.set(lastLoggedSignatureRef, signature);\n if (errors.size === 0) return;\n yield* logCoalescedBuildErrors(dedupeWatcherErrors(errors));\n });\n\n/**\n * Block until the signal queue has been quiet for `quiescence`. esbuild\n * watchers' `onEnd` events for a single user edit can be spread across\n * hundreds of milliseconds, so a fixed window misses late arrivals.\n * Bounded by `maxWait` so pathological signal floods can't pin the\n * loop forever.\n */\nconst drainUntilQuiescent = (\n signal: Queue.Queue<void>,\n quiescence: Duration.Duration,\n maxWait: Duration.Duration,\n) =>\n Effect.gen(function* () {\n const start = yield* Clock.currentTimeMillis;\n const maxMillis = Duration.toMillis(maxWait);\n yield* Effect.iterate(true as boolean, {\n while: (keepGoing) => keepGoing,\n body: () =>\n Effect.gen(function* () {\n yield* Effect.sleep(quiescence);\n const drained = yield* Queue.takeAll(signal);\n if (Chunk.isEmpty(drained)) return false;\n const now = yield* Clock.currentTimeMillis;\n return now - start < maxMillis;\n }),\n });\n });\n\nconst syncLoop = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n initialFunctionPaths: FunctionPaths.FunctionPaths,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.gen(function* () {\n const functionPathsRef = yield* Ref.make(initialFunctionPaths);\n const lastLoggedErrorsRef = yield* Ref.make<string>(\"\");\n\n return yield* Effect.forever(\n Effect.gen(function* () {\n yield* Effect.logDebug(\"Running sync loop…\");\n // Wait for the first signal of a burst, then keep absorbing\n // follow-up signals from other watchers' onEnds until the queue\n // stays quiet for `COALESCE_QUIESCENCE`.\n yield* Queue.take(signal);\n yield* drainUntilQuiescent(\n signal,\n COALESCE_QUIESCENCE,\n COALESCE_MAX_WAIT,\n );\n\n yield* logChangedWatcherErrors(watcherErrorsRef, lastLoggedErrorsRef);\n\n const pending = yield* Ref.getAndSet(pendingRef, pendingInit);\n\n if (!isPendingDirty(pending)) {\n // No-op signal (e.g. a late echo after a previous cycle\n // already drained). Stay silent.\n return;\n }\n\n yield* logPending(\"Dependencies may have changed, reloading…\");\n\n if (pending.specDirty) {\n const current = yield* codegenHandler.pipe(\n Effect.tap(({ functionPaths: nextFunctionPaths }) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(functionPathsRef);\n yield* logFunctionPathDiff(previous, nextFunctionPaths);\n yield* Ref.set(functionPathsRef, nextFunctionPaths);\n }),\n ),\n CodegenError.catchAndLog,\n );\n if (Option.isNone(current)) {\n return;\n }\n // Drain any stragglers from this cycle's burst (slow watchers\n // whose onEnd fired after the first quiescence) plus, when\n // codegen wrote, the echo signals esbuild emits in response\n // to our writes. Reset `pendingRef` so those drained signals\n // don't carry a dirty flag into the next cycle.\n if (current.value.anyWritesHappened) {\n yield* Effect.sleep(ECHO_COOLDOWN);\n }\n yield* drainUntilQuiescent(\n signal,\n COALESCE_QUIESCENCE,\n COALESCE_MAX_WAIT,\n );\n yield* Ref.set(pendingRef, pendingInit);\n }\n\n const dirtyOptionalFiles = [\n ...(pending.httpDirty\n ? [syncOptionalFile(generateHttp, \"http.ts\")]\n : []),\n ...(pending.cronsDirty\n ? [syncOptionalFile(generateCrons, \"crons.ts\")]\n : []),\n ...(pending.authDirty\n ? [syncOptionalFile(generateAuthConfig, \"auth.config.ts\")]\n : []),\n ];\n\n yield* Array.isNonEmptyReadonlyArray(dirtyOptionalFiles)\n ? Effect.all(dirtyOptionalFiles, { concurrency: \"unbounded\" })\n : Effect.void;\n\n yield* logSuccess(\"Generated files are up-to-date\");\n }),\n );\n });\n\ninterface EntryPoint {\n readonly absolutePath: string;\n readonly displayPath: string;\n readonly pendingKey: PendingKey;\n}\n\n/**\n * Every file whose import graph codegen should react to. Each one becomes\n * its own scoped esbuild watcher; the union of their watches gives us\n * dependency-aware tracking of anything reachable from `confect/`,\n * including files outside `confect/`.\n */\nconst discoverEntryPoints = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const projectRoot = yield* ProjectRoot.get;\n const confectDirectory = yield* ConfectDirectory.get;\n\n const tryEntry = (relativePath: string, pendingKey: PendingKey) =>\n Effect.gen(function* () {\n const absolutePath = path.join(confectDirectory, relativePath);\n if (!(yield* fs.exists(absolutePath))) {\n return Option.none<EntryPoint>();\n }\n return Option.some<EntryPoint>({\n absolutePath,\n displayPath: path.relative(projectRoot, absolutePath),\n pendingKey,\n });\n });\n\n const fixedEntryOptions = yield* Effect.all([\n tryEntry(GENERATED_SPEC_PATH, \"specDirty\"),\n tryEntry(GENERATED_NODE_SPEC_PATH, \"specDirty\"),\n tryEntry(\"schema.ts\", \"specDirty\"),\n tryEntry(\"http.ts\", \"httpDirty\"),\n tryEntry(\"crons.ts\", \"cronsDirty\"),\n tryEntry(\"auth.ts\", \"authDirty\"),\n ]);\n\n const implRelativePaths = yield* discoverLeafImplFiles;\n const implEntryOptions = yield* Effect.forEach(\n implRelativePaths,\n (relativePath) => tryEntry(relativePath, \"specDirty\"),\n );\n\n return Array.getSomes([...fixedEntryOptions, ...implEntryOptions]);\n});\n\nconst esbuildOptions = (\n entry: EntryPoint,\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) => {\n // First `onEnd` fires when esbuild finishes the watcher's initial\n // build. At startup that's an echo of the just-completed initial\n // codegen pass; for a watcher spawned mid-session (e.g. a newly\n // added impl) it's an echo of the codegen run that triggered the\n // restart. Either way, the entry's contents were already accounted\n // for, so we record any errors but don't flip dirty or push a\n // signal — only genuine subsequent rebuilds should do that.\n const initialBuildSeenRef = Ref.unsafeMake(false);\n return {\n entryPoints: [entry.absolutePath],\n bundle: true,\n write: false,\n metafile: true,\n platform: \"node\" as const,\n format: \"esm\" as const,\n logLevel: \"silent\" as const,\n plugins: [\n absoluteExternalsPlugin,\n {\n name: \"notify-rebuild\",\n setup(build: esbuild.PluginBuild) {\n build.onEnd((result) => {\n Effect.runPromise(\n Effect.gen(function* () {\n const wasInitial = yield* Ref.getAndSet(\n initialBuildSeenRef,\n true,\n );\n const isInitial = !wasInitial;\n yield* Ref.update(watcherErrorsRef, (current) => {\n const next = new Map(current);\n if (result.errors.length > 0) {\n next.set(entry.absolutePath, result.errors);\n } else {\n next.delete(entry.absolutePath);\n }\n return next;\n });\n if (isInitial && result.errors.length === 0) return;\n yield* Ref.update(pendingRef, (p) => ({\n ...p,\n [entry.pendingKey]: true,\n }));\n yield* Queue.offer(signal, undefined);\n }),\n );\n });\n },\n },\n ],\n };\n};\n\nconst createEntryPointWatcher = (\n entry: EntryPoint,\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.acquireRelease(\n Effect.promise(async () => {\n const ctx = await esbuild.context(\n esbuildOptions(entry, signal, pendingRef, watcherErrorsRef),\n );\n await ctx.watch();\n return ctx;\n }),\n (ctx) =>\n Effect.gen(function* () {\n yield* Effect.promise(() => ctx.dispose());\n // Clear any errors recorded by this watcher so a disposed\n // watcher can't leave stale errors visible to the sync loop.\n yield* Ref.update(watcherErrorsRef, (current) => {\n if (!current.has(entry.absolutePath)) return current;\n const next = new Map(current);\n next.delete(entry.absolutePath);\n return next;\n });\n yield* Effect.logDebug(\n `esbuild watcher disposed: ${entry.displayPath}`,\n );\n }),\n );\n\n/**\n * Holds one scoped esbuild watcher per entry point and reconciles the set\n * whenever something offers to `restartQueue`. Adding or removing an entry\n * point only spawns/disposes the affected watcher; unchanged entries keep\n * their existing context, so a structural change doesn't churn watchers\n * for unrelated files.\n */\nconst entryPointsWatcher = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n restartQueue: Queue.Queue<void>,\n watcherErrorsRef: Ref.Ref<WatcherErrors>,\n) =>\n Effect.gen(function* () {\n const parentScope = yield* Effect.scope;\n const scopesRef = yield* Ref.make(new Map<string, Scope.CloseableScope>());\n\n const sync = Effect.gen(function* () {\n const desired = yield* discoverEntryPoints;\n const desiredByPath = new Map(\n desired.map((entryPoint) => [entryPoint.absolutePath, entryPoint]),\n );\n const current = yield* Ref.get(scopesRef);\n\n yield* Effect.forEach(\n Array.fromIterable(current),\n ([absolutePath, childScope]) =>\n desiredByPath.has(absolutePath)\n ? Effect.void\n : Scope.close(childScope, Exit.void).pipe(\n Effect.andThen(\n Ref.update(scopesRef, (scopes) => {\n const updated = new Map(scopes);\n updated.delete(absolutePath);\n return updated;\n }),\n ),\n ),\n );\n\n yield* Effect.forEach(desired, (entry) =>\n Effect.gen(function* () {\n const existing = yield* Ref.get(scopesRef);\n if (existing.has(entry.absolutePath)) return;\n\n const childScope = yield* Scope.fork(\n parentScope,\n ExecutionStrategy.sequential,\n );\n yield* createEntryPointWatcher(\n entry,\n signal,\n pendingRef,\n watcherErrorsRef,\n ).pipe(Scope.extend(childScope));\n yield* Ref.update(scopesRef, (scopes) => {\n const updated = new Map(scopes);\n updated.set(entry.absolutePath, childScope);\n return updated;\n });\n }),\n );\n });\n\n yield* sync;\n\n return yield* Effect.forever(\n Queue.take(restartQueue).pipe(Effect.andThen(sync)),\n );\n });\n\n/**\n * Single recursive `fs.watch` on `confect/`. Flips the matching dirty flag\n * for any change to an entry-point-shaped file (so codegen runs without\n * waiting on a newly spawned esbuild watcher), and offers to\n * `restartQueue` when an entry point is created or removed so the watcher\n * manager picks up the new set.\n */\nconst confectStructureWatcher = (\n signal: Queue.Queue<void>,\n pendingRef: Ref.Ref<Pending>,\n restartQueue: Queue.Queue<void>,\n) =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const confectDirectory = yield* ConfectDirectory.get;\n\n yield* pipe(\n fs.watch(confectDirectory, { recursive: true }),\n Stream.debounce(Duration.millis(200)),\n Stream.runForEach((event) =>\n handleConfectChange({\n relativePath: path.relative(confectDirectory, event.path),\n eventTag: event._tag,\n signal,\n pendingRef,\n restartQueue,\n }),\n ),\n );\n });\n\nconst TOP_LEVEL_OPTIONAL_KEYS: ReadonlyMap<string, PendingKey> = new Map([\n [\"http.ts\", \"httpDirty\"],\n [\"crons.ts\", \"cronsDirty\"],\n [\"auth.ts\", \"authDirty\"],\n]);\n\nconst flipDirtyAndSignal = (\n pendingRef: Ref.Ref<Pending>,\n signal: Queue.Queue<void>,\n key: PendingKey,\n restartQueue: Queue.Queue<void>,\n restart: boolean,\n) =>\n pipe(\n Ref.update(pendingRef, (p) => ({ ...p, [key]: true })),\n Effect.andThen(Queue.offer(signal, undefined)),\n Effect.andThen(\n restart ? Queue.offer(restartQueue, undefined) : Effect.void,\n ),\n );\n\nconst handleConfectChange = ({\n relativePath,\n eventTag,\n signal,\n pendingRef,\n restartQueue,\n}: {\n relativePath: string;\n eventTag: \"Create\" | \"Update\" | \"Remove\";\n signal: Queue.Queue<void>;\n pendingRef: Ref.Ref<Pending>;\n restartQueue: Queue.Queue<void>;\n}) => {\n // _generated/ files are written by codegen itself; reacting to them here\n // would form a loop. The esbuild watchers track the generated specs as\n // entry points, so changes there flow back through `notify-rebuild`.\n if (relativePath.split(/[/\\\\]/).includes(\"_generated\")) {\n return Effect.void;\n }\n\n if (!relativePath.endsWith(\".ts\")) {\n return Effect.void;\n }\n\n const isLifecycleChange = eventTag !== \"Update\";\n\n const topLevelKey = TOP_LEVEL_OPTIONAL_KEYS.get(relativePath);\n if (topLevelKey !== undefined) {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n topLevelKey,\n restartQueue,\n isLifecycleChange,\n );\n }\n\n if (relativePath === \"schema.ts\") {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n isLifecycleChange,\n );\n }\n\n if (isLeafSpecPath(relativePath) || isLeafImplPath(relativePath)) {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n isLifecycleChange,\n );\n }\n\n // Any other `.ts` under `confect/` (helpers like `tables/Notes.ts`).\n // Updates to such files are handled by the esbuild watcher for whichever\n // entry point imports them — its onEnd flips the right dirty flag.\n // Creates are our safety net: when a previously-missing import is added,\n // esbuild may not have its parent directory on a poll path, so we\n // re-run codegen on Create here.\n if (eventTag === \"Create\") {\n return flipDirtyAndSignal(\n pendingRef,\n signal,\n \"specDirty\",\n restartQueue,\n false,\n );\n }\n\n return Effect.void;\n};\n\nconst syncOptionalFile = (generate: typeof generateHttp, convexFile: string) =>\n pipe(\n generate,\n Effect.andThen(\n Option.match({\n onSome: ({ change, convexFilePath }) =>\n Match.value(change).pipe(\n Match.when(\"Unchanged\", () => Effect.void),\n Match.whenOr(\"Added\", \"Modified\", (addedOrModified) =>\n logFileChangeIndented(addedOrModified, convexFilePath),\n ),\n Match.exhaustive,\n ),\n onNone: () =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const convexDirectory = yield* ConvexDirectory.get;\n const convexFilePath = path.join(convexDirectory, convexFile);\n\n if (yield* fs.exists(convexFilePath)) {\n yield* fs.remove(convexFilePath);\n yield* logFileChangeIndented(\"Removed\", convexFilePath);\n }\n }),\n }),\n ),\n );\n"],"mappings":"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"}
|
package/dist/package.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@confect/cli",
|
|
3
|
-
"version": "9.0.0-next.
|
|
3
|
+
"version": "9.0.0-next.4",
|
|
4
4
|
"description": "Developer tooling for codegen and sync",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"effect": "^3.21.2",
|
|
68
|
-
"@confect/core": "^9.0.0-next.
|
|
69
|
-
"@confect/server": "^9.0.0-next.
|
|
68
|
+
"@confect/core": "^9.0.0-next.4",
|
|
69
|
+
"@confect/server": "^9.0.0-next.4"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=22",
|
package/src/Bundler.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { pathToFileURL } from "node:url";
|
|
3
2
|
import { Path } from "@effect/platform";
|
|
4
3
|
import { Array, Effect, Option, pipe } from "effect";
|
|
@@ -10,39 +9,61 @@ export interface Bundled {
|
|
|
10
9
|
readonly metafile: esbuild.Metafile;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"@effect/*",
|
|
18
|
-
];
|
|
12
|
+
const isRelativeOrAbsolutePath = (importPath: string) =>
|
|
13
|
+
importPath.startsWith("./") ||
|
|
14
|
+
importPath.startsWith("../") ||
|
|
15
|
+
importPath.startsWith("/");
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
// Recursion guard for `absoluteExternalsPlugin`. When the plugin asks esbuild
|
|
18
|
+
// to resolve a bare specifier via `build.resolve(...)`, esbuild invokes every
|
|
19
|
+
// registered `onResolve` hook again for that same specifier — including this
|
|
20
|
+
// one. The flag (carried through the recursive call via `pluginData`) tells
|
|
21
|
+
// the recursive invocation to skip rewriting and fall through to esbuild's
|
|
22
|
+
// built-in resolver, which is what we wanted from `build.resolve` in the
|
|
23
|
+
// first place.
|
|
24
|
+
const PLUGIN_DATA_SKIP = Symbol("absolute-externals.skip");
|
|
27
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Mark every bare-specifier import as external and rewrite it to an absolute
|
|
28
|
+
* `file://` URL. Resolution is delegated to esbuild's own resolver via
|
|
29
|
+
* `build.resolve(...)`, which honors each package's `exports` map (preferring
|
|
30
|
+
* the `import` condition under `format: "esm"`) and falls back to
|
|
31
|
+
* `module`/`main` exactly the way Node's ESM resolution algorithm does.
|
|
32
|
+
*
|
|
33
|
+
* Bundles produced with this plugin are loaded via a data URL `import(...)`
|
|
34
|
+
* (see {@link bundle}); rewriting bare externals to absolute file URLs is what
|
|
35
|
+
* makes them resolvable at runtime, since a data URL has no parent file from
|
|
36
|
+
* which a bare specifier could be resolved.
|
|
37
|
+
*
|
|
38
|
+
* Relative/absolute-path imports are left to esbuild to bundle as usual, and
|
|
39
|
+
* `node:*` built-ins are passed through unchanged.
|
|
40
|
+
*/
|
|
28
41
|
export const absoluteExternalsPlugin: esbuild.Plugin = {
|
|
29
42
|
name: "absolute-externals",
|
|
30
43
|
setup(build) {
|
|
31
44
|
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
45
|
+
if (args.pluginData?.[PLUGIN_DATA_SKIP]) return;
|
|
32
46
|
if (args.kind !== "import-statement" && args.kind !== "dynamic-import")
|
|
33
47
|
return;
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if (isRelativeOrAbsolutePath(args.path)) return;
|
|
49
|
+
if (args.path.startsWith("node:")) {
|
|
50
|
+
return { path: args.path, external: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolved = await build.resolve(args.path, {
|
|
54
|
+
kind: args.kind,
|
|
55
|
+
resolveDir: args.resolveDir,
|
|
56
|
+
pluginData: { [PLUGIN_DATA_SKIP]: true },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (resolved.errors.length > 0) {
|
|
60
|
+
return { errors: resolved.errors, warnings: resolved.warnings };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
path: pathToFileURL(resolved.path).href,
|
|
65
|
+
external: true,
|
|
66
|
+
};
|
|
46
67
|
});
|
|
47
68
|
},
|
|
48
69
|
};
|
package/src/confect/dev.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "effect";
|
|
25
25
|
import * as esbuild from "esbuild";
|
|
26
26
|
import { logCoalescedBuildErrors } from "../BuildError";
|
|
27
|
-
import { absoluteExternalsPlugin
|
|
27
|
+
import { absoluteExternalsPlugin } from "../Bundler";
|
|
28
28
|
import * as CodegenError from "../CodegenError";
|
|
29
29
|
import { ConfectDirectory } from "../ConfectDirectory";
|
|
30
30
|
import { ConvexDirectory } from "../ConvexDirectory";
|
|
@@ -450,7 +450,6 @@ const esbuildOptions = (
|
|
|
450
450
|
platform: "node" as const,
|
|
451
451
|
format: "esm" as const,
|
|
452
452
|
logLevel: "silent" as const,
|
|
453
|
-
external: EXTERNAL_PACKAGES,
|
|
454
453
|
plugins: [
|
|
455
454
|
absoluteExternalsPlugin,
|
|
456
455
|
{
|