@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.
- package/README.md +35 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/lib/apply-intents.d.ts +10 -0
- package/dist/lib/apply-intents.js +24 -0
- package/dist/lib/config-store.d.ts +6 -0
- package/dist/lib/config-store.js +12 -0
- package/dist/lib/debug-settings.d.ts +16 -0
- package/dist/lib/debug-settings.js +44 -0
- package/dist/lib/json-schema.d.ts +67 -0
- package/dist/lib/json-schema.js +136 -0
- package/dist/lib/legacy-url.d.ts +167 -0
- package/dist/lib/legacy-url.js +322 -0
- package/dist/lib/load-config.d.ts +26 -0
- package/dist/lib/load-config.js +286 -0
- package/dist/lib/local-storage-codec.d.ts +44 -0
- package/dist/lib/local-storage-codec.js +76 -0
- package/dist/lib/normalize-fp-svg.d.ts +26 -0
- package/dist/lib/normalize-fp-svg.js +36 -0
- package/dist/lib/normalize-legacy-data.d.ts +13 -0
- package/dist/lib/normalize-legacy-data.js +90 -0
- package/dist/lib/rebooking.d.ts +19 -0
- package/dist/lib/rebooking.js +48 -0
- package/dist/lib/resource-urls.d.ts +52 -0
- package/dist/lib/resource-urls.js +196 -0
- package/dist/lib/serialize-config-resources.d.ts +22 -0
- package/dist/lib/serialize-config-resources.js +20 -0
- package/dist/lib/strip-defaults.d.ts +27 -0
- package/dist/lib/strip-defaults.js +55 -0
- package/dist/lib/url-codec.d.ts +46 -0
- package/dist/lib/url-codec.js +165 -0
- package/dist/lib/url-intents.d.ts +30 -0
- package/dist/lib/url-intents.js +58 -0
- package/dist/lib/validate-flag.d.ts +10 -0
- package/dist/lib/validate-flag.js +36 -0
- package/dist/lib/validate-timing.d.ts +3 -0
- package/dist/lib/validate-timing.js +52 -0
- package/dist/lib/visit-resources.d.ts +85 -0
- package/dist/lib/visit-resources.js +198 -0
- 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
|
+
}
|