@expofp/config 3.18.1 → 3.20.0

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/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { applyIntents } from './lib/apply-intents.js';
2
2
  export { getConfig, setConfig } from './lib/config-store.js';
3
3
  export { type DebugSettingDescriptor, toDebugSettings } from './lib/debug-settings.js';
4
- export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, type UrlSelection, } from './lib/legacy-url.js';
4
+ export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, splitPlatformParams, type UrlSelection, } from './lib/legacy-url.js';
5
5
  export { loadConfig } from './lib/load-config.js';
6
6
  export { type StorageLike } from './lib/local-storage-codec.js';
7
7
  export { type ConfigResourceStore, serializeConfigResources, } from './lib/serialize-config-resources.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { applyIntents } from './lib/apply-intents.js';
2
2
  export { getConfig, setConfig } from './lib/config-store.js';
3
3
  export { toDebugSettings } from './lib/debug-settings.js';
4
- export { parseLegacyQuery, serializeSelection, } from './lib/legacy-url.js';
4
+ export { parseLegacyQuery, serializeSelection, splitPlatformParams, } from './lib/legacy-url.js';
5
5
  export { loadConfig } from './lib/load-config.js';
6
6
  export { serializeConfigResources, } from './lib/serialize-config-resources.js';
7
7
  export { parseFromUrlTolerant } from './lib/url-codec.js';
@@ -157,6 +157,21 @@ export type UrlSelection = {
157
157
  value: string;
158
158
  };
159
159
  export declare function parseLegacyQuery(search: string): LegacyUrlQuery;
160
+ /**
161
+ * Split a raw `location.search` into the floor plan's own query and the
162
+ * platform's routing params (`?expo=…` — {@link platformParamKeys}).
163
+ * Raw-segment level (split on `&`, no decoding): platform params travel
164
+ * outside the legacy grammar's single-`encodeURIComponent`-unit payload, and
165
+ * that payload must pass through untouched. The floor plan's single URL
166
+ * writer (`url-sync`) hides the platform half from what the app reads back
167
+ * and re-attaches it verbatim to every URL it writes — a preview URL
168
+ * `/s/<sha>/?expo=…` must survive reload and sharing after any rewrite.
169
+ * When no platform param is present, `search` is returned byte-identical.
170
+ */
171
+ export declare function splitPlatformParams(search: string): {
172
+ search: string;
173
+ platform: string;
174
+ };
160
175
  /**
161
176
  * The selection's canonical query — `''` for none, else `?<payload>` with the
162
177
  * whole payload as one `encodeURIComponent` unit (the legacy contract). This is
@@ -44,6 +44,23 @@ const oneShotKeys = new Set(['blue-dot', 'viewermode', 'previewMode', 'uiscale',
44
44
  * one of these words (a booth named "center") still resolves via the catalog.
45
45
  */
46
46
  const legacyCameraKeys = new Set(['centerxy', 'center', 'z', 'bearing', 'zoomtime', 'roll']);
47
+ /**
48
+ * Query keys owned by the DELIVERY PLATFORM, not the floor plan: `?expo=` is
49
+ * the routing param the sha-delivery platform materializes for expo-aware
50
+ * origins, and preview URLs (`/s/<sha>/?expo=…`) carry it literally in the
51
+ * page URL (design note 2026-08-05-sha-delivery-iac.md § 2). Not a grammar
52
+ * extension — a third partition of the query alongside config-owned and
53
+ * one-shot keys: the parser strips these from the residual slug (in their
54
+ * `key=value` form only — a bare slug that merely spells `expo` still
55
+ * resolves via the catalog), and the floor plan's URL writer carries them
56
+ * verbatim across query rewrites ({@link splitPlatformParams}).
57
+ *
58
+ * Unlike config-owned keys, platform params do NOT classify a query as
59
+ * `ownedByConfig`: `?expo=…` alone DESCRIBES the empty selection — browser
60
+ * back onto it must clear the selection like any genuinely empty query,
61
+ * whereas config keys are instructions riding alongside the selection state.
62
+ */
63
+ const platformParamKeys = new Set(['expo']);
47
64
  export function parseLegacyQuery(search) {
48
65
  const rawQuery = search.startsWith('?') ? search.slice(1) : search;
49
66
  // the legacy contract: the payload is one encodeURIComponent unit
@@ -246,8 +263,9 @@ function finiteNumber(value) {
246
263
  }
247
264
  /**
248
265
  * The slug is the decoded query minus every key another consumer owns: the
249
- * config layer's keys and intent shortcuts (read by `loadConfig`), and the
250
- * one-shot keys consumed above. `heatmap`/`type`/`subtype` stay in the slug —
266
+ * config layer's keys and intent shortcuts (read by `loadConfig`), the
267
+ * one-shot keys consumed above, and the platform's routing params.
268
+ * `heatmap`/`type`/`subtype` stay in the slug —
251
269
  * heatmap mode historically froze the URL with them in place (its data loader
252
270
  * re-reads them), and the select fallback knows not to treat a query carrying
253
271
  * `heatmap=true` as search text.
@@ -270,6 +288,8 @@ function residualSlug(decoded) {
270
288
  const key = part.split('=')[0].split('[')[0];
271
289
  if (legacyCameraKeys.has(key) && part.includes('='))
272
290
  return false;
291
+ if (platformParamKeys.has(key) && part.includes('='))
292
+ return false;
273
293
  // Carve-out (retirement plan §4.8): `heatmap` is a UrlConfigSchema key
274
294
  // now, so the canonical bracketed `heatmap[…]=` form is config-owned and
275
295
  // strips like any other — but the legacy scalar `?heatmap=true` must
@@ -282,6 +302,33 @@ function residualSlug(decoded) {
282
302
  })
283
303
  .join('&');
284
304
  }
305
+ /* ── Platform routing params: the writer-side partition ────────────────────── */
306
+ /**
307
+ * Split a raw `location.search` into the floor plan's own query and the
308
+ * platform's routing params (`?expo=…` — {@link platformParamKeys}).
309
+ * Raw-segment level (split on `&`, no decoding): platform params travel
310
+ * outside the legacy grammar's single-`encodeURIComponent`-unit payload, and
311
+ * that payload must pass through untouched. The floor plan's single URL
312
+ * writer (`url-sync`) hides the platform half from what the app reads back
313
+ * and re-attaches it verbatim to every URL it writes — a preview URL
314
+ * `/s/<sha>/?expo=…` must survive reload and sharing after any rewrite.
315
+ * When no platform param is present, `search` is returned byte-identical.
316
+ */
317
+ export function splitPlatformParams(search) {
318
+ const raw = search.startsWith('?') ? search.slice(1) : search;
319
+ if (!raw)
320
+ return { search, platform: '' };
321
+ const segments = raw.split('&');
322
+ const platform = segments.filter(isPlatformParam);
323
+ if (!platform.length)
324
+ return { search, platform: '' };
325
+ const app = segments.filter((segment) => !isPlatformParam(segment));
326
+ return { search: app.length ? '?' + app.join('&') : '', platform: platform.join('&') };
327
+ }
328
+ function isPlatformParam(segment) {
329
+ const eq = segment.indexOf('=');
330
+ return eq > 0 && platformParamKeys.has(segment.slice(0, eq));
331
+ }
285
332
  /* ── Serialize: selection → location.search ────────────────────────────────── */
286
333
  /**
287
334
  * The selection's canonical query — `''` for none, else `?<payload>` with the
@@ -6,8 +6,9 @@ import { type StorageLike } from './local-storage-codec.js';
6
6
  * expo data — by layering, lowest to highest precedence:
7
7
  * manifest (+ its legacy data.js) → options → localStorage → URL.
8
8
  *
9
- * With a `legacyDataUrlBase` (live events), the version probe settles the
10
- * `?v=` cache-buster, refs to the sibling drawing / wayfinding files are
9
+ * With a `legacyDataUrlBase` (live events), the manifest's `legacyDataVersion`
10
+ * pin — or, absent one, the version probe — settles the `?v=` cache-buster,
11
+ * refs to the sibling drawing / wayfinding files are
11
12
  * derived from it (`data.js` never carries them), and all three legacy files —
12
13
  * `data.js` evaluated to its `__data` document like every legacy `.js` file,
13
14
  * asset-resolved and merged in — are fetched in parallel. A manifest can
@@ -17,8 +17,9 @@ const log = debug('efp:config');
17
17
  * expo data — by layering, lowest to highest precedence:
18
18
  * manifest (+ its legacy data.js) → options → localStorage → URL.
19
19
  *
20
- * With a `legacyDataUrlBase` (live events), the version probe settles the
21
- * `?v=` cache-buster, refs to the sibling drawing / wayfinding files are
20
+ * With a `legacyDataUrlBase` (live events), the manifest's `legacyDataVersion`
21
+ * pin — or, absent one, the version probe — settles the `?v=` cache-buster,
22
+ * refs to the sibling drawing / wayfinding files are
22
23
  * derived from it (`data.js` never carries them), and all three legacy files —
23
24
  * `data.js` evaluated to its `__data` document like every legacy `.js` file,
24
25
  * asset-resolved and merged in — are fetched in parallel. A manifest can
@@ -45,28 +46,44 @@ export async function loadConfig(manifest, options, url, storage) {
45
46
  // manifest carries the full event data (booths, exhibitors, …), and
46
47
  // `normalizeLegacyData` below mutates those nested objects in place.
47
48
  manifest = validateOrClone(ManifestSchema, await resolve(manifest));
48
- // The legacy event-data directory (and the old manifests' version pointer,
49
- // replaced by the version.json probe) are INPUTS, consumed right here — the
50
- // effective config carries only the refs derived from them, so neither is
51
- // merged.
52
- const { legacyDataUrlBase, legacyDataVersion: _manifestVersion, ...manifestFields } = manifest;
49
+ // The legacy event-data directory and the data-revision pin are INPUTS,
50
+ // consumed right here — the effective config carries only the refs derived
51
+ // from them, so neither is merged.
52
+ const { legacyDataUrlBase, legacyDataVersion: manifestPin, ...manifestFields } = manifest;
53
53
  applyConsentAlias(manifestFields);
54
54
  assignDefined(config, manifestFields);
55
55
  // don't trust, parse
56
56
  options = OptionsSchema.parse(options);
57
57
  applyConsentAlias(options);
58
- // every legacy file URL is ?v= cache-busted; a timestamp busts when no
59
- // version.json exists (TODO: drop once the legacy data directory is gone)
58
+ // every legacy file URL is ?v= cache-busted; a timestamp busts when neither
59
+ // the manifest nor version.json pins a revision (TODO: drop once the legacy
60
+ // data directory is gone)
60
61
  let legacyDataVersion = String(Date.now());
61
- // live legacy directory: version probe, then the sibling file refs — data.js
62
- // never carries the payload refs, so both are known before it lands
62
+ // live legacy directory: settle the version (manifest pin, else the
63
+ // version.json probe), then the sibling file refs — data.js never carries
64
+ // the payload refs, so both are known before it lands
63
65
  if (legacyDataUrlBase) {
64
66
  if (isFromDesignerReferrer()) {
65
67
  // The designer previews in-progress edits — the directory's files change
66
- // without a version bump, so the published version must not pin `?v=`;
67
- // the unique timestamp stands and every load fetches fresh files.
68
+ // without a version bump, so a published version (the manifest pin and
69
+ // version.json alike) must not pin `?v=`; the unique timestamp stands
70
+ // and every load fetches fresh files.
68
71
  log('loadConfig', 'designer referrer — cache-busting with a timestamp');
69
72
  }
73
+ else if (typeof manifestPin === 'string' && manifestPin) {
74
+ // The manifest pins the data revision (get-manifest already probed
75
+ // version.json server-side) — honor it, no client probe: manifest and
76
+ // data stay consistent by construction.
77
+ legacyDataVersion = manifestPin;
78
+ }
79
+ else if (manifestPin === null) {
80
+ // The generator probed and there IS no version.json for this expo — the
81
+ // per-load timestamp stands, and the client must not re-probe (on the
82
+ // cross-origin contour that probe could only fail). Only an ABSENT pin
83
+ // (legacy manifests; the retired `$ref` pointer counts as absent) falls
84
+ // through to the probe below.
85
+ log('loadConfig', 'manifest pins "no version" — cache-busting with a timestamp');
86
+ }
70
87
  else {
71
88
  try {
72
89
  legacyDataVersion = (await resolve({ $ref: `${legacyDataUrlBase}version.json` })).version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/config",
3
- "version": "3.18.1",
3
+ "version": "3.20.0",
4
4
  "type": "module",
5
5
  "description": "ExpoFP SDK internal: config layer and schemas",
6
6
  "homepage": "https://developer.expofp.com/",
@@ -29,8 +29,8 @@
29
29
  "debug": "^4.4.3",
30
30
  "tslib": "^2.3.0",
31
31
  "zod": "4.4.3",
32
- "@expofp/resolve": "3.18.1",
33
- "@expofp/utils": "3.18.1",
34
- "@expofp/schema": "3.18.1"
32
+ "@expofp/schema": "3.20.0",
33
+ "@expofp/utils": "3.20.0",
34
+ "@expofp/resolve": "3.20.0"
35
35
  }
36
36
  }