@expofp/config
Advanced tools
| /** | ||
| * TEMPORARY compat shim for the deprecated `?noOverlay=`, `?allowConsent=` and | ||
| * `?agenda`, translated into config fields while integrators move to the | ||
| * canonical `?visibility[overlay]=`, `?consent=` and `?showList=sessions`. | ||
| * | ||
| * A module of its own, outside `UrlConfigSchema`, so the whole shim retires in | ||
| * one move: delete this file and the compiler points at every consumer; the | ||
| * keys then join `retiredKeys` in the legacy codec. `?agenda` broke when the | ||
| * panel was renamed to sessions with no URL alias left (b227ed255, Oct 2025) — | ||
| * installed mobile-SDK apps emit the old spelling, and no SDK release can fix | ||
| * them; this entry is the missing alias. | ||
| * | ||
| * Precedence: an alias is dropped when the same query carries its canonical | ||
| * key, resolved HERE rather than by merge order (`noOverlay` lands in a | ||
| * different field than `visibility[overlay]`, one the FloorPlan latches at | ||
| * load), so both consumers apply the slice without precedence logic. | ||
| */ | ||
| import { type Config, type Consent, type Intent, type Visibility } from '@expofp/schema'; | ||
| /** The deprecated keys for the legacy codec's partition — derived, the schema is the only list. */ | ||
| export declare const DEPRECATED_URL_KEYS: string[]; | ||
| /** | ||
| * The canonical readings the aliases defer to — callers that already parsed | ||
| * the query pass theirs in (`intents`: every canonical intent, bracketed | ||
| * entries before shortcut keys); otherwise the shim reads the query itself. | ||
| */ | ||
| export interface CanonicalAliasTargets { | ||
| visibility?: Visibility; | ||
| consent?: Consent; | ||
| intents?: Intent[]; | ||
| } | ||
| /** The deprecated query params as a config slice; `{}` when the URL has none. */ | ||
| export declare function parseDeprecatedUrlParams(url: string | URL, canonical?: CanonicalAliasTargets): Partial<Config>; | ||
| //# sourceMappingURL=deprecated-url-params.d.ts.map |
| /** | ||
| * TEMPORARY compat shim for the deprecated `?noOverlay=`, `?allowConsent=` and | ||
| * `?agenda`, translated into config fields while integrators move to the | ||
| * canonical `?visibility[overlay]=`, `?consent=` and `?showList=sessions`. | ||
| * | ||
| * A module of its own, outside `UrlConfigSchema`, so the whole shim retires in | ||
| * one move: delete this file and the compiler points at every consumer; the | ||
| * keys then join `retiredKeys` in the legacy codec. `?agenda` broke when the | ||
| * panel was renamed to sessions with no URL alias left (b227ed255, Oct 2025) — | ||
| * installed mobile-SDK apps emit the old spelling, and no SDK release can fix | ||
| * them; this entry is the missing alias. | ||
| * | ||
| * Precedence: an alias is dropped when the same query carries its canonical | ||
| * key, resolved HERE rather than by merge order (`noOverlay` lands in a | ||
| * different field than `visibility[overlay]`, one the FloorPlan latches at | ||
| * load), so both consumers apply the slice without precedence logic. | ||
| */ | ||
| import { UrlConfigSchema, } from '@expofp/schema'; | ||
| import * as z from 'zod'; | ||
| import { parseFromUrlTolerant } from './url-codec.js'; | ||
| import { parseIntentsFromUrl } from './url-intents.js'; | ||
| /** | ||
| * Parsed through the same tolerant codec as the canonical URL slice, so the | ||
| * accepted spellings stay exactly what these params always accepted. | ||
| */ | ||
| const DeprecatedUrlSchema = z.object({ | ||
| /** → `visibility.overlay` (inverted). */ | ||
| noOverlay: z.boolean().optional(), | ||
| /** → `consent`: `true` → `'granted'`, `false` → `'denied'`. */ | ||
| allowConsent: z.boolean().optional(), | ||
| /** → the `showList` intent on `sessions`. A string: the SDKs emit a bare `?agenda`. */ | ||
| agenda: z.string().optional(), | ||
| }); | ||
| /** The deprecated keys for the legacy codec's partition — derived, the schema is the only list. */ | ||
| export const DEPRECATED_URL_KEYS = Object.keys(DeprecatedUrlSchema.shape); | ||
| /** The deprecated query params as a config slice; `{}` when the URL has none. */ | ||
| export function parseDeprecatedUrlParams(url, canonical = parseCanonicalTargets(url)) { | ||
| const { value } = parseFromUrlTolerant(DeprecatedUrlSchema, url); | ||
| const slice = {}; | ||
| // stays the `noOverlay` config field: routing through it leaves the | ||
| // visibility node untouched — exactly what this param always did | ||
| if (value.noOverlay !== undefined && canonical.visibility?.overlay === undefined) { | ||
| slice.noOverlay = value.noOverlay; | ||
| } | ||
| if (value.allowConsent !== undefined && canonical.consent === undefined) { | ||
| slice.consent = value.allowConsent ? 'granted' : 'denied'; | ||
| } | ||
| // the panel is `sessions` — and any canonical list panel wins: two | ||
| // `showList` intents would play in sequence, the alias taking the screen | ||
| if (value.agenda !== undefined && !opensList(canonical.intents)) { | ||
| slice.intents = [{ name: 'showList', args: ['sessions'] }]; | ||
| } | ||
| return slice; | ||
| } | ||
| function opensList(intents) { | ||
| return (intents ?? []).some((intent) => intent.name === 'showList'); | ||
| } | ||
| /** The canonical keys each alias defers to, read from the same query. */ | ||
| const CanonicalUrlSchema = UrlConfigSchema.pick({ | ||
| visibility: true, | ||
| consent: true, | ||
| intents: true, | ||
| }); | ||
| /** Self-parse fallback: the same canonical readings, from the query itself. */ | ||
| function parseCanonicalTargets(url) { | ||
| const { value } = parseFromUrlTolerant(CanonicalUrlSchema, url); | ||
| return { | ||
| visibility: value.visibility, | ||
| consent: value.consent, | ||
| intents: [...(value.intents ?? []), ...parseIntentsFromUrl(url).intents], | ||
| }; | ||
| } |
+1
-0
| export { applyIntents } from './lib/apply-intents.js'; | ||
| export { getConfig, setConfig } from './lib/config-store.js'; | ||
| export { type DebugSettingDescriptor, toDebugSettings } from './lib/debug-settings.js'; | ||
| export { parseDeprecatedUrlParams } from './lib/deprecated-url-params.js'; | ||
| export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, splitPlatformParams, type UrlSelection, } from './lib/legacy-url.js'; | ||
@@ -5,0 +6,0 @@ export { loadConfig } from './lib/load-config.js'; |
+3
-0
| export { applyIntents } from './lib/apply-intents.js'; | ||
| export { getConfig, setConfig } from './lib/config-store.js'; | ||
| export { toDebugSettings } from './lib/debug-settings.js'; | ||
| // TEMPORARY, with the shim itself — the floor plan needs it for the runtime | ||
| // `applyParameters` path, which parses the query a second time | ||
| export { parseDeprecatedUrlParams } from './lib/deprecated-url-params.js'; | ||
| export { parseLegacyQuery, serializeSelection, splitPlatformParams, } from './lib/legacy-url.js'; | ||
@@ -5,0 +8,0 @@ export { loadConfig } from './lib/load-config.js'; |
@@ -30,4 +30,8 @@ /** | ||
| * `packages/floorplan/src/services/url-dispatch.ts` (lint-enforced). | ||
| * | ||
| * Keys whose capability has been retired keep one line each in | ||
| * {@link retiredKeys} — no translation, just enough for an old link to degrade | ||
| * to a clean plan instead of replaying as search text. | ||
| */ | ||
| import { type Camera, type ListPanel } from '@expofp/schema'; | ||
| import { type ListPanel } from '@expofp/schema'; | ||
| /** The list panels addressable by keyword (`?bookmarks`, `?sessions`, …). */ | ||
@@ -62,18 +66,2 @@ export type LegacyListPanel = ListPanel; | ||
| } | { | ||
| type: 'language'; | ||
| langId?: string; | ||
| } | { | ||
| type: 'printPdf'; | ||
| } | { | ||
| type: 'visibility'; | ||
| hidden: string[]; | ||
| } | { | ||
| type: 'buildRoute'; | ||
| } | { | ||
| type: 'pathway'; | ||
| pathwayId: string; | ||
| boothIds: string[]; | ||
| exhibitorIds: string[]; | ||
| tourId?: string; | ||
| } | { | ||
| type: 'selectExhibitors'; | ||
@@ -117,9 +105,2 @@ values: string[]; | ||
| type: 'resetUiScale'; | ||
| } | { | ||
| type: 'legacyBookmarks'; | ||
| appendExhibitorId: number | null; | ||
| } | { | ||
| type: 'camera'; | ||
| camera: Camera; | ||
| roll?: number; | ||
| }; | ||
@@ -126,0 +107,0 @@ export interface LegacyUrlQuery { |
+169
-121
@@ -30,5 +30,10 @@ /** | ||
| * `packages/floorplan/src/services/url-dispatch.ts` (lint-enforced). | ||
| * | ||
| * Keys whose capability has been retired keep one line each in | ||
| * {@link retiredKeys} — no translation, just enough for an old link to degrade | ||
| * to a clean plan instead of replaying as search text. | ||
| */ | ||
| import { IntentsSchema, UrlConfigSchema } from '@expofp/schema'; | ||
| import { safeDecode } from '@expofp/utils'; | ||
| import { DEPRECATED_URL_KEYS } from './deprecated-url-params.js'; | ||
| /* ── Parse: location.search → commands ─────────────────────────────────────── */ | ||
@@ -43,8 +48,83 @@ /** Query keys consumed by `loadConfig` before dispatch: config slice + intent shortcuts. */ | ||
| /** | ||
| * The legacy camera params, translated into `config.camera`'s shape. Stripped | ||
| * from the slug only in their `key=value` form — a bare slug that merely spells | ||
| * one of these words (a booth named "center") still resolves via the catalog. | ||
| * RETIRED keys — capability gone, only swallowed (in `key=value` form: a bare | ||
| * slug that merely spells one of these words still resolves via the catalog), | ||
| * so an old link opens a clean plan instead of replaying as search text. The | ||
| * list only ever shrinks; a parameter that still DOES something belongs in | ||
| * `UrlConfigSchema` or `IntentsSchema`, never here. | ||
| * | ||
| * | retired | canonical form today | | ||
| * | ---------------------- | ------------------------------- | | ||
| * | `centerxy=x,y` | `camera[x]` / `camera[y]` | | ||
| * | `center=lat,lng` | `camera[lat]` / `camera[lng]` | | ||
| * | `z=` | `camera[floor]` | | ||
| * | `bearing=` | `camera[bearing]` | | ||
| * | `zoomtime=` | `camera[zoomTime]` | | ||
| * | `roll=` | — internal renderer knob | | ||
| * | `hide=a,b` | `visibility[a]=false` | | ||
| * | `lang=` | `changeLanguage=` | | ||
| * | `preview=` | — the preview link needs none | | ||
| * | `copy_exh=` | — a removed development seed | | ||
| * | `pathway=id` | `showPathway[0]=id` | | ||
| * | `booths=a,b` | — part of `showPathway` | | ||
| * | `b=` / `ba=` | — bookmark import, removed | | ||
| * | `-pdf` | — a print mode that never was | | ||
| * | `build-route` | `startBuildRoute` | | ||
| * | `sw=` / `yah=` / `k=` | — readers live on in legacy | | ||
| * | `kiosk_<framing>=` | `kiosk[…]` — the anchor node | | ||
| * | ||
| * `sw`/`yah`/`k` still have readers in the legacy runtime (service-worker | ||
| * toggle, YAH-marker command, saved-kiosk id) but none here — the mobile SDK | ||
| * enums emit them and they die out with app installs; their sibling `?agenda` | ||
| * is translated by the shim instead. `kiosk_<framing>` is the 11 fields the | ||
| * old 14-key QR channel spoke beyond the surviving `kiosk_x/y/z` (those | ||
| * partition via {@link directReadKeys}); unswallowed, one replayed as search | ||
| * text — and beside a route slug its tail fused into the last colon-part, | ||
| * silently flipping the accessible flag. `tour=` | ||
| * keeps its live meaning (`selectTour` covers what the pathway link used it | ||
| * for), and retiring `b=`/`ba=` as plain keys drops their emulated query wipe: | ||
| * one-shots beside them now apply. | ||
| */ | ||
| const legacyCameraKeys = new Set(['centerxy', 'center', 'z', 'bearing', 'zoomtime', 'roll']); | ||
| const retiredKeys = new Set([ | ||
| 'centerxy', | ||
| 'center', | ||
| 'z', | ||
| 'bearing', | ||
| 'zoomtime', | ||
| 'roll', | ||
| 'hide', | ||
| 'lang', | ||
| 'preview', | ||
| 'copy_exh', | ||
| 'pathway', | ||
| 'booths', | ||
| 'b', | ||
| 'ba', | ||
| 'sw', | ||
| 'yah', | ||
| 'k', | ||
| // the framing/cosmetic 11 of the old 14-key kiosk QR channel — `kiosk_x/y/z` | ||
| // live on in directReadKeys, and the field names mirror KioskSchema | ||
| 'kiosk_heading', | ||
| 'kiosk_lat', | ||
| 'kiosk_lng', | ||
| 'kiosk_iconSizePercent', | ||
| 'kiosk_uiScale', | ||
| 'kiosk_mapRollDegrees', | ||
| 'kiosk_mapInitialPtScale', | ||
| 'kiosk_mapPtScale', | ||
| 'kiosk_mapCenterX', | ||
| 'kiosk_mapCenterY', | ||
| 'kiosk_mapPitch', | ||
| ]); | ||
| /** Retired keywords: a whole-slug word rather than a `key=value` pair. */ | ||
| const retiredKeywords = new Set(['-pdf', 'build-route']); | ||
| /** | ||
| * The deprecated aliases the `loadConfig` compat shim still consumes — config | ||
| * keys as far as this codec is concerned, so they partition with | ||
| * `configOwnedKeys` and match in both spellings (which is what catches the | ||
| * SDKs' valueless `?agenda`, at the cost of a booth slugged `agenda`). | ||
| * TEMPORARY: when the shim goes, move these into `retiredKeys`. | ||
| */ | ||
| const deprecatedKeys = new Set(DEPRECATED_URL_KEYS); | ||
| /** | ||
| * Query keys owned by the DELIVERY PLATFORM, not the floor plan: `?expo=` is | ||
@@ -66,2 +146,25 @@ * the routing param the sha-delivery platform materializes for expo-aware | ||
| const platformParamKeys = new Set(['expo']); | ||
| /** | ||
| * Query keys the floor plan reads STRAIGHT from `location.search`: `?layer=` | ||
| * (init-layers) and `?kiosk_x/y/z` (the anchor a kiosk's route QR hands the | ||
| * scanning phone). Spelled out here — this package must not depend on the | ||
| * floor plan — so a reader change has to be mirrored. They partition like the | ||
| * platform's params: stripped in `key=value` form only, never `ownedByConfig`. | ||
| * Unpartitioned they replayed as search text, and the route QR's own link | ||
| * (`?route:…:true&kiosk_x=…`) lost its accessible flag — the 4th colon-part of | ||
| * a slug that had swallowed the whole `&kiosk_*` tail. | ||
| */ | ||
| const directReadKeys = new Set(['layer', 'kiosk_x', 'kiosk_y', 'kiosk_z']); | ||
| /** | ||
| * Companion keys of a primary form — read from `params` by its classifier, | ||
| * never part of the slug: `&title=` names a shared route (the route branch | ||
| * reads it; no canonical form exists). Stripped in `key=value` form only, like | ||
| * the partitions above — left in place, the tail fused into the route slug's | ||
| * last colon-part, where it silently flipped `:true` to false or ate a | ||
| * trailing FROM. A bare slug that merely spells `title` still resolves via | ||
| * the catalog. (The planner's `&source=`/`&from=` companions need no entry: | ||
| * that branch classifies on `params` before the slug is consulted, and the | ||
| * slug of a params-classified query is never replayed as search text.) | ||
| */ | ||
| const companionKeys = new Set(['title']); | ||
| export function parseLegacyQuery(search) { | ||
@@ -72,22 +175,8 @@ const rawQuery = search.startsWith('?') ? search.slice(1) : search; | ||
| const params = new URLSearchParams(decoded); | ||
| const oneShots = collectOneShots(rawQuery, params); | ||
| // `?preview=`, `?b=`/`?ba=` (leading param) and `?copy_exh` historically | ||
| // wiped the whole query before the other one-shots could see it — only the | ||
| // heatmap flags (processed first) and the bookmark redirect itself survive | ||
| const wiped = rawQuery.startsWith('preview=') || | ||
| rawQuery.startsWith('b=') || | ||
| rawQuery.startsWith('ba=') || | ||
| decoded.includes('copy_exh'); | ||
| if (wiped) { | ||
| return { | ||
| primary: { type: 'select', slug: '' }, | ||
| oneShots: oneShots.filter((s) => s.type === 'heatmap' || s.type === 'legacyBookmarks'), | ||
| }; | ||
| } | ||
| return { | ||
| primary: classifyPrimary(residualSlug(decoded), params, hasConfigOwnedParts(decoded)), | ||
| oneShots, | ||
| primary: classifyPrimary(residualSlug(decoded), params, hasOwnedParts(decoded)), | ||
| oneShots: collectOneShots(params), | ||
| }; | ||
| } | ||
| function classifyPrimary(slug, params, configOwned) { | ||
| function classifyPrimary(slug, params, owned) { | ||
| if (params.has('kiosk')) { | ||
@@ -126,5 +215,2 @@ const value = params.get('kiosk'); | ||
| return { type: 'list', list: 'language' }; | ||
| if (params.has('lang')) { | ||
| return { type: 'language', langId: params.get('lang')?.toLowerCase() }; | ||
| } | ||
| if (slug === 'sessions') | ||
@@ -136,32 +222,21 @@ return { type: 'list', list: 'sessions' }; | ||
| return { type: 'list', list: 'speakers' }; | ||
| if (slug === '-pdf') | ||
| return { type: 'printPdf' }; | ||
| if (slug.startsWith('hide')) { | ||
| const hidden = (new URLSearchParams(slug).get('hide') ?? '').split(',').filter(Boolean); | ||
| return { type: 'visibility', hidden }; | ||
| if (slug.startsWith('exhibitors=')) { | ||
| const value = slug.slice('exhibitors='.length); | ||
| // `?exhibitors=` and `?exhibitors=true` are exactly the two values the | ||
| // FILTER manager reads as "open the exhibitors list" — classified as a | ||
| // selection, the link also selected an exhibitor named "true". Anything | ||
| // else after `exhibitors=` is a display-name list. | ||
| if (value === '' || value === 'true') | ||
| return { type: 'ownedByFilters' }; | ||
| return { type: 'selectExhibitors', values: value.split(',') }; | ||
| } | ||
| if (slug === 'build-route') | ||
| return { type: 'buildRoute' }; | ||
| const pathwayId = params.get('pathway'); | ||
| if (pathwayId) { | ||
| return { | ||
| type: 'pathway', | ||
| pathwayId, | ||
| boothIds: params.get('booths')?.split(',') ?? [], | ||
| exhibitorIds: params.get('exhibitors')?.split(',') ?? [], | ||
| tourId: params.get('tour') ?? undefined, | ||
| }; | ||
| } | ||
| if (slug.startsWith('exhibitors') && slug.includes('=')) { | ||
| return { type: 'selectExhibitors', values: slug.split('=')[1].split(',') }; | ||
| } | ||
| if (slug.includes('=') && (slug.startsWith('categories=') || /^poiTypes?=/.test(slug))) { | ||
| return { type: 'ownedByFilters' }; | ||
| } | ||
| // Last, after every params-based branch above (`?kiosk=1&camera[x]=5` is | ||
| // still the kiosk toggle): an empty slug that got empty because config/ | ||
| // intent keys owned the whole query is NOT the "clear the selection" | ||
| // command a genuinely empty query is — replaying it as `select('')` would | ||
| // close whatever is open. | ||
| if (!slug && configOwned) { | ||
| // Last, after every params-based branch above: a slug emptied because | ||
| // config/intent keys (or the deprecated aliases) owned the whole query is | ||
| // NOT the clear-selection command an empty query is — `select('')` would | ||
| // close whatever is open. Retired-only queries deliberately ARE (see | ||
| // hasOwnedParts). | ||
| if (!slug && owned) { | ||
| return { type: 'ownedByConfig' }; | ||
@@ -171,3 +246,3 @@ } | ||
| } | ||
| function collectOneShots(rawQuery, params) { | ||
| function collectOneShots(params) { | ||
| const oneShots = []; | ||
@@ -181,10 +256,2 @@ if (params.get('heatmap') === 'true') { | ||
| } | ||
| if (rawQuery.startsWith('b=') || rawQuery.startsWith('ba=')) { | ||
| const appendRaw = new URLSearchParams(rawQuery).get('ba'); | ||
| const appendExhibitorId = appendRaw === null ? null : parseInt(appendRaw, 10); | ||
| oneShots.push({ | ||
| type: 'legacyBookmarks', | ||
| appendExhibitorId: Number.isFinite(appendExhibitorId) ? appendExhibitorId : null, | ||
| }); | ||
| } | ||
| const blueDot = params.get('blue-dot'); | ||
@@ -229,63 +296,40 @@ if (blueDot !== null) { | ||
| } | ||
| const camera = legacyCamera(params); | ||
| if (camera) { | ||
| oneShots.push(camera); | ||
| } | ||
| return oneShots; | ||
| } | ||
| /** Translate the legacy camera params into the schema-typed `Camera` shape. */ | ||
| function legacyCamera(params) { | ||
| const camera = {}; | ||
| const centerxy = numberPair(params.get('centerxy')); | ||
| if (centerxy) | ||
| [camera.x, camera.y] = centerxy; | ||
| const center = numberPair(params.get('center')); | ||
| if (center) | ||
| [camera.lat, camera.lng] = center; | ||
| const floor = params.get('z'); | ||
| if (floor) | ||
| camera.floor = floor; | ||
| const bearing = finiteNumber(params.get('bearing')); | ||
| if (bearing !== undefined) | ||
| camera.bearing = bearing; | ||
| const zoomTime = finiteNumber(params.get('zoomtime')); | ||
| if (zoomTime !== undefined) | ||
| camera.zoomTime = zoomTime; | ||
| const roll = finiteNumber(params.get('roll')); | ||
| if (!Object.keys(camera).length && roll === undefined) | ||
| return null; | ||
| return roll === undefined ? { type: 'camera', camera } : { type: 'camera', camera, roll }; | ||
| /** A retired part, matched in `key=value` form only (see {@link retiredKeys}). */ | ||
| function isRetiredPart(part) { | ||
| if (retiredKeywords.has(part)) | ||
| return true; | ||
| return retiredKeys.has(part.split('=')[0]) && part.includes('='); | ||
| } | ||
| function numberPair(value) { | ||
| if (!value) | ||
| return undefined; | ||
| const parts = value.split(',').map(parseFloat); | ||
| if (parts.length !== 2 || !parts.every(Number.isFinite)) | ||
| return undefined; | ||
| return [parts[0], parts[1]]; | ||
| /** | ||
| * A part `loadConfig` consumes: a config-slice key, an intent shortcut, or a | ||
| * deprecated alias — bracketed sub-keys and the valueless spelling included. | ||
| */ | ||
| function isConsumedByConfig(part) { | ||
| const rawKey = part.split('=')[0]; | ||
| // the legacy scalar `?heatmap=true` is slug-resident (see residualSlug) | ||
| if (rawKey === 'heatmap') | ||
| return false; | ||
| const key = rawKey.split('[')[0]; | ||
| return configOwnedKeys.has(key) || deprecatedKeys.has(key); | ||
| } | ||
| function finiteNumber(value) { | ||
| if (value === null || value === '') | ||
| return undefined; | ||
| const parsed = parseFloat(value); | ||
| return Number.isFinite(parsed) ? parsed : undefined; | ||
| /** | ||
| * Whether the query holds a part the config layer consumes. Retired and | ||
| * one-shot keys deliberately do NOT count: a query made only of those means | ||
| * nothing, keeping the legacy empty select (which closes the open panel). | ||
| */ | ||
| function hasOwnedParts(decoded) { | ||
| return decoded.split('&').some((part) => !!part && isConsumedByConfig(part)); | ||
| } | ||
| /** | ||
| * The slug is the decoded query minus every key another consumer owns: the | ||
| * config layer's keys and intent shortcuts (read by `loadConfig`), the | ||
| * one-shot keys consumed above, and the platform's routing params. | ||
| * `heatmap`/`type`/`subtype` stay in the slug — | ||
| * heatmap mode historically froze the URL with them in place (its data loader | ||
| * re-reads them), and the select fallback knows not to treat a query carrying | ||
| * `heatmap=true` as search text. | ||
| * retired keys swallowed above, the platform's routing params, the keys the | ||
| * floor plan reads straight from the query ({@link directReadKeys}), the | ||
| * grammar's own companion keys ({@link companionKeys}), the config layer's | ||
| * keys and intent shortcuts (read by `loadConfig`), and the one-shot keys. `heatmap`/`type`/`subtype` stay in the slug — heatmap mode historically | ||
| * froze the URL with them in place (its data loader re-reads them), and the | ||
| * select fallback knows not to treat a query carrying `heatmap=true` as search | ||
| * text. | ||
| */ | ||
| function hasConfigOwnedParts(decoded) { | ||
| return decoded.split('&').some((part) => { | ||
| const rawKey = part.split('=')[0]; | ||
| // the legacy scalar `?heatmap=true` is slug-resident (see residualSlug) | ||
| if (rawKey === 'heatmap') | ||
| return false; | ||
| return configOwnedKeys.has(rawKey.split('[')[0]); | ||
| }); | ||
| } | ||
| function residualSlug(decoded) { | ||
@@ -297,16 +341,20 @@ return decoded | ||
| return false; | ||
| if (isRetiredPart(part)) | ||
| return false; | ||
| const key = part.split('=')[0].split('[')[0]; | ||
| if (legacyCameraKeys.has(key) && part.includes('=')) | ||
| // all stripped in `key=value` form only, so a booth named "expo", | ||
| // "layer" or "title" still resolves via the catalog | ||
| if ((platformParamKeys.has(key) || directReadKeys.has(key) || companionKeys.has(key)) && | ||
| part.includes('=')) { | ||
| return false; | ||
| if (platformParamKeys.has(key) && part.includes('=')) | ||
| return false; | ||
| } | ||
| // Carve-out (retirement plan §4.8): `heatmap` is a UrlConfigSchema key | ||
| // now, so the canonical bracketed `heatmap[…]=` form is config-owned and | ||
| // strips like any other — but the legacy scalar `?heatmap=true` must | ||
| // KEEP the slug residency described above (the select fallback | ||
| // recognizes the literal `heatmap=true`; the frozen URL carries it for | ||
| // the data loader). Discriminate on the raw key: no bracket ⇒ legacy. | ||
| if (part.split('=')[0] === 'heatmap') | ||
| return true; | ||
| return !configOwnedKeys.has(key) && !oneShotKeys.has(key); | ||
| // KEEP its slug residency (the select fallback recognizes the literal | ||
| // `heatmap=true`; the frozen URL carries it for the data loader). | ||
| // `isConsumedByConfig` discriminates on the raw key: no bracket ⇒ legacy. | ||
| if (isConsumedByConfig(part)) | ||
| return false; | ||
| return !oneShotKeys.has(key); | ||
| }) | ||
@@ -313,0 +361,0 @@ .join('&'); |
@@ -6,2 +6,3 @@ /// <reference lib="dom" /> | ||
| import debug from 'debug'; | ||
| import { parseDeprecatedUrlParams } from './deprecated-url-params.js'; | ||
| import { parseFromStorage } from './local-storage-codec.js'; | ||
@@ -166,9 +167,20 @@ import { normalizeFpSvgLayerAliases } from './normalize-fp-svg.js'; | ||
| const shortcuts = parseIntentsFromUrl(url); | ||
| if (shortcuts.intents.length) { | ||
| urlConfig.intents = [...(urlConfig.intents ?? []), ...shortcuts.intents]; | ||
| } | ||
| const canonicalIntents = [...(urlConfig.intents ?? []), ...shortcuts.intents]; | ||
| // TEMPORARY: the deprecated shim, handed the canonical readings parsed | ||
| // above so it does not re-parse the query. Its fields merge first so the | ||
| // canonical keys below win; its `?agenda` intent appends, since assigning | ||
| // the slice would drop what the canonical keys put there. Delete with the | ||
| // module. | ||
| const { intents: aliasIntents = [], ...deprecatedFields } = parseDeprecatedUrlParams(url, { | ||
| visibility: urlConfig.visibility, | ||
| consent: urlConfig.consent, | ||
| intents: canonicalIntents, | ||
| }); | ||
| const intents = [...canonicalIntents, ...aliasIntents]; | ||
| if (intents.length) | ||
| urlConfig.intents = intents; | ||
| invalidKeys.push(...shortcuts.invalidKeys); | ||
| if (invalidKeys.length) | ||
| log('loadConfig', 'ignoring invalid URL config params:', invalidKeys); | ||
| applyConsentAlias(urlConfig); | ||
| assignDefined(config, deprecatedFields); | ||
| assignDefined(config, urlConfig); | ||
@@ -175,0 +187,0 @@ } |
+4
-4
| { | ||
| "name": "@expofp/config", | ||
| "version": "3.22.0", | ||
| "version": "3.23.0", | ||
| "type": "module", | ||
@@ -32,6 +32,6 @@ "description": "ExpoFP SDK internal: config layer and schemas", | ||
| "zod": "4.4.3", | ||
| "@expofp/resolve": "3.22.0", | ||
| "@expofp/utils": "3.22.0", | ||
| "@expofp/schema": "3.22.0" | ||
| "@expofp/resolve": "3.23.0", | ||
| "@expofp/schema": "3.23.0", | ||
| "@expofp/utils": "3.23.0" | ||
| } | ||
| } |
+3
-1
@@ -30,3 +30,5 @@ # @expofp/config | ||
| shared links stay bidirectional. The floor plan's `url-dispatch` service binds the commands to | ||
| stores and public methods; this codec stays pure — no DOM, no stores. | ||
| stores and public methods; this codec stays pure — no DOM, no stores. Keys whose capability has | ||
| been retired (`?hide=`, `?centerxy=`, `?lang=`, …) are listed in `retiredKeys` and swallowed, so an | ||
| old link opens a clean plan instead of replaying its query as search text. | ||
@@ -33,0 +35,0 @@ Also: `applyIntents` (dispatch `selectBooth` / `changeLanguage` / … to the floor plan), |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
136898
7.48%42
5%2752
5.72%37
5.71%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
Updated
Updated
Updated