diff --git a/.changeset/21980-copy-overlays-shipped-names.md b/.changeset/21980-copy-overlays-shipped-names.md new file mode 100644 index 00000000000..f3ef8468e20 --- /dev/null +++ b/.changeset/21980-copy-overlays-shipped-names.md @@ -0,0 +1,27 @@ +--- +'@objectstack/metadata-protocol': minor +--- + +A package's stored copy of a view container it ships overlays that package's shipped views, so a withdrawal saved in the copy holds at the anonymous form endpoints; the runtime save door refuses three copies it accepted before + +Clause-②: no (narrowing) + + + +**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier view container refusals shipped with. + +- **What was wrong.** The source loaders register a view container a package ships as `OBJECT.KEY` views for that package, whichever package owns the object. When that package stored a copy of the same container (a `PUT /api/v1/meta/view/NAME` of the container) and the object belonged to another code package, the copy expanded under its own name instead, as `OBJECT.CONTAINER.KEY` (a bare `list` as `OBJECT.CONTAINER`). So the copy overlaid none of the views its package ships: a form withdrawn from anonymous intake in the copy stayed open in the package's shipped form of that name, and the anonymous form endpoints kept serving it. +- **What it does now.** A package's stored copy of a container that package ships, bound to the same object, expands as the loaders expand the shipped container: each member is served under the loaders' name, `OBJECT.KEY`, in the copying package's own slot on the view list and on the by-name read naming that package. So a withdrawal saved in the copy holds at the anonymous form endpoints. The copy still declares no default view for an object another package owns. Any other container on another package's object keeps expanding under its own name, unchanged. +- **The by-name read on an unscoped kernel.** On a kernel with no environment id, a stored container's expanded views are also registered in the schema registry, under the bare name. A view whose name another package also ships is no longer registered there: the registry answered that bare entry ahead of the other package's own view, so `getMetaItem` naming the other package served this container's view. Every kernel's by-name read already serves such a view from its stored row, for its own package and for a read that names no package. Two packages that ship one container, with one of them storing a copy, were affected before this change too. + +**What is refused now.** `saveMetaItem`, which `PUT /api/v1/meta/view/NAME` and the dispatcher's metadata save both call, judges a copy at the names it now expands to, so three copies it accepted before are refused with `VALIDATION_ERROR` / 400, before anything is stored. Each is a package's copy of a container it ships on another package's object: + +- The copy adds a bare `list` whose name, `OBJECT.default`, only the other package ships. Before: accepted, served as `OBJECT.CONTAINER`. After: refused, naming the package that ships `OBJECT.default`. +- The copy adds a keyed member (a `formViews` or `listViews` entry, a named `list`, or a `form`) whose name, `OBJECT.KEY`, only the other package ships. Before: accepted, served as `OBJECT.CONTAINER.KEY`. After: refused, naming the package that ships `OBJECT.KEY`. +- Another stored container, saved under a different name, already expands a name the copy now expands. Before: accepted. After: refused, naming that stored container. + +**The fix.** For the first two, give the added member a key of its own that no package ships and no stored container expands, or save a view item (`name`, `object`, `viewKind`, `config`) under that name to override the other package's view. For the third, add the view as a member of the stored container that already expands the name, or save a view item under that name. + +**What still saves.** A copy that keeps the members its package's shipped container has, their contents edited, under the container's own name. A copy that adds a member under a key no package ships and no stored container expands. A view item under any of these names. Every container that is not a copy of its own package's shipped container, as before. + +**Rows stored before this change.** They keep their bytes. A stored copy of a container its package ships, on another package's object, is now served under the loaders' names (`OBJECT.KEY`) instead of `OBJECT.CONTAINER.KEY`, so it overlays the package's shipped views from the next read on, with no re-save. A reference to one of its old names (a navigation `viewName`, a form action `target`) no longer resolves; point it at `OBJECT.KEY`. A new save of a stored copy in one of the refused shapes, a re-save included, is refused until its body stops colliding. Delete stays open. diff --git a/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts b/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts index 81bcde1743d..02075cacb21 100644 --- a/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts +++ b/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts @@ -1561,4 +1561,243 @@ describe('each package\'s copy of a view container expands into its own package\ }); } }); + + // The family's closing enumeration. The loaders register a container a + // package ships as `.` for that package, whichever package + // owns the object, so a stored copy of that container overlays those + // names, in that package's own slot. Any other container on another + // package's object keeps the own-name arm. Every placement a package's + // stored copy can take is derived below as a cross product: who owns the + // object (the copying package, another package, none), whether the + // copying package ships the container, and which member the copy changes + // (the bare list, a keyed member, the default form). For each one, on + // both kernels: + // - the copy's member is served under the name its placement gives it, + // in the copying package's slot of the env-wide list and on the + // by-name read naming that package; + // - no name it writes is another package's, and every name the other + // package ships keeps that package's body on both read doors; + // - for a form, the withdrawal saved in the copy closes the anonymous + // doors (their own composition, as above), and the same copy saved + // open leaves them serving. A list carries no anonymous intake + // (`anonymousFormIntakeCandidates` reads forms only), so the bare + // list's reach is pinned on the two read doors alone. + describe('(f) every placement a package\'s stored copy of a view container can take', () => { + const COPYING = 'pkg_b'; + const OTHER = 'pkg_a'; + // The other package's own form is its own to serve: it gets a slug of + // its own, so the doors' answer at SLUG is the copy's alone. + const OTHER_SLUG = 'owner-intake'; + const COPY_TITLE = `Intake (${COPYING} copy)`; + const SHIPPED_TITLE = (pkg: string) => `Intake (shipped by ${pkg})`; + const OWNERS = [ + ['the copying package owns the object', COPYING], + ['another package owns the object', OTHER], + ['no code package owns the object', undefined], + ] as const; + const SHIPPING = [ + ['the copying package ships the container', true], + ['the copying package ships no container of that name', false], + ] as const; + const form = (allowAnonymous: boolean, title: string, slug: string) => ({ + title, sharing: { enabled: true, allowAnonymous, publicLink: `/forms/${slug}` }, + }); + interface Member { + readonly member: string; + readonly isForm: boolean; + /** The name the own-name arm gives it: the container's own name, `task`, under the object. */ + readonly ownNameAs: string; + readonly body: (title: string, allowAnonymous: boolean, slug: string) => Record; + } + const MEMBERS: readonly Member[] = [ + { + member: 'the bare list', isForm: false, ownNameAs: 'task.task', + body: (title) => ({ list: { type: 'grid', label: title, columns: ['name'] } }), + }, + { + member: 'a keyed member', isForm: true, ownNameAs: 'task.task.intake_form', + body: (title, allowAnonymous, slug) => ({ formViews: { intake_form: form(allowAnonymous, title, slug) } }), + }, + { + member: 'the default form', isForm: true, ownNameAs: 'task.task.form', + body: (title, allowAnonymous, slug) => ({ form: form(allowAnonymous, title, slug) }), + }, + ]; + const PLACEMENTS = OWNERS.flatMap(([ownership, owner]) => SHIPPING.flatMap(([shipping, ships]) => + MEMBERS.map((m) => ({ ownership, owner, shipping, ships, m })))); + type Placement = (typeof PLACEMENTS)[number]; + + /** The names a source loader registers for a container body: the spec's expander under the object's name. */ + const loaderNames = (body: Record) => expandViewContainer('task', body).map((vi) => String(vi.name)); + const shippedBy = (p: Placement, pkg: string) => ({ + object: 'task', ...p.m.body(SHIPPED_TITLE(pkg), true, pkg === COPYING ? SLUG : OTHER_SLUG), + }); + /** What each package ships in the placement: the object's other owner its own member, the copying package the container it copies. */ + const shippedIn = (p: Placement): Array<[string, Record]> => [ + ...(p.owner === OTHER ? [[OTHER, shippedBy(p, OTHER)] as [string, Record]] : []), + ...(p.ships ? [[COPYING, shippedBy(p, COPYING)] as [string, Record]] : []), + ]; + /** + * The name the copy's member is served under: the name the loaders + * gave the copying package where it ships the container, the own-name + * arm on another package's object otherwise, and `.` + * on an object of its own or of none. + */ + const servedName = (p: Placement) => (p.owner === OTHER && !p.ships + ? p.m.ownNameAs + : loaderNames(p.m.body(COPY_TITLE, true, SLUG))[0]); + const label = (p: Placement) => `${p.ownership}, ${p.shipping}, ${p.m.member}`; + + async function saveCopyOf(protocol: any, p: Placement, allowAnonymous: boolean) { + expect((await protocol.saveMetaItem({ + type: 'view', name: 'task', packageId: COPYING, + item: { name: 'task', object: 'task', ...p.m.body(COPY_TITLE, allowAnonymous, SLUG) }, + })).success).toBe(true); + } + /** An organization overlay of the form under `name`, open, as a rollback restores it. */ + async function restoreOpenOverlayAt(protocol: any, name: string) { + await protocol.ensureOverlayIndex(); + await protocol.getOverlayRepo('org_a').put( + { type: 'view', name, org: 'org_a' }, + { name, label: 'Intake (org)', object: 'task', viewKind: 'form', config: form(true, 'Intake (org)', SLUG) }, + { parentVersion: null, actor: null, source: 'test.restored', intent: 'runtime-only', state: 'active', packageId: null }, + ); + } + const titleOf = (v: any): unknown => v?.config?.title ?? v?.config?.label; + /** The titles a package's slot of `name` serves in the env-wide list. */ + async function slotTitles(protocol: any, name: string, pkg: string): Promise { + const envWide: any = await protocol.getMetaItems({ type: 'view' }); + return (envWide.items as any[]).filter((v) => v?.name === name && v._packageId === pkg).map(titleOf); + } + /** Every item through which the env-wide list serves the copy's body for the copying package. */ + async function itemsOfTheCopy(protocol: any): Promise { + const envWide: any = await protocol.getMetaItems({ type: 'view' }); + return (envWide.items as any[]).filter((v) => v?._packageId === COPYING && titleOf(v) === COPY_TITLE); + } + async function byNameTitle(protocol: any, name: string, pkg: string): Promise { + return titleOf((await protocol.getMetaItem({ type: 'view', name, packageId: pkg })).item); + } + + it('the population is the cross product of the three dimensions, each placement once', () => { + expect(PLACEMENTS).toHaveLength(OWNERS.length * SHIPPING.length * MEMBERS.length); + expect(new Set(PLACEMENTS.map(label)).size).toBe(PLACEMENTS.length); + // The three members, as the loaders place them: the bare list, a keyed member, the default form. + expect(MEMBERS.map((m) => expandViewContainer('task', m.body('x', true, SLUG)) + .map((vi) => [vi.name, vi.viewKind, vi.isDefault === true]))) + .toEqual([[['task.default', 'list', true]], [['task.intake_form', 'form', false]], [['task.form', 'form', true]]]); + }); + + for (const [kernel, environmentId] of KERNELS) { + for (const p of PLACEMENTS) { + it(`${kernel}; ${label(p)}: served as ${servedName(p)} in the copying package's slot on both read doors, writing no other package's name`, async () => { + const { protocol } = harness(shippedIn(p), environmentId, p.owner); + await saveCopyOf(protocol, p, false); + const name = servedName(p); + + const copyItems = await itemsOfTheCopy(protocol); + expect(copyItems.map((v) => v.name), 'the names the copy is served under').toEqual([name]); + // The seat's answer on the own-name arm stands for every container on + // another package's object, a copy included: it declares no default. + const declaresDefault = expandViewContainer('task', p.m.body(COPY_TITLE, false, SLUG))[0]?.isDefault === true; + expect(copyItems.map((v) => v.isDefault === true), 'the default it declares') + .toEqual([p.owner !== OTHER && declaresDefault]); + expect(await slotTitles(protocol, name, COPYING), 'the copying package\'s slot').toEqual([COPY_TITLE]); + expect(await byNameTitle(protocol, name, COPYING), 'the by-name read naming the copying package').toBe(COPY_TITLE); + + const otherNames = p.owner === OTHER ? loaderNames(shippedBy(p, OTHER)) : []; + const ownNames = p.ships ? loaderNames(shippedBy(p, COPYING)) : []; + expect(otherNames.includes(name) && !ownNames.includes(name), 'a name only another package ships').toBe(false); + for (const shipped of otherNames) { + expect(await slotTitles(protocol, shipped, OTHER), `${OTHER}'s slot of ${shipped}`).toEqual([SHIPPED_TITLE(OTHER)]); + expect(await byNameTitle(protocol, shipped, OTHER), `the by-name read of ${shipped} naming ${OTHER}`) + .toBe(SHIPPED_TITLE(OTHER)); + } + }); + + if (p.m.isForm) { + it(`${kernel}; ${label(p)}: the withdrawal saved in the copy closes the anonymous doors, and saved open the doors serve`, async () => { + for (const allowAnonymous of [false, true]) { + const { protocol } = harness(shippedIn(p), environmentId, p.owner); + await restoreOpenOverlayAt(protocol, servedName(p)); + await saveCopyOf(protocol, p, allowAnonymous); + const served = await doorsServe(protocol); + if (allowAnonymous) expect(served.length, 'saved open: the doors serve the overlay').toBeGreaterThan(0); + else expect(served, 'withdrawn in the copy: the doors serve no copy of the overlay').toEqual([]); + } + }); + } + } + } + + // Two packages ship the container on an object no code package owns, + // and only one of them stores a copy. On an unscoped kernel the copy's + // expansion used to be registered under the bare name, which the + // registry answers ahead of either package's own entry, so the by-name + // read naming the OTHER package served the copy while the list served + // that package's own item. Each package's by-name read answers its own + // item, on both kernels, whichever package stores the copy. + describe('(g) two packages ship the container and one stores a copy: the by-name read naming each package answers its own item', () => { + const PACKAGES = [OTHER, COPYING] as const; + for (const [kernel, environmentId] of KERNELS) { + for (const m of MEMBERS) { + for (const copying of PACKAGES) { + it(`${kernel}; ${m.member}; ${copying} stores the copy`, async () => { + const copyTitle = `Intake (${copying} copy)`; + const shipped = PACKAGES.map((pkg): [string, Record] => + [pkg, { object: 'task', ...m.body(SHIPPED_TITLE(pkg), true, SLUG) }]); + const { protocol } = harness(shipped, environmentId); + expect((await protocol.saveMetaItem({ + type: 'view', name: 'task', packageId: copying, + item: { name: 'task', object: 'task', ...m.body(copyTitle, false, SLUG) }, + })).success).toBe(true); + const [name] = loaderNames(m.body('x', true, SLUG)); + for (const pkg of PACKAGES) { + const own = pkg === copying ? copyTitle : SHIPPED_TITLE(pkg); + expect(await byNameTitle(protocol, name, pkg), `the by-name read of ${name} naming ${pkg}`).toBe(own); + expect(await slotTitles(protocol, name, pkg), `${pkg}'s slot of ${name}`).toEqual([own]); + } + }); + } + } + } + }); + + // The condition on the hydration line above: the by-name read that + // names NO package is the generic reader of a hydrated bare entry. For + // a name two packages ship, after one of them stores a copy, it answers + // on both kernels, never an absence; both kernels answer the same body; + // and that body is one the env-wide list serves under the name. Which + // package's body it is stays ADR-0048's ambiguous case (two packages + // ship one name): it is the copy's, the last expansion of the name, as + // the read naming no package selects (servedViewExpansion). + describe('(h) a name two packages ship: the by-name read naming no package answers on both kernels, with the same body', () => { + const ORDERS = [[OTHER, COPYING], [COPYING, OTHER]] as const; + for (const m of MEMBERS) { + for (const owner of [OTHER, undefined] as const) { + for (const order of ORDERS) { + const ownership = owner ? 'another package owns the object' : 'no code package owns the object'; + it(`${m.member}; ${ownership}; ${order[0]} registered first`, async () => { + const [name] = loaderNames(m.body('x', true, SLUG)); + const answers: unknown[] = []; + for (const [kernel, environmentId] of KERNELS) { + const shipped = order.map((pkg): [string, Record] => + [pkg, { object: 'task', ...m.body(SHIPPED_TITLE(pkg), true, pkg === COPYING ? SLUG : OTHER_SLUG) }]); + const { protocol } = harness(shipped, environmentId, owner); + const placement = PLACEMENTS.find((q) => q.m === m && q.owner === owner && q.ships); + if (!placement) throw new Error(`no placement for ${ownership}, ${m.member}`); + await saveCopyOf(protocol, placement, false); + const item = (await protocol.getMetaItem({ type: 'view', name })).item; + expect(item, `${kernel}: the read naming no package answers ${name}`).toBeTruthy(); + const envWide: any = await protocol.getMetaItems({ type: 'view' }); + const listed = (envWide.items as any[]).filter((v) => v?.name === name).map(titleOf); + expect(listed, `${kernel}: a body the env-wide list serves under ${name}`).toContain(titleOf(item)); + answers.push(titleOf(item)); + } + expect(answers[1], 'the unscoped kernel answers what the environment-scoped kernel answers').toBe(answers[0]); + }); + } + } + } + }); + }); }); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 92d8053a6a5..625c2c452ef 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -18542,6 +18542,30 @@ export class ObjectStackProtocolImplementation implements * switcher's default stays the owning package's (the by-name override and * a user's own saved default are the routes that change it). * + * ## …except a stored copy of a container its own package ships (#21980) + * + * The source loaders register a container a package ships under the + * object's name, whichever package owns the object: every member is + * `.`, registered for the shipping package + * (`objectql`'s boot loop and `metadata`'s artifact loader, both through + * the spec's `expandViewContainer`). Those names are that package's, + * already published, and an overlay is keyed by its own name (ADR-0005), + * so the package's stored copy of that container overlays them: it + * expands as the loaders expand the container, and each name lands in + * the copying package's own slot, which the list and the by-name read + * select per package ({@link servedViewExpansion}). Under its own name, + * the copy overlaid none of them, so a form withdrawn in the copy stayed + * open in the package's shipped form, at the anonymous doors too. + * + * The copy is the container the package ships under the copy's name, + * bound to the same object ({@link copiesOwnShippedViewContainer}). Every + * other container on another package's object keeps the own-name arm, + * and a copy, like any container on another package's object, still + * declares no default. No expansion writes a name another package + * ships: the copy's names are its own package's, and the save door + * refuses a member whose name only another package ships + * ({@link viewContainerNameCollisionRefusal}). + * * Every expanded item carries the container's OWN package and, where that * package ships an artifact of the same name, that artifact's envelope — * never the envelope of an artifact another package ships. @@ -18565,15 +18589,15 @@ export class ObjectStackProtocolImplementation implements if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'view') return []; if (!isAggregatedViewContainer(data)) return []; const container = data as Record; - const viewObject = - (typeof container.object === 'string' && container.object ? container.object : undefined) - ?? container?.list?.data?.object - ?? container?.form?.data?.object - ?? (typeof container.name === 'string' ? container.name : undefined); + const viewObject = this.runtimeViewContainerObject(container); if (!viewObject) return []; const ownPackageId = this.runtimeViewContainerPackage(type, container, options); const crossPackage = this.isAnotherPackagesObject(viewObject, ownPackageId); - const expanded: ReadonlyArray> = crossPackage + // [#21980] A copy of its own package's shipped container takes the + // loaders' names, not the own-name arm. + const underOwnName = crossPackage + && !this.copiesOwnShippedViewContainer(type, viewObject, container, ownPackageId); + const expanded: ReadonlyArray> = underOwnName ? this.expandUnderOwnName(type, viewObject, container, ownPackageId) : (expandViewContainer(viewObject, container) as unknown as Record[]); const out: Record[] = []; @@ -18603,6 +18627,54 @@ export class ObjectStackProtocolImplementation implements return out; } + /** + * [#13407] The object a runtime view container binds: its own top-level + * `object`, then `list.data.object`, `form.data.object`, and its own + * `name`. See {@link expandRuntimeViewContainer}'s "Object-name + * derivation". + */ + private runtimeViewContainerObject(container: Record): string | undefined { + return (typeof container.object === 'string' && container.object ? container.object : undefined) + ?? container?.list?.data?.object + ?? container?.form?.data?.object + ?? (typeof container.name === 'string' ? container.name : undefined); + } + + /** + * [#21639, #21980] The view container `packageId` ships under `name`: the + * registry's artifact read ({@link lookupArtifactItem}) in that package, + * kept only when it is a container and that package's own, since the read + * falls back to another package's artifact of the name. `undefined` for + * no package, and where the package ships no container of the name. + */ + private shippedViewContainerOf( + type: string, + name: unknown, + packageId: string | undefined, + ): Record | undefined { + if (packageId === undefined || typeof name !== 'string' || name === '') return undefined; + const shipped = this.lookupArtifactItem(type, name, packageId) as Record | undefined; + return isAggregatedViewContainer(shipped) && shipped?._packageId === packageId ? shipped : undefined; + } + + /** + * [#21980] True when `container`, bound to `object`, is a stored copy of a + * container its own package ships: `ownPackageId` ships a view container + * under the copy's name ({@link shippedViewContainerOf}), bound to the + * same object. The loaders registered that container's views under + * `.` for that package, so the copy's names are that + * package's, and the copy expands to them. + */ + private copiesOwnShippedViewContainer( + type: string, + object: string, + container: Record, + ownPackageId: string | undefined, + ): boolean { + const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); + return shipped !== undefined && this.runtimeViewContainerObject(shipped) === object; + } + /** * [#21334] The package a runtime view container row belongs to: the * package its row is bound to, else — for a package-less row that is the @@ -18658,7 +18730,9 @@ export class ObjectStackProtocolImplementation implements /** * [#21334] Expand a container on another package's object under its own - * name. The spec's expander runs with `.` as its + * name: every such container but a stored copy of a container its own + * package ships, which takes the loaders' names ([#21980], + * {@link copiesOwnShippedViewContainer}). The spec's expander runs with `.` as its * base, so every member it knows — today a named `list`, `listViews`, * `formViews`, `form` — comes out as `..`, * de-duplicated by the spec's own rule, and a member kind the spec adds @@ -18794,6 +18868,21 @@ export class ObjectStackProtocolImplementation implements * own `code` layer (for a package-less container too, through the * runtime-only `getItem` arm), where `env_local`, which registers nothing, * reported neither. With the marker both kernels give one answer. + * + * ## [#21980] Never under a name another package ships + * + * The registry keeps one bare slot per name, and `SchemaRegistry.getItem` + * answers it before any package's own entry, whichever package the read + * names. So an expansion registered there under a name another package + * also ships answered THAT package's by-name read on an unscoped kernel: + * `getMetaItem` naming the other package fell through to the registry and + * served this container's view, while the list served the other package's + * own item in its slot ({@link servedViewExpansion}). Such an expansion is + * not registered. Every kernel's by-name read already answers it from its + * stored row, ahead of the registry ({@link resolveRowlessExpandedView}), + * for its own package and for a read that names none, and the list expands + * the row itself. A name that only the container's own package ships, or + * that no package ships, is registered as before. */ private hydrateExpandedViewItems( type: string, @@ -18801,7 +18890,13 @@ export class ObjectStackProtocolImplementation implements options: { packageId?: string | null; organizationId: string | null }, registry: any, ): void { + let shipping: ShippingPackages | undefined; for (const item of this.expandRuntimeViewContainer(type, data, { ...options, tenantAuthored: true })) { + shipping ??= this.shippingPackagesOf(type); + const own = item._packageId; + const anotherShips = this.shippedArtifactsOf(type, String(item.name), shipping) + .some((artifact) => (artifact as { _packageId?: unknown })._packageId !== own); + if (anotherShips) continue; registry.registerItem(type, item, 'name' as any); } } @@ -19660,9 +19755,8 @@ export class ObjectStackProtocolImplementation implements packageId: string | null | undefined, ): ReadonlySet { const ownPackageId = this.runtimeViewContainerPackage(type, container, { packageId }); - if (ownPackageId === undefined || typeof container.name !== 'string') return new Set(); - const shipped = this.lookupArtifactItem(type, container.name, ownPackageId) as Record | undefined; - if (!isAggregatedViewContainer(shipped) || shipped?._packageId !== ownPackageId) return new Set(); + const shipped = this.shippedViewContainerOf(type, container.name, ownPackageId); + if (shipped === undefined) return new Set(); return new Set( this.expandRuntimeViewContainer(type, shipped, { packageId: ownPackageId }).map((view) => String(view.name)), );