@wavemaker/foundation-css 11.15.1-rc.248 → 11.15.2-rc.249

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 (209) hide show
  1. package/cjs/foundation-css.cjs +1260 -12
  2. package/cjs/src_tokens_mobile_components_accordion-pane_accordion-pane_json.foundation-css.cjs +14 -0
  3. package/cjs/src_tokens_mobile_components_accordion_accordion_json.foundation-css.cjs +1 -1
  4. package/cjs/src_tokens_mobile_components_anchor_anchor_json.foundation-css.cjs +1 -1
  5. package/cjs/src_tokens_mobile_components_barcodescanner_barcodescanner_json.foundation-css.cjs +1 -1
  6. package/cjs/src_tokens_mobile_components_bottomsheet_bottomsheet_json.foundation-css.cjs +1 -1
  7. package/cjs/src_tokens_mobile_components_button_button_json.foundation-css.cjs +1 -1
  8. package/cjs/src_tokens_mobile_components_calendar_calendar_json.foundation-css.cjs +1 -1
  9. package/cjs/src_tokens_mobile_components_camera_camera_json.foundation-css.cjs +1 -1
  10. package/cjs/src_tokens_mobile_components_cards_cards_json.foundation-css.cjs +1 -1
  11. package/cjs/src_tokens_mobile_components_carousel_carousel_json.foundation-css.cjs +1 -1
  12. package/cjs/src_tokens_mobile_components_checkboxset_checkboxset_json.foundation-css.cjs +1 -1
  13. package/cjs/src_tokens_mobile_components_chips_chips_json.foundation-css.cjs +1 -1
  14. package/cjs/src_tokens_mobile_components_container_container_json.foundation-css.cjs +1 -1
  15. package/cjs/src_tokens_mobile_components_currency_currency_json.foundation-css.cjs +1 -1
  16. package/cjs/src_tokens_mobile_components_dropdown-menu_dropdown-menu_json.foundation-css.cjs +1 -1
  17. package/cjs/src_tokens_mobile_components_form-controls_form-controls_json.foundation-css.cjs +1 -1
  18. package/cjs/src_tokens_mobile_components_form-wrapper_form-wrapper_json.foundation-css.cjs +1 -1
  19. package/cjs/src_tokens_mobile_components_label_label_json.foundation-css.cjs +1 -1
  20. package/cjs/src_tokens_mobile_components_list_list_json.foundation-css.cjs +1 -1
  21. package/cjs/src_tokens_mobile_components_login_login_json.foundation-css.cjs +1 -1
  22. package/cjs/src_tokens_mobile_components_lottie_lottie_json.foundation-css.cjs +1 -1
  23. package/cjs/src_tokens_mobile_components_message_message_json.foundation-css.cjs +1 -1
  24. package/cjs/src_tokens_mobile_components_modal-dialog_modal-dialog_json.foundation-css.cjs +1 -1
  25. package/cjs/src_tokens_mobile_components_nav_nav_json.foundation-css.cjs +1 -1
  26. package/cjs/src_tokens_mobile_components_navbar_navbar_json.foundation-css.cjs +1 -1
  27. package/cjs/src_tokens_mobile_components_navitem_navitem_json.foundation-css.cjs +1 -1
  28. package/cjs/src_tokens_mobile_components_page-left-nav_page-left-nav_json.foundation-css.cjs +14 -0
  29. package/cjs/src_tokens_mobile_components_panel_panel_json.foundation-css.cjs +1 -1
  30. package/cjs/src_tokens_mobile_components_partial-container_partial-container_json.foundation-css.cjs +14 -0
  31. package/cjs/src_tokens_mobile_components_picture_picture_json.foundation-css.cjs +1 -1
  32. package/cjs/src_tokens_mobile_components_popover_popover_json.foundation-css.cjs +1 -1
  33. package/cjs/src_tokens_mobile_components_progress-bar_progress-bar_json.foundation-css.cjs +1 -1
  34. package/cjs/src_tokens_mobile_components_radioset_radioset_json.foundation-css.cjs +1 -1
  35. package/cjs/src_tokens_mobile_components_rating_rating_json.foundation-css.cjs +1 -1
  36. package/cjs/src_tokens_mobile_components_search_search_json.foundation-css.cjs +1 -1
  37. package/cjs/src_tokens_mobile_components_select_select_json.foundation-css.cjs +1 -1
  38. package/cjs/src_tokens_mobile_components_spinner_spinner_json.foundation-css.cjs +1 -1
  39. package/cjs/src_tokens_mobile_components_switch_switch_json.foundation-css.cjs +1 -1
  40. package/cjs/src_tokens_mobile_components_tabbar_tabbar_json.foundation-css.cjs +1 -1
  41. package/cjs/src_tokens_mobile_components_tabs_tabs_json.foundation-css.cjs +1 -1
  42. package/cjs/src_tokens_mobile_components_tile_tile_json.foundation-css.cjs +1 -1
  43. package/cjs/src_tokens_mobile_components_webview_webview_json.foundation-css.cjs +1 -1
  44. package/cjs/src_tokens_mobile_components_wizard_wizard_json.foundation-css.cjs +1 -1
  45. package/index.d.ts +1 -0
  46. package/index.js +1 -0
  47. package/index.js.map +1 -1
  48. package/native_mobile.index.d.ts +17 -0
  49. package/native_mobile.index.js +17 -5
  50. package/native_mobile.index.js.map +1 -1
  51. package/npm-shrinkwrap.json +95 -117
  52. package/package-lock.json +95 -117
  53. package/package.json +13 -4
  54. package/src/node/index.d.ts +1 -0
  55. package/src/node/index.js +2 -0
  56. package/src/node/index.js.map +1 -0
  57. package/src/styles/mobile/components/advanced/carousel.less +6 -12
  58. package/src/styles/mobile/components/advanced/login.less +1 -4
  59. package/src/styles/mobile/components/advanced/webview.less +1 -4
  60. package/src/styles/mobile/components/basic/anchor.less +2 -2
  61. package/src/styles/mobile/components/basic/bottomsheet.less +5 -12
  62. package/src/styles/mobile/components/basic/button.less +3 -12
  63. package/src/styles/mobile/components/basic/buttongroup.less +3 -12
  64. package/src/styles/mobile/components/basic/label.less +7 -4
  65. package/src/styles/mobile/components/basic/lottie.less +7 -4
  66. package/src/styles/mobile/components/basic/message.less +6 -10
  67. package/src/styles/mobile/components/basic/picture.less +4 -16
  68. package/src/styles/mobile/components/basic/progress-bar.less +0 -1
  69. package/src/styles/mobile/components/basic/search.less +5 -20
  70. package/src/styles/mobile/components/basic/spinner.less +1 -1
  71. package/src/styles/mobile/components/container/accordion/accordionpane.less +6 -0
  72. package/src/styles/mobile/components/container/accordion.less +35 -3
  73. package/src/styles/mobile/components/container/panel.less +0 -5
  74. package/src/styles/mobile/components/container/tabs/tab-header.less +14 -3
  75. package/src/styles/mobile/components/container/tile.less +1 -4
  76. package/src/styles/mobile/components/container/wizard.less +3 -9
  77. package/src/styles/mobile/components/container.less +8 -32
  78. package/src/styles/mobile/components/data/card.less +1 -4
  79. package/src/styles/mobile/components/data/form.less +143 -37
  80. package/src/styles/mobile/components/data/list.less +1 -4
  81. package/src/styles/mobile/components/device/barcodescanner.less +2 -5
  82. package/src/styles/mobile/components/device/camera.less +1 -4
  83. package/src/styles/mobile/components/dialogs/dialog.less +17 -4
  84. package/src/styles/mobile/components/input/calendar.less +46 -38
  85. package/src/styles/mobile/components/input/checkboxset.less +1 -2
  86. package/src/styles/mobile/components/input/chips.less +8 -19
  87. package/src/styles/mobile/components/input/currency.less +14 -17
  88. package/src/styles/mobile/components/input/radioset.less +1 -2
  89. package/src/styles/mobile/components/input/rating.less +1 -1
  90. package/src/styles/mobile/components/input/select.less +26 -64
  91. package/src/styles/mobile/components/input/switch.less +1 -2
  92. package/src/styles/mobile/components/input/toggle.less +1 -1
  93. package/src/styles/mobile/components/navigation/appnavbar.less +11 -15
  94. package/src/styles/mobile/components/navigation/menu.less +3 -11
  95. package/src/styles/mobile/components/navigation/popover.less +22 -15
  96. package/src/styles/mobile/components/page/left-panel.less +0 -1
  97. package/src/styles/mobile/components/page/partial-container.less +1 -0
  98. package/src/styles/mobile/components/page/tabbar.less +9 -8
  99. package/src/styles/mobile/components/tokens.dark.css +25 -9
  100. package/src/styles/mobile/components/tokens.light.css +198 -241
  101. package/src/styles/mobile/components/variables/accordion.variant.less +4 -0
  102. package/src/styles/mobile/components/variables/button.variant.less +19 -79
  103. package/src/styles/mobile/components/variables/carousel.variant.less +2 -3
  104. package/src/styles/mobile/components/variables/container.variant.less +6 -24
  105. package/src/styles/mobile/components/variables/form-controls.variant.less +9 -8
  106. package/src/styles/mobile/components/variables/picture.variant.less +3 -12
  107. package/src/styles/mobile/components/variables/tabbar.variant.less +1 -0
  108. package/src/styles/mobile/components/variables/tabs.variant.less +7 -0
  109. package/src/styles/mobile/studio/advanced/carousel.less +4 -2
  110. package/src/styles/mobile/studio/advanced/login.less +1 -4
  111. package/src/styles/mobile/studio/advanced/webview.less +1 -4
  112. package/src/styles/mobile/studio/basic/label.less +0 -8
  113. package/src/styles/mobile/studio/basic/message.less +1 -2
  114. package/src/styles/mobile/studio/container/accordion.less +1 -4
  115. package/src/styles/mobile/studio/container/container.less +2 -8
  116. package/src/styles/mobile/studio/container/tabs.less +10 -2
  117. package/src/styles/mobile/studio/data/card.less +1 -1
  118. package/src/styles/mobile/studio/device/barcode.less +2 -8
  119. package/src/styles/mobile/studio/input/calendar.less +66 -6
  120. package/src/styles/mobile/studio/input/checkboxset.less +21 -6
  121. package/src/styles/mobile/studio/input/currency.less +9 -1
  122. package/src/styles/mobile/studio/input/form.less +10 -40
  123. package/src/styles/mobile/studio/input/rating.less +1 -1
  124. package/src/styles/mobile/studio/input/select.less +33 -0
  125. package/src/styles/mobile/studio/input/toggle.less +3 -2
  126. package/src/styles/mobile/studio/layouts/tabbar.less +4 -5
  127. package/src/styles/mobile/studio/navigation/nav.less +0 -17
  128. package/src/styles/mobile/studio/navigation/popover.less +2 -5
  129. package/src/token-validation/component-token-schema.d.ts +169 -0
  130. package/src/token-validation/component-token-schema.js +146 -0
  131. package/src/token-validation/component-token-schema.js.map +1 -0
  132. package/src/token-validation/constants.d.ts +41 -0
  133. package/src/token-validation/constants.js +54 -0
  134. package/src/token-validation/constants.js.map +1 -0
  135. package/src/token-validation/global-token-schema.d.ts +37 -0
  136. package/src/token-validation/global-token-schema.js +41 -0
  137. package/src/token-validation/global-token-schema.js.map +1 -0
  138. package/src/token-validation/validate-token.d.ts +72 -0
  139. package/src/token-validation/validate-token.js +939 -0
  140. package/src/token-validation/validate-token.js.map +1 -0
  141. package/src/token-validation/validate-value.d.ts +13 -0
  142. package/src/token-validation/validate-value.js +57 -0
  143. package/src/token-validation/validate-value.js.map +1 -0
  144. package/src/token-validation/validator-utils/common/constants.d.ts +172 -0
  145. package/src/token-validation/validator-utils/common/constants.js +258 -0
  146. package/src/token-validation/validator-utils/common/constants.js.map +1 -0
  147. package/src/token-validation/validator-utils/common/primitives.d.ts +14 -0
  148. package/src/token-validation/validator-utils/common/primitives.js +35 -0
  149. package/src/token-validation/validator-utils/common/primitives.js.map +1 -0
  150. package/src/token-validation/validator-utils/common/utils.d.ts +152 -0
  151. package/src/token-validation/validator-utils/common/utils.js +316 -0
  152. package/src/token-validation/validator-utils/common/utils.js.map +1 -0
  153. package/src/token-validation/validator-utils/native_mobile.d.ts +1 -0
  154. package/src/token-validation/validator-utils/native_mobile.js +120 -0
  155. package/src/token-validation/validator-utils/native_mobile.js.map +1 -0
  156. package/src/token-validation/validator-utils/web.d.ts +1 -0
  157. package/src/token-validation/validator-utils/web.js +72 -0
  158. package/src/token-validation/validator-utils/web.js.map +1 -0
  159. package/src/tokens/mobile/components/accordion/accordion.json +172 -4
  160. package/src/tokens/mobile/components/accordion-pane/accordion-pane.json +65 -0
  161. package/src/tokens/mobile/components/anchor/anchor.json +5 -15
  162. package/src/tokens/mobile/components/barcodescanner/barcodescanner.json +10 -38
  163. package/src/tokens/mobile/components/bottomsheet/bottomsheet.json +28 -92
  164. package/src/tokens/mobile/components/button/button.json +24 -121
  165. package/src/tokens/mobile/components/calendar/calendar.json +262 -158
  166. package/src/tokens/mobile/components/camera/camera.json +5 -31
  167. package/src/tokens/mobile/components/cards/cards.json +1 -27
  168. package/src/tokens/mobile/components/carousel/carousel.json +25 -69
  169. package/src/tokens/mobile/components/checkboxset/checkboxset.json +6 -16
  170. package/src/tokens/mobile/components/chips/chips.json +8 -80
  171. package/src/tokens/mobile/components/container/container.json +22 -158
  172. package/src/tokens/mobile/components/currency/currency.json +89 -5
  173. package/src/tokens/mobile/components/dropdown-menu/dropdown-menu.json +15 -87
  174. package/src/tokens/mobile/components/form-controls/form-controls.json +140 -142
  175. package/src/tokens/mobile/components/form-wrapper/form-wrapper.json +0 -8
  176. package/src/tokens/mobile/components/label/label.json +0 -34
  177. package/src/tokens/mobile/components/list/list.json +5 -31
  178. package/src/tokens/mobile/components/login/login.json +5 -31
  179. package/src/tokens/mobile/components/lottie/lottie.json +23 -29
  180. package/src/tokens/mobile/components/message/message.json +37 -61
  181. package/src/tokens/mobile/components/modal-dialog/modal-dialog.json +54 -8
  182. package/src/tokens/mobile/components/nav/nav.json +1 -48
  183. package/src/tokens/mobile/components/navbar/navbar.json +61 -95
  184. package/src/tokens/mobile/components/navitem/navitem.json +4 -4
  185. package/src/tokens/mobile/components/{left-nav/left-nav.json → page-left-nav/page-left-nav.json} +1 -9
  186. package/src/tokens/mobile/components/panel/panel.json +0 -10
  187. package/src/tokens/mobile/components/partial-container/partial-container.json +23 -0
  188. package/src/tokens/mobile/components/picture/picture.json +9 -63
  189. package/src/tokens/mobile/components/popover/popover.json +105 -97
  190. package/src/tokens/mobile/components/progress-bar/progress-bar.json +0 -8
  191. package/src/tokens/mobile/components/radioset/radioset.json +5 -15
  192. package/src/tokens/mobile/components/rating/rating.json +5 -7
  193. package/src/tokens/mobile/components/search/search.json +25 -155
  194. package/src/tokens/mobile/components/select/select.json +223 -165
  195. package/src/tokens/mobile/components/spinner/spinner.json +5 -7
  196. package/src/tokens/mobile/components/switch/switch.json +5 -15
  197. package/src/tokens/mobile/components/tabbar/tabbar.json +48 -54
  198. package/src/tokens/mobile/components/tabs/tabs.json +113 -5
  199. package/src/tokens/mobile/components/tile/tile.json +5 -31
  200. package/src/tokens/mobile/components/webview/webview.json +5 -31
  201. package/src/tokens/mobile/components/wizard/wizard.json +8 -56
  202. package/cjs/src_tokens_mobile_components_left-nav_left-nav_json.foundation-css.cjs +0 -14
  203. package/src/styles/mobile/components/input/epoch/date.less +0 -44
  204. package/src/styles/mobile/components/input/epoch/datetime.less +0 -38
  205. package/src/styles/mobile/components/input/epoch/time.less +0 -38
  206. package/src/styles/mobile/components/input/number.less +0 -40
  207. package/src/styles/mobile/components/input/text.less +0 -69
  208. package/src/styles/mobile/components/input/textarea.less +0 -63
  209. package/src/styles/mobile/components/variables/select.variant.less +0 -3
@@ -0,0 +1,939 @@
1
+ import fs from "fs";
2
+ import Ajv from "ajv";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import geComponentTokenSchema from "./component-token-schema.js";
6
+ import getGlobalTokenSchema from "./global-token-schema.js";
7
+ import { VALIDATION_RULES, ERROR_TYPES, TOKEN_TYPES, ALLOWED_PROPERTIES_BY_CONTEXT, VALIDATION_SCOPE, VALUE_RELATED_KEYWORDS, PLATFORM, FOLDER_TYPE } from "./constants.js";
8
+ //Maps each token type to its validation schema and the rules enabled during validation.
9
+ const TOKEN_TYPE_CONFIG = {
10
+ [TOKEN_TYPES.GLOBAL]: {
11
+ getSchema: () => getGlobalTokenSchema(),
12
+ resolveRules: () => ({
13
+ [VALIDATION_RULES.STRUCTURE]: false,
14
+ [VALIDATION_RULES.METADATA]: false,
15
+ [VALIDATION_RULES.REFERENCES]: true,
16
+ [VALIDATION_RULES.OVERRIDES]: false
17
+ })
18
+ },
19
+ [TOKEN_TYPES.COMPONENT]: {
20
+ getSchema: (enabledRules) => geComponentTokenSchema(enabledRules),
21
+ resolveRules: (validationOptions) => resolveEnabledValidationRules(validationOptions)
22
+ }
23
+ };
24
+ /*
25
+ * Normalizes and stores a validation error.
26
+ * Inputs:
27
+ * - errorType: category of validation error
28
+ * - fieldName: dot-separated path of the invalid field
29
+ * - message: human-readable error message
30
+ * - fileName: name of the file being validated (optional)
31
+ * - allowedValues: list of allowed values for the field (optional)
32
+ * Output: - Adds a normalized error object to the internal validation error list
33
+ */
34
+ const pushError = ({ errorType, fieldName, message, fileName, allowedValues, actualValue }, validationErrors) => {
35
+ const error = { errorType, fieldName, message };
36
+ if (allowedValues)
37
+ error.allowedValues = allowedValues;
38
+ if (fileName)
39
+ error.fileName = fileName;
40
+ if (actualValue !== undefined)
41
+ error.actualValue = actualValue;
42
+ validationErrors.push(error);
43
+ };
44
+ /*
45
+ * Validates that leaf token nodes define a valid string value.
46
+ * Inputs:
47
+ * - _schema: AJV keyword schema value (not used)
48
+ * - node: token node being validated
49
+ * - ctx: AJV validation context containing the instance path
50
+ * Output:
51
+ * - Returns true if node is valid or not applicable
52
+ * - Returns false and reports validation errors if leaf token is invalid
53
+ */
54
+ function leafMustHaveValueValidate(_schema, node, ctx) {
55
+ // Skip meta subtree
56
+ if (ctx.instancePath?.includes("/meta") ||
57
+ ctx.instancePath?.includes("/attributes") ||
58
+ ctx.instancePath?.endsWith("/type")) {
59
+ return true;
60
+ }
61
+ // Primitive string leaf
62
+ if (typeof node === "string") {
63
+ leafMustHaveValueValidate.errors = [
64
+ {
65
+ keyword: "leafMustHaveValue",
66
+ message: `Invalid token definition.\n` +
67
+ `Leaf tokens must be objects with a "value" property.\n\n` +
68
+ `Expected format: { "value": "16px" }`
69
+ }
70
+ ];
71
+ return false;
72
+ }
73
+ // null node
74
+ if (node === null) {
75
+ leafMustHaveValueValidate.errors = [
76
+ {
77
+ keyword: "leafMustHaveValue",
78
+ message: `Invalid token definition.\n` +
79
+ `Token cannot be null.\n\n` +
80
+ `Expected format: { "value": "16px" }`
81
+ }
82
+ ];
83
+ return false;
84
+ }
85
+ //Ignore meta keys when deciding leaf
86
+ const TOKEN_META_KEYS = new Set(["value", "type", "attributes", "meta"]);
87
+ const childKeys = Object.keys(node).filter(k => !TOKEN_META_KEYS.has(k));
88
+ const hasNestedTokenChildren = childKeys.length > 0 && childKeys.some(k => node[k] && typeof node[k] === "object");
89
+ const hasValue = "value" in node;
90
+ //nodes with "value" cannot have nested token children
91
+ if (hasValue && hasNestedTokenChildren) {
92
+ leafMustHaveValueValidate.errors = [
93
+ {
94
+ keyword: "leafMustHaveValue",
95
+ message: `Invalid token structure.\n` +
96
+ `A token that defines "value" cannot contain nested token objects.\n` +
97
+ `Move nested tokens to a separate level.`
98
+ }
99
+ ];
100
+ return false;
101
+ }
102
+ // Object leaf without value
103
+ if (!hasNestedTokenChildren && !hasValue) {
104
+ leafMustHaveValueValidate.errors = [
105
+ {
106
+ keyword: "leafMustHaveValue",
107
+ message: `Invalid token definition.\n` +
108
+ `Leaf tokens must be objects with a "value" property.\n` +
109
+ `Expected format: { "value": "16px" }`
110
+ }
111
+ ];
112
+ return false;
113
+ }
114
+ // Invalid string literals
115
+ const INVALID_STRING_LITERALS = new Set(["null", "undefined", "nan"]);
116
+ if (typeof node.value === "string" &&
117
+ INVALID_STRING_LITERALS.has(node.value.trim().toLowerCase())) {
118
+ leafMustHaveValueValidate.errors = [
119
+ {
120
+ keyword: "leafMustHaveValue",
121
+ message: `Invalid token value.\n` +
122
+ `"${node.value}" is not allowed as a token value.`
123
+ }
124
+ ];
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+ /*
130
+ * Validates token string values for reference placeholders.
131
+ * Inputs:
132
+ * - _schema: AJV keyword schema value (not used, required by AJV signature)
133
+ * - tokenValue: string token value being validated
134
+ * Output:
135
+ * - Returns true if all references are valid or rule is disabled
136
+ * - Returns false and attaches validation errors if any reference is invalid
137
+ */
138
+ function validateTokenReference(_unusedSchema, tokenValue) {
139
+ const { enabledRules, globalTokenRegistry } = this.context;
140
+ validateTokenReference.errors = null;
141
+ if (!enabledRules?.[VALIDATION_RULES.REFERENCES])
142
+ return true;
143
+ const referencePlaceholders = typeof tokenValue === "string" ? tokenValue.match(/{[^}]+}/g) : null;
144
+ if (!referencePlaceholders)
145
+ return true;
146
+ const invalidReference = referencePlaceholders.find(referenceExpression => {
147
+ const rawReferencePath = referenceExpression.replace(/[{}]/g, "")?.trim();
148
+ // Reject empty references
149
+ if (!rawReferencePath) {
150
+ validateTokenReference.errors = [
151
+ {
152
+ keyword: "refExists",
153
+ message: `Empty reference is not allowed → ${referenceExpression}`
154
+ }
155
+ ];
156
+ return true;
157
+ }
158
+ // Ensure reference ends with `.value`
159
+ if (!rawReferencePath.endsWith(".value")) {
160
+ validateTokenReference.errors = [
161
+ {
162
+ keyword: "refExists",
163
+ message: `Reference must end with .value → ${referenceExpression}`
164
+ }
165
+ ];
166
+ return true;
167
+ }
168
+ const resolvedTokenPath = rawReferencePath.replace(/\.value$/, "");
169
+ // Ensure referenced token exists globally
170
+ return !globalTokenRegistry[resolvedTokenPath];
171
+ });
172
+ if (invalidReference) {
173
+ if (!validateTokenReference.errors) {
174
+ validateTokenReference.errors = [
175
+ {
176
+ keyword: "refExists",
177
+ message: `Reference does not exist → ${invalidReference}`
178
+ }
179
+ ];
180
+ }
181
+ return false;
182
+ }
183
+ return true;
184
+ }
185
+ /*
186
+ * Creates the override validation function used by the overrideContext AJV keyword.
187
+ * Inputs:
188
+ * - _schema: AJV keyword schema value (not used)
189
+ * - _parentSchema: parent schema (not used)
190
+ * Output:
191
+ * - Returns a validator function that AJV executes during validation
192
+ */
193
+ function compileOverrideKeyword() {
194
+ return function validateOverrideAgainstBaseMapping(overrideNode, ctx) {
195
+ const { enabledRules, foundationMappingRegistry } = this.context;
196
+ //STOP — do not run override validation on primitives
197
+ if (overrideNode === null || typeof overrideNode !== "object") {
198
+ return true;
199
+ }
200
+ //STOP — do not run override validation on token leaf
201
+ if ("value" in overrideNode) {
202
+ return true;
203
+ }
204
+ // Rule disabled
205
+ if (!enabledRules?.[VALIDATION_RULES.OVERRIDES])
206
+ return true;
207
+ const instancePath = ctx.instancePath;
208
+ const isOverrideRoot = instancePath.endsWith("/mapping") ||
209
+ instancePath.match(/\/states\/[^/]+$/) ||
210
+ instancePath.match(/\/variantGroups\/[^/]+\/[^/]+$/) ||
211
+ instancePath.match(/\/variantGroups\/[^/]+\/[^/]+\/states\/[^/]+$/);
212
+ if (!isOverrideRoot)
213
+ return true;
214
+ const root = ctx.rootData;
215
+ // Component name from override JSON
216
+ const componentName = Object.keys(root)[0];
217
+ // BASE STRUCTURE MUST COME FROM FOUNDATION
218
+ const baseStructure = foundationMappingRegistry[componentName];
219
+ // If foundation has no mapping, skip validation
220
+ if (!baseStructure)
221
+ return true;
222
+ const overrideErrors = [];
223
+ const basePath = deriveBasePath(componentName, instancePath);
224
+ validateOverridePathRecursively(overrideNode, baseStructure, basePath, overrideErrors, instancePath);
225
+ if (overrideErrors.length) {
226
+ validateOverrideAgainstBaseMapping.errors = overrideErrors.map(e => ({
227
+ instancePath: e.instancePath,
228
+ keyword: "overrideContext",
229
+ message: e.message
230
+ }));
231
+ return false;
232
+ }
233
+ return true;
234
+ };
235
+ }
236
+ // Resolves the root directory of foundation tokens.
237
+ function resolveFoundationTokenRootDir(platform = PLATFORM.WEB) {
238
+ if (!Object.values(PLATFORM).includes(platform)) {
239
+ throw new Error("Platform property is invalid, allowed properties are " +
240
+ Object.values(PLATFORM).join(", "));
241
+ }
242
+ // Full path of this file (validate-token.js)
243
+ const currentFilePath = fileURLToPath(import.meta.url);
244
+ // Directory containing this file (scripts/)
245
+ const currentDirPath = path.dirname(currentFilePath);
246
+ const folder = platform == PLATFORM.MOBILE ? FOLDER_TYPE.MOBILE : FOLDER_TYPE.WEB;
247
+ return path.resolve(currentDirPath, "../../src/tokens", folder);
248
+ }
249
+ /*
250
+ * Validates override paths against the foundation component mapping recursively.
251
+ * Inputs:
252
+ * - overrideNode: override token node being validated
253
+ * - baseNode: corresponding foundation mapping node
254
+ * - path: dot-separated path representing the current base mapping location
255
+ * - overrideErrors: array used to collect override validation errors
256
+ * - instancePath: AJV instance path of the current override node
257
+ * Output:
258
+ * - Populates the overrideErrors array with invalid override path errors
259
+ */
260
+ function validateOverridePathRecursively(overrideNode, baseNode, path, overrideErrors, instancePath) {
261
+ if (path.endsWith(".states")) {
262
+ return;
263
+ }
264
+ //stop recursion at token leaf object
265
+ if (overrideNode &&
266
+ typeof overrideNode === "object" &&
267
+ ("value" in overrideNode || "type" in overrideNode || "attributes" in overrideNode)) {
268
+ return;
269
+ }
270
+ // Base missing
271
+ if (!baseNode) {
272
+ overrideErrors.push({
273
+ instancePath,
274
+ message: `Override path does NOT exist in base mapping → ${path}`
275
+ });
276
+ return;
277
+ }
278
+ // Override must be object
279
+ if (overrideNode === null || typeof overrideNode !== "object") {
280
+ return;
281
+ }
282
+ // BASE IS LEAF
283
+ if (baseNode.__leaf) {
284
+ for (const key of Object.keys(overrideNode || {})) {
285
+ if (!["value", "type", "attributes"].includes(key)) {
286
+ overrideErrors.push({
287
+ instancePath: `${instancePath}/${key}`,
288
+ message: `Override path does NOT exist in base mapping → ${path}.${key}`
289
+ });
290
+ }
291
+ }
292
+ return;
293
+ }
294
+ for (const key of Object.keys(overrideNode)) {
295
+ // NEVER validate "states" structurally
296
+ if (key === "states") {
297
+ continue;
298
+ }
299
+ if (!baseNode[key]) {
300
+ overrideErrors.push({
301
+ instancePath: `${instancePath}/${key}`,
302
+ message: `Override path does NOT exist in base mapping → ${path}.${key}`
303
+ });
304
+ continue;
305
+ }
306
+ validateOverridePathRecursively(overrideNode[key], baseNode[key], `${path}.${key}`, overrideErrors, `${instancePath}/${key}`);
307
+ }
308
+ }
309
+ /*
310
+ * Registers all token paths that define a concrete value by traversing a token JSON tree.
311
+ * Inputs:
312
+ * - tokenNode: token JSON object to traverse
313
+ * - parentPath: dot-separated parent token path (used during recursion)
314
+ * Output:
315
+ * - Populates the global token registry with resolved token paths
316
+ */
317
+ function registerTokenDefinitionsRecursively(tokenNode, registry, parentPath = "") {
318
+ if (!tokenNode || typeof tokenNode !== "object")
319
+ return;
320
+ Object.keys(tokenNode).forEach(tokenKey => {
321
+ const currentTokenPath = parentPath
322
+ ? `${parentPath}.${tokenKey}`
323
+ : tokenKey;
324
+ const tokenValue = tokenNode[tokenKey];
325
+ // Register tokens that define a concrete value
326
+ if (tokenValue &&
327
+ typeof tokenValue === "object" &&
328
+ "value" in tokenValue) {
329
+ if (!registry[currentTokenPath]) {
330
+ registry[currentTokenPath] = true;
331
+ }
332
+ }
333
+ // Continue traversal for nested token objects
334
+ if (tokenValue && typeof tokenValue === "object") {
335
+ registerTokenDefinitionsRecursively(tokenValue, registry, currentTokenPath);
336
+ }
337
+ });
338
+ }
339
+ /*
340
+ * Registers all discovered token definitions into the global token registry.
341
+ * Inputs:
342
+ * - directoryPath: root directory to scan for token JSON files
343
+ * Output:
344
+ * - Registers all discovered token definitions into the global token registry
345
+ */
346
+ function collectAllTokenDefinitionsFromDir(directoryPath, registry) {
347
+ fs.readdirSync(directoryPath).forEach(entryName => {
348
+ const entryPath = path.join(directoryPath, entryName);
349
+ const entryStats = fs.statSync(entryPath);
350
+ // Recurse into subdirectories
351
+ if (entryStats.isDirectory()) {
352
+ return collectAllTokenDefinitionsFromDir(entryPath, registry);
353
+ }
354
+ // Process only JSON token files
355
+ if (!entryName.endsWith(".json"))
356
+ return;
357
+ const tokenFileContent = JSON.parse(fs.readFileSync(entryPath, "utf8"));
358
+ registerTokenDefinitionsRecursively(tokenFileContent, registry);
359
+ });
360
+ }
361
+ //Builds the foundation global tokens directory path and passes it for token definition collection.
362
+ function collectFoundationGlobalTokens(foundationRootDir) {
363
+ const registry = {};
364
+ const globalDir = path.join(foundationRootDir, "global");
365
+ collectAllTokenDefinitionsFromDir(globalDir, registry);
366
+ return registry;
367
+ }
368
+ //Builds the foundation components directory path and collects component mapping structures for override validation.
369
+ function collectFoundationComponentMappings(foundationRootDir) {
370
+ const registry = {};
371
+ const componentsDir = path.join(foundationRootDir, "components");
372
+ collectFoundationMappings(componentsDir, registry);
373
+ return registry;
374
+ }
375
+ /*
376
+ * Determines which properties are allowed for a given JSON path.
377
+ * Inputs:
378
+ * - instancePath: AJV instance path of the current validation error
379
+ * Output:
380
+ * - Returns a list of allowed property names for the path
381
+ * - Returns undefined if no specific rule applies
382
+ */
383
+ function resolveAllowedProperties(instancePath) {
384
+ const normalizedPath = instancePath
385
+ ? instancePath.replace(/^\//, "").replace(/\//g, ".")
386
+ : "";
387
+ for (const rule of ALLOWED_PROPERTIES_BY_CONTEXT) {
388
+ if (rule.match(normalizedPath)) {
389
+ return rule.allowed;
390
+ }
391
+ }
392
+ return undefined;
393
+ }
394
+ /*
395
+ * Resolves which validation rules are enabled for the current validation run.
396
+ * Inputs: - validationOptions: configuration that may specify enabled validation rules
397
+ * Output: - Returns an object mapping each validation rule to a boolean flag
398
+ */
399
+ function resolveEnabledValidationRules({ rules = [], isFoundationValidation = false } = {}) {
400
+ const allAvailableRules = Object.values(VALIDATION_RULES);
401
+ // Define rules when no rules are set explicitly
402
+ const defaultEnabledRules = new Set([VALIDATION_RULES.STRUCTURE, VALIDATION_RULES.METADATA]);
403
+ return Object.fromEntries(allAvailableRules.map(rule => {
404
+ if (isFoundationValidation) {
405
+ //Excluding reference rule for foundation css
406
+ if (rule === VALIDATION_RULES.REFERENCES) {
407
+ return [rule, false];
408
+ }
409
+ return [rule, true];
410
+ }
411
+ //If rules set explicity then enable only those rules
412
+ if (rules.length > 0)
413
+ return [rule, rules.includes(rule)];
414
+ //Enable only if NOT in the default enabled set of rules for component validation
415
+ return [rule, !defaultEnabledRules.has(rule)];
416
+ }));
417
+ }
418
+ /*
419
+ * Processes AJV validation errors and converts them into normalized validation errors.
420
+ * Inputs:
421
+ * - validate: compiled AJV validation function containing validation errors
422
+ * - filePath: path of the JSON file being validated (optional)
423
+ */
424
+ function handleValidationErrors({ validate, filePath }, validationErrors) {
425
+ const fileName = filePath ? filePath.split("/").pop() : undefined;
426
+ if (validate.errors) {
427
+ validate.errors.forEach(ajvError => {
428
+ const { fieldName, errorMessage, errorMetadata } = extractValidationErrorDetails(ajvError);
429
+ // Default error object
430
+ let errorObj = {
431
+ errorType: ERROR_TYPES.INVALID_VALUE,
432
+ fieldName,
433
+ message: errorMessage,
434
+ fileName
435
+ };
436
+ switch (ajvError.keyword) {
437
+ // RULE-3 → Reference errors
438
+ case "refExists":
439
+ errorObj.errorType = ERROR_TYPES.INVALID_REFERENCE;
440
+ break;
441
+ // RULE-4 → Invalid override fields
442
+ case "overrideContext":
443
+ errorObj.errorType = ERROR_TYPES.INVALID_FIELD;
444
+ break;
445
+ // Leaf / enum value validation
446
+ case "leafMustHaveValue":
447
+ case "enum":
448
+ if (errorMetadata.allowedValues) {
449
+ errorObj.allowedValues = errorMetadata.allowedValues;
450
+ }
451
+ break;
452
+ // TYPE errors → structure or value
453
+ case "type": {
454
+ const expectedType = ajvError.params?.type;
455
+ const actualType = Array.isArray(ajvError.data)
456
+ ? "array"
457
+ : typeof ajvError.data;
458
+ if (expectedType === "object") {
459
+ errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
460
+ errorObj.message = `Expected a token object, but found ${actualType}`;
461
+ }
462
+ break;
463
+ }
464
+ // JSON structure issues
465
+ case "required":
466
+ case "minProperties":
467
+ case "maxProperties":
468
+ errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
469
+ break;
470
+ case "additionalProperties":
471
+ errorObj.errorType = ERROR_TYPES.INVALID_STRUCTURE;
472
+ const allowedValues = resolveAllowedProperties(ajvError.instancePath);
473
+ if (allowedValues) {
474
+ errorObj.allowedValues = allowedValues;
475
+ }
476
+ break;
477
+ default:
478
+ // RULE-2 → Missing metadata
479
+ if (errorMessage.includes('must match "then" schema')) {
480
+ errorObj.errorType = ERROR_TYPES.MISSING_FIELD;
481
+ errorObj.message =
482
+ "Missing required metadata: type, attributes.subtype, attributes.description";
483
+ }
484
+ }
485
+ if (VALUE_RELATED_KEYWORDS.has(ajvError.keyword)) {
486
+ errorObj.actualValue = ajvError.data;
487
+ }
488
+ pushError(errorObj, validationErrors);
489
+ });
490
+ }
491
+ }
492
+ /*
493
+ * Extracts readable validation error details from an AJV error object.
494
+ *
495
+ * Input:
496
+ * - ajvError: a single validation error produced by AJV
497
+ *
498
+ * Output:
499
+ * - fieldName: dot-separated path of the invalid field
500
+ * - errorMessage: human-readable validation message
501
+ * - errorMetadata: additional details such as allowed values (when applicable)
502
+ */
503
+ function extractValidationErrorDetails(ajvError) {
504
+ const fieldName = ajvError.instancePath
505
+ ? ajvError.instancePath.replace(/^\//, "").replace(/\//g, ".")
506
+ : "(root)";
507
+ const errorMessage = ajvError.message || "Invalid token value";
508
+ const errorMetadata = {};
509
+ // ENUM-specific metadata
510
+ if (ajvError.keyword === "enum" && Array.isArray(ajvError.params?.allowedValues)) {
511
+ errorMetadata.allowedValues = ajvError.params.allowedValues;
512
+ }
513
+ return {
514
+ fieldName,
515
+ errorMessage,
516
+ errorMetadata
517
+ };
518
+ }
519
+ //Validates the token type against supported values and normalizes it.
520
+ function validateAndNormalizeTokenType(tokenType) {
521
+ if (typeof tokenType !== "string") {
522
+ throw new Error(`tokenType must be a string`);
523
+ }
524
+ const normalizedTokenType = tokenType.toLowerCase();
525
+ if (!TOKEN_TYPE_CONFIG[normalizedTokenType]) {
526
+ throw new Error(`Invalid tokenType "${tokenType}". Allowed values: ${Object.keys(TOKEN_TYPE_CONFIG).join(", ")}`);
527
+ }
528
+ return normalizedTokenType;
529
+ }
530
+ /*
531
+ * Collects structural mapping definitions for all foundation components.
532
+ * Input:
533
+ * - directoryPath: root directory containing foundation component token files
534
+ *
535
+ * Output:
536
+ * - Populates the foundation mapping registry with structure-only representations of each component’s mapping
537
+ */
538
+ function collectFoundationMappings(directoryPath, componentsMappingregistry = {}) {
539
+ // Read all entries(files + folders ) in the directory
540
+ fs.readdirSync(directoryPath).forEach(entryName => {
541
+ // Absolute path to the current entry
542
+ const entryPath = path.join(directoryPath, entryName);
543
+ // File system metadata (file or directory)
544
+ const entryStats = fs.statSync(entryPath);
545
+ // Recurse into subdirectories
546
+ if (entryStats.isDirectory()) {
547
+ collectFoundationMappings(entryPath, componentsMappingregistry);
548
+ return;
549
+ }
550
+ if (!entryName.endsWith(".json"))
551
+ return;
552
+ const componentJson = JSON.parse(fs.readFileSync(entryPath, "utf8"));
553
+ for (const componentName of Object.keys(componentJson)) {
554
+ const mapping = componentJson[componentName]?.mapping;
555
+ if (!mapping)
556
+ continue;
557
+ componentsMappingregistry[componentName] = extractMappingStructure(mapping);
558
+ }
559
+ });
560
+ return componentsMappingregistry;
561
+ }
562
+ /*
563
+ * Extracts the structural shape of a foundation component mapping for override validation.
564
+ *
565
+ * Input:
566
+ * - componentMapping / node: the `mapping` object of a foundation component token
567
+ *
568
+ * Output:
569
+ * - Returns a structure-only representation of the mapping
570
+ * - Leaf nodes are marked to indicate valid override endpoints
571
+ */
572
+ function extractMappingStructure(node) {
573
+ if (!node || typeof node !== "object") {
574
+ return { __leaf: true };
575
+ }
576
+ const keys = Object.keys(node).filter(k => !["value", "type", "attributes", "states"].includes(k));
577
+ // LEAF TOKEN
578
+ if (keys.length === 0) {
579
+ return { __leaf: true };
580
+ }
581
+ const structure = {};
582
+ for (const key of keys) {
583
+ structure[key] = extractMappingStructure(node[key]);
584
+ }
585
+ return structure;
586
+ }
587
+ //Finds the correct starting point for validating an override.
588
+ function deriveBasePath(componentName, instancePath) {
589
+ if (instancePath.includes("/variantGroups")) {
590
+ return `${componentName}.variantGroups`;
591
+ }
592
+ if (instancePath.includes("/states")) {
593
+ return `${componentName}.states`;
594
+ }
595
+ return `${componentName}.mapping`;
596
+ }
597
+ /*
598
+ * Collects token definitions from an array of JSON objects.
599
+ * Inputs: - jsonArray: an array of token JSON objects
600
+ * Output: Registers all token paths that define a concrete value into the internal global token registry
601
+ */
602
+ function collectTokenDefinitionsFromJsonArray(registry, jsonArray = []) {
603
+ if (!Array.isArray(jsonArray))
604
+ return;
605
+ jsonArray.forEach((json) => {
606
+ if (!json || typeof json !== "object")
607
+ return;
608
+ registerTokenDefinitionsRecursively(json, registry);
609
+ });
610
+ }
611
+ /*
612
+ * Validates a single token JSON object (global or component).
613
+ * Inputs:
614
+ * - json: The token JSON object to validate.
615
+ * - tokenType: The type of token being validated ("global" or "component").
616
+ * - validationConfig:Controls which validation rules are enabled for this run.
617
+ * - rules: optional list of rule names to enable explicitly
618
+ * - isFoundationValidation: when true, all rules are enabled
619
+ * - externalGlobalTokenJsons: Optional additional global token JSONs whose token values can be referenced during validation.
620
+ * Output:Validation errors are collected internally and can be retrieved using getValidationErrors().
621
+ * - In the next iteration, project directory overrides will be used instead of externalGlobalTokenJsons.
622
+ */
623
+ export class TokenValidator {
624
+ constructor(args) {
625
+ this.externalGlobalTokenJsons = args.externalGlobalTokenJsons || [];
626
+ this.validationErrors = [];
627
+ this.ajvEngine = new TokenAjvEngine();
628
+ this.ajv = this.ajvEngine.getValidatorEngine();
629
+ this.platform = args.platform ?? PLATFORM.WEB;
630
+ this.foundationTokensDir = resolveFoundationTokenRootDir(this.platform);
631
+ const globalTokenRegistry = collectFoundationGlobalTokens(this.foundationTokensDir);
632
+ if (Array.isArray(this.externalGlobalTokenJsons)) {
633
+ collectTokenDefinitionsFromJsonArray(globalTokenRegistry, this.externalGlobalTokenJsons);
634
+ }
635
+ this.ajv.context.globalTokenRegistry = globalTokenRegistry;
636
+ }
637
+ // Validates a token JSON object against the appropriate schema and rules based on its type.
638
+ validateTokenJson({ json, validationConfig = {}, tokenType }) {
639
+ if (!json || typeof json !== "object") {
640
+ throw new Error("validateTokenJson requires a valid JSON object");
641
+ }
642
+ // Reset validation errors for each run
643
+ this.validationErrors.length = 0;
644
+ const resolvedTokenType = validateAndNormalizeTokenType(tokenType);
645
+ const tokenConfig = TOKEN_TYPE_CONFIG[resolvedTokenType];
646
+ this.ajv.context.enabledRules = tokenConfig.resolveRules(validationConfig);
647
+ this.ajv.context.foundationMappingRegistry = {};
648
+ if (tokenType === TOKEN_TYPES.COMPONENT) {
649
+ this.ajv.context.foundationMappingRegistry = collectFoundationComponentMappings(this.foundationTokensDir);
650
+ }
651
+ const schema = tokenConfig.getSchema(this.ajv.context.enabledRules);
652
+ const validate = this.ajv.compile(schema);
653
+ validate(json);
654
+ handleValidationErrors({ validate }, this.validationErrors);
655
+ }
656
+ getValidationErrors() {
657
+ return this.validationErrors;
658
+ }
659
+ }
660
+ // Custom AJV engine class to encapsulate validator instance and custom keyword registrations
661
+ class TokenAjvEngine {
662
+ constructor() {
663
+ this.validatorEngine = new Ajv({ allErrors: true, verbose: true, allowMatchingProperties: true });
664
+ this.validatorEngine.context = {
665
+ enabledRules: null,
666
+ globalTokenRegistry: {},
667
+ foundationMappingRegistry: {},
668
+ };
669
+ this.registerCustomValidations();
670
+ }
671
+ registerCustomValidations() {
672
+ const CUSTOM_VALIDATIONS = [
673
+ {
674
+ keyword: "refExists",
675
+ type: "string",
676
+ errors: true,
677
+ validate: validateTokenReference
678
+ },
679
+ {
680
+ keyword: "overrideContext",
681
+ type: "object",
682
+ errors: true,
683
+ compile: compileOverrideKeyword
684
+ },
685
+ {
686
+ keyword: "leafMustHaveValue",
687
+ type: ["object", "string"],
688
+ errors: true,
689
+ validate: leafMustHaveValueValidate
690
+ }
691
+ ];
692
+ CUSTOM_VALIDATIONS.forEach(k => this.validatorEngine.addKeyword(k));
693
+ }
694
+ getValidatorEngine() {
695
+ return this.validatorEngine;
696
+ }
697
+ }
698
+ /*
699
+ Validates an entire folder hierarchy of token JSON files.
700
+ * Constructor inputs:
701
+ * 1) validationScope:
702
+ * - Determines the validation mode.
703
+ * - Possible values:
704
+ * - VALIDATION_SCOPE.FOUNDATION → validates foundation CSS tokens
705
+ * - VALIDATION_SCOPE.PROJECT → validates project override tokens
706
+ *
707
+ * 2) projectOverrideDirs - Exact directories where project token validation should run.
708
+ *
709
+ * 3) isWebFolder:
710
+ * - Indicates whether validation should target the web or mobile
711
+ * foundation token folder.
712
+ * - Used to resolve the correct foundation token root directory.
713
+ *
714
+ */
715
+ export class TokenFolderValidator {
716
+ constructor(args) {
717
+ this.validationScope = args.validationScope || VALIDATION_SCOPE.PROJECT;
718
+ this.projectOverrideDirs = args.projectOverrideDirs || [];
719
+ this.validationErrors = [];
720
+ this.ajvEngine = new TokenAjvEngine();
721
+ this.ajv = this.ajvEngine.getValidatorEngine();
722
+ this.platform = args.platform ?? PLATFORM.WEB;
723
+ this.foundationTokensDir = resolveFoundationTokenRootDir(this.platform);
724
+ this.createAjvContext();
725
+ this.resolvedValidationTargets = this.resolveValidationTargets();
726
+ }
727
+ createAjvContext() {
728
+ this.isFoundation = this.validationScope === VALIDATION_SCOPE.FOUNDATION;
729
+ this.isProject = this.validationScope === VALIDATION_SCOPE.PROJECT;
730
+ if (this.isProject && this.projectOverrideDirs.length === 0) {
731
+ pushError({
732
+ errorType: ERROR_TYPES.BUILD_ERROR,
733
+ fieldName: "Root",
734
+ message: "Project validation requires at least one project override directory, but none were provided."
735
+ }, this.validationErrors);
736
+ return;
737
+ }
738
+ this.ajv.context.foundationMappingRegistry = collectFoundationComponentMappings(this.foundationTokensDir);
739
+ const globalTokenRegistry = collectFoundationGlobalTokens(this.foundationTokensDir);
740
+ if (this.isProject) {
741
+ this.projectOverrideDirs.forEach(projectDir => {
742
+ const { globalDir } = this.resolveProjectOverrideDirs(projectDir);
743
+ if (globalDir && fs.existsSync(globalDir)) {
744
+ collectAllTokenDefinitionsFromDir(globalDir, globalTokenRegistry);
745
+ }
746
+ });
747
+ }
748
+ this.ajv.context.globalTokenRegistry = globalTokenRegistry;
749
+ }
750
+ // Resolves which folders should be validated.
751
+ resolveValidationTargets() {
752
+ const validationTargets = [];
753
+ if (this.isProject) {
754
+ this.projectOverrideDirs.forEach(projectDir => {
755
+ if (!fs.existsSync(projectDir))
756
+ return;
757
+ const { globalDir, componentsDir } = this.resolveProjectOverrideDirs(projectDir);
758
+ if (globalDir && fs.existsSync(globalDir)) {
759
+ validationTargets.push({ type: TOKEN_TYPES.GLOBAL, dir: globalDir });
760
+ }
761
+ if (componentsDir && fs.existsSync(componentsDir)) {
762
+ validationTargets.push({ type: TOKEN_TYPES.COMPONENT, dir: componentsDir });
763
+ }
764
+ });
765
+ }
766
+ //Resolve folders to validate
767
+ if (this.isFoundation) {
768
+ const globalDir = path.join(this.foundationTokensDir, "global");
769
+ const componentsDir = path.join(this.foundationTokensDir, "components");
770
+ validationTargets.push({ type: TOKEN_TYPES.GLOBAL, dir: globalDir });
771
+ validationTargets.push({ type: TOKEN_TYPES.COMPONENT, dir: componentsDir });
772
+ }
773
+ return validationTargets;
774
+ }
775
+ /* Main entry point that validates all token JSON files in the resolved folders
776
+ Inputs :
777
+ 1) validationConfig:Controls which validation rules are enabled for this run.
778
+ * - rules: optional list of rule names to enable explicitly
779
+ * - isFoundationValidation: when true, all rules are enabled
780
+ */
781
+ validateFolder(validationConfig = {}) {
782
+ // Nothing to validate
783
+ if (this.isProject && this.projectOverrideDirs.length === 0) {
784
+ return;
785
+ }
786
+ // Clear previous errors
787
+ this.validationErrors.length = 0;
788
+ this.resolvedValidationTargets.forEach(({ type, dir }) => {
789
+ const tokenConfig = TOKEN_TYPE_CONFIG[type];
790
+ // Enable rules for this run
791
+ this.ajv.context.enabledRules = tokenConfig.resolveRules({
792
+ ...validationConfig,
793
+ isFoundationValidation: this.isFoundation
794
+ });
795
+ const schema = tokenConfig.getSchema(this.ajv.context.enabledRules);
796
+ const validate = this.ajv.compile(schema);
797
+ this.traverseAndValidateJsonFiles(dir, validate);
798
+ });
799
+ }
800
+ // Returns all collected validation errors.
801
+ getValidationErrors() {
802
+ return this.validationErrors;
803
+ }
804
+ //Resolves the available global and component override directories for a project.
805
+ resolveProjectOverrideDirs(overridesDir) {
806
+ const globalDir = path.join(overridesDir, "global");
807
+ const componentsDir = path.join(overridesDir, "components");
808
+ return {
809
+ globalDir: fs.existsSync(globalDir) ? globalDir : null,
810
+ componentsDir: fs.existsSync(componentsDir) ? componentsDir : null
811
+ };
812
+ }
813
+ /*
814
+ * Validates a single JSON file using schema and custom validation rules.
815
+ * Inputs:
816
+ * - filePath: path to the JSON file being validated
817
+ * - validate: compiled AJV validation function
818
+ * Output:
819
+ * - Parses and validates the file
820
+ * - Collects validation errors if any are found
821
+ */
822
+ validateFile(filePath, validate) {
823
+ const { ok, json, error } = this.parseJsonSafely(filePath);
824
+ const file = path.basename(filePath);
825
+ //JSON STRUCTURE VALIDATION
826
+ if (!ok) {
827
+ pushError({
828
+ errorType: ERROR_TYPES.INVALID_JSON,
829
+ fieldName: "(root)",
830
+ message: error.message,
831
+ fileName: file
832
+ });
833
+ return; // don't run AJV
834
+ }
835
+ validate(json);
836
+ handleValidationErrors({
837
+ validate,
838
+ filePath
839
+ }, this.validationErrors);
840
+ }
841
+ /*
842
+ * Safely reads and parses a JSON file without throwing runtime errors.
843
+ * Inputs: filePath: path to the JSON file
844
+ * Output: Returns an object with either the parsed JSON or the parsing error
845
+ */
846
+ parseJsonSafely(filePath) {
847
+ try {
848
+ const content = fs.readFileSync(filePath, "utf8");
849
+ return { ok: true, json: JSON.parse(content) };
850
+ }
851
+ catch (err) {
852
+ return { ok: false, error: err };
853
+ }
854
+ }
855
+ /*
856
+ * Recursively traverses a directory and validates all JSON files found.
857
+ * Inputs:
858
+ * - dir: root directory to traverse
859
+ * - validate: compiled AJV validation function
860
+ * Output: - Validates each JSON file and collects validation errors
861
+ */
862
+ traverseAndValidateJsonFiles(dir, validate, visitedSet = new Set()) {
863
+ // Resolve the real path of the current directory
864
+ const realDir = fs.realpathSync(dir);
865
+ // If we’ve already visited this path, skip it to avoid cycles
866
+ if (visitedSet.has(realDir))
867
+ return;
868
+ visitedSet.add(realDir);
869
+ // Read directory contents
870
+ fs.readdirSync(dir).forEach(name => {
871
+ const full = path.join(dir, name);
872
+ let stat;
873
+ try {
874
+ stat = fs.statSync(full);
875
+ }
876
+ catch (err) {
877
+ console.warn(`Skipping inaccessible file: ${full}`);
878
+ return;
879
+ }
880
+ if (stat.isDirectory()) {
881
+ this.traverseAndValidateJsonFiles(full, validate, visitedSet);
882
+ }
883
+ else if (name.endsWith(".json")) {
884
+ this.validateFile(full, validate);
885
+ }
886
+ });
887
+ }
888
+ /**
889
+ * Generates a Markdown validation report summarizing all errors.
890
+ * Output:
891
+ * - Creates a README-style markdown file listing validation errors in generated folder
892
+ */
893
+ generateValidationReport(outputDir) {
894
+ // Default to <project-root>/generated if outputDir is not provided
895
+ const baseDir = outputDir || path.join(process.cwd(), "generated");
896
+ // Ensure folder exists
897
+ if (!fs.existsSync(baseDir)) {
898
+ fs.mkdirSync(baseDir, { recursive: true });
899
+ }
900
+ if (!this.validationErrors.length) {
901
+ const content = `# Token Validation Report\n\n No validation errors found.\n`;
902
+ fs.writeFileSync(path.join(baseDir, "VALIDATION_REPORT.md"), content);
903
+ return;
904
+ }
905
+ const groupedByFile = {};
906
+ // Group errors by file
907
+ this.validationErrors.forEach(err => {
908
+ const file = err.fileName || "(unknown file)";
909
+ if (!groupedByFile[file])
910
+ groupedByFile[file] = [];
911
+ groupedByFile[file].push(err);
912
+ });
913
+ let md = `# Token Validation Report\n\n`;
914
+ md += `Generated on: ${new Date().toLocaleString()}\n\n`;
915
+ md += `Total Errors: **${this.validationErrors.length}**\n\n`;
916
+ md += `---\n`;
917
+ Object.entries(groupedByFile).forEach(([file, errors]) => {
918
+ md += `## ${file} – ${errors.length} error${errors.length > 1 ? "s" : ""}\n\n`;
919
+ errors.forEach((err, index) => {
920
+ md += `### ${index + 1}. ${err.errorType}\n`;
921
+ md += `- **Path**: \`${err.fieldName}\`\n`;
922
+ md += `- **Message**: ${err.message}\n`;
923
+ if (err.allowedValues) {
924
+ md += `- **Allowed Values**: ${err.allowedValues.join(", ")}\n`;
925
+ }
926
+ if (err.actualValue) {
927
+ md += `- **Actual value**: ${err.actualValue}\n `;
928
+ }
929
+ md += `\n`;
930
+ });
931
+ md += `---\n`;
932
+ });
933
+ const reportName = this.platform === PLATFORM.MOBILE
934
+ ? "mobile_tokens_validation_report.md"
935
+ : "web_tokens_validation_report.md";
936
+ fs.writeFileSync(path.join(baseDir, reportName), md, "utf8");
937
+ }
938
+ }
939
+ //# sourceMappingURL=validate-token.js.map