|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * `@objectstack/metadata/view-container` — the one spelling of "which object |
| 5 | + * does an aggregated `defineView` container bind to", as a LEAF entry point. |
| 6 | + * |
| 7 | + * ## Why this subpath exists |
| 8 | + * |
| 9 | + * This is the `/errors` pattern taken a second time, for the same reason |
| 10 | + * (`src/errors.ts` carries the first telling). `@objectstack/metadata`'s ROOT |
| 11 | + * entry pulls `MetadataPlugin` → `NodeMetadataManager` → `chokidar`, plus |
| 12 | + * `glob` and `js-yaml`; a cross-package consumer that wants a six-line pure |
| 13 | + * function should not have to load any of that. |
| 14 | + * |
| 15 | + * The consumer that forced it is `packages/objectql`'s ADR-0076 lean entry. |
| 16 | + * `@objectstack/objectql/core` re-exports `engine.ts`, whose boot-loop |
| 17 | + * registrar has to mint the same registry key as this package's registrars |
| 18 | + * (#14399), so it imports {@link deriveViewContainerObject} rather than |
| 19 | + * hand-copying the chain a fifth time — and reaching it through the root entry |
| 20 | + * made that lean closure load the manager and the filesystem machinery for a |
| 21 | + * function that touches neither. Measured on the built artifacts (#14680): |
| 22 | + * through the root entry that call site loaded SIX extra modules into |
| 23 | + * `@objectstack/objectql/core`'s module-init closure — |
| 24 | + * `packages/metadata/dist/index.js`, `js-yaml`, `glob`, `chokidar` (two |
| 25 | + * files) and `readdirp`, 499,162 B — for ~22 ms of extra init. Through this |
| 26 | + * entry it loads one 469-byte module and nothing else. |
| 27 | + * |
| 28 | + * ## Why the function LIVES here rather than being re-exported from |
| 29 | + * `view-container-expansion.ts` |
| 30 | + * |
| 31 | + * A re-export shim was written first and measured, because `/errors`' header |
| 32 | + * states the requirement the shim has to meet: "this entry re-exports one leaf |
| 33 | + * module and nothing else, so the cross-package edge stays a leaf edge". |
| 34 | + * `view-container-expansion.ts` is not a leaf — its other export, |
| 35 | + * `expandRuntimeViewContainer`, needs `@objectstack/spec` and |
| 36 | + * `@objectstack/spec/shared` — and esbuild tree-shakes the unused FUNCTION but |
| 37 | + * keeps both `import` statements, since it cannot prove an external package is |
| 38 | + * side-effect-free. Measured: the shim's own closure was 84 |
| 39 | + * modules / 3,035 KiB (all of `@objectstack/spec` and `zod`, reached through an |
| 40 | + * import statement for a function that had been shaken out); this module's is |
| 41 | + * 1 module / 469 B. |
| 42 | + * |
| 43 | + * So the derivation lives in this module, which imports nothing at all, and |
| 44 | + * `view-container-expansion.ts` imports it from here and re-exports it — every |
| 45 | + * existing importer (`index.ts`'s root export, `plugin.ts`) keeps its spelling. |
| 46 | + * |
| 47 | + * ## Scope of the promise |
| 48 | + * |
| 49 | + * Only {@link deriveViewContainerObject} is exported here. Its former module |
| 50 | + * sibling `expandRuntimeViewContainer` is deliberately left off: it is internal |
| 51 | + * to this package (`metadata-manager.ts` is its only caller, and the root entry |
| 52 | + * does not export it either), and an exported symbol nobody imports is a promise |
| 53 | + * made for nothing — Prime Directive #10 pointed at our own API surface, the |
| 54 | + * same call `src/errors.ts` made about `isSchemaAlreadyExistsError`. |
| 55 | + * |
| 56 | + * The symbol stays on the ROOT entry as well: this subpath is an additional |
| 57 | + * door, not a relocation, and the root export is a published promise with |
| 58 | + * possible out-of-repo consumers. |
| 59 | + */ |
| 60 | + |
| 61 | +/** |
| 62 | + * Which object an aggregated view container binds to. |
| 63 | + * |
| 64 | + * The container's OWN top-level `object` field — `ViewSchema.object`, |
| 65 | + * documented there as "how a stack-level `views: [...]` entry says which object |
| 66 | + * its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`" |
| 67 | + * — is the authorial, explicit signal and is consulted FIRST (#13407). The |
| 68 | + * three-deep fallback below it is kept unchanged for every container written |
| 69 | + * before that field was read here: `list.data.object`, then `form.data.object`, |
| 70 | + * then the row's own `name` — which is the bound object only by convention, and |
| 71 | + * is why a container that set the top-level field but not `list.data.object` |
| 72 | + * used to bind under the wrong key or not at all. |
| 73 | + * |
| 74 | + * Returns `undefined` when no binding can be derived; every caller treats that |
| 75 | + * as "no expansion" rather than an error. |
| 76 | + */ |
| 77 | +export function deriveViewContainerObject(container: unknown): string | undefined { |
| 78 | + if (!container || typeof container !== 'object') return undefined; |
| 79 | + const c = container as Record<string, any>; |
| 80 | + const own = typeof c.object === 'string' && c.object ? c.object : undefined; |
| 81 | + const byName = typeof c.name === 'string' && c.name ? c.name : undefined; |
| 82 | + return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName; |
| 83 | +} |
0 commit comments