@openfairygui/functions 0.1.0 → 0.1.1

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/src/validate.ts CHANGED
@@ -1,186 +1,186 @@
1
1
  import type { Document, Transform } from '@openfairygui/core';
2
2
  import { createTransform } from './utils.js';
3
3
  import type { HasOptionalSrc } from './shared-types.js';
4
-
5
- /**
6
- * Severity of a validation issue.
7
- */
8
- export enum ValidationSeverity {
9
- ERROR = 'error',
10
- WARNING = 'warning',
11
- INFO = 'info',
12
- }
13
-
14
- /**
15
- * A single validation issue found in the project.
16
- */
17
- export interface ValidationIssue {
18
- severity: ValidationSeverity;
19
- message: string;
20
- /** Package name where the issue was found. */
21
- packageName?: string;
22
- /** Component name where the issue was found. */
23
- componentName?: string;
24
- /** Resource/object name related to the issue. */
25
- resourceName?: string;
26
- }
27
-
28
- /**
29
- * Result returned by `validate()`.
30
- */
31
- export interface ValidationResult {
32
- ok: boolean;
33
- errors: ValidationIssue[];
34
- warnings: ValidationIssue[];
35
- infos: ValidationIssue[];
36
- }
37
-
38
- export interface ValidateOptions {
39
- /** If true, the transform throws on errors. Default: false. */
40
- throwOnError?: boolean;
41
- }
42
-
43
- const VALIDATE_DEFAULTS: Required<ValidateOptions> = {
44
- throwOnError: false,
45
- };
46
-
47
- /**
48
- * Validates a FairyGUI project for common issues:
49
- * - Missing resource IDs
50
- * - Broken `ui://` references (src pointing to non-existent resources)
51
- * - Empty components (no children)
52
- * - Controllers with no pages
53
- * - Duplicate resource IDs within a package
54
- *
55
- * The validation result is stored in `doc.getRoot().getExtras()._validation`.
56
- *
57
- * ```ts
58
- * await doc.transform(validate({ throwOnError: true }));
59
- * ```
60
- */
61
- export function validate(_options: ValidateOptions = {}): Transform {
62
- const options = { ...VALIDATE_DEFAULTS, ..._options };
63
-
64
- return createTransform('validate', (doc: Document): void => {
65
- const issues: ValidationIssue[] = [];
66
- const root = doc.getRoot();
67
-
68
- if (!root.getProjectId()) {
69
- issues.push({
70
- severity: ValidationSeverity.WARNING,
71
- message: 'Project has no ID.',
72
- });
73
- }
74
-
75
- // Build global resource ID map for cross-reference validation
76
- const globalResources = new Map<string, string>(); // id → "pkg/name"
77
-
78
- for (const pkg of root.listPackages()) {
79
- if (!pkg.getId()) {
80
- issues.push({
81
- severity: ValidationSeverity.ERROR,
82
- message: `Package "${pkg.getName()}" has no ID.`,
83
- packageName: pkg.getName(),
84
- });
85
- }
86
-
4
+
5
+ /**
6
+ * Severity of a validation issue.
7
+ */
8
+ export enum ValidationSeverity {
9
+ ERROR = 'error',
10
+ WARNING = 'warning',
11
+ INFO = 'info',
12
+ }
13
+
14
+ /**
15
+ * A single validation issue found in the project.
16
+ */
17
+ export interface ValidationIssue {
18
+ severity: ValidationSeverity;
19
+ message: string;
20
+ /** Package name where the issue was found. */
21
+ packageName?: string;
22
+ /** Component name where the issue was found. */
23
+ componentName?: string;
24
+ /** Resource/object name related to the issue. */
25
+ resourceName?: string;
26
+ }
27
+
28
+ /**
29
+ * Result returned by `validate()`.
30
+ */
31
+ export interface ValidationResult {
32
+ ok: boolean;
33
+ errors: ValidationIssue[];
34
+ warnings: ValidationIssue[];
35
+ infos: ValidationIssue[];
36
+ }
37
+
38
+ export interface ValidateOptions {
39
+ /** If true, the transform throws on errors. Default: false. */
40
+ throwOnError?: boolean;
41
+ }
42
+
43
+ const VALIDATE_DEFAULTS: Required<ValidateOptions> = {
44
+ throwOnError: false,
45
+ };
46
+
47
+ /**
48
+ * Validates a FairyGUI project for common issues:
49
+ * - Missing resource IDs
50
+ * - Broken `ui://` references (src pointing to non-existent resources)
51
+ * - Empty components (no children)
52
+ * - Controllers with no pages
53
+ * - Duplicate resource IDs within a package
54
+ *
55
+ * The validation result is stored in `doc.getRoot().getExtras()._validation`.
56
+ *
57
+ * ```ts
58
+ * await doc.transform(validate({ throwOnError: true }));
59
+ * ```
60
+ */
61
+ export function validate(_options: ValidateOptions = {}): Transform {
62
+ const options = { ...VALIDATE_DEFAULTS, ..._options };
63
+
64
+ return createTransform('validate', (doc: Document): void => {
65
+ const issues: ValidationIssue[] = [];
66
+ const root = doc.getRoot();
67
+
68
+ if (!root.getProjectId()) {
69
+ issues.push({
70
+ severity: ValidationSeverity.WARNING,
71
+ message: 'Project has no ID.',
72
+ });
73
+ }
74
+
75
+ // Build global resource ID map for cross-reference validation
76
+ const globalResources = new Map<string, string>(); // id → "pkg/name"
77
+
78
+ for (const pkg of root.listPackages()) {
79
+ if (!pkg.getId()) {
80
+ issues.push({
81
+ severity: ValidationSeverity.ERROR,
82
+ message: `Package "${pkg.getName()}" has no ID.`,
83
+ packageName: pkg.getName(),
84
+ });
85
+ }
86
+
87
87
  // Check for duplicate resource IDs within the package
88
88
  const idSet = new Set<string>();
89
89
  for (const res of pkg.listResources()) {
90
90
  const resId = res.getId();
91
91
  if (!resId) {
92
92
  issues.push({
93
- severity: ValidationSeverity.WARNING,
94
- message: `Resource "${res.getName()}" has no ID.`,
95
- packageName: pkg.getName(),
96
- resourceName: res.getName(),
97
- });
98
- continue;
99
- }
100
- if (idSet.has(resId)) {
101
- issues.push({
102
- severity: ValidationSeverity.ERROR,
103
- message: `Duplicate resource ID "${resId}" in package "${pkg.getName()}".`,
104
- packageName: pkg.getName(),
105
- resourceName: res.getName(),
106
- });
107
- }
108
- idSet.add(resId);
109
- globalResources.set(`${pkg.getId()}${resId}`, `${pkg.getName()}/${res.getName()}`);
110
- }
111
-
112
- // Validate components
113
- for (const comp of pkg.listComponents()) {
114
- const children = comp.listChildren();
115
- if (children.length === 0) {
116
- issues.push({
117
- severity: ValidationSeverity.INFO,
118
- message: `Component "${comp.getName()}" has no children.`,
119
- packageName: pkg.getName(),
120
- componentName: comp.getName(),
121
- });
122
- }
123
-
124
- for (const ctrl of comp.listControllers()) {
125
- if (ctrl.listPages().length === 0) {
126
- issues.push({
127
- severity: ValidationSeverity.WARNING,
128
- message: `Controller "${ctrl.getName()}" in "${comp.getName()}" has no pages.`,
129
- packageName: pkg.getName(),
130
- componentName: comp.getName(),
131
- });
132
- }
133
- }
134
-
93
+ severity: ValidationSeverity.WARNING,
94
+ message: `Resource "${res.getName()}" has no ID.`,
95
+ packageName: pkg.getName(),
96
+ resourceName: res.getName(),
97
+ });
98
+ continue;
99
+ }
100
+ if (idSet.has(resId)) {
101
+ issues.push({
102
+ severity: ValidationSeverity.ERROR,
103
+ message: `Duplicate resource ID "${resId}" in package "${pkg.getName()}".`,
104
+ packageName: pkg.getName(),
105
+ resourceName: res.getName(),
106
+ });
107
+ }
108
+ idSet.add(resId);
109
+ globalResources.set(`${pkg.getId()}${resId}`, `${pkg.getName()}/${res.getName()}`);
110
+ }
111
+
112
+ // Validate components
113
+ for (const comp of pkg.listComponents()) {
114
+ const children = comp.listChildren();
115
+ if (children.length === 0) {
116
+ issues.push({
117
+ severity: ValidationSeverity.INFO,
118
+ message: `Component "${comp.getName()}" has no children.`,
119
+ packageName: pkg.getName(),
120
+ componentName: comp.getName(),
121
+ });
122
+ }
123
+
124
+ for (const ctrl of comp.listControllers()) {
125
+ if (ctrl.listPages().length === 0) {
126
+ issues.push({
127
+ severity: ValidationSeverity.WARNING,
128
+ message: `Controller "${ctrl.getName()}" in "${comp.getName()}" has no pages.`,
129
+ packageName: pkg.getName(),
130
+ componentName: comp.getName(),
131
+ });
132
+ }
133
+ }
134
+
135
135
  // Validate src references in display objects
136
136
  for (const child of children) {
137
137
  const src = (child as HasOptionalSrc).getSrc?.();
138
138
  if (!src) continue;
139
-
140
- // ui://[8-char-packageId][resourceId] format
141
- if (src.startsWith('ui://')) {
142
- const idPart = src.slice(5);
143
- if (idPart.length > 8) {
144
- const pkgId = idPart.slice(0, 8);
145
- const resId = idPart.slice(8);
146
- const key = `${pkgId}${resId}`;
147
- if (!globalResources.has(key)) {
148
- issues.push({
149
- severity: ValidationSeverity.ERROR,
150
- message: `Broken reference "${src}" in "${child.getName()}" (component "${comp.getName()}").`,
151
- packageName: pkg.getName(),
152
- componentName: comp.getName(),
153
- resourceName: child.getName(),
154
- });
155
- }
156
- }
157
- }
158
- }
159
- }
160
- }
161
-
162
- // Store result
163
- const errors = issues.filter((i) => i.severity === ValidationSeverity.ERROR);
164
- const warnings = issues.filter((i) => i.severity === ValidationSeverity.WARNING);
165
- const infos = issues.filter((i) => i.severity === ValidationSeverity.INFO);
166
-
167
- const result: ValidationResult = {
168
- ok: errors.length === 0,
169
- errors,
170
- warnings,
171
- infos,
172
- };
173
-
174
- root.setExtras({ ...root.getExtras(), _validation: result });
175
-
176
- // Log summary
177
- const logger = doc.getLogger();
178
- if (errors.length) logger.warn(`validate: ${errors.length} error(s) found.`);
179
- if (warnings.length) logger.warn(`validate: ${warnings.length} warning(s) found.`);
180
- logger.info(`validate: ${issues.length} issue(s) total.`);
181
-
182
- if (options.throwOnError && errors.length > 0) {
183
- throw new Error(`Validation failed with ${errors.length} error(s):\n${errors.map((e) => ` - ${e.message}`).join('\n')}`);
184
- }
185
- });
186
- }
139
+
140
+ // ui://[8-char-packageId][resourceId] format
141
+ if (src.startsWith('ui://')) {
142
+ const idPart = src.slice(5);
143
+ if (idPart.length > 8) {
144
+ const pkgId = idPart.slice(0, 8);
145
+ const resId = idPart.slice(8);
146
+ const key = `${pkgId}${resId}`;
147
+ if (!globalResources.has(key)) {
148
+ issues.push({
149
+ severity: ValidationSeverity.ERROR,
150
+ message: `Broken reference "${src}" in "${child.getName()}" (component "${comp.getName()}").`,
151
+ packageName: pkg.getName(),
152
+ componentName: comp.getName(),
153
+ resourceName: child.getName(),
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // Store result
163
+ const errors = issues.filter((i) => i.severity === ValidationSeverity.ERROR);
164
+ const warnings = issues.filter((i) => i.severity === ValidationSeverity.WARNING);
165
+ const infos = issues.filter((i) => i.severity === ValidationSeverity.INFO);
166
+
167
+ const result: ValidationResult = {
168
+ ok: errors.length === 0,
169
+ errors,
170
+ warnings,
171
+ infos,
172
+ };
173
+
174
+ root.setExtras({ ...root.getExtras(), _validation: result });
175
+
176
+ // Log summary
177
+ const logger = doc.getLogger();
178
+ if (errors.length) logger.warn(`validate: ${errors.length} error(s) found.`);
179
+ if (warnings.length) logger.warn(`validate: ${warnings.length} warning(s) found.`);
180
+ logger.info(`validate: ${issues.length} issue(s) total.`);
181
+
182
+ if (options.throwOnError && errors.length > 0) {
183
+ throw new Error(`Validation failed with ${errors.length} error(s):\n${errors.map((e) => ` - ${e.message}`).join('\n')}`);
184
+ }
185
+ });
186
+ }