Skip to content

Commit 9f605a1

Browse files
authored
fix(runner): preserve metadata when rewriting legacy aliases (commontoolsinc#1803)
fix: preserve metadata when rewriting legacy aliases - ensure toJSONWithLegacyAliases no longer drops schema info while normalizing alias depths - add explanatory comments for the whole function - extend json-utils tests to cover regression scenario
1 parent 985d516 commit 9f605a1

File tree

2 files changed

+102
-9
lines changed

2 files changed

+102
-9
lines changed

packages/runner/src/builder/json-utils.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export function toJSONWithLegacyAliases(
2929
ignoreSelfAliases: boolean = false,
3030
path: PropertyKey[] = [],
3131
): JSONValue | undefined {
32-
// Convert regular cells to opaque refs
32+
// Turn strongly typed builder values into legacy JSON structures while
33+
// preserving alias metadata for consumers that still rely on it.
34+
35+
// Convert regular cells and results from Cell.get() to opaque refs
3336
if (canBeOpaqueRef(value)) value = makeOpaqueRef(value);
3437

3538
// Verify that opaque refs are not in a parent frame
@@ -45,12 +48,13 @@ export function toJSONWithLegacyAliases(
4548
if (external) return external as JSONValue;
4649
}
4750

51+
// Otherwise it's an internal reference. Extract the schema and output a link.
4852
if (isOpaqueRef(value) || isShadowRef(value)) {
4953
const pathToCell = paths.get(value);
5054
if (pathToCell) {
5155
if (ignoreSelfAliases && deepEqual(path, pathToCell)) return undefined;
5256

53-
// Get schema from exported value if available
57+
// Add schema from exported value if available
5458
const exported = isOpaqueRef(value) ? value.export() : undefined;
5559
return {
5660
$alias: {
@@ -67,8 +71,13 @@ export function toJSONWithLegacyAliases(
6771
} else throw new Error(`Cell not found in paths`);
6872
}
6973

74+
// If we encounter a link, it's from a nested recipe.
7075
if (isLegacyAlias(value)) {
7176
const alias = (value as LegacyAlias).$alias;
77+
// If this was a shadow ref, i.e. a closed over reference, see whether
78+
// we're now at the level that it should be resolved to the actual cell.
79+
// (i.e. we're generating the recipe from which the closed over reference
80+
// was captured)
7281
if (isShadowRef(alias.cell)) {
7382
const cell = alias.cell.shadowOf;
7483
if (cell.export().frame !== getTopFrame()) {
@@ -81,18 +90,30 @@ export function toJSONWithLegacyAliases(
8190
`Shadow ref alias with parent cell not found in current frame`,
8291
);
8392
}
93+
// If we're not at the top level, just emit it again. This will be
94+
// converted once the higher level recipe is being processed.
8495
return value as JSONValue;
8596
}
8697
if (!paths.has(cell)) throw new Error(`Cell not found in paths`);
98+
// If in top frame, it's an alias to another cell on the process cell. So
99+
// we emit the alias without the cell reference (it will be filled in
100+
// later with the process cell) and concatenate the path.
101+
const { cell: _, ...aliasWithoutCell } = alias;
87102
return {
88103
$alias: {
104+
// Keep any extra metadata (schema, rootSchema, etc.) that might have
105+
// been attached to the legacy alias originally.
106+
...aliasWithoutCell,
89107
path: [...paths.get(cell)!, ...alias.path] as (string | number)[],
90108
},
91109
} satisfies LegacyAlias;
92110
} else if (!("cell" in alias) || typeof alias.cell === "number") {
111+
// If we encounter an existing alias and it isn't an absolute reference
112+
// with a cell id, then increase the nesting level.
93113
return {
94114
$alias: {
95-
cell: ((alias.cell as number) ?? 0) + 1,
115+
...alias, // Preserve existing metadata.
116+
cell: ((alias.cell as number) ?? 0) + 1, // Increase nesting level.
96117
path: alias.path as (string | number)[],
97118
},
98119
} satisfies LegacyAlias;
@@ -101,20 +122,23 @@ export function toJSONWithLegacyAliases(
101122
}
102123
}
103124

125+
// If this is an array, process each element recursively.
104126
if (Array.isArray(value)) {
105127
return (value as Opaque<any>).map((v: Opaque<any>, i: number) =>
106128
toJSONWithLegacyAliases(v, paths, ignoreSelfAliases, [...path, i])
107129
);
108130
}
109131

132+
// If this is an object or a recipe, process each key recursively.
110133
if (isRecord(value) || isRecipe(value)) {
134+
// If this is a recipe, call its toJSON method to get the properly
135+
// serialized version.
111136
const valueToProcess = (isRecipe(value) &&
112137
typeof (value as unknown as toJSON).toJSON === "function")
113138
? (value as unknown as toJSON).toJSON() as Record<string, any>
114139
: (value as Record<string, any>);
115140

116141
const result: any = {};
117-
let hasValue = false;
118142
for (const key in valueToProcess as any) {
119143
const jsonValue = toJSONWithLegacyAliases(
120144
valueToProcess[key],
@@ -124,13 +148,13 @@ export function toJSONWithLegacyAliases(
124148
);
125149
if (jsonValue !== undefined) {
126150
result[key] = jsonValue;
127-
hasValue = true;
128151
}
129152
}
130153

154+
// Retain the original recipe reference for downstream processing.
131155
if (isRecipe(value)) result[unsafe_originalRecipe] = value;
132156

133-
return hasValue || Object.keys(result).length === 0 ? result : undefined;
157+
return result;
134158
}
135159

136160
return value;

packages/runner/test/json-utils.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { afterEach, beforeEach, describe, it } from "@std/testing/bdd";
22
import { expect } from "@std/expect";
3-
import { createJsonSchema } from "../src/builder/json-utils.ts";
4-
import { Runtime } from "../src/runtime.ts";
5-
import type { JSONSchema } from "../src/builder/types.ts";
3+
64
import { Identity } from "@commontools/identity";
75
import { StorageManager } from "@commontools/runner/storage/cache.deno";
86

7+
import {
8+
createJsonSchema,
9+
toJSONWithLegacyAliases,
10+
} from "../src/builder/json-utils.ts";
11+
import {
12+
isOpaqueRefMarker,
13+
type JSONSchema,
14+
type Opaque,
15+
type OpaqueRef,
16+
type ShadowRef,
17+
} from "../src/builder/types.ts";
18+
import type { LegacyAlias } from "../src/sigil-types.ts";
19+
import { Runtime } from "../src/runtime.ts";
20+
921
const signer = await Identity.fromPassphrase("test operator");
1022
const space = signer.did();
1123

@@ -317,3 +329,60 @@ describe("createJsonSchema", () => {
317329
});
318330
});
319331
});
332+
333+
describe("toJSONWithLegacyAliases", () => {
334+
it("preserves metadata when expanding shadow ref aliases", () => {
335+
const cell = {
336+
export: () => ({ frame: undefined }),
337+
[isOpaqueRefMarker]: true,
338+
} as unknown as OpaqueRef<unknown>;
339+
const paths = new Map<OpaqueRef<unknown>, PropertyKey[]>([
340+
[cell, ["root"]],
341+
]);
342+
const schema = { type: "string" as const };
343+
const alias: LegacyAlias = {
344+
$alias: {
345+
cell: { shadowOf: cell } as ShadowRef,
346+
path: ["child"],
347+
schema,
348+
},
349+
};
350+
351+
const result = toJSONWithLegacyAliases(
352+
alias as unknown as Opaque<LegacyAlias>,
353+
paths,
354+
);
355+
356+
expect(result).toEqual({
357+
$alias: {
358+
path: ["root", "child"],
359+
schema,
360+
},
361+
});
362+
});
363+
364+
it("increments numeric alias cells without dropping schemas", () => {
365+
const alias: LegacyAlias = {
366+
$alias: {
367+
cell: 2,
368+
path: ["child"],
369+
schema: { type: "boolean" as const },
370+
rootSchema: { type: "boolean" as const },
371+
},
372+
};
373+
374+
const result = toJSONWithLegacyAliases(
375+
alias as unknown as Opaque<LegacyAlias>,
376+
new Map(),
377+
);
378+
379+
expect(result).toEqual({
380+
$alias: {
381+
cell: 3,
382+
path: ["child"],
383+
schema: { type: "boolean" as const },
384+
rootSchema: { type: "boolean" as const },
385+
},
386+
});
387+
});
388+
});

0 commit comments

Comments
 (0)