cht-conf 3.11.1 → 3.12.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/cli/usage.js +3 -0
- package/src/fn/validate-app-forms.js +12 -0
- package/src/fn/validate-collect-forms.js +12 -0
- package/src/fn/validate-contact-forms.js +12 -0
- package/src/fn/watch-project.js +16 -0
- package/src/lib/environment.js +5 -2
- package/src/lib/main.js +39 -11
- package/src/lib/upload-forms.js +0 -2
- package/src/lib/validate-forms.js +57 -39
- package/src/lib/validation/form/README.md +32 -0
- package/src/lib/validation/form/can-generate-xform.js +18 -0
- package/src/lib/validation/form/has-instance-id.js +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [3.12.0](https://github.com/medic/cht-conf/compare/v3.11.1...v3.12.0) (2022-06-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **#484:** add actions for validating forms ([f2a6a8e](https://github.com/medic/cht-conf/commit/f2a6a8ef75561426ce2a6b542cdf1a255c869c6e)), closes [#484](https://github.com/medic/cht-conf/issues/484) [#481](https://github.com/medic/cht-conf/issues/481)
|
|
7
|
+
|
|
1
8
|
## [3.11.1](https://github.com/medic/cht-conf/compare/v3.11.0...v3.11.1) (2022-06-13)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/cli/usage.js
CHANGED
|
@@ -63,6 +63,9 @@ ${bold('OPTIONS')}
|
|
|
63
63
|
--skip-translation-check
|
|
64
64
|
Skips checking message translations
|
|
65
65
|
|
|
66
|
+
--skip-validate
|
|
67
|
+
Skips form validation
|
|
68
|
+
|
|
66
69
|
--force
|
|
67
70
|
CAN BE DANGEROUS! Passes yes to all commands and any where that would prompt to overwrite changes will overwrite automatically.
|
|
68
71
|
`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const validateForms = require('../lib/validate-forms');
|
|
2
|
+
const environment = require('../lib/environment');
|
|
3
|
+
|
|
4
|
+
const validateAppForms = (forms) => {
|
|
5
|
+
return validateForms(environment.pathToProject, 'app', { forms });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
requiresInstance: false,
|
|
10
|
+
validateAppForms,
|
|
11
|
+
execute: () => validateAppForms(environment.extraArgs)
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const validateForms = require('../lib/validate-forms');
|
|
2
|
+
const environment = require('../lib/environment');
|
|
3
|
+
|
|
4
|
+
const validateCollectForms = (forms) => {
|
|
5
|
+
return validateForms(environment.pathToProject, 'collect', { forms });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
requiresInstance: false,
|
|
10
|
+
validateCollectForms,
|
|
11
|
+
execute: () => validateCollectForms(environment.extraArgs)
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const validateForms = require('../lib/validate-forms');
|
|
2
|
+
const environment = require('../lib/environment');
|
|
3
|
+
|
|
4
|
+
const validateContactForms = (forms) => {
|
|
5
|
+
return validateForms(environment.pathToProject, 'contact', { forms });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
requiresInstance: false,
|
|
10
|
+
validateContactForms,
|
|
11
|
+
execute: () => validateContactForms(environment.extraArgs)
|
|
12
|
+
};
|
package/src/fn/watch-project.js
CHANGED
|
@@ -5,6 +5,9 @@ const fs = require('fs');
|
|
|
5
5
|
const { error, warn, info } = require('../lib/log');
|
|
6
6
|
const Queue = require('queue-promise');
|
|
7
7
|
const watcher = require('@parcel/watcher');
|
|
8
|
+
const { validateAppForms } = require('./validate-app-forms');
|
|
9
|
+
const { validateContactForms } = require('./validate-contact-forms');
|
|
10
|
+
const { validateCollectForms } = require('./validate-collect-forms');
|
|
8
11
|
const { uploadAppForms } = require('./upload-app-forms');
|
|
9
12
|
const { uploadContactForms } = require('./upload-contact-forms');
|
|
10
13
|
const { uploadCollectForms } = require('./upload-collect-forms');
|
|
@@ -25,8 +28,17 @@ const watcherEvents = {
|
|
|
25
28
|
UpdateEvent: 'update'
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
const runValidation = (validation, forms) => {
|
|
32
|
+
if(environment.skipValidate) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return validation(forms);
|
|
36
|
+
};
|
|
37
|
+
|
|
28
38
|
const uploadInitialState = async (api) => {
|
|
29
39
|
await uploadResources();
|
|
40
|
+
await runValidation(validateAppForms, environment.extraArgs);
|
|
41
|
+
await runValidation(validateContactForms, environment.extraArgs);
|
|
30
42
|
await uploadAppForms(environment.extraArgs);
|
|
31
43
|
await uploadContactForms(environment.extraArgs);
|
|
32
44
|
await uploadCustomTranslations();
|
|
@@ -82,6 +94,7 @@ const processAppForm = (eventType, fileName) => {
|
|
|
82
94
|
let form = uploadForms.formFileMatcher(fileName);
|
|
83
95
|
if (form) {
|
|
84
96
|
eventQueue.enqueue(async () => {
|
|
97
|
+
await runValidation(validateAppForms, [form]);
|
|
85
98
|
await uploadAppForms([form]);
|
|
86
99
|
return fileName;
|
|
87
100
|
});
|
|
@@ -103,6 +116,7 @@ const processAppFormMedia = (formMediaDir, fileName) => {
|
|
|
103
116
|
const form = uploadForms.formMediaMatcher(formMediaDir);
|
|
104
117
|
if (form) {
|
|
105
118
|
eventQueue.enqueue(async () => {
|
|
119
|
+
await runValidation(validateAppForms,[form]);
|
|
106
120
|
await uploadAppForms([form]);
|
|
107
121
|
return fileName;
|
|
108
122
|
});
|
|
@@ -127,6 +141,7 @@ const processContactForm = (eventType, fileName) => {
|
|
|
127
141
|
form = uploadForms.formFileMatcher(fileName);
|
|
128
142
|
if (form) {
|
|
129
143
|
eventQueue.enqueue(async () => {
|
|
144
|
+
await runValidation(validateContactForms,[form]);
|
|
130
145
|
await uploadContactForms([form]);
|
|
131
146
|
return fileName;
|
|
132
147
|
});
|
|
@@ -142,6 +157,7 @@ const processCollectForm = (eventType, fileName) => {
|
|
|
142
157
|
let form = uploadForms.formFileMatcher(fileName);
|
|
143
158
|
if (form) {
|
|
144
159
|
eventQueue.enqueue(async () => {
|
|
160
|
+
await runValidation(validateCollectForms,[form]);
|
|
145
161
|
await uploadCollectForms([form]);
|
|
146
162
|
return fileName;
|
|
147
163
|
});
|
package/src/lib/environment.js
CHANGED
|
@@ -12,7 +12,8 @@ const initialize = (
|
|
|
12
12
|
extraArgs,
|
|
13
13
|
apiUrl,
|
|
14
14
|
force,
|
|
15
|
-
skipTranslationCheck
|
|
15
|
+
skipTranslationCheck,
|
|
16
|
+
skipValidate
|
|
16
17
|
) => {
|
|
17
18
|
if (state.initialized) {
|
|
18
19
|
throw Error('environment is already initialized');
|
|
@@ -26,7 +27,8 @@ const initialize = (
|
|
|
26
27
|
isArchiveMode,
|
|
27
28
|
pathToProject,
|
|
28
29
|
force,
|
|
29
|
-
skipTranslationCheck
|
|
30
|
+
skipTranslationCheck,
|
|
31
|
+
skipValidate
|
|
30
32
|
});
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -51,6 +53,7 @@ module.exports = {
|
|
|
51
53
|
get apiUrl() { return getState('apiUrl'); },
|
|
52
54
|
get force() { return getState('force'); },
|
|
53
55
|
get skipTranslationCheck() { return getState('skipTranslationCheck'); },
|
|
56
|
+
get skipValidate() { return getState('skipValidate'); },
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* Return `true` if the environment **seems** to be production.
|
package/src/lib/main.js
CHANGED
|
@@ -21,6 +21,9 @@ const defaultActions = [
|
|
|
21
21
|
'convert-app-forms',
|
|
22
22
|
'convert-collect-forms',
|
|
23
23
|
'convert-contact-forms',
|
|
24
|
+
'validate-app-forms',
|
|
25
|
+
'validate-collect-forms',
|
|
26
|
+
'validate-contact-forms',
|
|
24
27
|
'backup-all-forms',
|
|
25
28
|
'delete-all-forms',
|
|
26
29
|
'upload-app-forms',
|
|
@@ -38,6 +41,9 @@ const defaultArchiveActions = [
|
|
|
38
41
|
'convert-app-forms',
|
|
39
42
|
'convert-collect-forms',
|
|
40
43
|
'convert-contact-forms',
|
|
44
|
+
'validate-app-forms',
|
|
45
|
+
'validate-collect-forms',
|
|
46
|
+
'validate-contact-forms',
|
|
41
47
|
'upload-app-forms',
|
|
42
48
|
'upload-collect-forms',
|
|
43
49
|
'upload-contact-forms',
|
|
@@ -123,19 +129,40 @@ module.exports = async (argv, env) => {
|
|
|
123
129
|
// Build up actions
|
|
124
130
|
//
|
|
125
131
|
let actions = cmdArgs._;
|
|
126
|
-
if (
|
|
132
|
+
if (actions.length) {
|
|
133
|
+
const unsupported = actions.filter(a => !supportedActions.includes(a));
|
|
134
|
+
if(unsupported.length) {
|
|
135
|
+
throw new Error(`Unsupported action(s): ${unsupported.join(' ')}`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
127
138
|
actions = !cmdArgs.archive ? defaultActions : defaultArchiveActions;
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
const unsupported = actions.filter(a => !supportedActions.includes(a));
|
|
131
|
-
if(unsupported.length) {
|
|
132
|
-
throw new Error(`Unsupported action(s): ${unsupported.join(' ')}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
141
|
if (cmdArgs['skip-git-check']) {
|
|
136
142
|
actions = actions.filter(a => a !== 'check-git');
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
const skipValidate = cmdArgs['skip-validate'];
|
|
146
|
+
if(skipValidate) {
|
|
147
|
+
warn('Skipping all form validation.');
|
|
148
|
+
const validateActions = [
|
|
149
|
+
'validate-app-forms',
|
|
150
|
+
'validate-collect-forms',
|
|
151
|
+
'validate-contact-forms'
|
|
152
|
+
];
|
|
153
|
+
actions = actions.filter(action => !validateActions.includes(action));
|
|
154
|
+
} else {
|
|
155
|
+
const addFormValidationIfNecessary = (formType) => {
|
|
156
|
+
const updateFormsIndex = actions.indexOf(`upload-${formType}-forms`);
|
|
157
|
+
if (updateFormsIndex >= 0 && actions.indexOf(`validate-${formType}-forms`) < 0) {
|
|
158
|
+
actions.splice(updateFormsIndex, 0, `validate-${formType}-forms`);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
addFormValidationIfNecessary('app');
|
|
162
|
+
addFormValidationIfNecessary('collect');
|
|
163
|
+
addFormValidationIfNecessary('contact');
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
actions = actions.map(actionName => {
|
|
140
167
|
const action = require(`../fn/${actionName}`);
|
|
141
168
|
|
|
@@ -157,9 +184,9 @@ module.exports = async (argv, env) => {
|
|
|
157
184
|
//
|
|
158
185
|
const projectName = fs.path.basename(pathToProject);
|
|
159
186
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
const apiUrl = getApiUrl(cmdArgs, env);
|
|
188
|
+
const requiresInstance = actions.some(action => action.requiresInstance);
|
|
189
|
+
if (requiresInstance) {
|
|
163
190
|
if (!apiUrl) {
|
|
164
191
|
throw new Error('Failed to obtain a url to the API');
|
|
165
192
|
}
|
|
@@ -177,10 +204,11 @@ module.exports = async (argv, env) => {
|
|
|
177
204
|
extraArgs,
|
|
178
205
|
apiUrl,
|
|
179
206
|
cmdArgs.force,
|
|
180
|
-
cmdArgs['skip-translation-check']
|
|
207
|
+
cmdArgs['skip-translation-check'],
|
|
208
|
+
skipValidate
|
|
181
209
|
);
|
|
182
210
|
|
|
183
|
-
if (apiUrl) {
|
|
211
|
+
if (requiresInstance && apiUrl) {
|
|
184
212
|
await api().available();
|
|
185
213
|
}
|
|
186
214
|
|
package/src/lib/upload-forms.js
CHANGED
|
@@ -13,7 +13,6 @@ const {
|
|
|
13
13
|
readTitleFrom,
|
|
14
14
|
readIdFrom
|
|
15
15
|
} = require('./forms-utils');
|
|
16
|
-
const validateForms = require('./validate-forms');
|
|
17
16
|
|
|
18
17
|
const SUPPORTED_PROPERTIES = ['context', 'icon', 'title', 'xml2sms', 'subject_key', 'hidden_fields'];
|
|
19
18
|
const FORM_EXTENSTION = '.xml';
|
|
@@ -40,7 +39,6 @@ const formMediaMatcher = (formMediaDir) => {
|
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
const execute = async (projectDir, subDirectory, options) => {
|
|
43
|
-
await validateForms(projectDir, subDirectory, options);
|
|
44
42
|
const db = pouch();
|
|
45
43
|
if (!options) options = {};
|
|
46
44
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
@@ -1,64 +1,82 @@
|
|
|
1
|
-
const api = require('./api');
|
|
2
1
|
const argsFormFilter = require('./args-form-filter');
|
|
2
|
+
const environment = require('./environment');
|
|
3
3
|
const fs = require('./sync-fs');
|
|
4
4
|
const log = require('./log');
|
|
5
5
|
const {
|
|
6
6
|
getFormDir,
|
|
7
|
-
getFormFilePaths
|
|
8
|
-
formHasInstanceId
|
|
7
|
+
getFormFilePaths
|
|
9
8
|
} = require('./forms-utils');
|
|
10
9
|
|
|
10
|
+
const VALIDATIONS_PATH = fs.path.resolve(__dirname, './validation/form');
|
|
11
|
+
const validations = fs.readdir(VALIDATIONS_PATH)
|
|
12
|
+
.filter(name => name.endsWith('.js'))
|
|
13
|
+
.map(validationName => {
|
|
14
|
+
const validation = require(fs.path.join(VALIDATIONS_PATH, validationName));
|
|
15
|
+
validation.name = validationName;
|
|
16
|
+
if(!Object.hasOwnProperty.call(validation, 'requiresInstance')) {
|
|
17
|
+
validation.requiresInstance = true;
|
|
18
|
+
}
|
|
19
|
+
if(!Object.hasOwnProperty.call(validation, 'skipFurtherValidation')) {
|
|
20
|
+
validation.skipFurtherValidation = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return validation;
|
|
24
|
+
})
|
|
25
|
+
.sort((a, b) => {
|
|
26
|
+
if(a.skipFurtherValidation && !b.skipFurtherValidation) {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
if(!a.skipFurtherValidation && b.skipFurtherValidation) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
return a.name.localeCompare(b.name);
|
|
33
|
+
});
|
|
34
|
+
|
|
11
35
|
module.exports = async (projectDir, subDirectory, options={}) => {
|
|
12
36
|
|
|
13
37
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
14
38
|
if(!formsDir) {
|
|
15
|
-
log.info(`Forms dir not found: ${
|
|
39
|
+
log.info(`Forms dir not found: ${projectDir}/forms/${subDirectory}`);
|
|
16
40
|
return;
|
|
17
41
|
}
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
let
|
|
43
|
+
const instanceProvided = environment.apiUrl;
|
|
44
|
+
let validationSkipped = false;
|
|
21
45
|
|
|
22
46
|
const fileNames = argsFormFilter(formsDir, '.xml', options);
|
|
23
|
-
|
|
47
|
+
|
|
48
|
+
let errorFound = false;
|
|
49
|
+
for(const fileName of fileNames) {
|
|
24
50
|
log.info(`Validating form: ${fileName}…`);
|
|
25
51
|
|
|
26
|
-
const { xformPath
|
|
52
|
+
const { xformPath } = getFormFilePaths(formsDir, fileName);
|
|
27
53
|
const xml = fs.read(xformPath);
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
const valParams = { xformPath, xmlStr: xml };
|
|
56
|
+
for(const validation of validations) {
|
|
57
|
+
if(validation.requiresInstance && !instanceProvided) {
|
|
58
|
+
validationSkipped = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = await validation.execute(valParams);
|
|
63
|
+
if(output.warnings) {
|
|
64
|
+
output.warnings.forEach(warnMsg => log.warn(warnMsg));
|
|
65
|
+
}
|
|
66
|
+
if(output.errors && output.errors.length) {
|
|
67
|
+
output.errors.forEach(errorMsg => log.error(errorMsg));
|
|
68
|
+
errorFound = true;
|
|
69
|
+
if(validation.skipFurtherValidation) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
|
-
// Once all the fails were checked raise an exception if there were errors
|
|
42
|
-
let errors = [];
|
|
43
|
-
if (!validateFormsPassed) {
|
|
44
|
-
errors.push('One or more forms appears to have errors found by the API validation endpoint.');
|
|
45
|
-
}
|
|
46
|
-
if (!idValidationsPassed) {
|
|
47
|
-
errors.push('One or more forms appears to be missing <meta><instanceID/></meta> node.');
|
|
48
|
-
}
|
|
49
|
-
if (errors.length) {
|
|
50
|
-
// the blank spaces are a trick to align the errors in the log ;)
|
|
51
|
-
throw new Error(errors.join('\n '));
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
'no form will be checked before push');
|
|
61
|
-
validateEndpointNotFoundLogged = true; // Just log the message once
|
|
76
|
+
if(validationSkipped) {
|
|
77
|
+
log.warn('Some validations have been skipped because they require a CHT instance.');
|
|
78
|
+
}
|
|
79
|
+
if(errorFound) {
|
|
80
|
+
throw new Error('One or more forms have failed validation.');
|
|
62
81
|
}
|
|
63
|
-
return resp;
|
|
64
82
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Form validations
|
|
2
|
+
|
|
3
|
+
Form validations should be added to this directory.
|
|
4
|
+
|
|
5
|
+
Their module should look something like this:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
module.exports = {
|
|
9
|
+
requiresInstance: false,
|
|
10
|
+
skipFurtherValidation: true,
|
|
11
|
+
execute: async ({ xformPath, xmlStr }) => {
|
|
12
|
+
...
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Module Exports
|
|
18
|
+
|
|
19
|
+
| Field | Required | Notes |
|
|
20
|
+
|-------------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
21
|
+
| `requiresInstance` | Optional, defaults to `true` | The validation needs the user to have provided a instance location, e.g. via `--local` or `--instance` |
|
|
22
|
+
| `skipFurtherValidation` | Optional, defaults to `false` | When `true`, additional validations will not be performed on a form that fails the current validation. The goal is to avoid unnecessary noise in the validation log for forms that are grossly invalid (e.g. XML files that are not actually xForms at all). |
|
|
23
|
+
| `execute` | Required | The function that is run when the validation is executed. The provided input argument contains the form XML string. |
|
|
24
|
+
|
|
25
|
+
## Result
|
|
26
|
+
|
|
27
|
+
The result has the following format:
|
|
28
|
+
|
|
29
|
+
| Field | Notes |
|
|
30
|
+
|----------|----------------------------------------------------------------------------------------------------|
|
|
31
|
+
| warnings | Array containing the descriptions of any warnings. |
|
|
32
|
+
| errors | Array containing the descriptions of any errors. If this is empty, the form has passed validation. |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const api = require('../../api');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
execute: async ({ xformPath, xmlStr }) => {
|
|
5
|
+
const warnings = [];
|
|
6
|
+
const errors = [];
|
|
7
|
+
try {
|
|
8
|
+
const resp = await api().formsValidate(xmlStr);
|
|
9
|
+
if (resp.formsValidateEndpointFound === false) {
|
|
10
|
+
warnings.push('Form validation endpoint not found in your version of CHT Core, no form will be checked before push');
|
|
11
|
+
}
|
|
12
|
+
} catch (err) {
|
|
13
|
+
errors.push(`Error found while validating "${xformPath}". Validation response: ${err.message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { errors, warnings };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { formHasInstanceId } = require('../../forms-utils');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
requiresInstance: false,
|
|
5
|
+
skipFurtherValidation: true,
|
|
6
|
+
execute: async({ xformPath, xmlStr }) => {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if(!formHasInstanceId(xmlStr)) {
|
|
9
|
+
errors.push(`Form at ${xformPath} appears to be missing <meta><instanceID/></meta> node. This form will not work on CHT webapp.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return { errors, warnings: [] };
|
|
13
|
+
}
|
|
14
|
+
};
|