@appsemble/utils 0.40.3-test.4 → 0.40.4

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
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.40.3-test.4/config/assets/logo.svg) Appsemble Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.40.4/config/assets/logo.svg) Appsemble Utilities
2
2
 
3
3
  > Internal utility functions used across multiple Appsemble projects.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/utils)](https://www.npmjs.com/package/@appsemble/utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.40.3-test.4/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.40.3-test.4)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.40.4/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.40.4)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -26,5 +26,5 @@ not guaranteed.
26
26
 
27
27
  ## License
28
28
 
29
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.40.3-test.4/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.40.4/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
package/appMessages.js CHANGED
@@ -1,4 +1,4 @@
1
- import { iterApp, } from '@appsemble/lang-sdk';
1
+ import { getRouteMessageId, iterApp, translatableRoutes, } from '@appsemble/lang-sdk';
2
2
  import { normalize } from './normalize.js';
3
3
  /**
4
4
  * Recursively find `string.format` remapper message IDs.
@@ -43,6 +43,7 @@ export function extractAppMessages(app, onBlock) {
43
43
  [`app.roles.${role}`, role],
44
44
  [`app.roles.${role}.description`, roleDefinition.description],
45
45
  ])),
46
+ ...Object.fromEntries(translatableRoutes.map((name) => [getRouteMessageId(name), name])),
46
47
  },
47
48
  messageIds: {},
48
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/utils",
3
- "version": "0.40.3-test.4",
3
+ "version": "0.40.4",
4
4
  "description": "Utility functions used in Appsemble internally",
5
5
  "keywords": [
6
6
  "app",
@@ -38,8 +38,8 @@
38
38
  "test": "vitest"
39
39
  },
40
40
  "dependencies": {
41
- "@appsemble/lang-sdk": "0.40.3-test.4",
42
- "@appsemble/types": "0.40.3-test.4",
41
+ "@appsemble/lang-sdk": "0.40.4",
42
+ "@appsemble/types": "0.40.4",
43
43
  "@fortawesome/fontawesome-common-types": "^6.0.0",
44
44
  "axios": "^1.0.0",
45
45
  "langmap": "^0.0.16",
@@ -4,3 +4,17 @@ export declare class AppMessageValidationError extends Error {
4
4
  constructor(message: string);
5
5
  }
6
6
  export declare function validateMessages(messages: AppsembleMessages, app: AppDefinition): void;
7
+ /**
8
+ * Validate the URL segments an app serves in a language.
9
+ *
10
+ * The app messages of the language are the extracted defaults of the definition, overlaid by each
11
+ * layer in order. An empty string in a layer keeps the value below it. A page's segment is its
12
+ * normalized translated name, or the normalized page name when the translated name has no Latin
13
+ * letters or digits. A route's segment is its `routes.<Name>` message as written.
14
+ *
15
+ * @param app The app definition the messages belong to.
16
+ * @param language The language the layers are served as, used in the error message.
17
+ * @param layers The stored messages that make up the language, lowest layer first: the base
18
+ * language row, then the regional row.
19
+ */
20
+ export declare function validateMessageSlugs(app: AppDefinition, language: string, layers: Partial<AppsembleMessages>[]): void;
@@ -1,6 +1,8 @@
1
- import { normalizeBlockName } from '@appsemble/lang-sdk';
1
+ import { getPageMessageId, getRouteMessageId, normalizeBlockName, translatableRoutes, } from '@appsemble/lang-sdk';
2
2
  import { extractAppMessages } from './appMessages.js';
3
3
  import { has } from './miscellaneous.js';
4
+ import { normalize } from './normalize.js';
5
+ const routeSegmentPattern = /^[A-Z][\dA-Za-z-]*$/;
4
6
  export class AppMessageValidationError extends Error {
5
7
  constructor(message) {
6
8
  super(message);
@@ -70,4 +72,64 @@ This block version is not used in the app`);
70
72
  }
71
73
  }
72
74
  }
75
+ function collectPages(pages) {
76
+ return pages.flatMap((page) => page.type === 'container' ? [page, ...collectPages(page.pages)] : [page]);
77
+ }
78
+ /**
79
+ * Validate the URL segments an app serves in a language.
80
+ *
81
+ * The app messages of the language are the extracted defaults of the definition, overlaid by each
82
+ * layer in order. An empty string in a layer keeps the value below it. A page's segment is its
83
+ * normalized translated name, or the normalized page name when the translated name has no Latin
84
+ * letters or digits. A route's segment is its `routes.<Name>` message as written.
85
+ *
86
+ * @param app The app definition the messages belong to.
87
+ * @param language The language the layers are served as, used in the error message.
88
+ * @param layers The stored messages that make up the language, lowest layer first: the base
89
+ * language row, then the regional row.
90
+ */
91
+ export function validateMessageSlugs(app, language, layers) {
92
+ const appMessages = { ...extractAppMessages(app).app };
93
+ for (const layer of layers) {
94
+ for (const [key, value] of Object.entries(layer.app ?? {})) {
95
+ if (typeof value === 'string' && value) {
96
+ appMessages[key] = value;
97
+ }
98
+ }
99
+ }
100
+ const pages = collectPages(app.pages ?? []);
101
+ const canonicalSegments = new Map(pages.map((page) => [normalize(page.name), page]));
102
+ const translatedSegments = new Map();
103
+ for (const page of pages) {
104
+ const canonical = normalize(page.name);
105
+ // A translated name without Latin letters or digits keeps the canonical segment.
106
+ const segment = normalize(appMessages[getPageMessageId(page.name)]) || canonical;
107
+ if (segment !== canonical) {
108
+ if (segment === 'debug') {
109
+ throw new AppMessageValidationError(`Language “${language}”: the translated name of page “${page.name}” has the reserved URL segment “debug”`);
110
+ }
111
+ const other = translatedSegments.get(segment) ?? canonicalSegments.get(segment);
112
+ if (other) {
113
+ throw new AppMessageValidationError(`Language “${language}”: pages “${other.name}” and “${page.name}” share the URL segment “${segment}”`);
114
+ }
115
+ }
116
+ translatedSegments.set(segment, page);
117
+ }
118
+ const routeSegments = translatableRoutes.map((name) => [name, appMessages[getRouteMessageId(name)]]);
119
+ for (const [name, segment] of routeSegments) {
120
+ if (segment === name) {
121
+ continue;
122
+ }
123
+ if (!routeSegmentPattern.test(segment)) {
124
+ throw new AppMessageValidationError(`Language “${language}”: the URL segment “${segment}” of route “${name}” must start with an uppercase letter and may only contain letters, digits and hyphens`);
125
+ }
126
+ if (segment === 'Callback' || segment.toLowerCase() === 'debug') {
127
+ throw new AppMessageValidationError(`Language “${language}”: the URL segment “${segment}” of route “${name}” is reserved`);
128
+ }
129
+ const other = routeSegments.find(([otherName, otherSegment]) => otherName !== name && (segment === otherName || segment === otherSegment));
130
+ if (other) {
131
+ throw new AppMessageValidationError(`Language “${language}”: routes “${name}” and “${other[0]}” share the URL segment “${segment}”`);
132
+ }
133
+ }
134
+ }
73
135
  //# sourceMappingURL=validateAppMessages.js.map