forked from teambit/bit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathversion-validator.ts
More file actions
310 lines (299 loc) · 13.9 KB
/
version-validator.ts
File metadata and controls
310 lines (299 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import { PJV } from 'package-json-validator';
import R from 'ramda';
import { lt } from 'semver';
import packageNameValidate from 'validate-npm-package-name';
import { BitId, BitIds } from '../bit-id';
import { DEPENDENCIES_FIELDS } from '../constants';
import { SchemaName } from '../consumer/component/component-schema';
import { Dependencies } from '../consumer/component/dependencies';
import { DEPENDENCIES_TYPES } from '../consumer/component/dependencies/dependencies';
import PackageJsonFile from '../consumer/component/package-json-file';
import { getArtifactsFiles } from '../consumer/component/sources/artifact-files';
import { componentOverridesForbiddenFields } from '../consumer/config/component-overrides';
import { nonPackageJsonFields } from '../consumer/config/consumer-overrides';
import { ExtensionDataEntry, ExtensionDataList } from '../consumer/config/extension-data';
import GeneralError from '../error/general-error';
import { isValidPath } from '../utils';
import { PathLinux } from '../utils/path';
import validateType from '../utils/validate-type';
import VersionInvalid from './exceptions/version-invalid';
import Version from './models/version';
/**
* make sure a Version instance is correct. throw an exceptions if it is not.
*/
export default function validateVersionInstance(version: Version): void {
const message = `unable to save Version object${
version.componentId ? ` of "${version.componentId.toString()}"` : ''
}`;
const validateBitId = (bitId: BitId, field: string, validateVersion = true, validateScope = true) => {
if (validateVersion && !bitId.hasVersion()) {
throw new VersionInvalid(`${message}, the ${field} ${bitId.toString()} does not have a version`);
}
if (validateScope && !bitId.scope) {
throw new VersionInvalid(`${message}, the ${field} ${bitId.toString()} does not have a scope`);
}
};
const _validatePackageDependencyValue = (packageValue, packageName) => {
// don't use semver.valid and semver.validRange to validate the package version because it
// can be also a URL, Git URL or Github URL. see here: https://docs.npmjs.com/files/package.json#dependencies
validateType(message, packageValue, `version of "${packageName}"`, 'string');
};
/**
* Validate that the package name and version are valid
* @param {*} packageName
* @param {*} packageVersion
*/
const _validatePackageDependency = (packageVersion, packageName) => {
const packageNameValidateResult = packageNameValidate(packageName);
if (!packageNameValidateResult.validForNewPackages && !packageNameValidateResult.validForOldPackages) {
const errors = packageNameValidateResult.errors || [];
throw new VersionInvalid(`${packageName} is invalid package name, errors: ${errors.join()}`);
}
_validatePackageDependencyValue(packageVersion, packageName);
};
const _validatePackageDependencies = (packageDependencies) => {
validateType(message, packageDependencies, 'packageDependencies', 'object');
R.forEachObjIndexed(_validatePackageDependency, packageDependencies);
};
const validateFile = (file, field: 'file' | 'dist-file' | 'artifact') => {
validateType(message, file, field, 'object');
if (!isValidPath(file.relativePath)) {
throw new VersionInvalid(`${message}, the ${field} ${file.relativePath} is invalid`);
}
if (!file.name && field !== 'artifact') {
throw new VersionInvalid(`${message}, the ${field} ${file.relativePath} is missing the name attribute`);
}
const ref = field === 'artifact' ? file.ref : file.file;
if (!ref) throw new VersionInvalid(`${message}, the ${field} ${file.relativePath} is missing the hash`);
if (file.name) validateType(message, file.name, `${field}.name`, 'string');
validateType(message, ref, `${field}.file`, 'object');
validateType(message, ref.hash, `${field}.file.hash`, 'string');
};
const _validateExtension = (extension: ExtensionDataEntry) => {
if (extension.extensionId) {
validateBitId(extension.extensionId, `extensions.${extension.extensionId.toString()}`, true, false);
}
// Make sure we don't insert the remove sign ("-") by mistake to the models
if (extension.config) {
validateType(message, extension.config, 'extension.config', 'object');
}
};
const validateArtifacts = (extensions: ExtensionDataList) => {
const artifactsFiles = getArtifactsFiles(extensions);
artifactsFiles.forEach((artifacts) => {
artifacts.refs.map((artifact) => validateFile(artifact, 'artifact'));
const filesPaths = artifacts.refs.map((artifact) => artifact.relativePath);
const duplicateArtifacts = filesPaths.filter(
(file) => filesPaths.filter((f) => file.toLowerCase() === f.toLowerCase()).length > 1
);
if (duplicateArtifacts.length) {
throw new VersionInvalid(
`${message} the following artifact files are duplicated ${duplicateArtifacts.join(', ')}`
);
}
});
};
const validateNoDuplicateExtensionEntry = (extensions: ExtensionDataList) => {
const existingMap = new Map();
const duplications: string[] = [];
extensions.forEach((ext) => {
const stringId = ext.stringId;
if (!stringId) {
return;
}
if (existingMap.has(stringId)) {
duplications.push(stringId);
} else {
existingMap.set(stringId, true);
}
});
if (duplications.length) {
// a bug causing duplicate aspects was fixed in https://github.com/teambit/bit/pull/6567
// all Version objects snapped before 0.0.882 might have this bug. ignore them.
if (!version.bitVersion || lt(version.bitVersion, '0.0.882')) {
return;
}
throw new VersionInvalid(`${message} the following extensions entries are duplicated ${duplications.join(', ')}`);
}
};
const _validateExtensions = (extensions: ExtensionDataList) => {
if (extensions) {
validateNoDuplicateExtensionEntry(extensions);
extensions.map(_validateExtension);
validateArtifacts(extensions);
}
};
if (!version.mainFile) throw new VersionInvalid(`${message}, the mainFile is missing`);
if (!isValidPath(version.mainFile)) {
throw new VersionInvalid(`${message}, the mainFile ${version.mainFile} is invalid`);
}
if (!version.files || !version.files.length) throw new VersionInvalid(`${message}, the files are missing`);
let foundMainFile = false;
validateType(message, version.files, 'files', 'array');
const filesPaths: PathLinux[] = [];
version.files.forEach((file) => {
validateFile(file, 'file');
filesPaths.push(file.relativePath);
if (file.relativePath === version.mainFile) foundMainFile = true;
});
if (!foundMainFile) {
throw new VersionInvalid(
`${message}, unable to find the mainFile ${version.mainFile} in the following files list: ${filesPaths.join(
', '
)}`
);
}
const duplicateFiles = filesPaths.filter(
(file) => filesPaths.filter((f) => file.toLowerCase() === f.toLowerCase()).length > 1
);
if (duplicateFiles.length) {
throw new VersionInvalid(`${message} the following files are duplicated ${duplicateFiles.join(', ')}`);
}
_validatePackageDependencies(version.packageDependencies);
_validatePackageDependencies(version.devPackageDependencies);
_validatePackageDependencies(version.peerPackageDependencies);
_validateExtensions(version.extensions);
DEPENDENCIES_TYPES.forEach((dependenciesType) => {
if (!(version[dependenciesType] instanceof Dependencies)) {
throw new VersionInvalid(
`${message}, ${dependenciesType} must be an instance of Dependencies, got ${typeof version[dependenciesType]}`
);
}
});
version.dependencies.validate(version.componentId);
version.devDependencies.validate(version.componentId);
if (!version.dependencies.isEmpty() && !version.flattenedDependencies.length) {
throw new VersionInvalid(`${message}, it has dependencies but its flattenedDependencies is empty`);
}
const validateFlattenedDependencies = (dependencies: BitIds) => {
validateType(message, dependencies, 'dependencies', 'array');
dependencies.forEach((dependency) => {
if (dependency.constructor.name !== BitId.name) {
throw new VersionInvalid(`${message}, a flattenedDependency expected to be BitId, got ${typeof dependency}`);
}
if (!dependency.hasVersion()) {
throw new VersionInvalid(
`${message}, the flattenedDependency ${dependency.toString()} does not have a version`
);
}
});
};
validateFlattenedDependencies(version.flattenedDependencies);
// extensions can be duplicate with other dependencies type. e.g. "test" can have "compile" as a
// dependency and extensionDependency. we can't remove it from extDep, otherwise, the ext won't
// be running
const allDependenciesIds = version.getDependenciesIdsExcludeExtensions();
const depsDuplications = allDependenciesIds.findDuplicationsIgnoreVersion();
if (!R.isEmpty(depsDuplications)) {
const duplicationStr = Object.keys(depsDuplications)
.map(
(id) => `"${id}" shows as the following: ${depsDuplications[id].map((depId) => depId.toString()).join(', ')} `
)
.join('\n');
throw new GeneralError(`some dependencies are duplicated, see details below.
if you added a dependency to "overrides" configuration with a plus sign, make sure to add it with a minus sign in the other dependency type
for example, { dependencies: { "bar/foo": "+" }, devDependencies: { "bar/foo": "-" } }
${duplicationStr}`);
// todo: once decided how to address duplicate dependencies, remove the line above and uncomment the line below
// throw new VersionInvalid(`${message}, some dependencies are duplicated:\n${duplicationStr}`);
}
if (!version.log) throw new VersionInvalid(`${message}, the log object is missing`);
validateType(message, version.log, 'log', 'object');
if (version.bindingPrefix) {
validateType(message, version.bindingPrefix, 'bindingPrefix', 'string');
}
const npmSpecs = PJV.getSpecMap('npm');
const validatePackageJsonField = (fieldName: string, fieldValue: any): string | null | undefined => {
if (!npmSpecs[fieldName]) {
// it's not a standard package.json field, can't validate
return null;
}
const validateResult = PJV.validateType(fieldName, npmSpecs[fieldName], fieldValue);
if (!validateResult.length) return null;
return validateResult.join(', ');
};
const validateOverrides = (fieldValue: Record<string, any>, fieldName: string) => {
const field = `overrides.${fieldName}`;
if (DEPENDENCIES_FIELDS.includes(fieldName)) {
validateType(message, fieldValue, field, 'object');
Object.keys(fieldValue).forEach((key) => {
validateType(message, key, `property name of ${field}`, 'string');
_validatePackageDependencyValue(fieldValue[key], key);
});
} else if (!nonPackageJsonFields.includes(fieldName)) {
const result = validatePackageJsonField(fieldName, fieldValue);
if (result) {
throw new VersionInvalid(
`${message}, "${field}" is a package.json field but is not compliant with npm requirements. ${result}`
);
}
}
};
Object.keys(version.overrides).forEach((field) => {
if (componentOverridesForbiddenFields.includes(field)) {
throw new VersionInvalid(`${message}, the "overrides" has a forbidden key "${field}"`);
}
validateOverrides(version.overrides[field], field);
});
validateType(message, version.packageJsonChangedProps, 'packageJsonChangedProps', 'object');
const forbiddenPackageJsonProps = PackageJsonFile.propsNonUserChangeable();
Object.keys(version.packageJsonChangedProps).forEach((prop) => {
validateType(message, prop, 'property name of packageJson', 'string');
if (forbiddenPackageJsonProps.includes(prop)) {
throw new VersionInvalid(`${message}, the packageJsonChangedProps should not override the prop ${prop}`);
}
const result = validatePackageJsonField(prop, version.packageJsonChangedProps[prop]);
if (result) {
throw new VersionInvalid(
`${message}, the generated package.json field "${prop}" is not compliant with npm requirements. ${result}`
);
}
});
if (version.parents) {
version.parents.forEach((parent) => {
if (parent.isEqual(version.hash())) {
throw new VersionInvalid(`${message}, its parent has the same hash as itself: ${parent.toString()}`);
}
});
}
const schema = version.schema || SchemaName.Legacy;
if (!version.isLegacy) {
const fieldsForSchemaCheck = ['compiler', 'tester', 'dists', 'mainDistFile'];
const fieldsForSchemaCheckNotEmpty = [
'customResolvedPaths',
'compilerPackageDependencies',
'testerPackageDependencies',
];
fieldsForSchemaCheck.forEach((field) => {
if (version[field]) {
throw new VersionInvalid(`${message}, the ${field} field is not permitted according to schema "${schema}"`);
}
});
fieldsForSchemaCheckNotEmpty.forEach((field) => {
if (version[field] && !R.isEmpty(version[field])) {
throw new VersionInvalid(
`${message}, the ${field} field is cannot have values according to schema "${schema}"`
);
}
});
['dependencies', 'devDependencies'].forEach((dependenciesField) => {
const deps: Dependencies = version[dependenciesField];
deps.dependencies.forEach((dep) => {
if (dep.relativePaths.length) {
throw new VersionInvalid(
`${message}, the ${dependenciesField} should not have relativePaths according to schema "${schema}"`
);
}
});
});
}
if (version.isLegacy) {
// mainly to make sure that all Harmony components are saved with schema
// if they don't have schema, they'll fail on this test
if (version.extensions && version.extensions.some((e) => e.name && e.name === 'teambit.pipelines/builder')) {
throw new VersionInvalid(
`${message}, the extensions should not include "teambit.pipelines/builder" as of the schema "${schema}"`
);
}
}
}