kitcn 0.27.3 → 0.27.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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # kitcn
2
2
 
3
+ ## 0.27.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#400](https://github.com/udecode/kitcn/pull/400) [`9963d33`](https://github.com/udecode/kitcn/commit/9963d33048ec532f0a2d9a06bb618486856178b1) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
8
+
9
+ - Speed up `kitcn analyze` by measuring every selected entry in one bundler pass
10
+ instead of one pass per entry: 4.3 s → 0.8 s of bundling on the 24-entry example
11
+ app, with byte-identical `OutMB`. Each entry is still sized as its own
12
+ independently tree-shaken isolate, so the ranking is unchanged.
13
+ - Report the hotspot `DepMB` and `Files` columns for the files that carry weight in
14
+ each entry's isolate. They previously counted every file the bundler parsed,
15
+ including files tree-shaking dropped entirely, which disagreed with the `--details`
16
+ package and input tables directly beneath them. Expect both columns to read lower
17
+ than before for the same code; `OutMB` and `LocMB` are unaffected.
18
+ - Fix `kitcn analyze` crashing with a bundler stack trace when a Convex entry fails
19
+ to build. It now lists the entry under `Failed entries:`, keeps reporting every
20
+ entry that did build, and still exits `1`. This needs esbuild `0.27.7`, which is
21
+ now the minimum.
22
+ - Warn which entries had their `./schema` imports externalized after a build error,
23
+ instead of leaving the default mode silent about approximate dependency sizes. The
24
+ approximation is applied per entry, so one unbuildable schema import no longer
25
+ shrinks the numbers for unrelated functions.
26
+ - Open the interactive analyzer's package and input panes without a second bundle, so
27
+ moving the selection no longer pauses to rebuild.
28
+ - Document the hotspot ranking columns, in `--help` and in the CLI reference, along
29
+ with the `--top-inputs` / `--top-packages` flags.
30
+
3
31
  ## 0.27.3
4
32
 
5
33
  ### Patch Changes
@@ -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-SzH57bhp.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-SzH57bhp.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
+ direction: "asc" | "desc";
138
+ field: string;
139
+ } | undefined;
140
140
  model: string;
141
141
  }, {
142
142
  limit: convex_values0.VFloat64<number | undefined, "optional">;
@@ -152,24 +152,24 @@ declare const adapterArgsValidator: convex_values0.VObject<{
152
152
  }, "optional", "direction" | "field">;
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.direction" | "sortBy.field">;
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;
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):"));
@@ -183,8 +183,8 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
183
183
  deleteMany: convex_server0.RegisteredMutation<"internal", {
184
184
  input: {
185
185
  where?: {
186
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
187
186
  connector?: "AND" | "OR" | undefined;
187
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
188
188
  value: string | number | boolean | string[] | number[] | null;
189
189
  field: string;
190
190
  }[] | undefined;
@@ -212,8 +212,8 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
212
212
  deleteOne: convex_server0.RegisteredMutation<"internal", {
213
213
  input: {
214
214
  where?: {
215
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
216
215
  connector?: "AND" | "OR" | undefined;
216
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
217
217
  value: string | number | boolean | string[] | number[] | null;
218
218
  field: string;
219
219
  }[] | undefined;
@@ -225,19 +225,20 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
225
225
  }, Promise<Record<string, unknown> | undefined>>;
226
226
  findMany: convex_server0.RegisteredQuery<"internal", {
227
227
  join?: any;
228
- limit?: number | undefined;
229
- offset?: number | undefined;
230
- sortBy?: {
231
- direction: "asc" | "desc";
232
- field: string;
233
- } | undefined;
234
228
  where?: {
235
229
  mode?: "sensitive" | "insensitive" | undefined;
236
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
237
230
  connector?: "AND" | "OR" | undefined;
231
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
238
232
  value: string | number | boolean | string[] | number[] | null;
239
233
  field: string;
240
234
  }[] | undefined;
235
+ limit?: number | undefined;
236
+ offset?: number | undefined;
237
+ sortBy?: {
238
+ direction: "asc" | "desc";
239
+ field: string;
240
+ } | undefined;
241
+ model: string;
241
242
  paginationOpts: {
242
243
  id?: number;
243
244
  endCursor?: string | null;
@@ -246,15 +247,14 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
246
247
  numItems: number;
247
248
  cursor: string | null;
248
249
  };
249
- model: string;
250
250
  }, Promise<PaginationResult<convex_server0.GenericDocument>>>;
251
251
  findOne: convex_server0.RegisteredQuery<"internal", {
252
252
  join?: any;
253
253
  select?: string[] | undefined;
254
254
  where?: {
255
255
  mode?: "sensitive" | "insensitive" | undefined;
256
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
257
256
  connector?: "AND" | "OR" | undefined;
257
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
258
258
  value: string | number | boolean | string[] | number[] | null;
259
259
  field: string;
260
260
  }[] | undefined;
@@ -265,8 +265,8 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
265
265
  updateMany: convex_server0.RegisteredMutation<"internal", {
266
266
  input: {
267
267
  where?: {
268
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
269
268
  connector?: "AND" | "OR" | undefined;
269
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
270
270
  value: string | number | boolean | string[] | number[] | null;
271
271
  field: string;
272
272
  }[] | undefined;
@@ -300,8 +300,8 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
300
300
  updateOne: convex_server0.RegisteredMutation<"internal", {
301
301
  input: {
302
302
  where?: {
303
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
304
303
  connector?: "AND" | "OR" | undefined;
304
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
305
305
  value: string | number | boolean | string[] | number[] | null;
306
306
  field: string;
307
307
  }[] | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitcn",
3
- "version": "0.27.3",
3
+ "version": "0.27.4",
4
4
  "description": "kitcn - React Query integration and CLI tools for Convex",
5
5
  "keywords": [
6
6
  "convex",
@@ -69,7 +69,7 @@
69
69
  "common-tags": "^1.8.2",
70
70
  "diff": "^8.0.2",
71
71
  "dotenv": "^17.3.1",
72
- "esbuild": "^0.27.3",
72
+ "esbuild": "^0.27.7",
73
73
  "execa": "^9.6.1",
74
74
  "jiti": "^2.6.1",
75
75
  "jotai": "^2.18.0",
@@ -644,6 +644,12 @@ bunx kitcn aggregate backfill --prod
644
644
  bunx kitcn analyze
645
645
  ```
646
646
 
647
+ `kitcn analyze` ranks each Convex function by the isolate it deploys into.
648
+ `OutMB` is that isolate's bundled size; `DepMB`/`LocMB` are the source size of
649
+ the dependency and own-Convex files inside it; `Files` counts the files
650
+ contributing bytes to it. A file that is imported but fully tree-shaken away is
651
+ counted in none of them. Use `--deploy` for deployment totals instead.
652
+
647
653
  Concave lane:
648
654
 
649
655
  ```bash