@appsemble/lang-sdk 0.39.2 → 0.40.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/BasicValidator.d.ts +2 -0
- package/BasicValidator.js +6 -10
- package/icons.d.ts +64 -0
- package/icons.js +90 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/schemas/AppDefinition.js +10 -0
- package/schemas/BasePageDefinition.js +4 -2
- package/schemas/IconRegistryEntryDefinition.d.ts +2 -0
- package/schemas/IconRegistryEntryDefinition.js +19 -0
- package/schemas/index.d.ts +1 -0
- package/schemas/index.js +1 -0
- package/schemas/json/JSONSchemaString.js +1 -0
- package/serverActions.d.ts +1 -1
- package/types/core.d.ts +26 -2
- package/validation.js +98 -0
package/BasicValidator.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CustomFormat, type Options, type Schema, Validator, type ValidatorResult } from 'jsonschema';
|
|
2
2
|
import { type JsonObject } from 'type-fest';
|
|
3
|
+
import { isValidIconReference } from './icons.js';
|
|
3
4
|
import { type BlockDefinition } from './index.js';
|
|
4
5
|
interface ValidatorFactoryOptions {
|
|
5
6
|
schemas: Record<string, Schema>;
|
|
@@ -15,6 +16,7 @@ export declare class BaseValidatorFactory {
|
|
|
15
16
|
byte: () => boolean;
|
|
16
17
|
binary: () => boolean;
|
|
17
18
|
password: () => boolean;
|
|
19
|
+
icon: typeof isValidIconReference;
|
|
18
20
|
};
|
|
19
21
|
private schemas;
|
|
20
22
|
private customFormats;
|
package/BasicValidator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Validator, } from 'jsonschema';
|
|
2
|
+
import { isValidIconReference } from './icons.js';
|
|
2
3
|
import { schemas as allSchemas } from './index.js';
|
|
3
4
|
import { escapeJsonPointer } from './jsonPointer.js';
|
|
4
5
|
import { has } from './miscellaneous.js';
|
|
@@ -9,7 +10,10 @@ export class BaseValidatorFactory {
|
|
|
9
10
|
}
|
|
10
11
|
build() {
|
|
11
12
|
const validator = new Validator();
|
|
12
|
-
for (const [key, format] of Object.entries(
|
|
13
|
+
for (const [key, format] of Object.entries({
|
|
14
|
+
...BaseValidatorFactory.defaultCustomFormats,
|
|
15
|
+
...this.customFormats,
|
|
16
|
+
})) {
|
|
13
17
|
validator.customFormats[key] = format;
|
|
14
18
|
}
|
|
15
19
|
for (const [key, schema] of Object.entries(this.schemas)) {
|
|
@@ -31,13 +35,13 @@ BaseValidatorFactory.defaultCustomFormats = {
|
|
|
31
35
|
byte: () => true,
|
|
32
36
|
binary: () => true,
|
|
33
37
|
password: () => true,
|
|
38
|
+
icon: isValidIconReference,
|
|
34
39
|
};
|
|
35
40
|
export class BlockExampleValidator {
|
|
36
41
|
constructor() {
|
|
37
42
|
this.validator = new BaseValidatorFactory({
|
|
38
43
|
schemas: allSchemas,
|
|
39
44
|
customFormats: {
|
|
40
|
-
...BaseValidatorFactory.defaultCustomFormats,
|
|
41
45
|
fontawesome: () => true,
|
|
42
46
|
remapper: () => true,
|
|
43
47
|
action: () => true,
|
|
@@ -98,7 +102,6 @@ export class BlockParamInstanceValidator {
|
|
|
98
102
|
this.validator = new BaseValidatorFactory({
|
|
99
103
|
schemas: {},
|
|
100
104
|
customFormats: {
|
|
101
|
-
...BaseValidatorFactory.defaultCustomFormats,
|
|
102
105
|
// TODO: validate more
|
|
103
106
|
fontawesome: () => true,
|
|
104
107
|
// TODO: validate more
|
|
@@ -132,7 +135,6 @@ export class BlockParamSchemaValidator {
|
|
|
132
135
|
this.validator = new BaseValidatorFactory({
|
|
133
136
|
schemas: allSchemas,
|
|
134
137
|
customFormats: {
|
|
135
|
-
...BaseValidatorFactory.defaultCustomFormats,
|
|
136
138
|
fontawesome: () => true,
|
|
137
139
|
remapper: () => true,
|
|
138
140
|
action: () => true,
|
|
@@ -149,9 +151,6 @@ export class AppValidator {
|
|
|
149
151
|
constructor() {
|
|
150
152
|
this.validator = new BaseValidatorFactory({
|
|
151
153
|
schemas: allSchemas,
|
|
152
|
-
customFormats: {
|
|
153
|
-
...BaseValidatorFactory.defaultCustomFormats,
|
|
154
|
-
},
|
|
155
154
|
}).build();
|
|
156
155
|
}
|
|
157
156
|
validateApp(app) {
|
|
@@ -162,9 +161,6 @@ export class RemapperValidator {
|
|
|
162
161
|
constructor() {
|
|
163
162
|
this.validator = new BaseValidatorFactory({
|
|
164
163
|
schemas: allSchemas,
|
|
165
|
-
customFormats: {
|
|
166
|
-
...BaseValidatorFactory.defaultCustomFormats,
|
|
167
|
-
},
|
|
168
164
|
}).build();
|
|
169
165
|
}
|
|
170
166
|
validateRemapper(remapper) {
|
package/icons.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type IconName } from '@fortawesome/fontawesome-common-types';
|
|
2
|
+
import { type IconReference, type IconRegistry } from './types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The prefix marking a reference to a key in the app’s `icons` registry.
|
|
5
|
+
*/
|
|
6
|
+
export declare const customIconPrefix = "icon:";
|
|
7
|
+
/**
|
|
8
|
+
* Check whether a value is a valid icon registry key or icon asset name.
|
|
9
|
+
*
|
|
10
|
+
* @param name The value to check.
|
|
11
|
+
* @returns Whether the value matches the icon name grammar.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isValidIconName(name: unknown): name is string;
|
|
14
|
+
export type ParsedIconReference = {
|
|
15
|
+
type: 'custom';
|
|
16
|
+
key: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'fontawesome';
|
|
19
|
+
name: IconName;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'invalid';
|
|
22
|
+
reason: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse an icon reference without consulting a registry.
|
|
26
|
+
*
|
|
27
|
+
* Bare names are treated as Font Awesome names without checking them against the Font Awesome
|
|
28
|
+
* icon set. Only the `icon:` prefix is recognized; any other colon-separated prefix is rejected.
|
|
29
|
+
*
|
|
30
|
+
* @param reference The value to parse.
|
|
31
|
+
* @returns The parsed reference, or an invalid result with a reason.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseIconReference(reference: unknown): ParsedIconReference;
|
|
34
|
+
/**
|
|
35
|
+
* Check whether a value is a syntactically valid icon reference.
|
|
36
|
+
*
|
|
37
|
+
* This is the check behind the `icon` schema format. It doesn’t consult a registry.
|
|
38
|
+
*
|
|
39
|
+
* @param reference The value to check.
|
|
40
|
+
* @returns Whether the value is a bare icon name or a well-formed `icon:<key>` reference.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isValidIconReference(reference: unknown): reference is IconReference;
|
|
43
|
+
export type ResolvedIcon = {
|
|
44
|
+
type: 'asset';
|
|
45
|
+
key: string;
|
|
46
|
+
asset: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'fontawesome';
|
|
49
|
+
name: IconName;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'invalid';
|
|
52
|
+
reason: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve an icon reference against an icon registry.
|
|
56
|
+
*
|
|
57
|
+
* Registry keys and entries are validated here as well, so the result is safe to render even if
|
|
58
|
+
* the registry wasn’t validated when it was published. Only own properties of the registry count.
|
|
59
|
+
*
|
|
60
|
+
* @param reference The icon reference to resolve.
|
|
61
|
+
* @param registry The app’s icon registry.
|
|
62
|
+
* @returns A Font Awesome icon name, a validated asset name, or an invalid result with a reason.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveIconReference(reference: unknown, registry?: IconRegistry | null): ResolvedIcon;
|
package/icons.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalized } from './constants/index.js';
|
|
2
|
+
import { has } from './miscellaneous.js';
|
|
3
|
+
/**
|
|
4
|
+
* The prefix marking a reference to a key in the app’s `icons` registry.
|
|
5
|
+
*/
|
|
6
|
+
export const customIconPrefix = 'icon:';
|
|
7
|
+
/**
|
|
8
|
+
* Check whether a value is a valid icon registry key or icon asset name.
|
|
9
|
+
*
|
|
10
|
+
* @param name The value to check.
|
|
11
|
+
* @returns Whether the value matches the icon name grammar.
|
|
12
|
+
*/
|
|
13
|
+
export function isValidIconName(name) {
|
|
14
|
+
return typeof name === 'string' && normalized.test(name);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse an icon reference without consulting a registry.
|
|
18
|
+
*
|
|
19
|
+
* Bare names are treated as Font Awesome names without checking them against the Font Awesome
|
|
20
|
+
* icon set. Only the `icon:` prefix is recognized; any other colon-separated prefix is rejected.
|
|
21
|
+
*
|
|
22
|
+
* @param reference The value to parse.
|
|
23
|
+
* @returns The parsed reference, or an invalid result with a reason.
|
|
24
|
+
*/
|
|
25
|
+
export function parseIconReference(reference) {
|
|
26
|
+
if (typeof reference !== 'string') {
|
|
27
|
+
return { type: 'invalid', reason: 'must be a string' };
|
|
28
|
+
}
|
|
29
|
+
if (!reference) {
|
|
30
|
+
return { type: 'invalid', reason: 'must not be empty' };
|
|
31
|
+
}
|
|
32
|
+
if (reference.startsWith(customIconPrefix)) {
|
|
33
|
+
const key = reference.slice(customIconPrefix.length);
|
|
34
|
+
if (!isValidIconName(key)) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'invalid',
|
|
37
|
+
reason: `has the invalid icon key “${key}”; keys must match ${normalized.source}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { type: 'custom', key };
|
|
41
|
+
}
|
|
42
|
+
if (reference.includes(':')) {
|
|
43
|
+
return {
|
|
44
|
+
type: 'invalid',
|
|
45
|
+
reason: `uses the unsupported prefix “${reference.slice(0, reference.indexOf(':') + 1)}”; only “${customIconPrefix}” is supported`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { type: 'fontawesome', name: reference };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check whether a value is a syntactically valid icon reference.
|
|
52
|
+
*
|
|
53
|
+
* This is the check behind the `icon` schema format. It doesn’t consult a registry.
|
|
54
|
+
*
|
|
55
|
+
* @param reference The value to check.
|
|
56
|
+
* @returns Whether the value is a bare icon name or a well-formed `icon:<key>` reference.
|
|
57
|
+
*/
|
|
58
|
+
export function isValidIconReference(reference) {
|
|
59
|
+
return parseIconReference(reference).type !== 'invalid';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an icon reference against an icon registry.
|
|
63
|
+
*
|
|
64
|
+
* Registry keys and entries are validated here as well, so the result is safe to render even if
|
|
65
|
+
* the registry wasn’t validated when it was published. Only own properties of the registry count.
|
|
66
|
+
*
|
|
67
|
+
* @param reference The icon reference to resolve.
|
|
68
|
+
* @param registry The app’s icon registry.
|
|
69
|
+
* @returns A Font Awesome icon name, a validated asset name, or an invalid result with a reason.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveIconReference(reference, registry) {
|
|
72
|
+
const parsed = parseIconReference(reference);
|
|
73
|
+
if (parsed.type !== 'custom') {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
const { key } = parsed;
|
|
77
|
+
if (typeof registry !== 'object' || !registry || !has(registry, key)) {
|
|
78
|
+
return { type: 'invalid', reason: `references the unknown icon key “${key}”` };
|
|
79
|
+
}
|
|
80
|
+
const entry = registry[key];
|
|
81
|
+
const asset = typeof entry === 'object' && entry ? entry.asset : null;
|
|
82
|
+
if (!isValidIconName(asset)) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'invalid',
|
|
85
|
+
reason: `references the icon key “${key}”, which has an invalid asset name`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { type: 'asset', key, asset };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=icons.js.map
|
package/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './blockUtils.js';
|
|
|
7
7
|
export * from './validation.js';
|
|
8
8
|
export * from './findPageByName.js';
|
|
9
9
|
export * from './gridUtils.js';
|
|
10
|
+
export * from './icons.js';
|
|
10
11
|
export * from './pageUtils.js';
|
|
11
12
|
export * from './iterApp.js';
|
|
12
13
|
export * from './serverActions.js';
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './blockUtils.js';
|
|
|
7
7
|
export * from './validation.js';
|
|
8
8
|
export * from './findPageByName.js';
|
|
9
9
|
export * from './gridUtils.js';
|
|
10
|
+
export * from './icons.js';
|
|
10
11
|
export * from './pageUtils.js';
|
|
11
12
|
export * from './iterApp.js';
|
|
12
13
|
export * from './serverActions.js';
|
package/package.json
CHANGED
package/schemas/AppDefinition.js
CHANGED
|
@@ -91,6 +91,16 @@ The most basic resource has a \`schema\` property and defines the minimal securi
|
|
|
91
91
|
theme: {
|
|
92
92
|
$ref: '#/components/schemas/Theme',
|
|
93
93
|
},
|
|
94
|
+
icons: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
description: `Custom icons that can be referenced as \`icon:<key>\` wherever an icon is accepted.
|
|
97
|
+
|
|
98
|
+
Each key names an app-level SVG asset by its name. Keys and asset names must consist of lower case
|
|
99
|
+
letters, digits, and single hyphens. Keys are case-sensitive, and \`icon:<key>\` never falls back
|
|
100
|
+
to a Font Awesome icon.
|
|
101
|
+
`,
|
|
102
|
+
additionalProperties: { $ref: '#/components/schemas/IconRegistryEntryDefinition' },
|
|
103
|
+
},
|
|
94
104
|
contentSecurityPolicy: {
|
|
95
105
|
type: 'object',
|
|
96
106
|
description: `Additional CSP source expressions for the published app page.
|
|
@@ -48,9 +48,11 @@ navigation. Set to \`hidden\` to display no navigational menus at all.
|
|
|
48
48
|
},
|
|
49
49
|
icon: {
|
|
50
50
|
type: 'string',
|
|
51
|
-
|
|
51
|
+
format: 'icon',
|
|
52
|
+
description: `An optional icon from the Font Awesome icon set, or a custom icon reference.
|
|
52
53
|
|
|
53
|
-
This will be displayed in the navigation menu.
|
|
54
|
+
This will be displayed in the navigation menu. Use \`icon:<key>\` to reference a key from the app’s
|
|
55
|
+
\`icons\` registry.
|
|
54
56
|
`,
|
|
55
57
|
},
|
|
56
58
|
parent: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { normalized } from '../constants/index.js';
|
|
2
|
+
export const IconRegistryEntryDefinition = {
|
|
3
|
+
type: 'object',
|
|
4
|
+
description: 'A custom icon in the app’s `icons` registry.',
|
|
5
|
+
required: ['asset'],
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
properties: {
|
|
8
|
+
asset: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
pattern: normalized.source,
|
|
11
|
+
description: `The name of the app-level asset holding the icon’s SVG artwork.
|
|
12
|
+
|
|
13
|
+
This is an asset name, not a URL or asset ID. The asset doesn’t need to exist when the app is
|
|
14
|
+
published, but the icon only renders once an SVG asset with this name has been uploaded.
|
|
15
|
+
`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=IconRegistryEntryDefinition.js.map
|
package/schemas/index.d.ts
CHANGED
package/schemas/index.js
CHANGED
|
@@ -132,4 +132,5 @@ export * from './NavbarDeviceGridLayoutDefinition.js';
|
|
|
132
132
|
export * from './NavbarLayoutDefinition.js';
|
|
133
133
|
export * from './PageLayoutDefinition.js';
|
|
134
134
|
export * from './PageParentDefinition.js';
|
|
135
|
+
export * from './IconRegistryEntryDefinition.js';
|
|
135
136
|
//# sourceMappingURL=index.js.map
|
package/serverActions.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const serverActions: Set<"
|
|
1
|
+
export declare const serverActions: Set<"email" | "log" | "static" | "noop" | "throw" | "resource.get" | "resource.patch" | "resource.query" | "resource.update" | "app.member.query" | "condition" | "each" | "notify" | "request" | "resource.create" | "resource.delete.all" | "resource.delete.bulk" | "resource.delete">;
|
|
2
2
|
export type ServerActionName = typeof serverActions extends Set<infer T> ? T : never;
|
package/types/core.d.ts
CHANGED
|
@@ -12,6 +12,23 @@ import { type Theme } from './theme.js';
|
|
|
12
12
|
import { type GridBreakpointsDefinition, type NavbarLayoutDefinition, type PageLayoutDefinition } from './gridLayout.js';
|
|
13
13
|
export type SettingName = 'email' | 'languages' | 'name' | 'password' | 'phoneNumber' | 'picture';
|
|
14
14
|
export type AppContentSecurityPolicy = Record<string, string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* A reference to an icon.
|
|
17
|
+
*
|
|
18
|
+
* A bare name refers to a Font Awesome icon. A name prefixed with `icon:` refers to a key in the
|
|
19
|
+
* app’s `icons` registry, which maps it to an uploaded SVG asset.
|
|
20
|
+
*/
|
|
21
|
+
export type IconReference = IconName | `icon:${string}`;
|
|
22
|
+
export interface IconRegistryEntry {
|
|
23
|
+
/**
|
|
24
|
+
* The name of the app-level asset holding the icon’s SVG artwork.
|
|
25
|
+
*/
|
|
26
|
+
asset: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A mapping of semantic icon keys to the assets holding their artwork.
|
|
30
|
+
*/
|
|
31
|
+
export type IconRegistry = Record<string, IconRegistryEntry>;
|
|
15
32
|
export interface AppDefinition {
|
|
16
33
|
/**
|
|
17
34
|
* The name of the app.
|
|
@@ -171,6 +188,12 @@ export interface AppDefinition {
|
|
|
171
188
|
* The global theme for the app.
|
|
172
189
|
*/
|
|
173
190
|
theme?: Partial<Theme>;
|
|
191
|
+
/**
|
|
192
|
+
* Custom icons that may be referenced as `icon:<key>` from pages, blocks, and login buttons.
|
|
193
|
+
*
|
|
194
|
+
* Each key maps to an app-level SVG asset by name.
|
|
195
|
+
*/
|
|
196
|
+
icons?: IconRegistry;
|
|
174
197
|
/**
|
|
175
198
|
* Extra content security policy source expressions for the published app page.
|
|
176
199
|
*
|
|
@@ -257,11 +280,12 @@ export interface BasePageDefinition {
|
|
|
257
280
|
*/
|
|
258
281
|
roles?: ViewRole[];
|
|
259
282
|
/**
|
|
260
|
-
* An optional icon from the
|
|
283
|
+
* An optional icon from the Font Awesome icon set, or an `icon:<key>` reference to the app’s
|
|
284
|
+
* `icons` registry.
|
|
261
285
|
*
|
|
262
286
|
* This will be displayed in the navigation menu.
|
|
263
287
|
*/
|
|
264
|
-
icon?:
|
|
288
|
+
icon?: IconReference;
|
|
265
289
|
/**
|
|
266
290
|
* Page parameters can be used for linking to a page that should display a single resource.
|
|
267
291
|
*/
|
package/validation.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
|
|
|
7
7
|
import { partialNormalized } from './constants/index.js';
|
|
8
8
|
import { findPageByName } from './findPageByName.js';
|
|
9
9
|
import { breadcrumbsGridArea, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, } from './gridUtils.js';
|
|
10
|
+
import { isValidIconName, parseIconReference } from './icons.js';
|
|
10
11
|
import { iterAction, iterApp } from './iterApp.js';
|
|
11
12
|
import { has } from './miscellaneous.js';
|
|
12
13
|
import { normalize } from './normalize.js';
|
|
@@ -1238,6 +1239,101 @@ function validateResourceReferences(definition, report) {
|
|
|
1238
1239
|
}
|
|
1239
1240
|
}
|
|
1240
1241
|
}
|
|
1242
|
+
function validateIcons({ icons }, report) {
|
|
1243
|
+
if (!icons) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
for (const key of Object.keys(icons)) {
|
|
1247
|
+
if (!isValidIconName(key)) {
|
|
1248
|
+
report(key, 'is not a valid icon key; use lower case letters, digits, and single hyphens', [
|
|
1249
|
+
'icons',
|
|
1250
|
+
key,
|
|
1251
|
+
]);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Collect the string values of an instance which its schema marks with the given format.
|
|
1257
|
+
*
|
|
1258
|
+
* Schema references, nested objects, arrays, and composition keywords are followed.
|
|
1259
|
+
*
|
|
1260
|
+
* @param instance The value to inspect.
|
|
1261
|
+
* @param schema The schema describing the value.
|
|
1262
|
+
* @param root The root schema, used to resolve local `$ref`s to its definitions.
|
|
1263
|
+
* @param format The format to look for.
|
|
1264
|
+
* @param path The path of the instance, used for reporting.
|
|
1265
|
+
* @param found The collected values, keyed by their JSON path.
|
|
1266
|
+
*/
|
|
1267
|
+
function collectFormattedStrings(instance, schema, root, format, path, found) {
|
|
1268
|
+
if (!schema || typeof schema !== 'object') {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (schema.$ref) {
|
|
1272
|
+
const match = /^#\/definitions\/(.+)$/.exec(schema.$ref);
|
|
1273
|
+
if (match) {
|
|
1274
|
+
collectFormattedStrings(instance, root.definitions?.[decodeURIComponent(match[1])], root, format, path, found);
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof instance === 'string') {
|
|
1279
|
+
if (schema.format === format) {
|
|
1280
|
+
found.set(JSON.stringify(path), [path, instance]);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
else if (Array.isArray(instance)) {
|
|
1284
|
+
for (const [index, item] of instance.entries()) {
|
|
1285
|
+
const itemSchema = Array.isArray(schema.items)
|
|
1286
|
+
? (schema.items[index] ??
|
|
1287
|
+
(typeof schema.additionalItems === 'object' ? schema.additionalItems : undefined))
|
|
1288
|
+
: schema.items;
|
|
1289
|
+
collectFormattedStrings(item, itemSchema, root, format, [...path, index], found);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
else if (typeof instance === 'object' && instance) {
|
|
1293
|
+
for (const [key, value] of Object.entries(instance)) {
|
|
1294
|
+
const propertySchema = has(schema.properties, key)
|
|
1295
|
+
? schema.properties[key]
|
|
1296
|
+
: (Object.entries(schema.patternProperties ?? {}).find(([pattern]) => new RegExp(pattern).test(key))?.[1] ??
|
|
1297
|
+
(typeof schema.additionalProperties === 'object'
|
|
1298
|
+
? schema.additionalProperties
|
|
1299
|
+
: undefined));
|
|
1300
|
+
collectFormattedStrings(value, propertySchema, root, format, [...path, key], found);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
for (const branches of [schema.anyOf, schema.oneOf, schema.allOf]) {
|
|
1304
|
+
for (const branch of branches ?? []) {
|
|
1305
|
+
collectFormattedStrings(instance, branch, root, format, path, found);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function validateIconReferences(definition, blockVersions, report) {
|
|
1310
|
+
const check = (reference, path) => {
|
|
1311
|
+
// Syntax errors are reported by the schema validation and invalid registry entries by
|
|
1312
|
+
// validateIcons; only check registry membership here.
|
|
1313
|
+
const parsed = parseIconReference(reference);
|
|
1314
|
+
if (parsed.type === 'custom' && !has(definition.icons, parsed.key)) {
|
|
1315
|
+
report(reference, `references the unknown icon key “${parsed.key}”`, path);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
iterApp(definition, {
|
|
1319
|
+
onPage(page, path) {
|
|
1320
|
+
if (typeof page.icon === 'string') {
|
|
1321
|
+
check(page.icon, [...path, 'icon']);
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
onBlock(block, path) {
|
|
1325
|
+
const version = blockVersions.get(normalizeBlockName(block.type))?.get(block.version);
|
|
1326
|
+
if (!version?.parameters || !block.parameters) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const found = new Map();
|
|
1330
|
+
collectFormattedStrings(block.parameters, version.parameters, version.parameters, 'icon', [...path, 'parameters'], found);
|
|
1331
|
+
for (const [iconPath, reference] of found.values()) {
|
|
1332
|
+
check(reference, iconPath);
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1241
1337
|
function validateLanguage({ defaultLanguage }, report) {
|
|
1242
1338
|
if (defaultLanguage != null && !languageTags.check(defaultLanguage)) {
|
|
1243
1339
|
report(defaultLanguage, 'is not a valid language code', ['defaultLanguage']);
|
|
@@ -1667,6 +1763,8 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
|
|
|
1667
1763
|
validateCronJobs(clonedDefinition, report);
|
|
1668
1764
|
validateDefaultPage(clonedDefinition, report);
|
|
1669
1765
|
validateHooks(clonedDefinition, report);
|
|
1766
|
+
validateIcons(clonedDefinition, report);
|
|
1767
|
+
validateIconReferences(clonedDefinition, blockVersionMap, report);
|
|
1670
1768
|
validateLanguage(clonedDefinition, report);
|
|
1671
1769
|
validateResourceReferences(clonedDefinition, report);
|
|
1672
1770
|
validateMembersSchema(clonedDefinition, report);
|