@open-mercato/shared 0.7.1-develop.7137.1.26575786c0 → 0.7.1-develop.7148.1.3076e5ccf7
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -2
- package/dist/lib/bootstrap/factory.js +1 -1
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/query/migration-reindex.js +45 -0
- package/dist/lib/query/migration-reindex.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/overrides.js +69 -9
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +2 -2
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +4 -1
- package/src/lib/bootstrap/factory.ts +1 -1
- package/src/lib/query/__tests__/migration-reindex.test.ts +80 -0
- package/src/lib/query/migration-reindex.ts +61 -0
- package/src/modules/__tests__/injection-widget-override-ids.test.ts +249 -0
- package/src/modules/overrides.ts +137 -12
- package/src/modules/widgets/injection-loader.ts +11 -2
package/src/modules/overrides.ts
CHANGED
|
@@ -258,11 +258,21 @@ const TRACKING_ISSUE_HINT =
|
|
|
258
258
|
* Call this exactly once from `apps/<app>/src/bootstrap.ts` BEFORE any
|
|
259
259
|
* registry first-loads. Calling it more than once is safe but
|
|
260
260
|
* accumulates per-domain entries each time.
|
|
261
|
+
*
|
|
262
|
+
* `options.domains` narrows the dispatch to the domains a given runtime wires.
|
|
263
|
+
* The browser re-registers the widget and notification registries from
|
|
264
|
+
* `ClientBootstrap`, so it needs those overrides too, but it never loads the
|
|
265
|
+
* server-only appliers — dispatching every domain there would report wired
|
|
266
|
+
* domains as unwired (#5152).
|
|
261
267
|
*/
|
|
262
268
|
export function applyModuleOverridesFromEnabledModules(
|
|
263
269
|
modules: ReadonlyArray<ModuleEntryWithOverrides>,
|
|
270
|
+
options?: { domains?: readonly ModuleOverrideDomain[] },
|
|
264
271
|
): void {
|
|
265
272
|
if (!Array.isArray(modules) || modules.length === 0) return
|
|
273
|
+
const selectedDomains = options?.domains
|
|
274
|
+
? DOMAIN_KEYS.filter((domain) => options.domains!.includes(domain))
|
|
275
|
+
: DOMAIN_KEYS
|
|
266
276
|
|
|
267
277
|
// Bucket entries by domain in module-load order.
|
|
268
278
|
const buckets = new Map<ModuleOverrideDomain, Array<ModuleOverrideEntry<unknown>>>()
|
|
@@ -272,7 +282,7 @@ export function applyModuleOverridesFromEnabledModules(
|
|
|
272
282
|
const overrides = entry.overrides
|
|
273
283
|
if (!overrides || typeof overrides !== 'object') continue
|
|
274
284
|
|
|
275
|
-
for (const domain of
|
|
285
|
+
for (const domain of selectedDomains) {
|
|
276
286
|
const value = (overrides as Record<string, unknown>)[domain]
|
|
277
287
|
if (value === undefined || value === null) continue
|
|
278
288
|
if (typeof value !== 'object') continue
|
|
@@ -572,14 +582,33 @@ function composeStore<T>(store: OverrideStore<T>): OverrideMap<T> {
|
|
|
572
582
|
return { ...store.modules, ...store.programmatic }
|
|
573
583
|
}
|
|
574
584
|
|
|
585
|
+
type ArrayOverrideOptions<T> = {
|
|
586
|
+
label: string
|
|
587
|
+
getId: (value: T) => string | null
|
|
588
|
+
/**
|
|
589
|
+
* Secondary identifiers the same item may also be addressed by. A domain whose
|
|
590
|
+
* entries carry more than one id (injection widgets expose both `key` and
|
|
591
|
+
* `widgetId`) would otherwise force authors to guess which spelling the
|
|
592
|
+
* override map is matched against — see issue #5152.
|
|
593
|
+
*/
|
|
594
|
+
getAliasIds?: (value: T) => readonly (string | undefined)[]
|
|
595
|
+
isReplacement?: (value: unknown) => value is T
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function resolveOverrideIds<T>(value: T, options: ArrayOverrideOptions<T>): string[] {
|
|
599
|
+
const ids: string[] = []
|
|
600
|
+
const primary = options.getId(value)
|
|
601
|
+
if (primary) ids.push(primary)
|
|
602
|
+
for (const alias of options.getAliasIds?.(value) ?? []) {
|
|
603
|
+
if (alias && !ids.includes(alias)) ids.push(alias)
|
|
604
|
+
}
|
|
605
|
+
return ids
|
|
606
|
+
}
|
|
607
|
+
|
|
575
608
|
function applyArrayOverrides<T>(
|
|
576
609
|
items: readonly T[] | undefined,
|
|
577
610
|
overrides: Readonly<OverrideMap<T>>,
|
|
578
|
-
options:
|
|
579
|
-
label: string
|
|
580
|
-
getId: (value: T) => string | null
|
|
581
|
-
isReplacement?: (value: unknown) => value is T
|
|
582
|
-
},
|
|
611
|
+
options: ArrayOverrideOptions<T>,
|
|
583
612
|
): { items: T[] | undefined; consumed: Set<string>; changed: boolean } {
|
|
584
613
|
if (!items || Object.keys(overrides).length === 0) {
|
|
585
614
|
return { items: items ? Array.from(items) : items, consumed: new Set(), changed: false }
|
|
@@ -590,12 +619,31 @@ function applyArrayOverrides<T>(
|
|
|
590
619
|
let changed = false
|
|
591
620
|
|
|
592
621
|
for (const item of items) {
|
|
593
|
-
const
|
|
594
|
-
|
|
622
|
+
const matched = resolveOverrideIds(item, options)
|
|
623
|
+
.filter((candidate) => Object.prototype.hasOwnProperty.call(overrides, candidate))
|
|
624
|
+
if (matched.length === 0) {
|
|
595
625
|
result.push(item)
|
|
596
626
|
continue
|
|
597
627
|
}
|
|
598
|
-
consumed
|
|
628
|
+
// Every matching spelling is consumed, not just the winner: addressing one item
|
|
629
|
+
// under both of its ids is one instruction written twice, and reporting the loser
|
|
630
|
+
// as stale is the log noise #5152 asks to remove. Genuinely conflicting values
|
|
631
|
+
// still get their own warning, since only one of them can take effect.
|
|
632
|
+
const id = matched[0]
|
|
633
|
+
for (const candidate of matched) consumed.add(candidate)
|
|
634
|
+
if (matched.some((candidate) => overrides[candidate] !== overrides[id])) {
|
|
635
|
+
// The two spellings resolve to different values, and only the first takes
|
|
636
|
+
// effect. Disable-versus-replace is the case a reader has to act on; two
|
|
637
|
+
// separate replacement objects are more often the same instruction written
|
|
638
|
+
// twice, so say which one this is rather than reporting both as a conflict.
|
|
639
|
+
const disablesAndReplaces = matched.some((candidate) => (overrides[candidate] === null) !== (overrides[id] === null))
|
|
640
|
+
logger.warn(
|
|
641
|
+
disablesAndReplaces
|
|
642
|
+
? 'Conflicting overrides for the same entry — one key disables it while another replaces it; the first matching key wins'
|
|
643
|
+
: 'Duplicate replacement overrides for the same entry — only the first matching key takes effect',
|
|
644
|
+
{ label: options.label, id, keys: matched },
|
|
645
|
+
)
|
|
646
|
+
}
|
|
599
647
|
changed = true
|
|
600
648
|
const replacement = overrides[id]
|
|
601
649
|
if (replacement === null) continue
|
|
@@ -604,12 +652,27 @@ function applyArrayOverrides<T>(
|
|
|
604
652
|
result.push(item)
|
|
605
653
|
continue
|
|
606
654
|
}
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
655
|
+
const replacementIds = resolveOverrideIds(replacement, options)
|
|
656
|
+
if (!replacementIds.includes(id)) {
|
|
609
657
|
logger.warn('Skipping malformed override — replacement id must match the override key', { label: options.label, id })
|
|
610
658
|
result.push(item)
|
|
611
659
|
continue
|
|
612
660
|
}
|
|
661
|
+
// Matching on ONE id is enough to accept the replacement, so the other one can
|
|
662
|
+
// still drift: an injection widget matched by `widgetId` may carry a foreign
|
|
663
|
+
// `key` (colliding with another entry's registry slot) and one matched by `key`
|
|
664
|
+
// a foreign `widgetId` (orphaning every table slot that placed the original).
|
|
665
|
+
// The override still applies — it named this entry — but it does not do it quietly.
|
|
666
|
+
const itemIds = resolveOverrideIds(item, options)
|
|
667
|
+
const divergent = itemIds.filter((candidate) => !replacementIds.includes(candidate))
|
|
668
|
+
if (divergent.length > 0) {
|
|
669
|
+
logger.warn('Replacement override changes an identifier it was not matched on — references to the old value are not rewritten', {
|
|
670
|
+
label: options.label,
|
|
671
|
+
id,
|
|
672
|
+
replaced: divergent,
|
|
673
|
+
replacementIds,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
613
676
|
result.push(replacement)
|
|
614
677
|
}
|
|
615
678
|
|
|
@@ -804,6 +867,7 @@ export function resetModuleContractOverridesForTests(): void {
|
|
|
804
867
|
clearStore(aclFeatureOverrideStore)
|
|
805
868
|
clearStore(encryptionMapOverrideStore)
|
|
806
869
|
clearStore(diOverrideStore)
|
|
870
|
+
getInjectionWidgetIdAliases().clear()
|
|
807
871
|
for (const key of Object.keys(setupOverridesByModule)) delete setupOverridesByModule[key]
|
|
808
872
|
const navState = getNavOverrideState()
|
|
809
873
|
navState.modules = null
|
|
@@ -1165,13 +1229,60 @@ function applyEntryListOverrides<TEntry extends { moduleId: string }, TValue>(
|
|
|
1165
1229
|
return changed ? result : Array.from(entries)
|
|
1166
1230
|
}
|
|
1167
1231
|
|
|
1232
|
+
/**
|
|
1233
|
+
* `entry.key` (`module:widget:file`) and `entry.widgetId` (the widget module's own
|
|
1234
|
+
* metadata id) are two spellings of the same widget, and the generator never emits
|
|
1235
|
+
* the same value for both. Injection tables reference widgets by `widgetId` while
|
|
1236
|
+
* the entries registry is keyed by `key`, so an override map keyed by one spelling
|
|
1237
|
+
* used to reach only one of the two consumers (#5152). Remembering the pairs lets
|
|
1238
|
+
* either spelling address the widget everywhere.
|
|
1239
|
+
*
|
|
1240
|
+
* Pairs are additive for the process lifetime — re-registration under HMR re-adds
|
|
1241
|
+
* identical pairs, which the sets deduplicate, and a renamed widget leaves behind a
|
|
1242
|
+
* pairing that can only ever over-match an override key naming something that no
|
|
1243
|
+
* longer exists. Only the test reset clears it.
|
|
1244
|
+
*
|
|
1245
|
+
* Persisted on `globalThis` for the same reason the nav state above is: the index is
|
|
1246
|
+
* written when `@open-mercato/ui` filters the entries and read when
|
|
1247
|
+
* `@open-mercato/core` filters the tables, and a standalone build can evaluate
|
|
1248
|
+
* `@open-mercato/shared` through more than one chunk — a module-local `Map` would let
|
|
1249
|
+
* the write and the read land in different instances.
|
|
1250
|
+
*/
|
|
1251
|
+
const GLOBAL_INJECTION_WIDGET_ID_ALIASES_KEY = '__openMercatoInjectionWidgetIdAliases__'
|
|
1252
|
+
|
|
1253
|
+
function getInjectionWidgetIdAliases(): Map<string, Set<string>> {
|
|
1254
|
+
const existing = (globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGET_ID_ALIASES_KEY]
|
|
1255
|
+
if (existing instanceof Map) return existing as Map<string, Set<string>>
|
|
1256
|
+
const initial = new Map<string, Set<string>>()
|
|
1257
|
+
;(globalThis as Record<string, unknown>)[GLOBAL_INJECTION_WIDGET_ID_ALIASES_KEY] = initial
|
|
1258
|
+
return initial
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function rememberInjectionWidgetIdAliases(entries: readonly ModuleInjectionWidgetEntry[] | undefined): void {
|
|
1262
|
+
if (!entries) return
|
|
1263
|
+
const aliases = getInjectionWidgetIdAliases()
|
|
1264
|
+
for (const entry of entries) {
|
|
1265
|
+
const ids = [entry?.key, entry?.widgetId].filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
1266
|
+
if (ids.length < 2) continue
|
|
1267
|
+
for (const id of ids) {
|
|
1268
|
+
const siblings = aliases.get(id) ?? new Set<string>()
|
|
1269
|
+
for (const sibling of ids) {
|
|
1270
|
+
if (sibling !== id) siblings.add(sibling)
|
|
1271
|
+
}
|
|
1272
|
+
aliases.set(id, siblings)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1168
1277
|
export function applyInjectionWidgetOverridesToEntries(
|
|
1169
1278
|
entries: readonly ModuleInjectionWidgetEntry[],
|
|
1170
1279
|
overrides: Readonly<InjectionWidgetOverridesMap> = composeInjectionWidgetOverrides(),
|
|
1171
1280
|
): ModuleInjectionWidgetEntry[] {
|
|
1281
|
+
rememberInjectionWidgetIdAliases(entries)
|
|
1172
1282
|
const applied = applyArrayOverrides(entries, overrides as OverrideMap<ModuleInjectionWidgetEntry>, {
|
|
1173
1283
|
label: 'widgets.injection',
|
|
1174
1284
|
getId: (entry) => entry.key,
|
|
1285
|
+
getAliasIds: (entry) => [entry.widgetId],
|
|
1175
1286
|
isReplacement: isInjectionWidgetEntry,
|
|
1176
1287
|
})
|
|
1177
1288
|
warnStaleOverrides('widgets.injection', overrides, applied.consumed)
|
|
@@ -1210,11 +1321,25 @@ export function applyWorkerOverridesToDescriptors<T extends { id: string; queue:
|
|
|
1210
1321
|
return applied.items ?? []
|
|
1211
1322
|
}
|
|
1212
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* Injection tables reference widgets by `widgetId`, so a `key`-spelled override has to
|
|
1326
|
+
* be expanded before the slots can be filtered. Pass `entries` whenever the caller has
|
|
1327
|
+
* them: the shared alias index is only a fallback for callers that do not, and it can
|
|
1328
|
+
* only expand pairs some earlier `applyInjectionWidgetOverridesToEntries` call already
|
|
1329
|
+
* recorded.
|
|
1330
|
+
*/
|
|
1213
1331
|
export function applyInjectionWidgetOverridesToTables(
|
|
1214
1332
|
tables: readonly { moduleId: string; table: ModuleInjectionTable }[],
|
|
1215
1333
|
overrides: Readonly<InjectionWidgetOverridesMap> = composeInjectionWidgetOverrides(),
|
|
1334
|
+
entries: readonly ModuleInjectionWidgetEntry[] = [],
|
|
1216
1335
|
): Array<{ moduleId: string; table: ModuleInjectionTable }> {
|
|
1217
|
-
|
|
1336
|
+
rememberInjectionWidgetIdAliases(entries)
|
|
1337
|
+
const disabled = new Set<string>()
|
|
1338
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
1339
|
+
if (value !== null) continue
|
|
1340
|
+
disabled.add(key)
|
|
1341
|
+
for (const alias of getInjectionWidgetIdAliases().get(key) ?? []) disabled.add(alias)
|
|
1342
|
+
}
|
|
1218
1343
|
if (disabled.size === 0) return Array.from(tables)
|
|
1219
1344
|
|
|
1220
1345
|
const filterSlot = (slot: unknown): unknown => {
|
|
@@ -164,11 +164,20 @@ export function getCoreInjectionWidgets(): ModuleInjectionWidgetEntry[] {
|
|
|
164
164
|
return _coreInjectionWidgetEntries
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
/**
|
|
168
|
+
* `widgetEntries` is optional and carries the *unfiltered* generated entries so a
|
|
169
|
+
* `key`-spelled injection-widget override can be resolved to the `widgetId` the table
|
|
170
|
+
* slots reference (#5152). Bootstraps that skip core widget registration have no other
|
|
171
|
+
* source for that mapping, since the registered entries are already override-filtered.
|
|
172
|
+
*/
|
|
173
|
+
export function registerCoreInjectionTables(
|
|
174
|
+
tables: Array<{ moduleId: string; table: ModuleInjectionTable }>,
|
|
175
|
+
widgetEntries?: readonly ModuleInjectionWidgetEntry[],
|
|
176
|
+
) {
|
|
168
177
|
if (_coreInjectionTables !== null && process.env.NODE_ENV === 'development') {
|
|
169
178
|
logger.debug('Core injection tables re-registered (this may occur during HMR)')
|
|
170
179
|
}
|
|
171
|
-
const finalTables = applyInjectionWidgetOverridesToTables(tables)
|
|
180
|
+
const finalTables = applyInjectionWidgetOverridesToTables(tables, undefined, widgetEntries)
|
|
172
181
|
_coreInjectionTables = finalTables
|
|
173
182
|
writeGlobalInjectionTables(finalTables)
|
|
174
183
|
notifyInjectionRegistryChanged()
|