kitcn 0.27.3 → 0.27.5

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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # kitcn
2
2
 
3
+ ## 0.27.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#414](https://github.com/udecode/kitcn/pull/414) [`7b01244`](https://github.com/udecode/kitcn/commit/7b0124498000e6e6ef190b4b529d10026f0a1f16) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
8
+
9
+ - Stop `insert().returning({ ... })` reading each row back after writing it. A
10
+ projected `returning()` is now answered from the values that were just
11
+ inserted, so an 8-row insert spends 0 reads on post-images instead of 8.
12
+ Argument-less `returning()` still reads, because `createdAt` is only known
13
+ once the row is stored.
14
+ - Stop `insert().onConflictDoUpdate({ ... }).returning()` reading the row back
15
+ after patching it.
16
+ - Stop `returning({ _count })` re-fetching each affected row before counting its
17
+ relations, on `insert()`, `update()` and `delete()`. That is one fewer read
18
+ per row, and the counts, their `where` filters and `delete()`'s
19
+ before-cascade ordering are unchanged.
20
+ - Keep reading the row back on tables with triggers, `aggregateIndex` or
21
+ `rankIndex`, where a hook can rewrite what gets stored.
22
+
23
+ ## 0.27.4
24
+
25
+ ### Patch Changes
26
+
27
+ - [#400](https://github.com/udecode/kitcn/pull/400) [`9963d33`](https://github.com/udecode/kitcn/commit/9963d33048ec532f0a2d9a06bb618486856178b1) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
28
+
29
+ - Speed up `kitcn analyze` by measuring every selected entry in one bundler pass
30
+ instead of one pass per entry: 4.3 s → 0.8 s of bundling on the 24-entry example
31
+ app, with byte-identical `OutMB`. Each entry is still sized as its own
32
+ independently tree-shaken isolate, so the ranking is unchanged.
33
+ - Report the hotspot `DepMB` and `Files` columns for the files that carry weight in
34
+ each entry's isolate. They previously counted every file the bundler parsed,
35
+ including files tree-shaking dropped entirely, which disagreed with the `--details`
36
+ package and input tables directly beneath them. Expect both columns to read lower
37
+ than before for the same code; `OutMB` and `LocMB` are unaffected.
38
+ - Fix `kitcn analyze` crashing with a bundler stack trace when a Convex entry fails
39
+ to build. It now lists the entry under `Failed entries:`, keeps reporting every
40
+ entry that did build, and still exits `1`. This needs esbuild `0.27.7`, which is
41
+ now the minimum.
42
+ - Warn which entries had their `./schema` imports externalized after a build error,
43
+ instead of leaving the default mode silent about approximate dependency sizes. The
44
+ approximation is applied per entry, so one unbuildable schema import no longer
45
+ shrinks the numbers for unrelated functions.
46
+ - Open the interactive analyzer's package and input panes without a second bundle, so
47
+ moving the selection no longer pauses to rebuild.
48
+ - Document the hotspot ranking columns, in `--help` and in the CLI reference, along
49
+ with the `--top-inputs` / `--top-packages` flags.
50
+
3
51
  ## 0.27.3
4
52
 
5
53
  ### Patch Changes
@@ -1,5 +1,5 @@
1
- import { Bn as ConvexTextBuilderInitial, Xt as ConvexTableWithColumns } from "../capabilities-CLCYgRdY.js";
2
- import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-DAPEBp1y.js";
1
+ import { Bn as ConvexTextBuilderInitial, Xt as ConvexTableWithColumns } from "../capabilities-DkfnnNp7.js";
2
+ import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-DuC8Nr7e.js";
3
3
  import * as convex_values0 from "convex/values";
4
4
  import { GenericId, Infer, Value } from "convex/values";
5
5
  import { DocumentByName, GenericDataModel, GenericDatabaseReader, GenericDatabaseWriter, TableNamesInDataModel } from "convex/server";
@@ -1,2 +1,2 @@
1
- import { S as defineAuth, _ as GenericAuthBeforeResult, b as GenericAuthTriggerHandlers, g as BetterAuthOptionsWithoutDatabase, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, v as GenericAuthDefinition, x as GenericAuthTriggers, y as GenericAuthTriggerChange } from "../../generated-contract-disabled-D4mw0d-V.js";
1
+ import { S as defineAuth, _ as GenericAuthBeforeResult, b as GenericAuthTriggerHandlers, g as BetterAuthOptionsWithoutDatabase, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, v as GenericAuthDefinition, x as GenericAuthTriggers, y as GenericAuthTriggerChange } from "../../generated-contract-disabled-BEc4d98x.js";
2
2
  export { type AuthRuntime, BetterAuthOptionsWithoutDatabase, type GeneratedAuthDisabledReasonKind, GenericAuthBeforeResult, GenericAuthDefinition, GenericAuthTriggerChange, GenericAuthTriggerHandlers, GenericAuthTriggers, createDisabledAuthRuntime, defineAuth, getGeneratedAuthDisabledReason };
@@ -1,7 +1,7 @@
1
1
  import { a as QueryCtxWithPreferredOrmQueryTable, n as LookupByIdResultByCtx, t as DocByCtx } from "../query-context-DJONf8X5.js";
2
2
  import { t as GetAuth } from "../types-Bf3XQex5.js";
3
3
  import { t as GenericCtx } from "../context-utils-DwZ3Cam1.js";
4
- import { S as defineAuth, _ as GenericAuthBeforeResult, a as AuthFunctions, b as GenericAuthTriggerHandlers, c as createApi, d as deleteOneHandler, f as findManyHandler, g as BetterAuthOptionsWithoutDatabase, h as updateOneHandler, i as getGeneratedAuthDisabledReason, l as createHandler, m as updateManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as findOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as deleteManyHandler, v as GenericAuthDefinition, x as GenericAuthTriggers, y as GenericAuthTriggerChange } from "../generated-contract-disabled-D4mw0d-V.js";
4
+ import { S as defineAuth, _ as GenericAuthBeforeResult, a as AuthFunctions, b as GenericAuthTriggerHandlers, c as createApi, d as deleteOneHandler, f as findManyHandler, g as BetterAuthOptionsWithoutDatabase, h as updateOneHandler, i as getGeneratedAuthDisabledReason, l as createHandler, m as updateManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as findOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as deleteManyHandler, v as GenericAuthDefinition, x as GenericAuthTriggers, y as GenericAuthTriggerChange } from "../generated-contract-disabled-BEc4d98x.js";
5
5
  import * as convex_values0 from "convex/values";
6
6
  import { Infer } from "convex/values";
7
7
  import { AuthConfig, DocumentByName, GenericDataModel, GenericMutationCtx, GenericQueryCtx, GenericSchema, PaginationOptions, PaginationResult, SchemaDefinition, TableNamesInDataModel } from "convex/server";
@@ -111,32 +111,32 @@ type AdapterPaginationOptions = PaginationOptions & {
111
111
  };
112
112
  declare const adapterWhereValidator: convex_values0.VObject<{
113
113
  mode?: "sensitive" | "insensitive" | undefined;
114
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
115
114
  connector?: "AND" | "OR" | undefined;
115
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
116
116
  value: string | number | boolean | string[] | number[] | null;
117
117
  field: string;
118
118
  }, {
119
119
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
120
120
  field: convex_values0.VString<string, "required">;
121
121
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
122
- operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
122
+ operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
123
123
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
124
- }, "required", "mode" | "value" | "operator" | "field" | "connector">;
124
+ }, "required", "mode" | "value" | "connector" | "field" | "operator">;
125
125
  declare const adapterArgsValidator: convex_values0.VObject<{
126
- limit?: number | undefined;
127
- offset?: number | undefined;
128
126
  select?: string[] | undefined;
129
- sortBy?: {
130
- direction: "asc" | "desc";
131
- field: string;
132
- } | undefined;
133
127
  where?: {
134
128
  mode?: "sensitive" | "insensitive" | undefined;
135
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
136
129
  connector?: "AND" | "OR" | undefined;
130
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
137
131
  value: string | number | boolean | string[] | number[] | null;
138
132
  field: string;
139
133
  }[] | undefined;
134
+ limit?: number | undefined;
135
+ offset?: number | undefined;
136
+ sortBy?: {
137
+ field: string;
138
+ direction: "asc" | "desc";
139
+ } | undefined;
140
140
  model: string;
141
141
  }, {
142
142
  limit: convex_values0.VFloat64<number | undefined, "optional">;
@@ -144,32 +144,32 @@ declare const adapterArgsValidator: convex_values0.VObject<{
144
144
  offset: convex_values0.VFloat64<number | undefined, "optional">;
145
145
  select: convex_values0.VArray<string[] | undefined, convex_values0.VString<string, "required">, "optional">;
146
146
  sortBy: convex_values0.VObject<{
147
- direction: "asc" | "desc";
148
147
  field: string;
148
+ direction: "asc" | "desc";
149
149
  } | undefined, {
150
150
  direction: convex_values0.VUnion<"asc" | "desc", [convex_values0.VLiteral<"asc", "required">, convex_values0.VLiteral<"desc", "required">], "required", never>;
151
151
  field: convex_values0.VString<string, "required">;
152
- }, "optional", "direction" | "field">;
152
+ }, "optional", "field" | "direction">;
153
153
  where: convex_values0.VArray<{
154
154
  mode?: "sensitive" | "insensitive" | undefined;
155
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
156
155
  connector?: "AND" | "OR" | undefined;
156
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
157
157
  value: string | number | boolean | string[] | number[] | null;
158
158
  field: string;
159
159
  }[] | undefined, convex_values0.VObject<{
160
160
  mode?: "sensitive" | "insensitive" | undefined;
161
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
162
161
  connector?: "AND" | "OR" | undefined;
162
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
163
163
  value: string | number | boolean | string[] | number[] | null;
164
164
  field: string;
165
165
  }, {
166
166
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
167
167
  field: convex_values0.VString<string, "required">;
168
168
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
169
- operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
169
+ operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
170
170
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
171
- }, "required", "mode" | "value" | "operator" | "field" | "connector">, "optional">;
172
- }, "required", "limit" | "model" | "offset" | "select" | "sortBy" | "where" | "sortBy.direction" | "sortBy.field">;
171
+ }, "required", "mode" | "value" | "connector" | "field" | "operator">, "optional">;
172
+ }, "required", "model" | "select" | "where" | "limit" | "offset" | "sortBy" | "sortBy.field" | "sortBy.direction">;
173
173
  declare const hasUniqueFields: (betterAuthSchema: BetterAuthDBSchema, model: string, input: Record<string, any>) => boolean;
174
174
  declare const checkUniqueFields: <Schema extends SchemaDefinition<any, any>>(ctx: GenericQueryCtx<GenericDataModel>, schema: Schema, betterAuthSchema: BetterAuthDBSchema, table: string, input: Record<string, any>, doc?: Record<string, any>) => Promise<void>;
175
175
  declare const selectFields: <T extends TableNamesInDataModel<GenericDataModel>, D extends DocumentByName<GenericDataModel, T>>(doc: D | null, select?: string[]) => D | null;
@@ -2896,6 +2896,24 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
2896
2896
  rls?: RlsContext | undefined, relationLoading?: {
2897
2897
  concurrency?: number;
2898
2898
  } | undefined, vectorSearchProvider?: VectorSearchProvider | undefined, configuredIndex?: PredicateWhereIndexConfig<TTableConfig> | undefined, countIndexReadiness?: Map<string, Promise<void>>);
2899
+ /**
2900
+ * Relation counts for a document the caller is already holding.
2901
+ *
2902
+ * `returning({ _count })` runs inside the mutation that just wrote the row,
2903
+ * so resolving it through `execute()` spends one `db.get` re-reading a
2904
+ * document that is already in the transaction's write set. The count engine
2905
+ * only ever reads the counted edges' source fields off its parent, and the
2906
+ * caller's document carries all of them, so the root read is pure waste.
2907
+ * Everything else `execute()` would have done to that row — the select-plan
2908
+ * assertion and the RLS select filter — still runs here.
2909
+ *
2910
+ * Reached through the static accessor below rather than exposed on the instance:
2911
+ * `GelRelationalQuery` is the declared return type of the public query
2912
+ * builders, so an instance method would land in every user's autocomplete.
2913
+ */
2914
+ private _countRelationsForHeldRow;
2915
+ /** @internal Accessor for `returning({ _count })`; see `returning-count.ts`. */
2916
+ static countRelationsForHeldRow(query: GelRelationalQuery<any, any, any>, row: Record<string, unknown>, countSelection: Record<string, unknown>): Promise<Record<string, number>>;
2899
2917
  /**
2900
2918
  * The aggregate-index runtime, registered at `createOrm()`.
2901
2919
  *
package/dist/cli.mjs CHANGED
@@ -454,6 +454,13 @@ Modes:
454
454
  (default) Hotspot analysis (per-function isolate ranking)
455
455
  --deploy Deploy analysis (single isolate bundle, Convex-like)
456
456
 
457
+ Hotspot columns (all describe what the entry actually bundles):
458
+ OutMB Bundled size of the isolate this entry deploys into
459
+ DepMB Source size of the dependency files in that isolate
460
+ LocMB Source size of your own Convex files in that isolate
461
+ Files Files contributing bytes to that isolate
462
+ Fns Convex handlers exported by the entry
463
+
457
464
  Flags:
458
465
  --details Show extra per-entry/package details
459
466
  --input Include top internal inputs in detail output
@@ -522,13 +529,46 @@ const packageFromInputPath = (inputPath) => {
522
529
  }
523
530
  return "(other)";
524
531
  };
525
- const buildHotspotEntry = (entryPoint, externalizeSchema) => loadEsbuild().build({
532
+ /** Matches the `out: 'e<index>'` names `createHotspotBuildOptions` assigns. */
533
+ const HOTSPOT_ENTRY_OUTPUT_REGEX = /^out\/e(\d+)\.[^/]+$/;
534
+ const isLocalInputPath = (inputPath) => inputPath.startsWith("convex/") || inputPath.startsWith("example/convex/") || inputPath.includes("/example/convex/");
535
+ /**
536
+ * Hotspot ranks each Convex function by the isolate it deploys into, so every
537
+ * entry needs its own fully tree-shaken closure.
538
+ *
539
+ * esbuild parses each input once per build no matter how many entry points it
540
+ * receives, and with splitting OFF every entry output still carries its own
541
+ * independently tree-shaken closure. Passing the whole entry set to one build
542
+ * therefore keeps per-entry byte counts identical to building each entry alone
543
+ * while reading the shared import graph once.
544
+ *
545
+ * Do not turn on `splitting` here. Shared chunks retain code a lone entry would
546
+ * have dropped, which inflates exactly the number this mode ranks by (measured
547
+ * up to +70% on `example`) and turns dynamic imports into extra outputs that
548
+ * carry an `entryPoint`. `--deploy` already owns the split-bundle view.
549
+ *
550
+ * Each entry is given an explicit output name so its result can be found by
551
+ * position instead of by path. esbuild resolves a symlinked source file to its
552
+ * real path before reporting `output.entryPoint`, so matching that field back
553
+ * against the selected entry would silently lose any entry reached through a
554
+ * symlink. Explicit names also make `foo.ts` and `foo.js` in one directory
555
+ * unable to collide on a single output path.
556
+ *
557
+ * `attributeHotspotBuild` must be given the same `entryPoints` array, since the
558
+ * output name is the entry's index in it.
559
+ */
560
+ const createHotspotBuildOptions = (entryPoints, projectRoot, externalizeSchema) => ({
561
+ absWorkingDir: projectRoot,
526
562
  bundle: true,
527
- entryPoints: [entryPoint],
563
+ entryPoints: entryPoints.map((entryPoint, index) => ({
564
+ in: entryPoint,
565
+ out: `e${index}`
566
+ })),
528
567
  external: ["convex", "convex/*"],
529
568
  format: "esm",
530
569
  logLevel: "silent",
531
570
  metafile: true,
571
+ outdir: "out",
532
572
  platform: "browser",
533
573
  target: ["esnext"],
534
574
  conditions: ["convex", "module"],
@@ -539,42 +579,67 @@ const buildHotspotEntry = (entryPoint, externalizeSchema) => loadEsbuild().build
539
579
  write: false,
540
580
  plugins: externalizeSchema ? [schemaExternalFallbackPlugin] : []
541
581
  });
542
- const analyzeHotspotEntry = async (entryPoint, projectRoot, includeDeepData) => {
543
- let result;
544
- let schemaExternalized = false;
582
+ /**
583
+ * `allowSchemaFallback` is off for the shared pass on purpose. Externalizing
584
+ * `./schema` removes real weight from every entry that imports it, not just the
585
+ * one that failed: forcing it across `example` moved 25 of 26 rows, one by
586
+ * -98%. Letting the shared pass fail and rebuilding per entry instead keeps the
587
+ * approximation on the entries that actually need it.
588
+ */
589
+ const buildHotspotBundle = async (entryPoints, projectRoot, allowSchemaFallback) => {
545
590
  try {
546
- result = await buildHotspotEntry(entryPoint, false);
591
+ return {
592
+ result: await loadEsbuild().build(createHotspotBuildOptions(entryPoints, projectRoot, false)),
593
+ schemaExternalized: false
594
+ };
547
595
  } catch (error) {
548
- if (!shouldRetryWithSchemaExternalized(error)) throw error;
549
- result = await buildHotspotEntry(entryPoint, true);
550
- schemaExternalized = true;
596
+ if (!(allowSchemaFallback && shouldRetryWithSchemaExternalized(error))) throw error;
597
+ return {
598
+ result: await loadEsbuild().build(createHotspotBuildOptions(entryPoints, projectRoot, true)),
599
+ schemaExternalized: true
600
+ };
601
+ }
602
+ };
603
+ /**
604
+ * Row metrics describe what actually weighs something in the entry's isolate.
605
+ *
606
+ * They count the inputs that emit bytes into that entry's output, which is the
607
+ * same set the `--details` package and input tables below them already report.
608
+ * The build-wide `meta.inputs` cannot answer this: it lists every file esbuild
609
+ * parsed, including ones tree-shaking dropped entirely (534 of 1545 on
610
+ * `example`), and a shared bundle makes it the union across entries rather than
611
+ * one entry's graph. It stays the lookup table for *source* bytes, so `LocMB`
612
+ * and `DepMB` keep measuring source weight rather than post-minify weight.
613
+ */
614
+ const buildHotspotRow = (params) => {
615
+ const { entryPoint, projectRoot, inputs, output, includeDeepData, schemaExternalized } = params;
616
+ const outputInputEntries = Object.entries(output.inputs ?? {});
617
+ let inputCount = 0;
618
+ let totalInputBytes = 0;
619
+ let localInputBytes = 0;
620
+ for (const [inputPath, value] of outputInputEntries) {
621
+ if ((value.bytesInOutput ?? 0) <= 0) continue;
622
+ inputCount += 1;
623
+ const sourceBytes = inputs[inputPath]?.bytes ?? 0;
624
+ totalInputBytes += sourceBytes;
625
+ if (isLocalInputPath(inputPath)) localInputBytes += sourceBytes;
551
626
  }
552
- const meta = result.metafile;
553
- if (!meta) throw new Error(`No metafile generated for ${entryPoint}`);
554
- const output = Object.values(meta.outputs).at(0);
555
- if (!output) throw new Error(`No output generated for ${entryPoint}`);
556
- const inputEntries = Object.entries(meta.inputs);
557
- const totalInputBytes = inputEntries.reduce((sum, [, value]) => sum + (value.bytes ?? 0), 0);
558
- const isLocalInput = (inputPath) => inputPath.startsWith("convex/") || inputPath.startsWith("example/convex/") || inputPath.includes("/example/convex/");
559
- const localInputBytes = inputEntries.filter(([inputPath]) => isLocalInput(inputPath)).reduce((sum, [, value]) => sum + (value.bytes ?? 0), 0);
560
627
  const row = {
561
628
  entry: path.relative(projectRoot, entryPoint),
562
- inputCount: inputEntries.length,
629
+ inputCount,
563
630
  localInputBytes,
564
631
  dependencyInputBytes: Math.max(0, totalInputBytes - localInputBytes),
565
- totalInputBytes,
566
632
  outputBytes: output.bytes,
567
633
  schemaExternalized
568
634
  };
569
635
  if (!includeDeepData) return row;
570
- const bytesByInput = new Map(inputEntries.map(([inputPath, value]) => [inputPath, value.bytes ?? 0]));
571
- const outputInputs = Object.entries(output.inputs ?? {}).map(([inputPath, value]) => ({
636
+ const outputInputs = outputInputEntries.map(([inputPath, value]) => ({
572
637
  path: inputPath,
573
638
  bytesInOutput: value.bytesInOutput ?? 0,
574
- sourceBytes: bytesByInput.get(inputPath) ?? 0
639
+ sourceBytes: inputs[inputPath]?.bytes ?? 0
575
640
  })).sort((a, b) => b.bytesInOutput - a.bytesInOutput);
576
- const inputSet = new Set(inputEntries.map(([inputPath]) => inputPath));
577
- const importsByInput = Object.fromEntries(inputEntries.map(([inputPath, value]) => [inputPath, Array.from(new Set((value.imports ?? []).map((entry) => entry.path).filter((importPath) => typeof importPath === "string" && inputSet.has(importPath))))]));
641
+ const inputSet = new Set(outputInputEntries.map(([inputPath]) => inputPath));
642
+ const importsByInput = Object.fromEntries(outputInputEntries.map(([inputPath]) => [inputPath, Array.from(new Set((inputs[inputPath]?.imports ?? []).map((entry) => entry.path).filter((importPath) => typeof importPath === "string" && inputSet.has(importPath))))]));
578
643
  return {
579
644
  ...row,
580
645
  deep: {
@@ -583,6 +648,34 @@ const analyzeHotspotEntry = async (entryPoint, projectRoot, includeDeepData) =>
583
648
  }
584
649
  };
585
650
  };
651
+ /**
652
+ * Pairs each entry with its output through the position `createHotspotBuildOptions`
653
+ * encoded in the output name, so `entryPoints` must be the array that built `meta`.
654
+ */
655
+ const attributeHotspotBuild = (params) => {
656
+ const { entryPoints, projectRoot, meta, includeDeepData, schemaExternalized } = params;
657
+ const inputs = meta.inputs;
658
+ const outputByEntryIndex = /* @__PURE__ */ new Map();
659
+ for (const [outputPath, output] of Object.entries(meta.outputs)) {
660
+ const index = HOTSPOT_ENTRY_OUTPUT_REGEX.exec(outputPath)?.[1];
661
+ if (index === void 0) continue;
662
+ outputByEntryIndex.set(Number(index), output);
663
+ }
664
+ const rowsByEntry = /* @__PURE__ */ new Map();
665
+ for (const [index, entryPoint] of entryPoints.entries()) {
666
+ const output = outputByEntryIndex.get(index);
667
+ if (!output) throw new Error(`esbuild produced no output for ${path.relative(projectRoot, entryPoint)}.`);
668
+ rowsByEntry.set(entryPoint, buildHotspotRow({
669
+ entryPoint,
670
+ includeDeepData,
671
+ inputs,
672
+ output,
673
+ projectRoot,
674
+ schemaExternalized
675
+ }));
676
+ }
677
+ return rowsByEntry;
678
+ };
586
679
  const printHotspotTopInputs = (row, options) => {
587
680
  if (!row.deep) return;
588
681
  const internalInputs = row.deep.outputInputs.filter((input) => input.bytesInOutput > 0 && !isNodeModulesInputPath(input.path));
@@ -866,37 +959,106 @@ const collectAnalyzeEntrySelection = async (roots, options) => {
866
959
  };
867
960
  const MAX_ANALYZE_CONCURRENCY = 8;
868
961
  /**
869
- * esbuild bundles are independent, so the entry sweep runs through a bounded
870
- * pool instead of one build at a time. The cap stays low enough to avoid
871
- * oversubscribing esbuild's worker pool on small machines.
962
+ * Only the per-entry recovery sweep still runs multiple builds, so the cap
963
+ * stays low enough to avoid oversubscribing esbuild's worker pool on small
964
+ * machines.
872
965
  */
873
966
  const resolveAnalyzeConcurrency = (taskCount) => {
874
967
  if (taskCount <= 1) return 1;
875
968
  const cpuCount = os.cpus().length || 1;
876
969
  return Math.max(1, Math.min(MAX_ANALYZE_CONCURRENCY, cpuCount - 1, taskCount));
877
970
  };
878
- const collectHotspotRows = async (roots, options, includeDeepData) => {
879
- const { isolateEntries, generatedEntries, entryPoints, handlerExportsByEntry } = await collectAnalyzeEntrySelection(roots, options);
880
- const rows = await mapWithConcurrency(entryPoints, resolveAnalyzeConcurrency(entryPoints.length), async (entryPoint) => {
971
+ const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
972
+ const runHotspotBuild = async (entryPoints, projectRoot, includeDeepData, allowSchemaFallback, build) => {
973
+ const { result, schemaExternalized } = await build(entryPoints, projectRoot, allowSchemaFallback);
974
+ const meta = result.metafile;
975
+ if (!meta) throw new Error("esbuild produced no metafile for the hotspot bundle.");
976
+ return attributeHotspotBuild({
977
+ entryPoints,
978
+ includeDeepData,
979
+ meta,
980
+ projectRoot,
981
+ schemaExternalized
982
+ });
983
+ };
984
+ /**
985
+ * Recovery path only. A shared build fails as a unit, so one unbuildable entry
986
+ * would take the whole report down. Rebuilding one entry at a time is the only
987
+ * way to say *which* entry is broken, and it reuses the same build options so
988
+ * surviving rows keep the numbers the shared pass would have produced. It is
989
+ * also where the schema fallback belongs, since here it only degrades the
990
+ * entries that actually need it.
991
+ *
992
+ * Reaching this path needs esbuild >= 0.27.7. Earlier builds threw
993
+ * `SyntaxError: Unexpected end of JSON input` out of esbuild's own stdout
994
+ * handler whenever a `metafile: true` build failed, which is outside any promise
995
+ * this module can catch, so the CLI died before any sweep could report a failed
996
+ * row.
997
+ */
998
+ const sweepHotspotEntriesIndividually = async (entryPoints, projectRoot, includeDeepData, build) => {
999
+ const results = await mapWithConcurrency(entryPoints, resolveAnalyzeConcurrency(entryPoints.length), async (entryPoint) => {
881
1000
  try {
882
1001
  return {
883
- ...await analyzeHotspotEntry(entryPoint, roots.projectRoot, includeDeepData),
884
- handlerExports: handlerExportsByEntry.get(entryPoint) ?? []
1002
+ rowsByEntry: await runHotspotBuild([entryPoint], projectRoot, includeDeepData, true, build),
1003
+ failedRows: []
885
1004
  };
886
1005
  } catch (error) {
887
1006
  return {
888
- entry: path.relative(roots.projectRoot, entryPoint),
889
- error: error instanceof Error ? error.message : String(error)
1007
+ rowsByEntry: /* @__PURE__ */ new Map(),
1008
+ failedRows: [{
1009
+ entry: path.relative(projectRoot, entryPoint),
1010
+ error: toErrorMessage(error)
1011
+ }]
890
1012
  };
891
1013
  }
892
1014
  });
1015
+ const rowsByEntry = /* @__PURE__ */ new Map();
1016
+ const failedRows = [];
1017
+ for (const result of results) {
1018
+ for (const [entryPoint, row] of result.rowsByEntry) rowsByEntry.set(entryPoint, row);
1019
+ failedRows.push(...result.failedRows);
1020
+ }
1021
+ return {
1022
+ rowsByEntry,
1023
+ failedRows
1024
+ };
1025
+ };
1026
+ const sweepHotspotEntries = async (entryPoints, projectRoot, includeDeepData, build = buildHotspotBundle) => {
1027
+ if (entryPoints.length === 0) return {
1028
+ rowsByEntry: /* @__PURE__ */ new Map(),
1029
+ failedRows: [],
1030
+ fallbackReason: null
1031
+ };
1032
+ try {
1033
+ return {
1034
+ rowsByEntry: await runHotspotBuild(entryPoints, projectRoot, includeDeepData, false, build),
1035
+ failedRows: [],
1036
+ fallbackReason: null
1037
+ };
1038
+ } catch (error) {
1039
+ return {
1040
+ ...await sweepHotspotEntriesIndividually(entryPoints, projectRoot, includeDeepData, build),
1041
+ fallbackReason: firstLine(toErrorMessage(error))
1042
+ };
1043
+ }
1044
+ };
1045
+ const collectHotspotRows = async (roots, options, includeDeepData) => {
1046
+ const { isolateEntries, generatedEntries, entryPoints, handlerExportsByEntry } = await collectAnalyzeEntrySelection(roots, options);
1047
+ const { rowsByEntry, failedRows, fallbackReason } = await sweepHotspotEntries(entryPoints, roots.projectRoot, includeDeepData);
893
1048
  return {
894
1049
  isolateEntries,
895
1050
  generatedEntries,
896
1051
  entryPoints,
897
- successRows: rows.filter((row) => !("error" in row)).sort((a, b) => b.outputBytes - a.outputBytes),
898
- failedRows: rows.filter((row) => "error" in row),
899
- handlerExportsByEntry
1052
+ successRows: entryPoints.flatMap((entryPoint) => {
1053
+ const row = rowsByEntry.get(entryPoint);
1054
+ if (!row) return [];
1055
+ return [{
1056
+ ...row,
1057
+ handlerExports: handlerExportsByEntry.get(entryPoint) ?? []
1058
+ }];
1059
+ }).sort((a, b) => b.outputBytes - a.outputBytes),
1060
+ failedRows,
1061
+ fallbackReason
900
1062
  };
901
1063
  };
902
1064
  const buildPackageImportGraphRows = (row, limit = 12) => {
@@ -1021,7 +1183,7 @@ const runHotspotInteractive = async (roots, options) => {
1021
1183
  ...options,
1022
1184
  includeGenerated: state.includeGenerated,
1023
1185
  entryPattern: null
1024
- }, false);
1186
+ }, true);
1025
1187
  let visibleRows = sortHotspotRows(filterHotspotRows(snapshot.successRows, state.filterQuery), state.sortKey);
1026
1188
  let watcher = null;
1027
1189
  let watchTimer = null;
@@ -1029,7 +1191,6 @@ const runHotspotInteractive = async (roots, options) => {
1029
1191
  let refreshQueued = false;
1030
1192
  let shouldQuit = false;
1031
1193
  let listViewportHeight = 10;
1032
- const deepRowCache = /* @__PURE__ */ new Map();
1033
1194
  const getSelectedEntry = () => visibleRows.at(state.selectedIndex)?.entry ?? null;
1034
1195
  const findRowByEntry = (entry) => {
1035
1196
  if (!entry) return null;
@@ -1064,8 +1225,7 @@ const runHotspotInteractive = async (roots, options) => {
1064
1225
  ...options,
1065
1226
  includeGenerated: state.includeGenerated,
1066
1227
  entryPattern: null
1067
- }, false);
1068
- deepRowCache.clear();
1228
+ }, true);
1069
1229
  syncVisibleRows(preferredEntry);
1070
1230
  state = reduceInteractiveState(state, {
1071
1231
  type: "setStatus",
@@ -1147,28 +1307,7 @@ const runHotspotInteractive = async (roots, options) => {
1147
1307
  stdin.setEncoding("utf8");
1148
1308
  }
1149
1309
  };
1150
- const ensureDeepRow = async (entry) => {
1151
- if (deepRowCache.has(entry)) return deepRowCache.get(entry) ?? null;
1152
- const entryPoint = path.join(roots.projectRoot, entry);
1153
- try {
1154
- const analyzed = await analyzeHotspotEntry(entryPoint, roots.projectRoot, true);
1155
- const fallbackHandlers = findRowByEntry(entry)?.handlerExports ?? [];
1156
- const mergedRow = {
1157
- ...analyzed,
1158
- handlerExports: snapshot.handlerExportsByEntry.get(entryPoint) ?? fallbackHandlers
1159
- };
1160
- deepRowCache.set(entry, mergedRow);
1161
- return mergedRow;
1162
- } catch (error) {
1163
- deepRowCache.set(entry, null);
1164
- state = reduceInteractiveState(state, {
1165
- type: "setStatus",
1166
- message: `Detail load failed for ${entry}: ${firstLine(error instanceof Error ? error.message : String(error))}`
1167
- });
1168
- return null;
1169
- }
1170
- };
1171
- const renderInteractive = async () => {
1310
+ const renderInteractive = () => {
1172
1311
  const layout = resolveInteractiveLayout(stdout.columns ?? outputWidth, stdout.rows ?? 30);
1173
1312
  listViewportHeight = layout.listViewportHeight;
1174
1313
  state = reduceInteractiveState(state, {
@@ -1176,8 +1315,7 @@ const runHotspotInteractive = async (roots, options) => {
1176
1315
  topIndex: fitListViewport(visibleRows.length, state.selectedIndex, layout.listViewportHeight, state.topIndex)
1177
1316
  });
1178
1317
  const activeEntry = (visibleRows.at(state.selectedIndex) ?? null)?.entry ?? null;
1179
- let detailRow = findRowByEntry(activeEntry);
1180
- if (!state.showHelp && activeEntry && state.detailPane !== "handlers") detailRow = await ensureDeepRow(activeEntry);
1318
+ const detailRow = findRowByEntry(activeEntry);
1181
1319
  const statusLine = state.statusMessage || "Ready";
1182
1320
  const headerLine = `entries=${visibleRows.length}/${snapshot.successRows.length} sort=${sortLabel(state.sortKey)} filter=${state.filterQuery || "∅"} all=${state.includeGenerated ? "on" : "off"} pane=${state.detailPane} watch=${state.watchEnabled ? "on" : "off"}`;
1183
1321
  const listLines = (() => {
@@ -1231,7 +1369,7 @@ const runHotspotInteractive = async (roots, options) => {
1231
1369
  stdin.setEncoding("utf8");
1232
1370
  try {
1233
1371
  while (!shouldQuit) {
1234
- await renderInteractive();
1372
+ renderInteractive();
1235
1373
  const key = await readKey();
1236
1374
  if (key === "" || key === "q") {
1237
1375
  shouldQuit = true;
@@ -1317,7 +1455,7 @@ const runHotspotInteractive = async (roots, options) => {
1317
1455
  const runHotspotAnalysis = async (roots, options) => {
1318
1456
  if (shouldPromptForHotspotPick(options)) return runHotspotInteractive(roots, options);
1319
1457
  const includeDeepData = options.details;
1320
- const { isolateEntries, generatedEntries, entryPoints, successRows, failedRows } = await collectHotspotRows(roots, options, includeDeepData);
1458
+ const { isolateEntries, generatedEntries, entryPoints, successRows, failedRows, fallbackReason } = await collectHotspotRows(roots, options, includeDeepData);
1321
1459
  if (entryPoints.length === 0) {
1322
1460
  if (options.includeGenerated) console.log("No matching entries found for the provided regex.");
1323
1461
  else console.log("No matching Convex handler entries found (files exporting query/mutation/action, including internal variants).");
@@ -1343,6 +1481,9 @@ const runHotspotAnalysis = async (roots, options) => {
1343
1481
  const actionRows = successRows;
1344
1482
  console.log(bold("Runtime hotspot analysis (least optimized functions)"));
1345
1483
  console.log(`isolateEntries=${isolateEntries.length} handlers=${totalHandlers} extraAll=${generatedEntries.length} selected=${entryPoints.length} ok=${successRows.length} failed=${failedRows.length} avg=${formatBytes(outputAverage)}${largest ? ` largest=${largest.entry} (${formatBytes(largest.outputBytes)})` : ""}`);
1484
+ if (fallbackReason) console.log(dim(`note: the shared bundle failed, so entries were rebuilt one at a time: ${fallbackReason}`));
1485
+ const approximateRows = successRows.filter((row) => row.schemaExternalized);
1486
+ if (approximateRows.length > 0) console.log(colorize(`note: schema imports were externalized after a build error for ${approximateRows.length} ${approximateRows.length === 1 ? "entry" : "entries"}; their dependency sizes are approximate (${approximateRows.map((row) => row.entry).join(", ")}).`, ANSI.yellow));
1346
1487
  console.log("");
1347
1488
  if (actionRows.length > 0) {
1348
1489
  console.log(bold("Agent queue (run top-down):"));