Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/lib-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>` 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])

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions packages/lib-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
114 changes: 114 additions & 0 deletions packages/lib-core/src/yup-schemas.test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
46 changes: 32 additions & 14 deletions packages/lib-core/src/yup-schemas.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -79,12 +79,12 @@ export const extensionSchema = object()
export const extensionArraySchema = array().of(extensionSchema).required();

/**
* Schema for Record<string, string> objects.
* Schema for `Record<string, string>` objects.
*/
export const recordStringStringSchema = object() // Rejects non-objects and null
.test(
'property?: Record<string, string>',
'Must be either undefined OR an object with string keys and values',
'Record<string, string> | 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) {
Expand All @@ -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<string, string>` objects where the values are valid semver ranges.
*/
export const recordStringSemverRangeSchema = recordStringStringSchema.test(
'Record<string, semver_range> | 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(),
});

Expand Down
2 changes: 1 addition & 1 deletion packages/lib-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion packages/lib-webpack/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>` schemas ([#289])
- Update `semver` dependency to `^7.7.3` ([#296])
- Update `lodash` dependency to `^4.17.23` ([#296])

## 5.0.0 - 2026-01-06

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions packages/lib-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
17 changes: 0 additions & 17 deletions packages/lib-webpack/src/webpack/DynamicRemotePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string[]>(
(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 ?? {}),
Expand Down
2 changes: 1 addition & 1 deletion packages/sample-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading