From fbaa042ebfeff35b8f3a1a15eaf2fad79e8da2bf Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 15:16:21 -0400 Subject: [PATCH 1/6] fork package-json-validator to a utils component --- .bitmap | 14 + components/package-json-validator/index.ts | 2 + .../package-json-validator.docs.mdx | 6 + .../package-json-validator.ts | 339 ++++++++++++++++++ src/scope/models/version.spec.ts | 2 +- src/scope/version-validator.ts | 4 +- 6 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 components/package-json-validator/index.ts create mode 100644 components/package-json-validator/package-json-validator.docs.mdx create mode 100644 components/package-json-validator/package-json-validator.ts diff --git a/.bitmap b/.bitmap index c653466005bc..7550546568b6 100644 --- a/.bitmap +++ b/.bitmap @@ -1052,6 +1052,20 @@ "mainFile": "index.ts", "rootDir": "scopes/api-reference/renderers/grouped-schema-nodes-overview-summary" }, + "package-json-validator": { + "name": "package-json-validator", + "scope": "", + "version": "", + "defaultScope": "teambit.toolbox", + "mainFile": "index.ts", + "rootDir": "components/package-json-validator", + "config": { + "bitdev.node/node-env@2.0.2": {}, + "teambit.envs/envs": { + "env": "bitdev.node/node-env" + } + } + }, "panels": { "name": "panels", "scope": "teambit.ui-foundation", diff --git a/components/package-json-validator/index.ts b/components/package-json-validator/index.ts new file mode 100644 index 000000000000..4962f791de36 --- /dev/null +++ b/components/package-json-validator/index.ts @@ -0,0 +1,2 @@ +export { PackageJsonValidator } from './package-json-validator'; +export type { ValidationOptions, ValidationResult, SpecField, SpecMap, Formats } from './package-json-validator'; diff --git a/components/package-json-validator/package-json-validator.docs.mdx b/components/package-json-validator/package-json-validator.docs.mdx new file mode 100644 index 000000000000..a94c31d5a858 --- /dev/null +++ b/components/package-json-validator/package-json-validator.docs.mdx @@ -0,0 +1,6 @@ +--- +labels: ['PackageJsonValidator', 'module'] +description: 'A PackageJsonValidator module.' +--- + +A packageJsonValidator module. diff --git a/components/package-json-validator/package-json-validator.ts b/components/package-json-validator/package-json-validator.ts new file mode 100644 index 000000000000..19a8847e19f8 --- /dev/null +++ b/components/package-json-validator/package-json-validator.ts @@ -0,0 +1,339 @@ +export type SpecMap = Record; + +export type SpecField = { + type?: string; + types?: string[]; + required?: boolean; + warning?: boolean; + recommended?: boolean; + format?: RegExp; + validate?: (name: string, value: any) => string[]; + or?: string; +}; + +export type ValidationResult = { + valid: boolean; + critical?: string; + errors?: string[]; + warnings?: string[]; + recommendations?: string[]; +}; + +export const Formats = { + packageFormat: /^[a-zA-Z0-9@\/][a-zA-Z0-9@\/\.\-_]*$/, + versionFormat: /^[0-9]+\.[0-9]+[0-9+a-zA-Z\.\-]+$/, + urlFormat: /^https*:\/\/[a-z.\-0-9]+/, + emailFormat: /\S+@\S+/, +}; + +export type ValidationOptions = { + warnings?: boolean; + recommendations?: boolean; +}; + +export class PackageJsonValidator { + static validate(data: string, specName: string = 'npm', options: ValidationOptions = {}): ValidationResult { + let parsed: any; + try { + parsed = this.parse(data); + } catch (error: any) { + return { valid: false, critical: `Invalid JSON - ${error.toString()}` }; + } + + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return { valid: false, critical: 'Invalid JSON - not an object' }; + } + + const specs = this.getSpecMap(specName); + + if (!specs) { + return { valid: false, critical: `Invalid specification name: ${specName}` }; + } + + const map = specs[specName]; + if (!map) { + return { valid: false, critical: `Invalid specification: ${specName}` }; + } + + let errors: string[] = []; + const warnings: string[] = []; + const recommendations: string[] = []; + + Object.keys(map).forEach((name) => { + const field = map[name]; + const fieldValue = parsed[name]; + + if (fieldValue === undefined && (!field.or || (field.or && parsed[field.or] === undefined))) { + if (field.required) { + errors.push(`Missing required field: ${name}`); + } else if (field.warning) { + warnings.push(`Missing recommended field: ${name}`); + } else if (field.recommended) { + recommendations.push(`Missing optional field: ${name}`); + } + return; + } + + if (fieldValue === undefined) { + return; + } + + if (field.types || field.type) { + const typeErrors = PackageJsonValidator.validateType(name, field, fieldValue); + if (typeErrors.length > 0) { + errors = errors.concat(typeErrors); + return; + } + } + + if (field.format && !field.format.test(fieldValue)) { + errors.push(`Value for field ${name}, ${fieldValue} does not match format: ${field.format}`); + } + + if (typeof field.validate === 'function') { + const validationErrors = field.validate(name, fieldValue); + errors = errors.concat(validationErrors); + } + }); + + const result: ValidationResult = { valid: errors.length === 0 }; + if (errors.length > 0) { + result.errors = errors; + } + if (options.warnings !== false && warnings.length > 0) { + result.warnings = warnings; + } + if (options.recommendations !== false && recommendations.length > 0) { + result.recommendations = recommendations; + } + + return result; + } + + private static parse(data: string): any { + try { + const parsed = JSON.parse(data); + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return `Invalid JSON - not an object: ${typeof parsed}`; + } + return parsed; + } catch (e: any) { + return `Invalid JSON - ${e.message}`; + } + } + + static getSpecMap(specName: string): SpecMap | null { + const specs: { [key: string]: SpecMap } = { + npm: { + name: { type: 'string', required: true, format: Formats.packageFormat }, + version: { type: 'string', required: true, format: Formats.versionFormat }, + description: { type: 'string', warning: true }, + keywords: { type: 'array', warning: true }, + homepage: { type: 'string', recommended: true, format: Formats.urlFormat }, + bugs: { warning: true, validate: this.validateUrlOrMailto }, + licenses: { type: 'array', warning: true, validate: this.validateUrlTypes, or: 'license' }, + license: { type: 'string' }, + author: { warning: true, validate: this.validatePeople }, + contributors: { warning: true, validate: this.validatePeople }, + files: { type: 'array' }, + main: { type: 'string' }, + bin: { types: ['string', 'object'] }, + man: { types: ['string', 'array'] }, + directories: { type: 'object' }, + repository: { types: ['string', 'object'], warning: true, validate: this.validateUrlTypes, or: 'repositories' }, + scripts: { type: 'object' }, + config: { type: 'object' }, + dependencies: { type: 'object', recommended: true, validate: this.validateDependencies }, + devDependencies: { type: 'object', validate: this.validateDependencies }, + bundledDependencies: { type: 'array' }, + bundleDependencies: { type: 'array' }, + optionalDependencies: { type: 'object', validate: this.validateDependencies }, + engines: { type: 'object', recommended: true }, + engineStrict: { type: 'boolean' }, + os: { type: 'array' }, + cpu: { type: 'array' }, + preferGlobal: { type: 'boolean' }, + private: { type: 'boolean' }, + publishConfig: { type: 'object' }, + }, + 'commonjs_1.0': { + name: { type: 'string', required: true, format: Formats.packageFormat }, + description: { type: 'string', required: true }, + version: { type: 'string', required: true, format: Formats.versionFormat }, + keywords: { type: 'array', required: true }, + maintainers: { type: 'array', required: true, validate: this.validatePeople }, + contributors: { type: 'array', required: true, validate: this.validatePeople }, + bugs: { type: 'string', required: true, validate: this.validateUrlOrMailto }, + licenses: { type: 'array', required: true, validate: this.validateUrlTypes }, + repositories: { type: 'object', required: true, validate: this.validateUrlTypes }, + dependencies: { type: 'object', required: true, validate: this.validateDependencies }, + homepage: { type: 'string', format: Formats.urlFormat }, + os: { type: 'array' }, + cpu: { type: 'array' }, + engine: { type: 'array' }, + builtin: { type: 'boolean' }, + directories: { type: 'object' }, + implements: { type: 'array' }, + scripts: { type: 'object' }, + checksums: { type: 'object' }, + }, + 'commonjs_1.1': { + name: { type: 'string', required: true, format: Formats.packageFormat }, + version: { type: 'string', required: true, format: Formats.versionFormat }, + main: { type: 'string', required: true }, + directories: { type: 'object', required: true }, + maintainers: { type: 'array', warning: true, validate: this.validatePeople }, + description: { type: 'string', warning: true }, + licenses: { type: 'array', warning: true, validate: this.validateUrlTypes }, + bugs: { type: 'string', warning: true, validate: this.validateUrlOrMailto }, + keywords: { type: 'array' }, + repositories: { type: 'array', validate: this.validateUrlTypes }, + contributors: { type: 'array', validate: this.validatePeople }, + dependencies: { type: 'object', validate: this.validateDependencies }, + homepage: { type: 'string', warning: true, format: Formats.urlFormat }, + os: { type: 'array' }, + cpu: { type: 'array' }, + engine: { type: 'array' }, + builtin: { type: 'boolean' }, + implements: { type: 'array' }, + scripts: { type: 'object' }, + overlay: { type: 'object' }, + checksums: { type: 'object' }, + }, + }; + + return specs[specName] || null; + } + + static validateUrlOrMailto(name: string, obj: any): string[] { + const errors: string[] = []; + if (typeof obj === 'string') { + if (!Formats.urlFormat.test(obj) && !Formats.emailFormat.test(obj)) { + errors.push(`${name} should be an email or a url`); + } + } else if (typeof obj === 'object') { + if (!obj.email && !obj.url && !obj.mail && !obj.web) { + errors.push(`${name} field should have one of: email, url, mail, web`); + } else { + if (obj.email && !Formats.emailFormat.test(obj.email)) { + errors.push(`Email not valid for ${name}: ${obj.email}`); + } + if (obj.mail && !Formats.emailFormat.test(obj.mail)) { + errors.push(`Email not valid for ${name}: ${obj.mail}`); + } + if (obj.url && !Formats.urlFormat.test(obj.url)) { + errors.push(`Url not valid for ${name}: ${obj.url}`); + } + if (obj.web && !Formats.urlFormat.test(obj.web)) { + errors.push(`Url not valid for ${name}: ${obj.web}`); + } + } + } else { + errors.push(`Type for field ${name} should be a string or an object`); + } + return errors; + } + + static validatePeople(name: string, obj: any): string[] { + const errors: string[] = []; + + function validatePerson(person: any) { + if (typeof person === 'string') { + const authorRegex = /^([^<\(\s]+[^<\(]*)?(\s*<(.*?)>)?(\s*\((.*?)\))?/; + const authorFields = authorRegex.exec(person); + if (authorFields) { + const authorName = authorFields[1]; + const authorEmail = authorFields[3]; + const authorUrl = authorFields[5]; + validatePerson({ name: authorName, email: authorEmail, url: authorUrl }); + } + } else if (typeof person === 'object') { + if (!person.name) { + errors.push(`${name} field should have name`); + } + if (person.email && !Formats.emailFormat.test(person.email)) { + errors.push(`Email not valid for ${name}: ${person.email}`); + } + if (person.url && !Formats.urlFormat.test(person.url)) { + errors.push(`Url not valid for ${name}: ${person.url}`); + } + if (person.web && !Formats.urlFormat.test(person.web)) { + errors.push(`Url not valid for ${name}: ${person.web}`); + } + } else { + errors.push('People field must be an object or a string'); + } + } + + if (Array.isArray(obj)) { + obj.forEach((person) => validatePerson(person)); + } else { + validatePerson(obj); + } + return errors; + } + + static validateDependencies(name: string, deps: { [key: string]: string }): string[] { + const errors: string[] = []; + Object.keys(deps).forEach((pkg) => { + if (!Formats.packageFormat.test(pkg)) { + errors.push(`Invalid dependency package name: ${pkg}`); + } + if (!PackageJsonValidator.isValidVersionRange(deps[pkg])) { + errors.push(`Invalid version range for dependency ${pkg}: ${deps[pkg]}`); + } + }); + return errors; + } + + static isValidVersionRange(version: string): boolean { + return ( + /^[\^<>=~]{0,2}[0-9.x]+/.test(version) || + Formats.urlFormat.test(version) || + version === '*' || + version === '' || + version === 'latest' || + version.startsWith('git') + ); + } + + static validateUrlTypes(name: string, obj: any): string[] { + const errors: string[] = []; + + function validateUrlType(item: any) { + if (!item.type) { + errors.push(`${name} field should have type`); + } + if (!item.url) { + errors.push(`${name} field should have url`); + } + if (item.url && !Formats.urlFormat.test(item.url)) { + errors.push(`Url not valid for ${name}: ${item.url}`); + } + } + + if (typeof obj === 'string') { + if (!Formats.urlFormat.test(obj)) { + errors.push(`Url not valid for ${name}: ${obj}`); + } + } else if (Array.isArray(obj)) { + obj.forEach((item) => validateUrlType(item)); + } else if (typeof obj === 'object') { + validateUrlType(obj); + } else { + errors.push(`Type for field ${name} should be a string or an object`); + } + + return errors; + } + + static validateType(name: string, field: { types?: string[]; type?: string }, value: any): string[] { + const errors: string[] = []; + const validFieldTypes = field.types || [field.type]; + const valueType = Array.isArray(value) ? 'array' : typeof value; + if (!validFieldTypes.includes(valueType)) { + errors.push(`Type for field ${name} was expected to be ${validFieldTypes.join(' or ')}, not ${valueType}`); + } + return errors; + } +} diff --git a/src/scope/models/version.spec.ts b/src/scope/models/version.spec.ts index 4c5b6a7ff3ef..0b7da61a789d 100644 --- a/src/scope/models/version.spec.ts +++ b/src/scope/models/version.spec.ts @@ -243,7 +243,7 @@ describe('Version', () => { version.overrides = { bin: 'my-file.js' }; expect(validateFunc).to.not.throw(); }); - it('should show the original error from package-json-validator when overrides has a package.json field that is non-compliant npm value', () => { + it.only('should show the original error from package-json-validator when overrides has a package.json field that is non-compliant npm value', () => { version.overrides = { scripts: false }; expect(validateFunc).to.throw('Type for field scripts, was expected to be object, not boolean'); }); diff --git a/src/scope/version-validator.ts b/src/scope/version-validator.ts index 189d50789570..81d7e77b6c03 100644 --- a/src/scope/version-validator.ts +++ b/src/scope/version-validator.ts @@ -1,4 +1,4 @@ -import { PJV } from 'package-json-validator'; +import { PackageJsonValidator as PJV } from '@teambit/package-json-validator'; import R from 'ramda'; import { lt, gt } from 'semver'; import packageNameValidate from 'validate-npm-package-name'; @@ -227,7 +227,7 @@ ${duplicationStr}`); } const npmSpecs = PJV.getSpecMap('npm'); const validatePackageJsonField = (fieldName: string, fieldValue: any): string | null | undefined => { - if (!npmSpecs[fieldName]) { + if (!npmSpecs?.[fieldName]) { // it's not a standard package.json field, can't validate return null; } From 7bfe07dbbce0060c31a2207e22b83c4ff473b2f2 Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 15:59:08 -0400 Subject: [PATCH 2/6] fix test --- components/package-json-validator/package-json-validator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/package-json-validator/package-json-validator.ts b/components/package-json-validator/package-json-validator.ts index 19a8847e19f8..1516de91031c 100644 --- a/components/package-json-validator/package-json-validator.ts +++ b/components/package-json-validator/package-json-validator.ts @@ -37,7 +37,7 @@ export class PackageJsonValidator { try { parsed = this.parse(data); } catch (error: any) { - return { valid: false, critical: `Invalid JSON - ${error.toString()}` }; + return { valid: false, critical: error.toString() }; } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { @@ -332,7 +332,7 @@ export class PackageJsonValidator { const validFieldTypes = field.types || [field.type]; const valueType = Array.isArray(value) ? 'array' : typeof value; if (!validFieldTypes.includes(valueType)) { - errors.push(`Type for field ${name} was expected to be ${validFieldTypes.join(' or ')}, not ${valueType}`); + errors.push(`Type for field ${name}, was expected to be ${validFieldTypes.join(' or ')}, not ${valueType}`); } return errors; } From a69ec3b29c89b4702566cfb31aa906ef3437cde7 Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 15:59:15 -0400 Subject: [PATCH 3/6] fix test --- .bitmap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.bitmap b/.bitmap index 7550546568b6..fc3e7c21abe5 100644 --- a/.bitmap +++ b/.bitmap @@ -1060,9 +1060,9 @@ "mainFile": "index.ts", "rootDir": "components/package-json-validator", "config": { - "bitdev.node/node-env@2.0.2": {}, + "teambit.harmony/node": {}, "teambit.envs/envs": { - "env": "bitdev.node/node-env" + "env": "teambit.harmony/node" } } }, From 5d6b803491d2bfa055499084310b5668554f7bec Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 15:59:42 -0400 Subject: [PATCH 4/6] fix test --- src/scope/models/version.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scope/models/version.spec.ts b/src/scope/models/version.spec.ts index 0b7da61a789d..4c5b6a7ff3ef 100644 --- a/src/scope/models/version.spec.ts +++ b/src/scope/models/version.spec.ts @@ -243,7 +243,7 @@ describe('Version', () => { version.overrides = { bin: 'my-file.js' }; expect(validateFunc).to.not.throw(); }); - it.only('should show the original error from package-json-validator when overrides has a package.json field that is non-compliant npm value', () => { + it('should show the original error from package-json-validator when overrides has a package.json field that is non-compliant npm value', () => { version.overrides = { scripts: false }; expect(validateFunc).to.throw('Type for field scripts, was expected to be object, not boolean'); }); From 07cbe6361e6b61e5f0ead42dc897ea2143ffd8d3 Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 16:02:29 -0400 Subject: [PATCH 5/6] add comments --- components/package-json-validator/package-json-validator.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/package-json-validator/package-json-validator.ts b/components/package-json-validator/package-json-validator.ts index 1516de91031c..0cd9daec07f4 100644 --- a/components/package-json-validator/package-json-validator.ts +++ b/components/package-json-validator/package-json-validator.ts @@ -1,3 +1,7 @@ +// this implementation is a typescript version of the original package-json-validator (https://github.com/TechNickAI/package.json-validator) +// we have forked it since the original package has a security vulnerability because of the optimist dependency they use for their cli tool +// we have removed the cli tool and only kept the validation logic + export type SpecMap = Record; export type SpecField = { From 07d40d16def17fa4a945a4e7830461d369d149dd Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 2 May 2024 17:13:54 -0400 Subject: [PATCH 6/6] remove component - install package --- .bitmap | 14 - components/package-json-validator/index.ts | 2 - .../package-json-validator.docs.mdx | 6 - .../package-json-validator.ts | 343 ------------------ package.json | 1 + src/scope/version-validator.ts | 2 +- 6 files changed, 2 insertions(+), 366 deletions(-) delete mode 100644 components/package-json-validator/index.ts delete mode 100644 components/package-json-validator/package-json-validator.docs.mdx delete mode 100644 components/package-json-validator/package-json-validator.ts diff --git a/.bitmap b/.bitmap index fc3e7c21abe5..c653466005bc 100644 --- a/.bitmap +++ b/.bitmap @@ -1052,20 +1052,6 @@ "mainFile": "index.ts", "rootDir": "scopes/api-reference/renderers/grouped-schema-nodes-overview-summary" }, - "package-json-validator": { - "name": "package-json-validator", - "scope": "", - "version": "", - "defaultScope": "teambit.toolbox", - "mainFile": "index.ts", - "rootDir": "components/package-json-validator", - "config": { - "teambit.harmony/node": {}, - "teambit.envs/envs": { - "env": "teambit.harmony/node" - } - } - }, "panels": { "name": "panels", "scope": "teambit.ui-foundation", diff --git a/components/package-json-validator/index.ts b/components/package-json-validator/index.ts deleted file mode 100644 index 4962f791de36..000000000000 --- a/components/package-json-validator/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PackageJsonValidator } from './package-json-validator'; -export type { ValidationOptions, ValidationResult, SpecField, SpecMap, Formats } from './package-json-validator'; diff --git a/components/package-json-validator/package-json-validator.docs.mdx b/components/package-json-validator/package-json-validator.docs.mdx deleted file mode 100644 index a94c31d5a858..000000000000 --- a/components/package-json-validator/package-json-validator.docs.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -labels: ['PackageJsonValidator', 'module'] -description: 'A PackageJsonValidator module.' ---- - -A packageJsonValidator module. diff --git a/components/package-json-validator/package-json-validator.ts b/components/package-json-validator/package-json-validator.ts deleted file mode 100644 index 0cd9daec07f4..000000000000 --- a/components/package-json-validator/package-json-validator.ts +++ /dev/null @@ -1,343 +0,0 @@ -// this implementation is a typescript version of the original package-json-validator (https://github.com/TechNickAI/package.json-validator) -// we have forked it since the original package has a security vulnerability because of the optimist dependency they use for their cli tool -// we have removed the cli tool and only kept the validation logic - -export type SpecMap = Record; - -export type SpecField = { - type?: string; - types?: string[]; - required?: boolean; - warning?: boolean; - recommended?: boolean; - format?: RegExp; - validate?: (name: string, value: any) => string[]; - or?: string; -}; - -export type ValidationResult = { - valid: boolean; - critical?: string; - errors?: string[]; - warnings?: string[]; - recommendations?: string[]; -}; - -export const Formats = { - packageFormat: /^[a-zA-Z0-9@\/][a-zA-Z0-9@\/\.\-_]*$/, - versionFormat: /^[0-9]+\.[0-9]+[0-9+a-zA-Z\.\-]+$/, - urlFormat: /^https*:\/\/[a-z.\-0-9]+/, - emailFormat: /\S+@\S+/, -}; - -export type ValidationOptions = { - warnings?: boolean; - recommendations?: boolean; -}; - -export class PackageJsonValidator { - static validate(data: string, specName: string = 'npm', options: ValidationOptions = {}): ValidationResult { - let parsed: any; - try { - parsed = this.parse(data); - } catch (error: any) { - return { valid: false, critical: error.toString() }; - } - - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - return { valid: false, critical: 'Invalid JSON - not an object' }; - } - - const specs = this.getSpecMap(specName); - - if (!specs) { - return { valid: false, critical: `Invalid specification name: ${specName}` }; - } - - const map = specs[specName]; - if (!map) { - return { valid: false, critical: `Invalid specification: ${specName}` }; - } - - let errors: string[] = []; - const warnings: string[] = []; - const recommendations: string[] = []; - - Object.keys(map).forEach((name) => { - const field = map[name]; - const fieldValue = parsed[name]; - - if (fieldValue === undefined && (!field.or || (field.or && parsed[field.or] === undefined))) { - if (field.required) { - errors.push(`Missing required field: ${name}`); - } else if (field.warning) { - warnings.push(`Missing recommended field: ${name}`); - } else if (field.recommended) { - recommendations.push(`Missing optional field: ${name}`); - } - return; - } - - if (fieldValue === undefined) { - return; - } - - if (field.types || field.type) { - const typeErrors = PackageJsonValidator.validateType(name, field, fieldValue); - if (typeErrors.length > 0) { - errors = errors.concat(typeErrors); - return; - } - } - - if (field.format && !field.format.test(fieldValue)) { - errors.push(`Value for field ${name}, ${fieldValue} does not match format: ${field.format}`); - } - - if (typeof field.validate === 'function') { - const validationErrors = field.validate(name, fieldValue); - errors = errors.concat(validationErrors); - } - }); - - const result: ValidationResult = { valid: errors.length === 0 }; - if (errors.length > 0) { - result.errors = errors; - } - if (options.warnings !== false && warnings.length > 0) { - result.warnings = warnings; - } - if (options.recommendations !== false && recommendations.length > 0) { - result.recommendations = recommendations; - } - - return result; - } - - private static parse(data: string): any { - try { - const parsed = JSON.parse(data); - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - return `Invalid JSON - not an object: ${typeof parsed}`; - } - return parsed; - } catch (e: any) { - return `Invalid JSON - ${e.message}`; - } - } - - static getSpecMap(specName: string): SpecMap | null { - const specs: { [key: string]: SpecMap } = { - npm: { - name: { type: 'string', required: true, format: Formats.packageFormat }, - version: { type: 'string', required: true, format: Formats.versionFormat }, - description: { type: 'string', warning: true }, - keywords: { type: 'array', warning: true }, - homepage: { type: 'string', recommended: true, format: Formats.urlFormat }, - bugs: { warning: true, validate: this.validateUrlOrMailto }, - licenses: { type: 'array', warning: true, validate: this.validateUrlTypes, or: 'license' }, - license: { type: 'string' }, - author: { warning: true, validate: this.validatePeople }, - contributors: { warning: true, validate: this.validatePeople }, - files: { type: 'array' }, - main: { type: 'string' }, - bin: { types: ['string', 'object'] }, - man: { types: ['string', 'array'] }, - directories: { type: 'object' }, - repository: { types: ['string', 'object'], warning: true, validate: this.validateUrlTypes, or: 'repositories' }, - scripts: { type: 'object' }, - config: { type: 'object' }, - dependencies: { type: 'object', recommended: true, validate: this.validateDependencies }, - devDependencies: { type: 'object', validate: this.validateDependencies }, - bundledDependencies: { type: 'array' }, - bundleDependencies: { type: 'array' }, - optionalDependencies: { type: 'object', validate: this.validateDependencies }, - engines: { type: 'object', recommended: true }, - engineStrict: { type: 'boolean' }, - os: { type: 'array' }, - cpu: { type: 'array' }, - preferGlobal: { type: 'boolean' }, - private: { type: 'boolean' }, - publishConfig: { type: 'object' }, - }, - 'commonjs_1.0': { - name: { type: 'string', required: true, format: Formats.packageFormat }, - description: { type: 'string', required: true }, - version: { type: 'string', required: true, format: Formats.versionFormat }, - keywords: { type: 'array', required: true }, - maintainers: { type: 'array', required: true, validate: this.validatePeople }, - contributors: { type: 'array', required: true, validate: this.validatePeople }, - bugs: { type: 'string', required: true, validate: this.validateUrlOrMailto }, - licenses: { type: 'array', required: true, validate: this.validateUrlTypes }, - repositories: { type: 'object', required: true, validate: this.validateUrlTypes }, - dependencies: { type: 'object', required: true, validate: this.validateDependencies }, - homepage: { type: 'string', format: Formats.urlFormat }, - os: { type: 'array' }, - cpu: { type: 'array' }, - engine: { type: 'array' }, - builtin: { type: 'boolean' }, - directories: { type: 'object' }, - implements: { type: 'array' }, - scripts: { type: 'object' }, - checksums: { type: 'object' }, - }, - 'commonjs_1.1': { - name: { type: 'string', required: true, format: Formats.packageFormat }, - version: { type: 'string', required: true, format: Formats.versionFormat }, - main: { type: 'string', required: true }, - directories: { type: 'object', required: true }, - maintainers: { type: 'array', warning: true, validate: this.validatePeople }, - description: { type: 'string', warning: true }, - licenses: { type: 'array', warning: true, validate: this.validateUrlTypes }, - bugs: { type: 'string', warning: true, validate: this.validateUrlOrMailto }, - keywords: { type: 'array' }, - repositories: { type: 'array', validate: this.validateUrlTypes }, - contributors: { type: 'array', validate: this.validatePeople }, - dependencies: { type: 'object', validate: this.validateDependencies }, - homepage: { type: 'string', warning: true, format: Formats.urlFormat }, - os: { type: 'array' }, - cpu: { type: 'array' }, - engine: { type: 'array' }, - builtin: { type: 'boolean' }, - implements: { type: 'array' }, - scripts: { type: 'object' }, - overlay: { type: 'object' }, - checksums: { type: 'object' }, - }, - }; - - return specs[specName] || null; - } - - static validateUrlOrMailto(name: string, obj: any): string[] { - const errors: string[] = []; - if (typeof obj === 'string') { - if (!Formats.urlFormat.test(obj) && !Formats.emailFormat.test(obj)) { - errors.push(`${name} should be an email or a url`); - } - } else if (typeof obj === 'object') { - if (!obj.email && !obj.url && !obj.mail && !obj.web) { - errors.push(`${name} field should have one of: email, url, mail, web`); - } else { - if (obj.email && !Formats.emailFormat.test(obj.email)) { - errors.push(`Email not valid for ${name}: ${obj.email}`); - } - if (obj.mail && !Formats.emailFormat.test(obj.mail)) { - errors.push(`Email not valid for ${name}: ${obj.mail}`); - } - if (obj.url && !Formats.urlFormat.test(obj.url)) { - errors.push(`Url not valid for ${name}: ${obj.url}`); - } - if (obj.web && !Formats.urlFormat.test(obj.web)) { - errors.push(`Url not valid for ${name}: ${obj.web}`); - } - } - } else { - errors.push(`Type for field ${name} should be a string or an object`); - } - return errors; - } - - static validatePeople(name: string, obj: any): string[] { - const errors: string[] = []; - - function validatePerson(person: any) { - if (typeof person === 'string') { - const authorRegex = /^([^<\(\s]+[^<\(]*)?(\s*<(.*?)>)?(\s*\((.*?)\))?/; - const authorFields = authorRegex.exec(person); - if (authorFields) { - const authorName = authorFields[1]; - const authorEmail = authorFields[3]; - const authorUrl = authorFields[5]; - validatePerson({ name: authorName, email: authorEmail, url: authorUrl }); - } - } else if (typeof person === 'object') { - if (!person.name) { - errors.push(`${name} field should have name`); - } - if (person.email && !Formats.emailFormat.test(person.email)) { - errors.push(`Email not valid for ${name}: ${person.email}`); - } - if (person.url && !Formats.urlFormat.test(person.url)) { - errors.push(`Url not valid for ${name}: ${person.url}`); - } - if (person.web && !Formats.urlFormat.test(person.web)) { - errors.push(`Url not valid for ${name}: ${person.web}`); - } - } else { - errors.push('People field must be an object or a string'); - } - } - - if (Array.isArray(obj)) { - obj.forEach((person) => validatePerson(person)); - } else { - validatePerson(obj); - } - return errors; - } - - static validateDependencies(name: string, deps: { [key: string]: string }): string[] { - const errors: string[] = []; - Object.keys(deps).forEach((pkg) => { - if (!Formats.packageFormat.test(pkg)) { - errors.push(`Invalid dependency package name: ${pkg}`); - } - if (!PackageJsonValidator.isValidVersionRange(deps[pkg])) { - errors.push(`Invalid version range for dependency ${pkg}: ${deps[pkg]}`); - } - }); - return errors; - } - - static isValidVersionRange(version: string): boolean { - return ( - /^[\^<>=~]{0,2}[0-9.x]+/.test(version) || - Formats.urlFormat.test(version) || - version === '*' || - version === '' || - version === 'latest' || - version.startsWith('git') - ); - } - - static validateUrlTypes(name: string, obj: any): string[] { - const errors: string[] = []; - - function validateUrlType(item: any) { - if (!item.type) { - errors.push(`${name} field should have type`); - } - if (!item.url) { - errors.push(`${name} field should have url`); - } - if (item.url && !Formats.urlFormat.test(item.url)) { - errors.push(`Url not valid for ${name}: ${item.url}`); - } - } - - if (typeof obj === 'string') { - if (!Formats.urlFormat.test(obj)) { - errors.push(`Url not valid for ${name}: ${obj}`); - } - } else if (Array.isArray(obj)) { - obj.forEach((item) => validateUrlType(item)); - } else if (typeof obj === 'object') { - validateUrlType(obj); - } else { - errors.push(`Type for field ${name} should be a string or an object`); - } - - return errors; - } - - static validateType(name: string, field: { types?: string[]; type?: string }, value: any): string[] { - const errors: string[] = []; - const validFieldTypes = field.types || [field.type]; - const valueType = Array.isArray(value) ? 'array' : typeof value; - if (!validFieldTypes.includes(valueType)) { - errors.push(`Type for field ${name}, was expected to be ${validFieldTypes.join(' or ')}, not ${valueType}`); - } - return errors; - } -} diff --git a/package.json b/package.json index 943d56132c24..a50c060d6487 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@teambit/toolbox.fs.readdir-skip-system-files": "~0.0.2", "@teambit/scope.modules.find-scope-path": "~0.0.1", "@teambit/toolbox.object.sorter": "~0.0.2", + "@teambit/pkg.package-json.validator": "0.0.1", "@viz-js/viz": "^3.4.0", "@types/normalize-path": "^3.0.0", "array-difference": "0.0.2", diff --git a/src/scope/version-validator.ts b/src/scope/version-validator.ts index 81d7e77b6c03..575483528e41 100644 --- a/src/scope/version-validator.ts +++ b/src/scope/version-validator.ts @@ -1,4 +1,4 @@ -import { PackageJsonValidator as PJV } from '@teambit/package-json-validator'; +import { PackageJsonValidator as PJV } from '@teambit/pkg.package-json.validator'; import R from 'ramda'; import { lt, gt } from 'semver'; import packageNameValidate from 'validate-npm-package-name';