@teambit/component-compare 1.0.1080 → 1.0.1082

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.
@@ -0,0 +1,124 @@
1
+ .page {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ height: 100%;
6
+ min-height: 0;
7
+ background: var(--surface-base-color, #fff);
8
+ }
9
+
10
+ .versionPickerRow {
11
+ flex: 0 0 auto;
12
+ padding: 8px 16px;
13
+ border-bottom: 1px solid var(--border-color, #e6e8eb);
14
+ }
15
+
16
+ .blankState {
17
+ flex: 1 1 auto;
18
+ min-height: 0;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ }
23
+
24
+ @keyframes compareSkeletonShimmer {
25
+ 0% {
26
+ background-position: 200% 0;
27
+ }
28
+ 100% {
29
+ background-position: -200% 0;
30
+ }
31
+ }
32
+
33
+ %compareSkeletonShape {
34
+ border-radius: 6px;
35
+ background: linear-gradient(
36
+ 90deg,
37
+ var(--surface-neutral-color, #f0f2f5) 25%,
38
+ var(--surface-hover-color, #e8ecf0) 37%,
39
+ var(--surface-neutral-color, #f0f2f5) 63%
40
+ );
41
+ background-size: 400% 100%;
42
+ animation: compareSkeletonShimmer 1.4s ease infinite;
43
+ }
44
+
45
+ .toolbarSkeleton {
46
+ flex: 0 0 auto;
47
+ display: flex;
48
+ gap: 8px;
49
+ padding: 10px 16px;
50
+ border-bottom: 1px solid var(--border-color, #e6e8eb);
51
+ }
52
+
53
+ .skelPill {
54
+ @extend %compareSkeletonShape;
55
+ width: 92px;
56
+ height: 28px;
57
+ }
58
+
59
+ .bodySkeleton {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 10px;
63
+ padding: 16px 20px;
64
+ }
65
+
66
+ .skelHeader {
67
+ @extend %compareSkeletonShape;
68
+ width: 40%;
69
+ height: 16px;
70
+ margin-bottom: 8px;
71
+ }
72
+
73
+ .skelLine {
74
+ @extend %compareSkeletonShape;
75
+ height: 12px;
76
+ }
77
+
78
+ .diffPane {
79
+ flex: 1 1 auto;
80
+ min-height: 0;
81
+ // shrink below content width so a wide diff scrolls inside its own file body instead of forcing
82
+ // the whole page horizontally wider.
83
+ min-width: 0;
84
+ overflow: auto;
85
+ // No padding/gap on the scroll container itself: the sticky component header (top:
86
+ // `--group-header-height`) and diff-file headers (top: `--group-header-height` +
87
+ // `--component-header-height`) are measured from the scrollport edge, so any padding here
88
+ // offsets them and they stop stacking flush. Inner cards/headers carry their own padding
89
+ // (mirrors lane-compare's padding-less `.diffPane`).
90
+ display: flex;
91
+ flex-direction: column;
92
+
93
+ // Tab visibility driven by `data-view-mode` on the pane. All `DeferredTab` siblings are mounted
94
+ // simultaneously so their internal query state survives mode switches; CSS hides the non-active
95
+ // tabs. Mirror of lane-compare's panel selectors so the same inline-* tabs work in both pages.
96
+ &[data-view-mode='code'] [data-tab-id]:not([data-tab-id='inline-code']) {
97
+ display: none;
98
+ }
99
+ &[data-view-mode='preview'] [data-tab-id]:not([data-tab-id='inline-preview']) {
100
+ display: none;
101
+ }
102
+ &[data-view-mode='dependencies'] [data-tab-id]:not([data-tab-id='inline-deps']) {
103
+ display: none;
104
+ }
105
+ &[data-view-mode='tests'] [data-tab-id]:not([data-tab-id='inline-tests']) {
106
+ display: none;
107
+ }
108
+ &[data-view-mode='config'] [data-tab-id]:not([data-tab-id='inline-config']) {
109
+ display: none;
110
+ }
111
+ &[data-view-mode='docs'] [data-tab-id]:not([data-tab-id='inline-docs']) {
112
+ display: none;
113
+ }
114
+ // The API view is not an inline `[data-tab-id]` tab — it's rendered as a direct child of the
115
+ // inline compare (only while active). Hide every inline tab so only the API element shows.
116
+ &[data-view-mode='api'] [data-tab-id] {
117
+ display: none;
118
+ }
119
+
120
+ // reveal the header note a view registered for the active view (e.g. the deps change tally).
121
+ &[data-view-mode='dependencies'] [data-header-extra-view='dependencies'] {
122
+ display: inline-flex;
123
+ }
124
+ }
@@ -35,14 +35,25 @@ function componentCompareSchema(componentCompareMain) {
35
35
  type ComponentCompareResult {
36
36
  # unique id for graphql - baseId + compareId
37
37
  id: String!
38
+ baseId: String!
39
+ compareId: String!
38
40
  code(fileName: String): [FileCompareResult!]!
39
41
  aspects(aspectName: String): [FieldCompareResult!]!
40
42
  tests(fileName: String): [FileCompareResult!]
41
43
  api: APIDiffResult
42
44
  }
43
45
 
46
+ input ComponentComparePair {
47
+ baseId: String!
48
+ compareId: String!
49
+ }
50
+
44
51
  extend type ComponentHost {
45
52
  compareComponent(baseId: String!, compareId: String!): ComponentCompareResult
53
+ # bulk compare a paginated slice of pairs; an element is null if that pair failed to compare
54
+ compareComponents(pairs: [ComponentComparePair!]!, offset: Int, limit: Int): [ComponentCompareResult]!
55
+ # bulk api-diff a paginated slice of pairs; an element is null if that pair's diff couldn't be computed
56
+ apiDiffs(pairs: [ComponentComparePair!]!, offset: Int, limit: Int): [APIDiffResult]!
46
57
  }
47
58
  `,
48
59
  resolvers: {
@@ -52,6 +63,29 @@ function componentCompareSchema(componentCompareMain) {
52
63
  compareId
53
64
  }) => {
54
65
  return componentCompareMain.compare(baseId, compareId);
66
+ },
67
+ compareComponents: async (_, {
68
+ pairs,
69
+ offset,
70
+ limit
71
+ }) => {
72
+ return componentCompareMain.compareComponents(pairs, {
73
+ offset,
74
+ limit
75
+ });
76
+ },
77
+ apiDiffs: async (_, {
78
+ pairs,
79
+ offset,
80
+ limit
81
+ }) => {
82
+ // each element is the plain record `getAPIDiff` returns (or null); the APIDiffResult
83
+ // fields resolve from it via graphql's default field resolvers, same as the single
84
+ // `apiDiff` resolver in @teambit/semantics.schema.
85
+ return componentCompareMain.apiDiffs(pairs, {
86
+ offset,
87
+ limit
88
+ });
55
89
  }
56
90
  },
57
91
  ComponentCompareResult: {
@@ -1 +1 @@
1
- {"version":3,"names":["_graphqlTag","data","require","ownKeys","e","r","t","Object","keys","getOwnPropertySymbols","o","filter","getOwnPropertyDescriptor","enumerable","push","apply","_objectSpread","arguments","length","forEach","_defineProperty","getOwnPropertyDescriptors","defineProperties","defineProperty","_toPropertyKey","value","configurable","writable","i","_toPrimitive","Symbol","toPrimitive","call","TypeError","String","Number","componentCompareSchema","componentCompareMain","typeDefs","gql","resolvers","ComponentHost","compareComponent","_","baseId","compareId","compare","ComponentCompareResult","id","result","code","fileName","codeFile","filePath","map","c","baseContent","fromContent","compareContent","toContent","tests","testFile","aspects","fieldName","fields","field","api","getAPIDiff"],"sources":["component-compare.graphql.ts"],"sourcesContent":["import { gql } from 'graphql-tag';\nimport type { Schema } from '@teambit/graphql';\nimport type { ComponentCompareMain, ComponentCompareResult } from './component-compare.main.runtime';\n\nexport function componentCompareSchema(componentCompareMain: ComponentCompareMain): Schema {\n return {\n typeDefs: gql`\n type FileCompareResult {\n fileName: String!\n baseContent: String!\n compareContent: String!\n status: String\n diffOutput: String\n }\n\n type FieldCompareResult {\n fieldName: String!\n diffOutput: String\n }\n\n type ComponentCompareResult {\n # unique id for graphql - baseId + compareId\n id: String!\n code(fileName: String): [FileCompareResult!]!\n aspects(aspectName: String): [FieldCompareResult!]!\n tests(fileName: String): [FileCompareResult!]\n api: APIDiffResult\n }\n\n extend type ComponentHost {\n compareComponent(baseId: String!, compareId: String!): ComponentCompareResult\n }\n `,\n resolvers: {\n ComponentHost: {\n compareComponent: async (_, { baseId, compareId }: { baseId: string; compareId: string }) => {\n return componentCompareMain.compare(baseId, compareId);\n },\n },\n ComponentCompareResult: {\n id: (result: ComponentCompareResult) => result.id,\n code: (result: ComponentCompareResult, { fileName }: { fileName?: string }) => {\n if (fileName) {\n return result.code\n .filter((codeFile) => codeFile.filePath === fileName)\n .map((c) => ({ ...c, fileName: c.filePath, baseContent: c.fromContent, compareContent: c.toContent }));\n }\n\n return result.code.map((c) => ({\n ...c,\n fileName: c.filePath,\n baseContent: c.fromContent,\n compareContent: c.toContent,\n }));\n },\n tests: (result: ComponentCompareResult, { fileName }: { fileName?: string }) => {\n if (fileName) {\n return result.tests\n .filter((testFile) => testFile.filePath === fileName)\n .map((c) => ({ ...c, fileName: c.filePath, baseContent: c.fromContent, compareContent: c.toContent }));\n }\n\n return result.tests.map((c) => ({\n ...c,\n fileName: c.filePath,\n baseContent: c.fromContent,\n compareContent: c.toContent,\n }));\n },\n aspects: (result: ComponentCompareResult, { fieldName }: { fieldName?: string }) => {\n if (fieldName) {\n return result.fields.filter((field) => field.fieldName === fieldName);\n }\n return result.fields;\n },\n api: async (result: ComponentCompareResult) => {\n return (await componentCompareMain.getAPIDiff(result.baseId, result.compareId)) ?? null;\n },\n },\n },\n };\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_graphqlTag","data","require","ownKeys","e","r","t","Object","keys","getOwnPropertySymbols","o","filter","getOwnPropertyDescriptor","enumerable","push","apply","_objectSpread","arguments","length","forEach","_defineProperty","getOwnPropertyDescriptors","defineProperties","defineProperty","_toPropertyKey","value","configurable","writable","i","_toPrimitive","Symbol","toPrimitive","call","TypeError","String","Number","componentCompareSchema","componentCompareMain","typeDefs","gql","resolvers","ComponentHost","compareComponent","_","baseId","compareId","compare","compareComponents","pairs","offset","limit","apiDiffs","ComponentCompareResult","id","result","code","fileName","codeFile","filePath","map","c","baseContent","fromContent","compareContent","toContent","tests","testFile","aspects","fieldName","fields","field","api","getAPIDiff"],"sources":["component-compare.graphql.ts"],"sourcesContent":["import { gql } from 'graphql-tag';\nimport type { Schema } from '@teambit/graphql';\nimport type { ComponentCompareMain, ComponentCompareResult } from './component-compare.main.runtime';\n\nexport function componentCompareSchema(componentCompareMain: ComponentCompareMain): Schema {\n return {\n typeDefs: gql`\n type FileCompareResult {\n fileName: String!\n baseContent: String!\n compareContent: String!\n status: String\n diffOutput: String\n }\n\n type FieldCompareResult {\n fieldName: String!\n diffOutput: String\n }\n\n type ComponentCompareResult {\n # unique id for graphql - baseId + compareId\n id: String!\n baseId: String!\n compareId: String!\n code(fileName: String): [FileCompareResult!]!\n aspects(aspectName: String): [FieldCompareResult!]!\n tests(fileName: String): [FileCompareResult!]\n api: APIDiffResult\n }\n\n input ComponentComparePair {\n baseId: String!\n compareId: String!\n }\n\n extend type ComponentHost {\n compareComponent(baseId: String!, compareId: String!): ComponentCompareResult\n # bulk compare a paginated slice of pairs; an element is null if that pair failed to compare\n compareComponents(pairs: [ComponentComparePair!]!, offset: Int, limit: Int): [ComponentCompareResult]!\n # bulk api-diff a paginated slice of pairs; an element is null if that pair's diff couldn't be computed\n apiDiffs(pairs: [ComponentComparePair!]!, offset: Int, limit: Int): [APIDiffResult]!\n }\n `,\n resolvers: {\n ComponentHost: {\n compareComponent: async (_, { baseId, compareId }: { baseId: string; compareId: string }) => {\n return componentCompareMain.compare(baseId, compareId);\n },\n compareComponents: async (\n _,\n {\n pairs,\n offset,\n limit,\n }: { pairs: Array<{ baseId: string; compareId: string }>; offset?: number; limit?: number }\n ) => {\n return componentCompareMain.compareComponents(pairs, { offset, limit });\n },\n apiDiffs: async (\n _,\n {\n pairs,\n offset,\n limit,\n }: { pairs: Array<{ baseId: string; compareId: string }>; offset?: number; limit?: number }\n ) => {\n // each element is the plain record `getAPIDiff` returns (or null); the APIDiffResult\n // fields resolve from it via graphql's default field resolvers, same as the single\n // `apiDiff` resolver in @teambit/semantics.schema.\n return componentCompareMain.apiDiffs(pairs, { offset, limit });\n },\n },\n ComponentCompareResult: {\n id: (result: ComponentCompareResult) => result.id,\n code: (result: ComponentCompareResult, { fileName }: { fileName?: string }) => {\n if (fileName) {\n return result.code\n .filter((codeFile) => codeFile.filePath === fileName)\n .map((c) => ({ ...c, fileName: c.filePath, baseContent: c.fromContent, compareContent: c.toContent }));\n }\n\n return result.code.map((c) => ({\n ...c,\n fileName: c.filePath,\n baseContent: c.fromContent,\n compareContent: c.toContent,\n }));\n },\n tests: (result: ComponentCompareResult, { fileName }: { fileName?: string }) => {\n if (fileName) {\n return result.tests\n .filter((testFile) => testFile.filePath === fileName)\n .map((c) => ({ ...c, fileName: c.filePath, baseContent: c.fromContent, compareContent: c.toContent }));\n }\n\n return result.tests.map((c) => ({\n ...c,\n fileName: c.filePath,\n baseContent: c.fromContent,\n compareContent: c.toContent,\n }));\n },\n aspects: (result: ComponentCompareResult, { fieldName }: { fieldName?: string }) => {\n if (fieldName) {\n return result.fields.filter((field) => field.fieldName === fieldName);\n }\n return result.fields;\n },\n api: async (result: ComponentCompareResult) => {\n return (await componentCompareMain.getAPIDiff(result.baseId, result.compareId)) ?? null;\n },\n },\n },\n };\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  import type { CLIMain } from '@teambit/cli';
2
2
  import type { Workspace } from '@teambit/workspace';
3
- import type { ComponentID } from '@teambit/component-id';
3
+ import { ComponentID } from '@teambit/component-id';
4
4
  import type { ScopeMain } from '@teambit/scope';
5
5
  import type { GraphqlMain } from '@teambit/graphql';
6
6
  import type { ModelComponent, Version } from '@teambit/objects';
@@ -10,7 +10,9 @@ import type { DiffOptions, DiffResults, FieldsDiff, FileDiff } from '@teambit/le
10
10
  import type { TesterMain } from '@teambit/tester';
11
11
  import type { Component, ComponentMain } from '@teambit/component';
12
12
  import type { SchemaMain } from '@teambit/schema';
13
+ import type { CacheMain } from '@teambit/cache';
13
14
  import type { ImporterMain } from '@teambit/importer';
15
+ import type { ComponentComparePair } from './compare-component-pairs';
14
16
  export type ComponentCompareResult = {
15
17
  id: string;
16
18
  baseId: string;
@@ -18,6 +20,12 @@ export type ComponentCompareResult = {
18
20
  code: FileDiff[];
19
21
  fields: FieldsDiff[];
20
22
  tests: FileDiff[];
23
+ /**
24
+ * true when the compare side is the live workspace (on-disk files, incl. uncommitted changes).
25
+ * such a result is inherently mutable, so it must never be persisted to the cross-run cache —
26
+ * only the in-flight single-flight dedupe applies. not exposed via graphql.
27
+ */
28
+ isLiveWorkspace?: boolean;
21
29
  };
22
30
  type ConfigDiff = {
23
31
  version?: string;
@@ -32,10 +40,61 @@ export declare class ComponentCompareMain {
32
40
  private depResolver;
33
41
  private importer;
34
42
  private schema;
43
+ private cache;
35
44
  private workspace?;
36
- constructor(componentAspect: ComponentMain, scope: ScopeMain, logger: Logger, tester: TesterMain, depResolver: DependencyResolverMain, importer: ImporterMain, schema: SchemaMain, workspace?: Workspace | undefined);
45
+ constructor(componentAspect: ComponentMain, scope: ScopeMain, logger: Logger, tester: TesterMain, depResolver: DependencyResolverMain, importer: ImporterMain, schema: SchemaMain, cache: CacheMain, workspace?: Workspace | undefined);
46
+ private compareInflight;
47
+ private apiDiffInflight;
48
+ /**
49
+ * Read-through cache with single-flight dedupe: serve a persisted result, else share an in-flight
50
+ * computation, else compute once and persist. Most `(baseId, compareId)` pairs are immutable (keyed
51
+ * on snap hashes), so a cached result never goes stale. `cacheable` gates which results are persisted.
52
+ *
53
+ * `skipPersistentCache` bypasses the persistent cache entirely (neither read nor write) while still
54
+ * sharing the in-flight computation. Callers must set it whenever the result depends on mutable state
55
+ * the key does not capture — e.g. a live-workspace diff against on-disk files — so a previously
56
+ * persisted snap-to-snap result for the same key is never served in its place.
57
+ */
58
+ private getOrCompute;
37
59
  compare(baseIdStr: string, compareIdStr: string): Promise<ComponentCompareResult>;
60
+ /**
61
+ * cheap, synchronous pre-check mirroring the `comparingWithLocalChanges` / `compareIsLiveWorkspace`
62
+ * logic in `computeCompare`: will this compare diff against live on-disk workspace files rather than
63
+ * two immutable snaps? errs toward `true` (skip the persistent cache) whenever the id cannot be
64
+ * classified, so a stale snap-to-snap result is never served in place of a live one.
65
+ */
66
+ private comparesLiveWorkspace;
67
+ /**
68
+ * whether this id refers to the component version currently checked out on disk — the one case
69
+ * where "the same versioned id" can produce different data over time (the user edits files). errs
70
+ * toward `true` when the id cannot be classified, so a stale cached result is never served.
71
+ */
72
+ private isLiveCheckout;
73
+ /** The original `compare()` body — moved here so the public method can wrap with memo + single-flight. */
74
+ private computeCompare;
75
+ /**
76
+ * compare a paginated slice of component pairs in one call.
77
+ * a pair that fails to compare (e.g. a component without versions) becomes `null` in the
78
+ * returned array rather than failing the whole batch. the array is aligned to the requested
79
+ * slice (`pairs[offset .. offset + limit]`).
80
+ */
81
+ compareComponents(pairs: ComponentComparePair[], options?: {
82
+ offset?: number;
83
+ limit?: number;
84
+ }): Promise<Array<ComponentCompareResult | null>>;
85
+ /**
86
+ * api-diff a paginated slice of component pairs in one call — the bulk counterpart of the single
87
+ * `getAPIDiff`, mirroring `compareComponents`. reuses `getAPIDiff` per pair (so its disk memo +
88
+ * single-flight dedupe still apply), turning a pair whose diff throws into `null` rather than
89
+ * failing the whole batch. the returned array is aligned to the requested slice.
90
+ */
91
+ apiDiffs(pairs: ComponentComparePair[], options?: {
92
+ offset?: number;
93
+ limit?: number;
94
+ }): Promise<Array<Record<string, any> | null>>;
95
+ private static isApiDiffCacheable;
38
96
  getAPIDiff(baseIdStr: string, compareIdStr: string): Promise<Record<string, any> | null>;
97
+ private computeAPIDiff;
39
98
  diffByCLIValues(pattern?: string, version?: string, toVersion?: string, { verbose, table, parent }?: {
40
99
  verbose?: boolean;
41
100
  table?: boolean;
@@ -58,7 +117,7 @@ export declare class ComponentCompareMain {
58
117
  static slots: never[];
59
118
  static dependencies: import("@teambit/harmony").Aspect[];
60
119
  static runtime: import("@teambit/harmony").RuntimeDefinition;
61
- static provider([graphql, component, scope, loggerMain, cli, workspace, tester, depResolver, importer, schema,]: [
120
+ static provider([graphql, component, scope, loggerMain, cli, workspace, tester, depResolver, importer, schema, cache,]: [
62
121
  GraphqlMain,
63
122
  ComponentMain,
64
123
  ScopeMain,
@@ -68,7 +127,8 @@ export declare class ComponentCompareMain {
68
127
  TesterMain,
69
128
  DependencyResolverMain,
70
129
  ImporterMain,
71
- SchemaMain
130
+ SchemaMain,
131
+ CacheMain
72
132
  ]): Promise<ComponentCompareMain>;
73
133
  }
74
134
  export default ComponentCompareMain;
@@ -102,6 +102,13 @@ function _schema() {
102
102
  };
103
103
  return data;
104
104
  }
105
+ function _cache() {
106
+ const data = require("@teambit/cache");
107
+ _cache = function () {
108
+ return data;
109
+ };
110
+ return data;
111
+ }
105
112
  function _componentCompare() {
106
113
  const data = require("./component-compare.graphql");
107
114
  _componentCompare = function () {
@@ -130,11 +137,31 @@ function _importer() {
130
137
  };
131
138
  return data;
132
139
  }
140
+ function _harmonyModules() {
141
+ const data = require("@teambit/harmony.modules.concurrency");
142
+ _harmonyModules = function () {
143
+ return data;
144
+ };
145
+ return data;
146
+ }
147
+ function _compareComponentPairs() {
148
+ const data = require("./compare-component-pairs");
149
+ _compareComponentPairs = function () {
150
+ return data;
151
+ };
152
+ return data;
153
+ }
133
154
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
134
155
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
135
156
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
157
+ /**
158
+ * expiry for persisted compare/api-diff results. the results themselves are immutable (keyed on snap
159
+ * hashes), but the payloads are heavy — full per-file contents per pair — so without a TTL the cache
160
+ * directory grows with every pair ever viewed. two weeks comfortably covers a review cycle.
161
+ */
162
+ const PERSISTENT_CACHE_TTL_MS = 14 * 24 * 60 * 60 * 1000;
136
163
  class ComponentCompareMain {
137
- constructor(componentAspect, scope, logger, tester, depResolver, importer, schema, workspace) {
164
+ constructor(componentAspect, scope, logger, tester, depResolver, importer, schema, cache, workspace) {
138
165
  this.componentAspect = componentAspect;
139
166
  this.scope = scope;
140
167
  this.logger = logger;
@@ -142,9 +169,89 @@ class ComponentCompareMain {
142
169
  this.depResolver = depResolver;
143
170
  this.importer = importer;
144
171
  this.schema = schema;
172
+ this.cache = cache;
145
173
  this.workspace = workspace;
174
+ // in-flight `compute` promises, so concurrent callers for the same pair share one computation
175
+ // instead of recomputing (the lane compare UI and lane-diff status hit the same pairs in parallel
176
+ // on a cold load). Persisted results survive restarts via the global `@teambit/cache` aspect.
177
+ _defineProperty(this, "compareInflight", new Map());
178
+ _defineProperty(this, "apiDiffInflight", new Map());
179
+ }
180
+ /**
181
+ * Read-through cache with single-flight dedupe: serve a persisted result, else share an in-flight
182
+ * computation, else compute once and persist. Most `(baseId, compareId)` pairs are immutable (keyed
183
+ * on snap hashes), so a cached result never goes stale. `cacheable` gates which results are persisted.
184
+ *
185
+ * `skipPersistentCache` bypasses the persistent cache entirely (neither read nor write) while still
186
+ * sharing the in-flight computation. Callers must set it whenever the result depends on mutable state
187
+ * the key does not capture — e.g. a live-workspace diff against on-disk files — so a previously
188
+ * persisted snap-to-snap result for the same key is never served in its place.
189
+ */
190
+ async getOrCompute(inflight, cacheKey, compute, cacheable = () => true, skipPersistentCache = false) {
191
+ const pending = inflight.get(cacheKey);
192
+ if (pending) return pending;
193
+ if (!skipPersistentCache) {
194
+ const cached = await this.cache.get(cacheKey);
195
+ if (cached !== undefined) return cached;
196
+ // a concurrent caller may have started computing while we awaited the cache read.
197
+ const started = inflight.get(cacheKey);
198
+ if (started) return started;
199
+ }
200
+ const promise = compute().then(result => {
201
+ // TTL keeps the cache bounded: compare payloads embed full per-file contents for every pair,
202
+ // so without an expiry every pair ever viewed stays on disk forever. entries are cheap to
203
+ // recompute after expiry (sources still cached in the scope), so a stale-eviction is harmless.
204
+ if (!skipPersistentCache && cacheable(result)) void this.cache.set(cacheKey, result, PERSISTENT_CACHE_TTL_MS);
205
+ return result;
206
+ }).finally(() => inflight.delete(cacheKey));
207
+ inflight.set(cacheKey, promise);
208
+ return promise;
146
209
  }
147
210
  async compare(baseIdStr, compareIdStr) {
211
+ return this.getOrCompute(this.compareInflight, `component-compare:result:${baseIdStr}|${compareIdStr}`, () => this.computeCompare(baseIdStr, compareIdStr),
212
+ // never persist a live-workspace diff: it reflects on-disk files (incl. uncommitted changes),
213
+ // so a cached copy would go stale the moment the user edits a file. the (baseId, compareId)
214
+ // pair is otherwise immutable (keyed on snap hashes), so those stay cacheable.
215
+ result => !result.isLiveWorkspace,
216
+ // whether this call *reads* the persistent cache is decided up front from the same signal:
217
+ // a live-workspace compare must skip the cache entirely, otherwise a snap-to-snap result
218
+ // persisted for this key in a prior run (or a non-live context) would mask on-disk changes.
219
+ this.comparesLiveWorkspace(baseIdStr, compareIdStr));
220
+ }
221
+
222
+ /**
223
+ * cheap, synchronous pre-check mirroring the `comparingWithLocalChanges` / `compareIsLiveWorkspace`
224
+ * logic in `computeCompare`: will this compare diff against live on-disk workspace files rather than
225
+ * two immutable snaps? errs toward `true` (skip the persistent cache) whenever the id cannot be
226
+ * classified, so a stale snap-to-snap result is never served in place of a live one.
227
+ */
228
+ comparesLiveWorkspace(baseIdStr, compareIdStr) {
229
+ if (!this.workspace) return false; // scope/remote host: every compare is an immutable snap-to-snap pair
230
+ if (baseIdStr === compareIdStr) return true; // the "local changes" view: checked-out snap vs on-disk files
231
+ return this.isLiveCheckout(compareIdStr);
232
+ }
233
+
234
+ /**
235
+ * whether this id refers to the component version currently checked out on disk — the one case
236
+ * where "the same versioned id" can produce different data over time (the user edits files). errs
237
+ * toward `true` when the id cannot be classified, so a stale cached result is never served.
238
+ */
239
+ isLiveCheckout(idStr) {
240
+ if (!this.workspace) return false;
241
+ let id;
242
+ try {
243
+ id = _componentId().ComponentID.fromString(idStr);
244
+ } catch {
245
+ return true; // unclassifiable id → assume live so a stale cached diff is never returned
246
+ }
247
+ const checkedOut = this.workspace.getIdIfExist(id);
248
+ if (!checkedOut) return false; // not checked out → a stored snap, safe to cache
249
+ // live only when this side is the exact version currently checked out on disk.
250
+ return !id.hasVersion() || checkedOut.version === id.version;
251
+ }
252
+
253
+ /** The original `compare()` body — moved here so the public method can wrap with memo + single-flight. */
254
+ async computeCompare(baseIdStr, compareIdStr) {
148
255
  const host = this.componentAspect.getHost();
149
256
  const [baseCompId, compareCompId] = await host.resolveMultipleComponentIds([baseIdStr, compareIdStr]);
150
257
  const modelComponent = await this.scope.legacyScope.getModelComponentIfExist(compareCompId);
@@ -163,7 +270,20 @@ class ComponentCompareMain {
163
270
  const baseComponent = components?.[0];
164
271
  const compareComponent = components?.[1];
165
272
  const componentWithoutVersion = await host.get((baseCompId || compareCompId).changeVersion(undefined));
166
- const diff = componentWithoutVersion ? await this.computeDiff(componentWithoutVersion, comparingWithLocalChanges ? undefined : baseVersion, comparingWithLocalChanges ? undefined : compareVersion, {}) : {
273
+
274
+ // When the compare side is the component currently checked out in the workspace, diff against the
275
+ // on-disk files rather than a stored snap: passing `undefined` as the compare version makes
276
+ // `computeDiff` fall back to `consumerComponent.files`, so uncommitted local changes are included.
277
+ // This covers two cases with one code path:
278
+ // - base === compare (the classic "local changes" view): checked-out model → workspace files.
279
+ // - base = an earlier version: that version's committed changes + any uncommitted changes on top.
280
+ // Without this, the default workspace compare resolves base and compare to the same checked-out
281
+ // snap and reports no changes, collapsing the compare view to only its always-on sections.
282
+ const checkedOutVersion = componentWithoutVersion?.id.version;
283
+ const compareIsLiveWorkspace = Boolean(this.workspace && checkedOutVersion && compareVersion === checkedOutVersion);
284
+ const effectiveBaseVersion = comparingWithLocalChanges ? undefined : baseVersion;
285
+ const effectiveCompareVersion = comparingWithLocalChanges || compareIsLiveWorkspace ? undefined : compareVersion;
286
+ const diff = componentWithoutVersion ? await this.computeDiff(componentWithoutVersion, effectiveBaseVersion, effectiveCompareVersion, {}) : {
167
287
  filesDiff: [],
168
288
  fieldsDiff: []
169
289
  };
@@ -177,10 +297,71 @@ class ComponentCompareMain {
177
297
  compareId: compareIdStr,
178
298
  code: diff.filesDiff || [],
179
299
  fields: diff.fieldsDiff || [],
180
- tests: testFilesDiff
300
+ tests: testFilesDiff,
301
+ isLiveWorkspace: compareIsLiveWorkspace
181
302
  };
182
303
  }
304
+
305
+ /**
306
+ * compare a paginated slice of component pairs in one call.
307
+ * a pair that fails to compare (e.g. a component without versions) becomes `null` in the
308
+ * returned array rather than failing the whole batch. the array is aligned to the requested
309
+ * slice (`pairs[offset .. offset + limit]`).
310
+ */
311
+ async compareComponents(pairs, options) {
312
+ return (0, _compareComponentPairs().compareComponentPairs)(pairs, (baseId, compareId) => this.compare(baseId, compareId), {
313
+ offset: options?.offset,
314
+ limit: options?.limit,
315
+ concurrency: (0, _harmonyModules().concurrentComponentsLimit)(),
316
+ onError: (pair, err) => {
317
+ this.logger.warn(`compareComponents: failed to compare ${pair.baseId} <> ${pair.compareId}`, err);
318
+ }
319
+ });
320
+ }
321
+
322
+ /**
323
+ * api-diff a paginated slice of component pairs in one call — the bulk counterpart of the single
324
+ * `getAPIDiff`, mirroring `compareComponents`. reuses `getAPIDiff` per pair (so its disk memo +
325
+ * single-flight dedupe still apply), turning a pair whose diff throws into `null` rather than
326
+ * failing the whole batch. the returned array is aligned to the requested slice.
327
+ */
328
+ async apiDiffs(pairs, options) {
329
+ return (0, _compareComponentPairs().compareComponentPairs)(pairs, (baseId, compareId) => this.getAPIDiff(baseId, compareId), {
330
+ offset: options?.offset,
331
+ limit: options?.limit,
332
+ concurrency: (0, _harmonyModules().concurrentComponentsLimit)(),
333
+ onError: (pair, err) => {
334
+ this.logger.warn(`apiDiffs: failed to compute api diff ${pair.baseId} <> ${pair.compareId}`, err);
335
+ }
336
+ });
337
+ }
338
+ static isApiDiffCacheable(result) {
339
+ // a live-extracted side reflects the current working tree, not the snap the cache key names —
340
+ // persisting it would serve a stale (possibly degraded) diff for that pair forever.
341
+ if (result.base?.live || result.compare?.live) return false;
342
+ if (result.status === 'COMPUTED') return true;
343
+ // A non-COMPUTED result is only safe to persist (disk cache, keyed on the immutable snap pair, no
344
+ // TTL) when it can never change for that pair. FAILED is transient. NOT_BUILT is *pending*: the snap
345
+ // simply hasn't been built yet, and once CI builds it (same hash) the schema appears — caching the
346
+ // pre-build "unavailable" answer would keep the API view blank forever. NO_EXTRACTOR/DISABLED are
347
+ // stable properties of the snap's env, so they stay cacheable.
348
+ const pendingOrTransient = reason => reason === 'FAILED' || reason === 'NOT_BUILT';
349
+ return !pendingOrTransient(result.base?.reason) && !pendingOrTransient(result.compare?.reason);
350
+ }
183
351
  async getAPIDiff(baseIdStr, compareIdStr) {
352
+ // never persist a result that can still change: `null` (snaps couldn't load), FAILED (schema
353
+ // retrieval threw) and NOT_BUILT (snap not yet built) must recompute next call; NO_EXTRACTOR/
354
+ // DISABLED are stable env properties and safe to cache (see `isApiDiffCacheable`).
355
+ // the version namespace invalidates older computed results on engine changes:
356
+ // v2 — availability-aware results; v3 — self-referential-returnType display fix.
357
+ // skip the persistent cache when EITHER side is the live checkout: SchemaMain live-extracts the
358
+ // schema of a modified checkout of the exact same versioned id, so a snap-to-snap result cached
359
+ // under this key in a prior (unmodified) run would mask the user's on-disk API changes. both
360
+ // sides are checked (unlike `compare()`, where only the compare side can be live) because the
361
+ // checked-out version can appear on either side of an API diff pair.
362
+ return this.getOrCompute(this.apiDiffInflight, `component-compare:api-diff:v3:${baseIdStr}|${compareIdStr}`, () => this.computeAPIDiff(baseIdStr, compareIdStr), v => v !== null && ComponentCompareMain.isApiDiffCacheable(v), this.isLiveCheckout(baseIdStr) || this.isLiveCheckout(compareIdStr));
363
+ }
364
+ async computeAPIDiff(baseIdStr, compareIdStr) {
184
365
  const host = this.componentAspect.getHost();
185
366
  const [baseCompId, compareCompId] = await host.resolveMultipleComponentIds([baseIdStr, compareIdStr]);
186
367
  await this.importer.importObjectsFromMainIfExist([baseCompId, compareCompId], {
@@ -329,9 +510,9 @@ class ComponentCompareMain {
329
510
  await updateFieldsDiff(fromVersionComponent, toVersionComponent, diffResult, diffOpts);
330
511
  return diffResult;
331
512
  }
332
- static async provider([graphql, component, scope, loggerMain, cli, workspace, tester, depResolver, importer, schema]) {
513
+ static async provider([graphql, component, scope, loggerMain, cli, workspace, tester, depResolver, importer, schema, cache]) {
333
514
  const logger = loggerMain.createLogger(_componentCompare2().ComponentCompareAspect.id);
334
- const componentCompareMain = new ComponentCompareMain(component, scope, logger, tester, depResolver, importer, schema, workspace);
515
+ const componentCompareMain = new ComponentCompareMain(component, scope, logger, tester, depResolver, importer, schema, cache, workspace);
335
516
  cli.register(new (_diffCmd().DiffCmd)(componentCompareMain));
336
517
  graphql.register(() => (0, _componentCompare().componentCompareSchema)(componentCompareMain));
337
518
  return componentCompareMain;
@@ -339,7 +520,7 @@ class ComponentCompareMain {
339
520
  }
340
521
  exports.ComponentCompareMain = ComponentCompareMain;
341
522
  _defineProperty(ComponentCompareMain, "slots", []);
342
- _defineProperty(ComponentCompareMain, "dependencies", [_graphql().GraphqlAspect, _component().ComponentAspect, _scope().ScopeAspect, _logger().LoggerAspect, _cli().CLIAspect, _workspace().WorkspaceAspect, _tester().TesterAspect, _dependencyResolver().DependencyResolverAspect, _importer().ImporterAspect, _schema().SchemaAspect]);
523
+ _defineProperty(ComponentCompareMain, "dependencies", [_graphql().GraphqlAspect, _component().ComponentAspect, _scope().ScopeAspect, _logger().LoggerAspect, _cli().CLIAspect, _workspace().WorkspaceAspect, _tester().TesterAspect, _dependencyResolver().DependencyResolverAspect, _importer().ImporterAspect, _schema().SchemaAspect, _cache().CacheAspect]);
343
524
  _defineProperty(ComponentCompareMain, "runtime", _cli().MainRuntime);
344
525
  function hasDiff(diffResult) {
345
526
  return !!(diffResult.filesDiff && diffResult.filesDiff.find(file => file.diffOutput) || diffResult.fieldsDiff);