@expofp/config 3.11.10

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.
Files changed (40) hide show
  1. package/README.md +35 -0
  2. package/dist/index.d.ts +11 -0
  3. package/dist/index.js +8 -0
  4. package/dist/lib/apply-intents.d.ts +10 -0
  5. package/dist/lib/apply-intents.js +24 -0
  6. package/dist/lib/config-store.d.ts +6 -0
  7. package/dist/lib/config-store.js +12 -0
  8. package/dist/lib/debug-settings.d.ts +16 -0
  9. package/dist/lib/debug-settings.js +44 -0
  10. package/dist/lib/json-schema.d.ts +67 -0
  11. package/dist/lib/json-schema.js +136 -0
  12. package/dist/lib/legacy-url.d.ts +167 -0
  13. package/dist/lib/legacy-url.js +322 -0
  14. package/dist/lib/load-config.d.ts +26 -0
  15. package/dist/lib/load-config.js +286 -0
  16. package/dist/lib/local-storage-codec.d.ts +44 -0
  17. package/dist/lib/local-storage-codec.js +76 -0
  18. package/dist/lib/normalize-fp-svg.d.ts +26 -0
  19. package/dist/lib/normalize-fp-svg.js +36 -0
  20. package/dist/lib/normalize-legacy-data.d.ts +13 -0
  21. package/dist/lib/normalize-legacy-data.js +90 -0
  22. package/dist/lib/rebooking.d.ts +19 -0
  23. package/dist/lib/rebooking.js +48 -0
  24. package/dist/lib/resource-urls.d.ts +52 -0
  25. package/dist/lib/resource-urls.js +196 -0
  26. package/dist/lib/serialize-config-resources.d.ts +22 -0
  27. package/dist/lib/serialize-config-resources.js +20 -0
  28. package/dist/lib/strip-defaults.d.ts +27 -0
  29. package/dist/lib/strip-defaults.js +55 -0
  30. package/dist/lib/url-codec.d.ts +46 -0
  31. package/dist/lib/url-codec.js +165 -0
  32. package/dist/lib/url-intents.d.ts +30 -0
  33. package/dist/lib/url-intents.js +58 -0
  34. package/dist/lib/validate-flag.d.ts +10 -0
  35. package/dist/lib/validate-flag.js +36 -0
  36. package/dist/lib/validate-timing.d.ts +3 -0
  37. package/dist/lib/validate-timing.js +52 -0
  38. package/dist/lib/visit-resources.d.ts +85 -0
  39. package/dist/lib/visit-resources.js +198 -0
  40. package/package.json +36 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * visit-resources — the one schema-guided resource walk, used for both jobs:
3
+ *
4
+ * - **Absolutization** (`documentRef`): every ref the config mints carries a
5
+ * `process` hook = "validate with my schema, then walk me with a handler
6
+ * that rebases relative paths onto the ref's own directory". Resolution
7
+ * therefore always settles documents whose asset URLs are absolute.
8
+ * - **Serialization** (`serializeConfigResources`): the same walk over the
9
+ * whole config with a storage handler — and a `$ref` is just another kind
10
+ * of asset, one whose handling resolves the document and recurses into it.
11
+ *
12
+ * The walk is value-driven and schema-guided: meta tags say what a string
13
+ * leaf holds (`asset`, `svgAsset`, `cssAsset`), and a
14
+ * `{ $ref }` object is an external document (its node's `refSchema` meta
15
+ * points at the document's schema).
16
+ * Which behaviors fire falls out of which hooks the handler provides — a
17
+ * handler without `document` leaves refs alone. `value` is mutated in place
18
+ * and must be an object or array.
19
+ */
20
+ import { getResolved, isResolved, resolve } from '@expofp/resolve';
21
+ import { deepClone } from '@expofp/utils';
22
+ import { refSchemaOf, toJsonSchema, typeMatches } from './json-schema.js';
23
+ import { collectCssUrls, collectSvgImageHrefs, inlineCssImports, isAbsoluteUrl, refDirectory, rewriteCssUrls, rewriteSvgImageHrefs, toAbsoluteUrl, } from './resource-urls.js';
24
+ import { timeValidation } from './validate-timing.js';
25
+ /**
26
+ * Gates every schema validation (`parse`) of the large expo documents —
27
+ * manifest, data.js, drawing/wayfinding payloads. The schemas are loose and
28
+ * transform-free, so a validating parse is also a faithful copy — which is
29
+ * exactly what {@link validateOrClone} exploits.
30
+ *
31
+ * OFF by default (parsing the large documents costs time); `loadConfig` flips it
32
+ * per page load via {@link setValidateManifest} from the `validateSchema` opt-in
33
+ * (`?validateSchema=1` / `efp-validateSchema`). Module-level, tracking the
34
+ * page-global URL/localStorage flag, so concurrent loads on a page agree.
35
+ */
36
+ let validateManifest = false;
37
+ /** Set by `loadConfig` from the `validateSchema` flag; gates the parses below. */
38
+ export function setValidateManifest(on) {
39
+ validateManifest = on;
40
+ }
41
+ /**
42
+ * An own, mutable copy of a document — validated when the flag is on.
43
+ *
44
+ * Every consumer of a resolved document needs its own copy anyway (`resolve`
45
+ * deep-freezes what it settles, and downstream code mutates in place), and a
46
+ * Zod parse of these loose, transform-free schemas already IS a fresh copy.
47
+ * So validation rides the copy that had to be made regardless: parse when
48
+ * validating, `deepClone` when not — one copy either way, never two.
49
+ */
50
+ export function validateOrClone(schema, value) {
51
+ if (validateManifest)
52
+ return timeValidation(() => schema.parse(value));
53
+ return deepClone(value);
54
+ }
55
+ /**
56
+ * Decorate a ref with the one processing rule for referenced documents: take
57
+ * an own copy — validated against `schema` when the flag is on (see
58
+ * `validateOrClone`) — then make every schema-tagged resource path absolute
59
+ * against the ref's own directory — the standard web rule for a live load.
60
+ * An optional `normalize` hook runs on the copy in between: it folds legacy
61
+ * shape quirks onto the canonical keys, so the schema-guided walk (and every
62
+ * consumer) sees the canonical shape (e.g. `normalizeFpSvgLayerAliases`).
63
+ *
64
+ * An offline copy instead references its saved documents by *relative*
65
+ * (page-relative, host-less) paths, and their assets already carry final
66
+ * serialized paths. Rebasing one of those onto the doc's own relative
67
+ * directory would double the path (`…/<doc-dir>/<doc-dir>/asset` → 404), so
68
+ * absolutization is skipped whenever the ref's directory is not itself
69
+ * absolute — the runtime then resolves the page-relative asset against the
70
+ * page, wherever the copy is mounted. (A rooted `/…` offline ref stays
71
+ * absolute, and its already-rooted assets pass through unchanged either way.)
72
+ *
73
+ * The rewrite runs on an own copy, so it never touches the fetch-cached raw
74
+ * document.
75
+ */
76
+ export function documentRef(ref, schema, normalize) {
77
+ const process = async (raw, refUrl) => {
78
+ const document = validateOrClone(schema, raw); // referenced documents are objects
79
+ normalize?.(document);
80
+ // A relative ref directory means an offline copy: the document's assets are
81
+ // already final serialized paths, so rebasing them here would double the path.
82
+ const base = refDirectory(refUrl);
83
+ const rebase = isAbsoluteUrl(base);
84
+ await visitResources(schema, document, {
85
+ asset: (url) => (rebase ? toAbsoluteUrl(url, base) : url),
86
+ });
87
+ return document;
88
+ };
89
+ return { ...ref, process };
90
+ }
91
+ /** Rewrite every schema-tagged resource of `value` through `handler`, in place. */
92
+ export async function visitResources(schema, value, handler, options) {
93
+ await walk(toJsonSchema(schema), value, unassignableRoot, handler, options);
94
+ }
95
+ async function walk(node, value, assign, handler, options) {
96
+ if (value === null || value === undefined)
97
+ return;
98
+ // follow unions down to the branch that fits the value
99
+ const branches = node?.anyOf ?? node?.oneOf;
100
+ if (branches) {
101
+ const branch = branches.find((candidate) => typeMatches(candidate, value));
102
+ return walk(branch, value, assign, handler, options);
103
+ }
104
+ // "$ref is an asset": an external document, when the handler wants them.
105
+ // Rooted refs are already local — honored verbatim, like every other
106
+ // rooted resource URL.
107
+ if (isRefShaped(value)) {
108
+ if (!handler.document || value.$ref.startsWith('/'))
109
+ return;
110
+ const refUrl = value.$ref;
111
+ try {
112
+ const documentSchema = node ? refSchemaOf(node) : undefined;
113
+ // Reuse the warm memo (eagerly-loaded refs settled processed documents);
114
+ // a cold ref is re-minted with its document schema — the walked value is
115
+ // a JSON copy, so the original decoration did not survive into it.
116
+ const resolved = isResolved(value)
117
+ ? getResolved(value)
118
+ : await resolve(documentSchema ? documentRef({ $ref: refUrl }, documentSchema) : { $ref: refUrl }, { signal: options?.signal });
119
+ const copy = { document: deepClone(resolved) };
120
+ if (documentSchema) {
121
+ await walk(toJsonSchema(documentSchema), copy.document, (v) => {
122
+ copy.document = v;
123
+ }, handler, options);
124
+ }
125
+ assign({ $ref: await handler.document(refUrl, copy.document) });
126
+ }
127
+ catch (error) {
128
+ if (!handler.documentError)
129
+ throw error;
130
+ handler.documentError(refUrl, error);
131
+ assign(undefined); // the field is dropped
132
+ }
133
+ return;
134
+ }
135
+ if (typeof value === 'string') {
136
+ if (node?.asset && value) {
137
+ assign(await handler.asset(value, node.optionalAsset));
138
+ }
139
+ else if (node?.svgAsset) {
140
+ const rewrites = new Map();
141
+ for (const href of collectSvgImageHrefs(value)) {
142
+ rewrites.set(href, await handler.asset(href));
143
+ }
144
+ assign(rewriteSvgImageHrefs(value, (href) => rewrites.get(href) ?? href));
145
+ }
146
+ else if (node?.cssAsset) {
147
+ // a handler that can fetch stylesheets gets @imports spliced in first,
148
+ // so the pass below localizes the assets they pull in (fonts etc.)
149
+ let css = value;
150
+ let external = new Set();
151
+ if (handler.stylesheetText) {
152
+ ({ css, external } = await inlineCssImports(css, handler.stylesheetText.bind(handler)));
153
+ }
154
+ const rewrites = new Map();
155
+ for (const url of collectCssUrls(css)) {
156
+ if (external.has(url))
157
+ 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)) {
160
+ rewrites.set(url, await handler.asset(url));
161
+ }
162
+ }
163
+ assign(rewriteCssUrls(css, (url) => rewrites.get(url) ?? url));
164
+ }
165
+ return;
166
+ }
167
+ if (Array.isArray(value)) {
168
+ for (let index = 0; index < value.length; index++) {
169
+ await walk(node?.items, value[index], (v) => {
170
+ value[index] = v;
171
+ }, handler, options);
172
+ }
173
+ return;
174
+ }
175
+ if (typeof value === 'object') {
176
+ const container = value;
177
+ // `z.record(...)` keys are dynamic, so the value schema rides on
178
+ // `additionalProperties`, not `properties` (e.g. `fpSvgLayerJsRefs`, whose
179
+ // per-layer `$ref` must resolve + localize like any other). Fall back to it
180
+ // so record values are walked, not skipped as untyped and left verbatim.
181
+ const additional = typeof node?.additionalProperties === 'object' ? node.additionalProperties : undefined;
182
+ for (const key of Object.keys(container)) {
183
+ await walk(node?.properties?.[key] ?? additional, container[key], (v) => {
184
+ if (v === undefined)
185
+ delete container[key];
186
+ else
187
+ container[key] = v;
188
+ }, handler, options);
189
+ }
190
+ }
191
+ }
192
+ function isRefShaped(value) {
193
+ return (typeof value === 'object' && value !== null && '$ref' in value && typeof value.$ref === 'string');
194
+ }
195
+ /** The root is an object/array container — leaves rewrite inside it. */
196
+ function unassignableRoot() {
197
+ throw new Error('visitResources: the top-level value must be an object or array');
198
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@expofp/config",
3
+ "version": "3.11.10",
4
+ "type": "module",
5
+ "description": "ExpoFP SDK internal: config layer and schemas",
6
+ "homepage": "https://developer.expofp.com/",
7
+ "license": "MIT",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ "./package.json": "./package.json",
16
+ ".": {
17
+ "@expofp/source": "./src/index.ts",
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "!**/*.tsbuildinfo",
26
+ "!**/*.map"
27
+ ],
28
+ "dependencies": {
29
+ "debug": "^4.4.3",
30
+ "tslib": "^2.3.0",
31
+ "zod": "4.3.5",
32
+ "@expofp/resolve": "3.11.12",
33
+ "@expofp/schema": "3.11.12",
34
+ "@expofp/utils": "3.11.12"
35
+ }
36
+ }