@expofp/config 3.28.3 → 3.29.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/README.md CHANGED
@@ -32,5 +32,9 @@ been retired (`?hide=`, `?centerxy=`, `?lang=`, …) are listed in `retiredKeys`
32
32
  old link opens a clean plan instead of replaying its query as search text.
33
33
 
34
34
  Also: `applyIntents` (dispatch `selectBooth` / `changeLanguage` / … to the floor plan),
35
- `serializeConfigResources` (collect schema-tagged assets + payloads for offline copies), and
36
- `toDebugSettings` (config schema → `@expofp/debug` panel). Types re-export from `@expofp/schema`.
35
+ `serializeConfigResources` (collect schema-tagged assets + payloads for offline copies and normalized
36
+ expo folders), `resolveUrl` / `compactUrl` (the `$/<expo>/<hash>/<name>` ⇄
37
+ `https://<expo>.expofp.com/immutable/<hash>/<name>` codec of a normalized folder's store refs —
38
+ `loadConfig` follows the folder's pointer `manifest.json` to its immutable snapshot and expands the
39
+ refs, so an effective config only ever carries full URLs), and `toDebugSettings` (config schema →
40
+ `@expofp/debug` panel). Types re-export from `@expofp/schema`.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegac
6
6
  export { loadConfig } from './lib/load-config.js';
7
7
  export { type StorageLike } from './lib/local-storage-codec.js';
8
8
  export { type ConfigResourceStore, serializeConfigResources, } from './lib/serialize-config-resources.js';
9
+ export { compactUrl, resolveUrl } from './lib/store-ref.js';
9
10
  export { parseFromUrlTolerant } from './lib/url-codec.js';
10
11
  export { parseIntentsFromUrl } from './lib/url-intents.js';
11
12
  export { type Config, type Consent, type Manifest, type Options } from '@expofp/schema';
package/dist/index.js CHANGED
@@ -7,5 +7,6 @@ export { parseDeprecatedUrlParams } from './lib/deprecated-url-params.js';
7
7
  export { parseLegacyQuery, serializeSelection, splitPlatformParams, } from './lib/legacy-url.js';
8
8
  export { loadConfig } from './lib/load-config.js';
9
9
  export { serializeConfigResources, } from './lib/serialize-config-resources.js';
10
+ export { compactUrl, resolveUrl } from './lib/store-ref.js';
10
11
  export { parseFromUrlTolerant } from './lib/url-codec.js';
11
12
  export { parseIntentsFromUrl } from './lib/url-intents.js';
@@ -8,10 +8,11 @@ import { parseFromStorage } from './local-storage-codec.js';
8
8
  import { normalizeFpSvgLayerAliases } from './normalize-fp-svg.js';
9
9
  import { normalizeLegacyData } from './normalize-legacy-data.js';
10
10
  import { applyRebookingData } from './rebooking.js';
11
+ import { resolveUrl } from './store-ref.js';
11
12
  import { parseFromUrlTolerant } from './url-codec.js';
12
13
  import { parseIntentsFromUrl } from './url-intents.js';
13
14
  import { readValidateFlag } from './validate-flag.js';
14
- import { documentRef, setValidateManifest, validateOrClone } from './visit-resources.js';
15
+ import { documentRef, isRefShaped, setValidateManifest, validateOrClone, visitResources, } from './visit-resources.js';
15
16
  const log = debug('efp:config');
16
17
  /**
17
18
  * Assemble the effective config — the runtime's single, complete copy of the
@@ -42,11 +43,17 @@ export async function loadConfig(manifest, options, url, storage) {
42
43
  // layers merge, so it can't come off the effective config. See readValidateFlag.
43
44
  const validateSchema = readValidateFlag(url, storage);
44
45
  setValidateManifest(validateSchema); // gates the referenced-document parses (visit-resources)
45
- // Take an OWN copy of the resolved manifest (validated when the flag is on —
46
- // see validateOrClone): `resolve` deep-freezes what it settles, an offline
47
- // manifest carries the full event data (booths, exhibitors, …), and
48
- // `normalizeLegacyData` below mutates those nested objects in place.
49
- manifest = validateOrClone(ManifestSchema, await resolve(manifest));
46
+ // A normalized expo folder's mutable `manifest.json` is a tiny pointer
47
+ // (`{"$ref": "$/<expo>/<hash>/manifest.json"}`) — follow the chain down to
48
+ // the snapshot first. Then take an OWN copy of the settled manifest
49
+ // (validated when the flag is on — see validateOrClone): `resolve`
50
+ // deep-freezes what it settles, an offline or normalized manifest carries
51
+ // the full event data (booths, exhibitors, …), and `normalizeLegacyData`
52
+ // below mutates those nested objects in place. Store refs (`$/…`) expand
53
+ // into their canonical immutable URLs right here — the effective config
54
+ // only ever carries full URLs, so no consumer sees the notation.
55
+ manifest = validateOrClone(ManifestSchema, await resolveManifestChain(manifest));
56
+ await expandManifestStoreRefs(manifest);
50
57
  // The legacy event-data directory and the data-revision pin are INPUTS,
51
58
  // consumed right here — the effective config carries only the refs derived
52
59
  // from them, so neither is merged.
@@ -195,6 +202,45 @@ export async function loadConfig(manifest, options, url, storage) {
195
202
  deepFreeze(config);
196
203
  return config;
197
204
  }
205
+ /**
206
+ * Hops a manifest pointer chain may take before it must have settled. The
207
+ * normalized form needs exactly one (mutable pointer → immutable snapshot);
208
+ * the ceiling only guards against a cyclic or runaway chain.
209
+ */
210
+ const MANIFEST_POINTER_HOPS_MAX = 3;
211
+ /**
212
+ * Follow the «pointer → snapshot» chain: a settled manifest that is itself
213
+ * ref-shaped is a pointer — resolve what it points at, until a document
214
+ * settles. A store ref (`$/…`) expands by the fixed convention before its
215
+ * fetch, the input ref included; a rooted local pointer resolves against
216
+ * the page like any rooted URL. Chain-walking lives here, not in
217
+ * `@expofp/resolve` — the resolver stays one-layered and domain-free.
218
+ */
219
+ async function resolveManifestChain(input) {
220
+ let current = input;
221
+ for (let hops = 0;; hops++) {
222
+ if (isRefShaped(current))
223
+ current = { ...current, $ref: resolveUrl(current.$ref) };
224
+ const settled = await resolve(current);
225
+ if (!isRefShaped(settled))
226
+ return settled;
227
+ if (hops >= MANIFEST_POINTER_HOPS_MAX) {
228
+ throw new Error(`loadConfig: manifest pointer chain deeper than ${MANIFEST_POINTER_HOPS_MAX} at ${settled.$ref}`);
229
+ }
230
+ log('loadConfig', 'manifest is a pointer, following it to', settled.$ref);
231
+ current = { $ref: settled.$ref };
232
+ }
233
+ }
234
+ /**
235
+ * Expand every store ref (`$/…`) of a normalized manifest — asset URLs and
236
+ * document `$ref`s alike — into its canonical URL, via the one schema-guided
237
+ * walk. Passthrough for other values, so compat and offline manifests come
238
+ * out untouched.
239
+ */
240
+ async function expandManifestStoreRefs(manifest) {
241
+ const expand = (url) => resolveUrl(url);
242
+ await visitResources(ManifestSchema, manifest, { asset: expand, ref: expand });
243
+ }
198
244
  async function loadLegacyDataJs(baseUrl, version) {
199
245
  // data.js is a window-assigning script like fp.svg.js — resolve() evaluates
200
246
  // it to its globals and the expo data document rides on `__data`. The
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The `$/` notation of the normalized expo form (plan G § 3) — the codec
3
+ * between a store ref and the canonical URL it aliases.
4
+ *
5
+ * Every object of an expo's content-addressed store lives at one URL fixed
6
+ * forever — `https://<expo>.expofp.com/immutable/<hash>/<name>`, the expo's
7
+ * production host whichever contour generated or serves the folder — and
8
+ * the store ref `$/<expo>/<hash>/<name>` is that URL's alias by pure prefix
9
+ * substitution. No document, page or base URL takes part, so a snapshot
10
+ * reads identically from a contour page, an integrator's host, a local
11
+ * `floorplan.js`, an inline copy or a Node tool. The two forms are
12
+ * equivalent and a creator compacts at will: shorter bytes, an explicit
13
+ * "store content" marker, and an alias that survives a domain move.
14
+ *
15
+ * The marker is unambiguous by generation invariant, not by URL illegality:
16
+ * a store only ever returns canonical URLs, and no passthrough value ever
17
+ * starts with `$/`. A ref left unexpanded by mistake breaks loudly — an
18
+ * honest 404 on `<base>/$/…` — where a `#/…` fragment would fail silently.
19
+ *
20
+ * The schema `.meta` walker (`visit-resources`) calls the codec at every
21
+ * URL-carrying place — manifest fields, document `$ref`s, CSS `url(…)`
22
+ * tokens, `<image>` hrefs of a drawing — so prose like `US$/day` is never
23
+ * touched, and the effective config only ever carries full URLs.
24
+ */
25
+ /** Whether a URL-carrying value is a store ref (`$/…`). */
26
+ export declare function isStoreRef(value: string): boolean;
27
+ /**
28
+ * Expand a store ref into the canonical URL it aliases:
29
+ * `$/demo/ZL-InrvJgYpz1-k2/logo.png` →
30
+ * `https://demo.expofp.com/immutable/ZL-InrvJgYpz1-k2/logo.png`. Any other
31
+ * value — a full URL, a rooted or relative path, a `data:` URI — passes
32
+ * through untouched. A malformed `$/…` is a generation bug and throws.
33
+ */
34
+ export declare function resolveUrl(ref: string): string;
35
+ /**
36
+ * Compact a canonical store URL into its `$/` alias — the inverse of
37
+ * {@link resolveUrl}; any other URL passes through untouched.
38
+ */
39
+ export declare function compactUrl(url: string): string;
40
+ //# sourceMappingURL=store-ref.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The `$/` notation of the normalized expo form (plan G § 3) — the codec
3
+ * between a store ref and the canonical URL it aliases.
4
+ *
5
+ * Every object of an expo's content-addressed store lives at one URL fixed
6
+ * forever — `https://<expo>.expofp.com/immutable/<hash>/<name>`, the expo's
7
+ * production host whichever contour generated or serves the folder — and
8
+ * the store ref `$/<expo>/<hash>/<name>` is that URL's alias by pure prefix
9
+ * substitution. No document, page or base URL takes part, so a snapshot
10
+ * reads identically from a contour page, an integrator's host, a local
11
+ * `floorplan.js`, an inline copy or a Node tool. The two forms are
12
+ * equivalent and a creator compacts at will: shorter bytes, an explicit
13
+ * "store content" marker, and an alias that survives a domain move.
14
+ *
15
+ * The marker is unambiguous by generation invariant, not by URL illegality:
16
+ * a store only ever returns canonical URLs, and no passthrough value ever
17
+ * starts with `$/`. A ref left unexpanded by mistake breaks loudly — an
18
+ * honest 404 on `<base>/$/…` — where a `#/…` fragment would fail silently.
19
+ *
20
+ * The schema `.meta` walker (`visit-resources`) calls the codec at every
21
+ * URL-carrying place — manifest fields, document `$ref`s, CSS `url(…)`
22
+ * tokens, `<image>` hrefs of a drawing — so prose like `US$/day` is never
23
+ * touched, and the effective config only ever carries full URLs.
24
+ */
25
+ const STORE_REF_PREFIX = '$/';
26
+ /** `$/<expo>/<key>` — the expo is one lowercase DNS label, the key is the rest verbatim. */
27
+ const STORE_REF = /^\$\/([a-z0-9-]+)\/(.+)$/;
28
+ /** The canonical store URL: the expo's production host, `/immutable/<key>`. */
29
+ const CANONICAL_URL = /^https:\/\/([a-z0-9-]+)\.expofp\.com\/immutable\/(.+)$/;
30
+ /** Whether a URL-carrying value is a store ref (`$/…`). */
31
+ export function isStoreRef(value) {
32
+ return value.startsWith(STORE_REF_PREFIX);
33
+ }
34
+ /**
35
+ * Expand a store ref into the canonical URL it aliases:
36
+ * `$/demo/ZL-InrvJgYpz1-k2/logo.png` →
37
+ * `https://demo.expofp.com/immutable/ZL-InrvJgYpz1-k2/logo.png`. Any other
38
+ * value — a full URL, a rooted or relative path, a `data:` URI — passes
39
+ * through untouched. A malformed `$/…` is a generation bug and throws.
40
+ */
41
+ export function resolveUrl(ref) {
42
+ if (!isStoreRef(ref))
43
+ return ref;
44
+ const match = STORE_REF.exec(ref);
45
+ if (!match)
46
+ throw new Error(`resolveUrl: malformed store ref ${JSON.stringify(ref)}`);
47
+ return `https://${match[1]}.expofp.com/immutable/${match[2]}`;
48
+ }
49
+ /**
50
+ * Compact a canonical store URL into its `$/` alias — the inverse of
51
+ * {@link resolveUrl}; any other URL passes through untouched.
52
+ */
53
+ export function compactUrl(url) {
54
+ const match = CANONICAL_URL.exec(url);
55
+ return match ? `${STORE_REF_PREFIX}${match[1]}/${match[2]}` : url;
56
+ }
@@ -21,13 +21,23 @@ import { type Ref } from '@expofp/resolve';
21
21
  import type * as z from 'zod';
22
22
  export interface ResourceHandler {
23
23
  /**
24
- * One asset URL; return the URL to write in its place. `optional` marks an
25
- * `optionalAsset`-tagged leaf — a URL minted without an existence check, so
26
- * the resource may not exist and storing it is best-effort.
24
+ * One asset URL; return the URL to write in its place, or `undefined` to
25
+ * drop the field. Dropping is only meaningful for `optional` leaves — an
26
+ * `optionalAsset` tag marks a URL minted without an existence check, so the
27
+ * resource may not exist and storing it is best-effort (a store that finds
28
+ * it missing drops the field rather than write a dead URL; today's optional
29
+ * assets are all object fields, where a drop deletes the key).
27
30
  */
28
- asset(url: string, optional?: boolean): string | Promise<string>;
31
+ asset(url: string, optional?: boolean): string | undefined | Promise<string | undefined>;
29
32
  /** A ref's resolved — and internally rewritten — document; return the new ref URL. */
30
33
  document?(refUrl: string, document: unknown): string | Promise<string>;
34
+ /**
35
+ * Rewrite a ref's URL in place WITHOUT resolving its document — the
36
+ * expansion hook for store refs (`$/…`) in a normalized manifest, where
37
+ * the document must stay lazy. When present it takes the whole ref
38
+ * branch: `document`/`documentError` do not fire on the same walk.
39
+ */
40
+ ref?(refUrl: string): string | Promise<string>;
31
41
  /**
32
42
  * Tolerate a ref that failed to resolve — the field is dropped. Omitted,
33
43
  * the failure propagates and aborts the walk; throw from here to abort
@@ -82,4 +92,8 @@ export declare function validateOrClone<T>(schema: z.ZodType, value: T): T;
82
92
  export declare function documentRef<R extends Ref<unknown>>(ref: R, schema: z.ZodType, normalize?: (document: object) => void): R;
83
93
  /** Rewrite every schema-tagged resource of `value` through `handler`, in place. */
84
94
  export declare function visitResources(schema: z.ZodType, value: object, handler: ResourceHandler, options?: VisitResourcesOptions): Promise<void>;
95
+ /** Package-internal: `loadConfig`'s pointer-chain loop probes with it too. */
96
+ export declare function isRefShaped(value: unknown): value is {
97
+ $ref: string;
98
+ };
85
99
  //# sourceMappingURL=visit-resources.d.ts.map
@@ -21,6 +21,7 @@ import { getResolved, isResolved, resolve } from '@expofp/resolve';
21
21
  import { deepClone } from '@expofp/utils';
22
22
  import { refSchemaOf, toJsonSchema, typeMatches } from './json-schema.js';
23
23
  import { collectCssUrls, collectSvgImageAssets, inlineCssImports, isAbsoluteUrl, refDirectory, rewriteCssUrls, rewriteSvgImageAssets, toAbsoluteUrl, } from './resource-urls.js';
24
+ import { isStoreRef, resolveUrl } from './store-ref.js';
24
25
  import { timeValidation } from './validate-timing.js';
25
26
  /**
26
27
  * Gates every schema validation (`parse`) of the large expo documents —
@@ -82,7 +83,10 @@ export function documentRef(ref, schema, normalize) {
82
83
  const base = refDirectory(refUrl);
83
84
  const rebase = isAbsoluteUrl(base);
84
85
  await visitResources(schema, document, {
85
- asset: (url) => (rebase ? toAbsoluteUrl(url, base) : url),
86
+ // A store ref (`$/…` — normalized snapshots) expands by the fixed
87
+ // convention, wherever the document came from (store-ref.ts); other
88
+ // paths get the standard web rebase.
89
+ asset: (url) => (isStoreRef(url) ? resolveUrl(url) : rebase ? toAbsoluteUrl(url, base) : url),
86
90
  });
87
91
  return document;
88
92
  };
@@ -105,6 +109,10 @@ async function walk(node, value, assign, handler, options) {
105
109
  // Rooted refs are already local — honored verbatim, like every other
106
110
  // rooted resource URL.
107
111
  if (isRefShaped(value)) {
112
+ if (handler.ref) {
113
+ assign({ $ref: await handler.ref(value.$ref) });
114
+ return;
115
+ }
108
116
  if (!handler.document || value.$ref.startsWith('/'))
109
117
  return;
110
118
  const refUrl = value.$ref;
@@ -155,8 +163,10 @@ async function walk(node, value, assign, handler, options) {
155
163
  for (const url of collectCssUrls(css)) {
156
164
  if (external.has(url))
157
165
  continue; // a kept @import's URL stays remote
158
- // only scheme-qualified CSS urls are fetchable resources (legacy parity)
159
- if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {
166
+ // Only scheme-qualified CSS urls are fetchable resources (legacy
167
+ // parity) — plus store refs (`$/…`), which a normalized snapshot
168
+ // carries there and the load must expand back symmetrically.
169
+ if (isStoreRef(url) || /^[a-z][a-z0-9+.-]*:/i.test(url)) {
160
170
  rewrites.set(url, await handler.asset(url));
161
171
  }
162
172
  }
@@ -189,7 +199,8 @@ async function walk(node, value, assign, handler, options) {
189
199
  }
190
200
  }
191
201
  }
192
- function isRefShaped(value) {
202
+ /** Package-internal: `loadConfig`'s pointer-chain loop probes with it too. */
203
+ export function isRefShaped(value) {
193
204
  return (typeof value === 'object' && value !== null && '$ref' in value && typeof value.$ref === 'string');
194
205
  }
195
206
  /** The root is an object/array container — leaves rewrite inside it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/config",
3
- "version": "3.28.3",
3
+ "version": "3.29.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.28.3",
33
- "@expofp/schema": "3.28.3",
34
- "@expofp/utils": "3.28.3"
32
+ "@expofp/resolve": "3.29.0",
33
+ "@expofp/schema": "3.29.0",
34
+ "@expofp/utils": "3.29.0"
35
35
  }
36
36
  }