diff --git a/package.json b/package.json index 7c634fe0..81165fb9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@types/pluralize": "^0.0.29", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", - "@types/semver": "^7.3.10", + "@types/semver": "^7.7.1", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.3.1", "@typescript-eslint/parser": "^5.3.1", @@ -67,7 +67,7 @@ "jest": "^27.5.1", "jest-axe": "^6.0.0", "jsonc-parser": "^3.2.0", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "minimatch": "^3.1.2", "prettier": "^2.4.1", "react": "^18.3.1", diff --git a/packages/lib-core/CHANGELOG.md b/packages/lib-core/CHANGELOG.md index ca06d0ee..65cb0690 100644 --- a/packages/lib-core/CHANGELOG.md +++ b/packages/lib-core/CHANGELOG.md @@ -9,6 +9,8 @@ - Set build target to `es2021` and use new JSX transform `react-jsx` ([#295]) - Allow extending `customProperties` object type in `PluginRuntimeMetadata` ([#290]) - Update `yup` dependency to `^1.7.1` and improve handling of `Record` schemas ([#289]) +- Update `semver` dependency to `^7.7.3` ([#296]) +- Update `lodash` dependency to `^4.17.23` ([#296]) - Add `cloneDeepOnlyCloneableValues` function intended for cloning extension objects ([#294]) - Add `LoadedAndResolvedExtension` type ([#292]) @@ -108,3 +110,4 @@ [#292]: https://github.com/openshift/dynamic-plugin-sdk/pull/292 [#294]: https://github.com/openshift/dynamic-plugin-sdk/pull/294 [#295]: https://github.com/openshift/dynamic-plugin-sdk/pull/295 +[#296]: https://github.com/openshift/dynamic-plugin-sdk/pull/296 diff --git a/packages/lib-core/package.json b/packages/lib-core/package.json index 98ffe569..75d4ba15 100644 --- a/packages/lib-core/package.json +++ b/packages/lib-core/package.json @@ -31,8 +31,8 @@ "react": "^18 || ^19" }, "dependencies": { - "lodash": "^4.17.21", - "semver": "^7.3.7", + "lodash": "^4.17.23", + "semver": "^7.7.3", "uuid": "^8.3.2", "yup": "^1.7.1" } diff --git a/packages/lib-core/src/yup-schemas.test.ts b/packages/lib-core/src/yup-schemas.test.ts new file mode 100644 index 00000000..d36e43ad --- /dev/null +++ b/packages/lib-core/src/yup-schemas.test.ts @@ -0,0 +1,114 @@ +import { ValidationError } from 'yup'; +import { + semverStringSchema, + recordStringStringSchema, + recordStringSemverRangeSchema, +} from './yup-schemas'; + +describe('semverStringSchema', () => { + test.each([ + '0.0.1', + '0.1.0', + '1.0.0', + '1.0.0-0.3.7', + '1.0.0-x.7.z.92', + '1.2.3-beta.1', + '10.20.30', + ])('valid semver string: %s', async (semver) => { + await expect(semverStringSchema.validate(semver)).resolves.toBe(semver); + }); + + test.each([ + 'Trusty Tahr', + '1.0', + '1.0.0+build..123', + '1.0.0-beta+build+123', + '1.0.0-beta..1', + '1.2.3+build.123', + '1.2.3-beta.1+build.123', + 'v1.0.0', + ])('invalid semver string: %s', async (semver) => { + await expect(semverStringSchema.validate(semver)).rejects.toThrow(ValidationError); + }); + + test.each([null, undefined, Symbol('sym'), 42, {}, []])( + 'invalid semver string of wrong type: %s', + async (value) => { + await expect(semverStringSchema.validate(value)).rejects.toThrow(ValidationError); + }, + ); +}); + +describe('recordStringStringSchema', () => { + test('valid when undefined', async () => { + await expect(recordStringStringSchema.validate(undefined)).resolves.toBeUndefined(); + }); + + test('valid with string values', async () => { + const validObj = { + key1: 'value1', + key2: 'value2', + }; + + await expect(recordStringStringSchema.validate(validObj)).resolves.toEqual(validObj); + }); + + test('invalid with non-string values', async () => { + const invalidObj = { + key1: 'value1', + key2: 42, // Invalid non-string value + }; + + await expect(recordStringStringSchema.validate(invalidObj)).rejects.toThrow(ValidationError); + }); + + test('invalid with symbol keys', async () => { + const sym = Symbol('symKey'); + const invalidObj = { + key1: 'value1', + [sym]: 'value2', // Invalid symbol key + }; + + await expect(recordStringStringSchema.validate(invalidObj)).rejects.toThrow(ValidationError); + }); +}); + +describe('recordStringSemverRangeSchema', () => { + test('valid when undefined', async () => { + await expect(recordStringSemverRangeSchema.validate(undefined)).resolves.toBeUndefined(); + }); + + test('valid with correct semver ranges', async () => { + const validObj = { + packageA: '^1.0.0', + packageB: '>=2.0.0 <3.0.0', + packageC: '~1.2.3', + packageD: '1.2.3 - 2.3.4', + packageE: '1.2.3', + }; + + await expect(recordStringSemverRangeSchema.validate(validObj)).resolves.toEqual(validObj); + }); + + test('invalid with incorrect semver ranges', async () => { + const invalidObj = { + packageA: '^1.0.0', + packageB: 'not-a-semver', // Invalid semver range + }; + + await expect(recordStringSemverRangeSchema.validate(invalidObj)).rejects.toThrow( + ValidationError, + ); + }); + + test('invalid with non-string values', async () => { + const invalidObj = { + packageA: '^1.0.0', + packageB: 42, // Invalid non-string value + }; + + await expect(recordStringSemverRangeSchema.validate(invalidObj)).rejects.toThrow( + ValidationError, + ); + }); +}); diff --git a/packages/lib-core/src/yup-schemas.ts b/packages/lib-core/src/yup-schemas.ts index 6852df85..f7be3216 100644 --- a/packages/lib-core/src/yup-schemas.ts +++ b/packages/lib-core/src/yup-schemas.ts @@ -1,17 +1,17 @@ // TODO(vojtech): suppress false positive https://github.com/jsx-eslint/eslint-plugin-react/pull/3326 /* eslint-disable react/forbid-prop-types */ -import { array, object, string, ValidationError } from 'yup'; +import { array, object, string } from 'yup'; +import { valid, validRange } from 'semver'; /** * Schema for a valid semver string. - * - * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string */ -const semverStringSchema = string() +export const semverStringSchema = string() .required() - .matches( - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/, - ); + .test('semver-string', 'Must be a strictly valid semver string', (value: string) => { + // valid may return a cleaned version (e.g., by stripping out leading 'v') but we need value to always be clean + return valid(value, { loose: false }) === value; + }); /** * Schema for a valid plugin name. @@ -79,12 +79,12 @@ export const extensionSchema = object() export const extensionArraySchema = array().of(extensionSchema).required(); /** - * Schema for Record objects. + * Schema for `Record` objects. */ export const recordStringStringSchema = object() // Rejects non-objects and null .test( - 'property?: Record', - 'Must be either undefined OR an object with string keys and values', + 'Record | undefined', + 'Must be an object with string keys and string values OR undefined', (obj: object) => { // Allow undefined because these fields are optional if (obj === undefined) { @@ -93,23 +93,41 @@ export const recordStringStringSchema = object() // Rejects non-objects and null // Objects can have Symbol() as keys, so ensure there are none if (Object.getOwnPropertySymbols(obj).length > 0) { - return new ValidationError('Must be an object with no symbols as keys'); + return false; } // Object keys can only be symbols or strings, but since we've ruled out symbols, - // we can assume that all keys are strings. We just need to check the values now + // we can assume that all keys are strings. We just need to check the values now. return Object.values(obj).every((value) => typeof value === 'string'); }, ); +/** + * Schema for `Record` objects where the values are valid semver ranges. + */ +export const recordStringSemverRangeSchema = recordStringStringSchema.test( + 'Record | undefined', + 'Must be an object with string keys and semver range string values OR undefined', + (obj: object) => { + // Allow undefined because these fields are optional + if (obj === undefined) { + return true; + } + + // recordStringStringSchema ensures that all keys and values are strings, + // so we just need to check that all values are valid semver ranges now. + return Object.values(obj).every((value) => validRange(value) !== null); + }, +); + /** * Schema for `PluginRuntimeMetadata` objects. */ export const pluginRuntimeMetadataSchema = object().required().shape({ name: pluginNameSchema, version: semverStringSchema, - dependencies: recordStringStringSchema, - optionalDependencies: recordStringStringSchema, + dependencies: recordStringSemverRangeSchema, + optionalDependencies: recordStringSemverRangeSchema, customProperties: object(), }); diff --git a/packages/lib-utils/package.json b/packages/lib-utils/package.json index 0eebf70b..723c7df7 100644 --- a/packages/lib-utils/package.json +++ b/packages/lib-utils/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "immutable": "^3.8.2", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "pluralize": "^8.0.0", "typesafe-actions": "^4.4.2", "uuid": "^8.3.2" diff --git a/packages/lib-webpack/CHANGELOG.md b/packages/lib-webpack/CHANGELOG.md index ff98c42d..c18291c3 100644 --- a/packages/lib-webpack/CHANGELOG.md +++ b/packages/lib-webpack/CHANGELOG.md @@ -2,7 +2,9 @@ ## 5.1.0 - TBD -- Update yup to 1.7.1 ([#289]) +- Update `yup` dependency to `^1.7.1` and improve handling of `Record` schemas ([#289]) +- Update `semver` dependency to `^7.7.3` ([#296]) +- Update `lodash` dependency to `^4.17.23` ([#296]) ## 5.0.0 - 2026-01-06 @@ -72,3 +74,4 @@ [#259]: https://github.com/openshift/dynamic-plugin-sdk/pull/259 [#280]: https://github.com/openshift/dynamic-plugin-sdk/pull/280 [#289]: https://github.com/openshift/dynamic-plugin-sdk/pull/289 +[#296]: https://github.com/openshift/dynamic-plugin-sdk/pull/296 diff --git a/packages/lib-webpack/package.json b/packages/lib-webpack/package.json index be1916b5..0e99babd 100644 --- a/packages/lib-webpack/package.json +++ b/packages/lib-webpack/package.json @@ -29,8 +29,8 @@ "webpack": "^5.100.0" }, "dependencies": { - "lodash": "^4.17.21", - "semver": "^7.3.7", + "lodash": "^4.17.23", + "semver": "^7.7.3", "yup": "^1.7.1" }, "engines": { diff --git a/packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts b/packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts index c29f4998..f134b489 100644 --- a/packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts +++ b/packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts @@ -4,7 +4,6 @@ import type { RemotePluginManifest, } from '@openshift/dynamic-plugin-sdk/src/shared-webpack'; import { identity, isEmpty, mapValues, intersection } from 'lodash'; -import { validRange } from 'semver'; import type { ValidationError } from 'yup'; import type { WebpackPluginInstance, Compiler, container } from 'webpack'; import type { PluginBuildMetadata } from '../types/plugin'; @@ -181,22 +180,6 @@ export class DynamicRemotePlugin implements WebpackPluginInstance { ); } - // TODO(vojtech): remove this code once the validation library supports this natively - const invalidDepNames = Object.entries({ - ...(this.adaptedOptions.pluginMetadata.optionalDependencies ?? {}), - ...(this.adaptedOptions.pluginMetadata.dependencies ?? {}), - }).reduce( - (acc, [depName, versionRange]) => - versionRange && validRange(versionRange) ? acc : [...acc, depName], - [], - ); - - if (invalidDepNames.length > 0) { - throw new Error( - `Dependency values must be valid semver ranges: ${invalidDepNames.join(', ')}`, - ); - } - const overlapDependencyNames = intersection( Object.keys(this.adaptedOptions.pluginMetadata.optionalDependencies ?? {}), Object.keys(this.adaptedOptions.pluginMetadata.dependencies ?? {}), diff --git a/packages/sample-app/package.json b/packages/sample-app/package.json index bb6df919..8be09635 100644 --- a/packages/sample-app/package.json +++ b/packages/sample-app/package.json @@ -27,7 +27,7 @@ "cypress": "^12.17.3", "html-webpack-plugin": "^5.6.5", "http-server": "^14.1.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mini-css-extract-plugin": "^2.9.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index f27f428b..04de7e9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -959,7 +959,7 @@ __metadata: "@types/pluralize": ^0.0.29 "@types/react": ^18.3.0 "@types/react-dom": ^18.3.0 - "@types/semver": ^7.3.10 + "@types/semver": ^7.7.1 "@types/uuid": ^8.3.4 "@typescript-eslint/eslint-plugin": ^5.3.1 "@typescript-eslint/parser": ^5.3.1 @@ -982,7 +982,7 @@ __metadata: jest: ^27.5.1 jest-axe: ^6.0.0 jsonc-parser: ^3.2.0 - lodash: ^4.17.21 + lodash: ^4.17.23 minimatch: ^3.1.2 prettier: ^2.4.1 react: ^18.3.1 @@ -1029,7 +1029,7 @@ __metadata: cypress: ^12.17.3 html-webpack-plugin: ^5.6.5 http-server: ^14.1.1 - lodash: ^4.17.21 + lodash: ^4.17.23 mini-css-extract-plugin: ^2.9.4 react: ^18.3.1 react-dom: ^18.3.1 @@ -1134,7 +1134,7 @@ __metadata: resolution: "@openshift/dynamic-plugin-sdk-utils@workspace:packages/lib-utils" dependencies: immutable: ^3.8.2 - lodash: ^4.17.21 + lodash: ^4.17.23 pluralize: ^8.0.0 typesafe-actions: ^4.4.2 uuid: ^8.3.2 @@ -1152,8 +1152,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk-webpack@portal:../lib-webpack::locator=%40monorepo%2Fsample-app%40workspace%3Apackages%2Fsample-app" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 yup: ^1.7.1 peerDependencies: webpack: ^5.100.0 @@ -1164,8 +1164,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk-webpack@portal:../lib-webpack::locator=%40monorepo%2Fsample-plugin%40workspace%3Apackages%2Fsample-plugin" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 yup: ^1.7.1 peerDependencies: webpack: ^5.100.0 @@ -1176,8 +1176,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk-webpack@workspace:packages/lib-webpack" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 yup: ^1.7.1 peerDependencies: webpack: ^5.100.0 @@ -1188,8 +1188,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk@portal:../lib-core::locator=%40monorepo%2Fsample-app%40workspace%3Apackages%2Fsample-app" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 uuid: ^8.3.2 yup: ^1.7.1 peerDependencies: @@ -1201,8 +1201,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk@portal:../lib-core::locator=%40monorepo%2Fsample-plugin%40workspace%3Apackages%2Fsample-plugin" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 uuid: ^8.3.2 yup: ^1.7.1 peerDependencies: @@ -1214,8 +1214,8 @@ __metadata: version: 0.0.0-use.local resolution: "@openshift/dynamic-plugin-sdk@workspace:packages/lib-core" dependencies: - lodash: ^4.17.21 - semver: ^7.3.7 + lodash: ^4.17.23 + semver: ^7.7.3 uuid: ^8.3.2 yup: ^1.7.1 peerDependencies: @@ -1859,10 +1859,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.10": - version: 7.3.12 - resolution: "@types/semver@npm:7.3.12" - checksum: 35536b2fc5602904f21cae681f6c9498e177dab3f54ae37c92f9a1b7e43c35f18bcd81e1c98c1cf0d33ee046bb06c771e9928c1c00a401d56a03f56549252a15 +"@types/semver@npm:^7.7.1": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 76d218e414482a398148d5c28f2bfa017108869f3fc18cda379c9d8d062348f8b9653ae2fa8642d3b5b52e211928fe8be34f22da4e1f08245c84e0e51e040673 languageName: node linkType: hard @@ -6988,10 +6988,10 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.14, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.7.0, lodash@npm:~4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 +"lodash@npm:^4.17.14, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.23, lodash@npm:^4.7.0, lodash@npm:~4.17.15": + version: 4.17.23 + resolution: "lodash@npm:4.17.23" + checksum: 7daad39758a72872e94651630fbb54ba76868f904211089721a64516ce865506a759d9ad3d8ff22a2a49a50a09db5d27c36f22762d21766e47e3ba918d6d7bab languageName: node linkType: hard @@ -8966,7 +8966,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.x, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:7.x, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: