From e42fbb985f791e808553ea95f73cde8b949f6b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serkan=20=C3=96ZAL?= Date: Wed, 29 Jan 2025 01:57:04 +0300 Subject: [PATCH] feat(opentelemetry-instrumentation): replace `semver` package with internal semantic versioning check implementation (#5305) Co-authored-by: Trent Mick --- CHANGELOG.md | 1 + .../package.json | 1 - .../src/platform/node/instrumentation.ts | 2 +- .../src/semver.ts | 618 ++++++++++++++++++ .../src/types.ts | 8 +- .../test/common/semver.test.ts | 96 +++ .../common/third-party/node-semver/LICENSE | 15 + .../third-party/node-semver/range-exclude.js | 107 +++ .../third-party/node-semver/range-include.js | 127 ++++ package-lock.json | 2 - 10 files changed, 969 insertions(+), 8 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation/src/semver.ts create mode 100644 experimental/packages/opentelemetry-instrumentation/test/common/semver.test.ts create mode 100644 experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/LICENSE create mode 100644 experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-exclude.js create mode 100644 experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-include.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 69abd9e274e..f1925505a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se * refactor(sdk-trace-base): rename `BasicTracerProvider.activeSpanProcessor` private property. [#5211](https://github.com/open-telemetry/opentelemetry-js/pull/5211) @david-luna * chore(selenium-tests): remove internal selenium-tests/ package, it wasn't being used @trentm * chore: update typescript `module` compiler option to `node16`. [#5347](https://github.com/open-telemetry/opentelemetry-js/pull/5347) @david-luna +* feat(opentelemetry-instrumentation): replace `semver` package with internal semantic versioning check implementation to get rid of `semver` package initialization overhead especially in the AWS Lambda environment during coldstart [#5305](https://github.com/open-telemetry/opentelemetry-js/pull/5305) @serkan-ozal ## 1.30.0 diff --git a/experimental/packages/opentelemetry-instrumentation/package.json b/experimental/packages/opentelemetry-instrumentation/package.json index 30a496d3465..42af4865966 100644 --- a/experimental/packages/opentelemetry-instrumentation/package.json +++ b/experimental/packages/opentelemetry-instrumentation/package.json @@ -74,7 +74,6 @@ "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index c99d52ea2c7..a74b687f4ad 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -17,7 +17,7 @@ import * as types from '../../types'; import * as path from 'path'; import { types as utilTypes } from 'util'; -import { satisfies } from 'semver'; +import { satisfies } from '../../semver'; import { wrap, unwrap, massWrap, massUnwrap } from 'shimmer'; import { InstrumentationAbstract } from '../../instrumentation'; import { diff --git a/experimental/packages/opentelemetry-instrumentation/src/semver.ts b/experimental/packages/opentelemetry-instrumentation/src/semver.ts new file mode 100644 index 00000000000..1e063352e1f --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/src/semver.ts @@ -0,0 +1,618 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is a custom semantic versioning implementation compatible with the +// `satisfies(version, range, options?)` function from the `semver` npm package; +// with the exception that the `loose` option is not supported. +// +// The motivation for the custom semver implementation is that +// `semver` package has some initialization delay (lots of RegExp init and compile) +// and this leads to coldstart overhead for the OTEL Lambda Node.js layer. +// Hence, we have implemented lightweight version of it internally with required functionalities. + +import { diag } from '@opentelemetry/api'; + +const VERSION_REGEXP = + /^(?:v)?(?(?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-]+)*))?$/; +const RANGE_REGEXP = + /^(?<|>|=|==|<=|>=|~|\^|~>)?\s*(?:v)?(?(?x|X|\*|0|[1-9]\d*)(?:\.(?x|X|\*|0|[1-9]\d*))?(?:\.(?x|X|\*|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-]+)*))?$/; + +const operatorResMap: { [op: string]: number[] } = { + '>': [1], + '>=': [0, 1], + '=': [0], + '<=': [-1, 0], + '<': [-1], + '!=': [-1, 1], +}; + +/** Interface for the options to configure semantic versioning satisfy check. */ +export interface SatisfiesOptions { + /** + * If set to true, the pre-release checks will be included + * as described [here](https://github.com/npm/node-semver#prerelease-tags). + */ + includePrerelease?: boolean; +} + +interface ParsedVersion { + op?: string; + + version?: string; + versionSegments?: string[]; + versionSegmentCount?: number; + + prerelease?: string; + prereleaseSegments?: string[]; + prereleaseSegmentCount?: number; + + build?: string; + + invalid?: boolean; +} + +/** + * Checks given version whether it satisfies given range expression. + * @param version the [version](https://github.com/npm/node-semver#versions) to be checked + * @param range the [range](https://github.com/npm/node-semver#ranges) expression for version check + * @param options options to configure semver satisfy check + */ +export function satisfies( + version: string, + range: string, + options?: SatisfiesOptions +): boolean { + // Strict semver format check + if (!_validateVersion(version)) { + diag.error(`Invalid version: ${version}`); + return false; + } + + // If range is empty, satisfy check succeeds regardless what version is + if (!range) { + return true; + } + + // Cleanup range + range = range.replace(/([<>=~^]+)\s+/g, '$1'); + + // Parse version + const parsedVersion: ParsedVersion | undefined = _parseVersion(version); + if (!parsedVersion) { + return false; + } + + const allParsedRanges: ParsedVersion[] = []; + + // Check given version whether it satisfies given range expression + const checkResult: boolean = _doSatisfies( + parsedVersion, + range, + allParsedRanges, + options + ); + + // If check result is OK, + // do another final check for pre-release, if pre-release check is included by option + if (checkResult && !options?.includePrerelease) { + return _doPreleaseCheck(parsedVersion, allParsedRanges); + } + return checkResult; +} + +function _validateVersion(version: unknown): boolean { + return typeof version === 'string' && VERSION_REGEXP.test(version); +} + +function _doSatisfies( + parsedVersion: ParsedVersion, + range: string, + allParsedRanges: ParsedVersion[], + options?: SatisfiesOptions +): boolean { + if (range.includes('||')) { + // A version matches a range if and only if + // every comparator in at least one of the ||-separated comparator sets is satisfied by the version + const ranges: string[] = range.trim().split('||'); + for (const r of ranges) { + if (_checkRange(parsedVersion, r, allParsedRanges, options)) { + return true; + } + } + return false; + } else if (range.includes(' - ')) { + // Hyphen ranges: https://github.com/npm/node-semver#hyphen-ranges-xyz---abc + range = replaceHyphen(range, options); + } else if (range.includes(' ')) { + // Multiple separated ranges and all needs to be satisfied for success + const ranges: string[] = range + .trim() + .replace(/\s{2,}/g, ' ') + .split(' '); + for (const r of ranges) { + if (!_checkRange(parsedVersion, r, allParsedRanges, options)) { + return false; + } + } + return true; + } + + // Check given parsed version with given range + return _checkRange(parsedVersion, range, allParsedRanges, options); +} + +function _checkRange( + parsedVersion: ParsedVersion, + range: string, + allParsedRanges: ParsedVersion[], + options?: SatisfiesOptions +): boolean { + range = _normalizeRange(range, options); + if (range.includes(' ')) { + // If there are multiple ranges separated, satisfy each of them + return _doSatisfies(parsedVersion, range, allParsedRanges, options); + } else { + // Validate and parse range + const parsedRange: ParsedVersion = _parseRange(range); + allParsedRanges.push(parsedRange); + // Check parsed version by parsed range + return _satisfies(parsedVersion, parsedRange); + } +} + +function _satisfies( + parsedVersion: ParsedVersion, + parsedRange: ParsedVersion +): boolean { + // If range is invalid, satisfy check fails (no error throw) + if (parsedRange.invalid) { + return false; + } + + // If range is empty or wildcard, satisfy check succeeds regardless what version is + if (!parsedRange.version || _isWildcard(parsedRange.version)) { + return true; + } + + // Compare version segment first + let comparisonResult: number = _compareVersionSegments( + parsedVersion.versionSegments || [], + parsedRange.versionSegments || [] + ); + + // If versions segments are equal, compare by pre-release segments + if (comparisonResult === 0) { + const versionPrereleaseSegments: string[] = + parsedVersion.prereleaseSegments || []; + const rangePrereleaseSegments: string[] = + parsedRange.prereleaseSegments || []; + if (!versionPrereleaseSegments.length && !rangePrereleaseSegments.length) { + comparisonResult = 0; + } else if ( + !versionPrereleaseSegments.length && + rangePrereleaseSegments.length + ) { + comparisonResult = 1; + } else if ( + versionPrereleaseSegments.length && + !rangePrereleaseSegments.length + ) { + comparisonResult = -1; + } else { + comparisonResult = _compareVersionSegments( + versionPrereleaseSegments, + rangePrereleaseSegments + ); + } + } + + // Resolve check result according to comparison operator + return operatorResMap[parsedRange.op!]?.includes(comparisonResult); +} + +function _doPreleaseCheck( + parsedVersion: ParsedVersion, + allParsedRanges: ParsedVersion[] +): boolean { + if (parsedVersion.prerelease) { + return allParsedRanges.some( + r => r.prerelease && r.version === parsedVersion.version + ); + } + return true; +} + +function _normalizeRange(range: string, options?: SatisfiesOptions): string { + range = range.trim(); + range = replaceCaret(range, options); + range = replaceTilde(range); + range = replaceXRange(range, options); + range = range.trim(); + return range; +} + +function isX(id?: string): boolean { + return !id || id.toLowerCase() === 'x' || id === '*'; +} + +function _parseVersion(versionString: string): ParsedVersion | undefined { + const match: RegExpMatchArray | null = versionString.match(VERSION_REGEXP); + if (!match) { + diag.error(`Invalid version: ${versionString}`); + return undefined; + } + + const version: string = match!.groups!.version; + const prerelease: string = match!.groups!.prerelease; + const build: string = match!.groups!.build; + + const versionSegments: string[] = version.split('.'); + const prereleaseSegments: string[] | undefined = prerelease?.split('.'); + + return { + op: undefined, + + version, + versionSegments, + versionSegmentCount: versionSegments.length, + + prerelease, + prereleaseSegments, + prereleaseSegmentCount: prereleaseSegments ? prereleaseSegments.length : 0, + + build, + }; +} + +function _parseRange(rangeString: string): ParsedVersion { + if (!rangeString) { + return {}; + } + + const match: RegExpMatchArray | null = rangeString.match(RANGE_REGEXP); + if (!match) { + diag.error(`Invalid range: ${rangeString}`); + return { + invalid: true, + }; + } + + let op: string = match!.groups!.op; + const version: string = match!.groups!.version; + const prerelease: string = match!.groups!.prerelease; + const build: string = match!.groups!.build; + + const versionSegments: string[] = version.split('.'); + const prereleaseSegments: string[] | undefined = prerelease?.split('.'); + + if (op === '==') { + op = '='; + } + + return { + op: op || '=', + + version, + versionSegments, + versionSegmentCount: versionSegments.length, + + prerelease, + prereleaseSegments, + prereleaseSegmentCount: prereleaseSegments ? prereleaseSegments.length : 0, + + build, + }; +} + +function _isWildcard(s: string | undefined): boolean { + return s === '*' || s === 'x' || s === 'X'; +} + +function _parseVersionString(v: string): string | number { + const n: number = parseInt(v, 10); + return isNaN(n) ? v : n; +} + +function _normalizeVersionType( + a: string | number, + b: string | number +): [string, string] | [number, number] { + if (typeof a === typeof b) { + if (typeof a === 'number') { + return [a as number, b as number]; + } else if (typeof a === 'string') { + return [a as string, b as string]; + } else { + throw new Error('Version segments can only be strings or numbers'); + } + } else { + return [String(a), String(b)]; + } +} + +function _compareVersionStrings(v1: string, v2: string): number { + if (_isWildcard(v1) || _isWildcard(v2)) { + return 0; + } + const [parsedV1, parsedV2] = _normalizeVersionType( + _parseVersionString(v1), + _parseVersionString(v2) + ); + if (parsedV1 > parsedV2) { + return 1; + } else if (parsedV1 < parsedV2) { + return -1; + } + return 0; +} + +function _compareVersionSegments(v1: string[], v2: string[]): number { + for (let i = 0; i < Math.max(v1.length, v2.length); i++) { + const res: number = _compareVersionStrings(v1[i] || '0', v2[i] || '0'); + if (res !== 0) { + return res; + } + } + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// The rest of this file is adapted from portions of https://github.com/npm/node-semver/tree/868d4bb +// License: +/* + * The ISC License + * + * Copyright (c) Isaac Z. Schlueter and Contributors + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +const LETTERDASHNUMBER = '[a-zA-Z0-9-]'; +const NUMERICIDENTIFIER = '0|[1-9]\\d*'; +const NONNUMERICIDENTIFIER = `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`; +const GTLT = '((?:<|>)?=?)'; + +const PRERELEASEIDENTIFIER = `(?:${NUMERICIDENTIFIER}|${NONNUMERICIDENTIFIER})`; +const PRERELEASE = `(?:-(${PRERELEASEIDENTIFIER}(?:\\.${PRERELEASEIDENTIFIER})*))`; + +const BUILDIDENTIFIER = `${LETTERDASHNUMBER}+`; +const BUILD = `(?:\\+(${BUILDIDENTIFIER}(?:\\.${BUILDIDENTIFIER})*))`; + +const XRANGEIDENTIFIER = `${NUMERICIDENTIFIER}|x|X|\\*`; +const XRANGEPLAIN = + `[v=\\s]*(${XRANGEIDENTIFIER})` + + `(?:\\.(${XRANGEIDENTIFIER})` + + `(?:\\.(${XRANGEIDENTIFIER})` + + `(?:${PRERELEASE})?${BUILD}?` + + `)?)?`; +const XRANGE = `^${GTLT}\\s*${XRANGEPLAIN}$`; +const XRANGE_REGEXP = new RegExp(XRANGE); + +const HYPHENRANGE = + `^\\s*(${XRANGEPLAIN})` + `\\s+-\\s+` + `(${XRANGEPLAIN})` + `\\s*$`; +const HYPHENRANGE_REGEXP = new RegExp(HYPHENRANGE); + +const LONETILDE = '(?:~>?)'; +const TILDE = `^${LONETILDE}${XRANGEPLAIN}$`; +const TILDE_REGEXP = new RegExp(TILDE); + +const LONECARET = '(?:\\^)'; +const CARET = `^${LONECARET}${XRANGEPLAIN}$`; +const CARET_REGEXP = new RegExp(CARET); + +// Borrowed from https://github.com/npm/node-semver/blob/868d4bbe3d318c52544f38d5f9977a1103e924c2/classes/range.js#L285 +// +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0 +// ~0.0.1 --> >=0.0.1 <0.1.0-0 +function replaceTilde(comp: string): string { + const r = TILDE_REGEXP; + return comp.replace(r, (_, M, m, p, pr) => { + let ret; + + if (isX(M)) { + ret = ''; + } else if (isX(m)) { + ret = `>=${M}.0.0 <${+M + 1}.0.0-0`; + } else if (isX(p)) { + // ~1.2 == >=1.2.0 <1.3.0-0 + ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`; + } else if (pr) { + ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`; + } else { + // ~1.2.3 == >=1.2.3 <1.3.0-0 + ret = `>=${M}.${m}.${p} <${M}.${+m + 1}.0-0`; + } + return ret; + }); +} + +// Borrowed from https://github.com/npm/node-semver/blob/868d4bbe3d318c52544f38d5f9977a1103e924c2/classes/range.js#L329 +// +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0-0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0-0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0 +// ^1.2.3 --> >=1.2.3 <2.0.0-0 +// ^1.2.0 --> >=1.2.0 <2.0.0-0 +// ^0.0.1 --> >=0.0.1 <0.0.2-0 +// ^0.1.0 --> >=0.1.0 <0.2.0-0 +function replaceCaret(comp: string, options?: SatisfiesOptions): string { + const r = CARET_REGEXP; + const z = options?.includePrerelease ? '-0' : ''; + return comp.replace(r, (_, M, m, p, pr) => { + let ret; + + if (isX(M)) { + ret = ''; + } else if (isX(m)) { + ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`; + } else if (isX(p)) { + if (M === '0') { + ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`; + } else { + ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`; + } + } else if (pr) { + if (M === '0') { + if (m === '0') { + ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`; + } else { + ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`; + } + } else { + ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`; + } + } else { + if (M === '0') { + if (m === '0') { + ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`; + } else { + ret = `>=${M}.${m}.${p}${z} <${M}.${+m + 1}.0-0`; + } + } else { + ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`; + } + } + return ret; + }); +} + +// Borrowed from https://github.com/npm/node-semver/blob/868d4bbe3d318c52544f38d5f9977a1103e924c2/classes/range.js#L390 +function replaceXRange(comp: string, options?: SatisfiesOptions): string { + const r = XRANGE_REGEXP; + return comp.replace(r, (ret, gtlt, M, m, p, pr) => { + const xM = isX(M); + const xm = xM || isX(m); + const xp = xm || isX(p); + const anyX = xp; + + if (gtlt === '=' && anyX) { + gtlt = ''; + } + + // if we're including prereleases in the match, then we need + // to fix this to -0, the lowest possible prerelease value + pr = options?.includePrerelease ? '-0' : ''; + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0-0'; + } else { + // nothing is forbidden + ret = '*'; + } + } else if (gtlt && anyX) { + // we know patch is an x, because we have any x at all. + // replace X with 0 + if (xm) { + m = 0; + } + p = 0; + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + gtlt = '>='; + if (xm) { + M = +M + 1; + m = 0; + p = 0; + } else { + m = +m + 1; + p = 0; + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<'; + if (xm) { + M = +M + 1; + } else { + m = +m + 1; + } + } + + if (gtlt === '<') { + pr = '-0'; + } + + ret = `${gtlt + M}.${m}.${p}${pr}`; + } else if (xm) { + ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0`; + } else if (xp) { + ret = `>=${M}.${m}.0${pr} <${M}.${+m + 1}.0-0`; + } + + return ret; + }); +} + +// Borrowed from https://github.com/npm/node-semver/blob/868d4bbe3d318c52544f38d5f9977a1103e924c2/classes/range.js#L488 +// +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0-0 +function replaceHyphen(comp: string, options?: SatisfiesOptions): string { + const r = HYPHENRANGE_REGEXP; + return comp.replace( + r, + (_, from, fM, fm, fp, fpr, fb, to, tM, tm, tp, tpr) => { + if (isX(fM)) { + from = ''; + } else if (isX(fm)) { + from = `>=${fM}.0.0${options?.includePrerelease ? '-0' : ''}`; + } else if (isX(fp)) { + from = `>=${fM}.${fm}.0${options?.includePrerelease ? '-0' : ''}`; + } else if (fpr) { + from = `>=${from}`; + } else { + from = `>=${from}${options?.includePrerelease ? '-0' : ''}`; + } + + if (isX(tM)) { + to = ''; + } else if (isX(tm)) { + to = `<${+tM + 1}.0.0-0`; + } else if (isX(tp)) { + to = `<${tM}.${+tm + 1}.0-0`; + } else if (tpr) { + to = `<=${tM}.${tm}.${tp}-${tpr}`; + } else if (options?.includePrerelease) { + to = `<${tM}.${tm}.${+tp + 1}-0`; + } else { + to = `<=${to}`; + } + + return `${from} ${to}`.trim(); + } + ); +} diff --git a/experimental/packages/opentelemetry-instrumentation/src/types.ts b/experimental/packages/opentelemetry-instrumentation/src/types.ts index b58054ac0df..27a8574fcd1 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/types.ts @@ -84,8 +84,8 @@ export interface InstrumentationModuleFile { /** Supported versions for the file. * * A module version is supported if one of the supportedVersions in the array satisfies the module version. - * The syntax of the version is checked with the `satisfies` function of "The semantic versioner for npm", see - * [`semver` package](https://www.npmjs.com/package/semver) + * The syntax of the version is checked with a function compatible + * with [node-semver's `satisfies()` function](https://github.com/npm/node-semver#ranges-1). * If the version is not supported, we won't apply instrumentation patch. * If omitted, all versions of the module will be patched. * @@ -116,8 +116,8 @@ export interface InstrumentationModuleDefinition { /** Supported version of module. * * A module version is supported if one of the supportedVersions in the array satisfies the module version. - * The syntax of the version is checked with the `satisfies` function of "The semantic versioner for npm", see - * [`semver` package](https://www.npmjs.com/package/semver) + * The syntax of the version is checked with the `satisfies` function of + * "The [semantic versioner](https://semver.org) for npm". * If the version is not supported, we won't apply instrumentation patch (see `enable` method). * If omitted, all versions of the module will be patched. * diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/semver.test.ts b/experimental/packages/opentelemetry-instrumentation/test/common/semver.test.ts new file mode 100644 index 00000000000..d9a66fb4b2e --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/common/semver.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These tests are adapted from semver's tests for `semver.satisfies()`. + * https://github.com/npm/node-semver/blob/868d4bbe3d318c52544f38d5f9977a1103e924c2/test/functions/satisfies.js + * License: + * + * The ISC License + * + * Copyright (c) Isaac Z. Schlueter and Contributors + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR + * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +import * as assert from 'assert'; + +import { satisfies, SatisfiesOptions } from '../../src/semver'; + +const rangeInclude = require('./third-party/node-semver/range-include.js'); +const rangeExclude = require('./third-party/node-semver/range-exclude.js'); + +describe('SemVer', () => { + describe('satisfies', () => { + function isOptionsSupported(options: any): boolean { + // We don't support + // - boolean typed options + // - 'loose' option parameter + if (options && (typeof options === 'boolean' || options.loose)) { + return false; + } + return true; + } + + it('when range is included', () => { + rangeInclude.forEach(([range, ver, options]: [string, string, any]) => { + if (!isOptionsSupported(options)) { + return; + } + assert.ok( + satisfies(ver, range, options), + `${range} satisfied by ${ver}` + ); + }); + }); + it('when range is not included', () => { + rangeExclude.forEach(([range, ver, options]: [string, string, any]) => { + if (!isOptionsSupported(options)) { + return; + } + assert.ok( + !satisfies(ver, range, options as SatisfiesOptions), + `${range} not satisfied by ${ver}` + ); + }); + }); + it('invalid ranges never satisfied (but do not throw)', () => { + const cases = [ + ['blerg', '1.2.3'], + ['git+https://user:password0123@github.com/foo', '123.0.0', true], + ['^1.2.3', '2.0.0-pre'], + ['0.x', undefined], + ['*', undefined], + ]; + cases.forEach(([range, ver]) => { + assert.ok( + !satisfies(ver as any, range as any), + `${range} not satisfied because invalid` + ); + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/LICENSE b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/LICENSE new file mode 100644 index 00000000000..19129e315fe --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-exclude.js b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-exclude.js new file mode 100644 index 00000000000..07e805d4a3a --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-exclude.js @@ -0,0 +1,107 @@ +// [range, version, options] +// version should not be included by range +module.exports = [ + ['1.0.0 - 2.0.0', '2.2.3'], + ['1.2.3+asdf - 2.4.3+asdf', '1.2.3-pre.2'], + ['1.2.3+asdf - 2.4.3+asdf', '2.4.3-alpha'], + ['^1.2.3+build', '2.0.0'], + ['^1.2.3+build', '1.2.0'], + ['^1.2.3', '1.2.3-pre'], + ['^1.2', '1.2.0-pre'], + ['>1.2', '1.3.0-beta'], + ['<=1.2.3', '1.2.3-beta'], + ['^1.2.3', '1.2.3-beta'], + ['=0.7.x', '0.7.0-asdf'], + ['>=0.7.x', '0.7.0-asdf'], + ['<=0.7.x', '0.7.0-asdf'], + ['1', '1.0.0beta', { loose: 420 }], + ['<1', '1.0.0beta', true], + ['< 1', '1.0.0beta', true], + ['1.0.0', '1.0.1'], + ['>=1.0.0', '0.0.0'], + ['>=1.0.0', '0.0.1'], + ['>=1.0.0', '0.1.0'], + ['>1.0.0', '0.0.1'], + ['>1.0.0', '0.1.0'], + ['<=2.0.0', '3.0.0'], + ['<=2.0.0', '2.9999.9999'], + ['<=2.0.0', '2.2.9'], + ['<2.0.0', '2.9999.9999'], + ['<2.0.0', '2.2.9'], + ['>=0.1.97', 'v0.1.93', true], + ['>=0.1.97', '0.1.93'], + ['0.1.20 || 1.2.4', '1.2.3'], + ['>=0.2.3 || <0.0.1', '0.0.3'], + ['>=0.2.3 || <0.0.1', '0.2.2'], + ['2.x.x', '1.1.3', { loose: NaN }], + ['2.x.x', '3.1.3'], + ['1.2.x', '1.3.3'], + ['1.2.x || 2.x', '3.1.3'], + ['1.2.x || 2.x', '1.1.3'], + ['2.*.*', '1.1.3'], + ['2.*.*', '3.1.3'], + ['1.2.*', '1.3.3'], + ['1.2.* || 2.*', '3.1.3'], + ['1.2.* || 2.*', '1.1.3'], + ['2', '1.1.2'], + ['2.3', '2.4.1'], + ['~0.0.1', '0.1.0-alpha'], + ['~0.0.1', '0.1.0'], + ['~2.4', '2.5.0'], // >=2.4.0 <2.5.0 + ['~2.4', '2.3.9'], + ['~>3.2.1', '3.3.2'], // >=3.2.1 <3.3.0 + ['~>3.2.1', '3.2.0'], // >=3.2.1 <3.3.0 + ['~1', '0.2.3'], // >=1.0.0 <2.0.0 + ['~>1', '2.2.3'], + ['~1.0', '1.1.0'], // >=1.0.0 <1.1.0 + ['<1', '1.0.0'], + ['>=1.2', '1.1.1'], + ['1', '2.0.0beta', true], + ['~v0.5.4-beta', '0.5.4-alpha'], + ['=0.7.x', '0.8.2'], + ['>=0.7.x', '0.6.2'], + ['<0.7.x', '0.7.2'], + ['<1.2.3', '1.2.3-beta'], + ['=1.2.3', '1.2.3-beta'], + ['>1.2', '1.2.8'], + ['^0.0.1', '0.0.2-alpha'], + ['^0.0.1', '0.0.2'], + ['^1.2.3', '2.0.0-alpha'], + ['^1.2.3', '1.2.2'], + ['^1.2', '1.1.9'], + ['*', 'v1.2.3-foo', true], + + // invalid versions never satisfy, but shouldn't throw + ['*', 'not a version'], + ['>=2', 'glorp'], + ['>=2', false], + + ['2.x', '3.0.0-pre.0', { includePrerelease: true }], + ['^1.0.0', '1.0.0-rc1', { includePrerelease: true }], + ['^1.0.0', '2.0.0-rc1', { includePrerelease: true }], + ['^1.2.3-rc2', '2.0.0', { includePrerelease: true }], + ['^1.0.0', '2.0.0-rc1'], + + ['1 - 2', '3.0.0-pre', { includePrerelease: true }], + ['1 - 2', '2.0.0-pre'], + ['1 - 2', '1.0.0-pre'], + ['1.0 - 2', '1.0.0-pre'], + + ['1.1.x', '1.0.0-a'], + ['1.1.x', '1.1.0-a'], + ['1.1.x', '1.2.0-a'], + ['1.1.x', '1.2.0-a', { includePrerelease: true }], + ['1.1.x', '1.0.0-a', { includePrerelease: true }], + ['1.x', '1.0.0-a'], + ['1.x', '1.1.0-a'], + ['1.x', '1.2.0-a'], + ['1.x', '0.0.0-a', { includePrerelease: true }], + ['1.x', '2.0.0-a', { includePrerelease: true }], + + ['>=1.0.0 <1.1.0', '1.1.0'], + ['>=1.0.0 <1.1.0', '1.1.0', { includePrerelease: true }], + ['>=1.0.0 <1.1.0', '1.1.0-pre'], + ['>=1.0.0 <1.1.0-pre', '1.1.0-pre'], + + ['== 1.0.0 || foo', '2.0.0', { loose: true }], +]; diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-include.js b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-include.js new file mode 100644 index 00000000000..92071b6068a --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/common/third-party/node-semver/range-include.js @@ -0,0 +1,127 @@ +// [range, version, options] +// version should be included by range +module.exports = [ + ['1.0.0 - 2.0.0', '1.2.3'], + ['^1.2.3+build', '1.2.3'], + ['^1.2.3+build', '1.3.0'], + ['1.2.3-pre+asdf - 2.4.3-pre+asdf', '1.2.3'], + ['1.2.3pre+asdf - 2.4.3-pre+asdf', '1.2.3', true], + ['1.2.3-pre+asdf - 2.4.3pre+asdf', '1.2.3', true], + ['1.2.3pre+asdf - 2.4.3pre+asdf', '1.2.3', true], + ['1.2.3-pre+asdf - 2.4.3-pre+asdf', '1.2.3-pre.2'], + ['1.2.3-pre+asdf - 2.4.3-pre+asdf', '2.4.3-alpha'], + ['1.2.3+asdf - 2.4.3+asdf', '1.2.3'], + ['1.0.0', '1.0.0'], + ['>=*', '0.2.4'], + ['', '1.0.0'], + ['*', '1.2.3', {}], + ['*', 'v1.2.3', { loose: 123 }], + ['>=1.0.0', '1.0.0', /asdf/], + ['>=1.0.0', '1.0.1', { loose: null }], + ['>=1.0.0', '1.1.0', { loose: 0 }], + ['>1.0.0', '1.0.1', { loose: undefined }], + ['>1.0.0', '1.1.0'], + ['<=2.0.0', '2.0.0'], + ['<=2.0.0', '1.9999.9999'], + ['<=2.0.0', '0.2.9'], + ['<2.0.0', '1.9999.9999'], + ['<2.0.0', '0.2.9'], + ['>= 1.0.0', '1.0.0'], + ['>= 1.0.0', '1.0.1'], + ['>= 1.0.0', '1.1.0'], + ['> 1.0.0', '1.0.1'], + ['> 1.0.0', '1.1.0'], + ['<= 2.0.0', '2.0.0'], + ['<= 2.0.0', '1.9999.9999'], + ['<= 2.0.0', '0.2.9'], + ['< 2.0.0', '1.9999.9999'], + ['<\t2.0.0', '0.2.9'], + ['>=0.1.97', 'v0.1.97', true], + ['>=0.1.97', '0.1.97'], + ['0.1.20 || 1.2.4', '1.2.4'], + ['>=0.2.3 || <0.0.1', '0.0.0'], + ['>=0.2.3 || <0.0.1', '0.2.3'], + ['>=0.2.3 || <0.0.1', '0.2.4'], + ['||', '1.3.4'], + ['2.x.x', '2.1.3'], + ['1.2.x', '1.2.3'], + ['1.2.x || 2.x', '2.1.3'], + ['1.2.x || 2.x', '1.2.3'], + ['x', '1.2.3'], + ['2.*.*', '2.1.3'], + ['1.2.*', '1.2.3'], + ['1.2.* || 2.*', '2.1.3'], + ['1.2.* || 2.*', '1.2.3'], + ['*', '1.2.3'], + ['2', '2.1.2'], + ['2.3', '2.3.1'], + ['~0.0.1', '0.0.1'], + ['~0.0.1', '0.0.2'], + ['~x', '0.0.9'], // >=2.4.0 <2.5.0 + ['~2', '2.0.9'], // >=2.4.0 <2.5.0 + ['~2.4', '2.4.0'], // >=2.4.0 <2.5.0 + ['~2.4', '2.4.5'], + ['~>3.2.1', '3.2.2'], // >=3.2.1 <3.3.0, + ['~1', '1.2.3'], // >=1.0.0 <2.0.0 + ['~>1', '1.2.3'], + ['~> 1', '1.2.3'], + ['~1.0', '1.0.2'], // >=1.0.0 <1.1.0, + ['~ 1.0', '1.0.2'], + ['~ 1.0.3', '1.0.12'], + ['~ 1.0.3alpha', '1.0.12', { loose: true }], + ['>=1', '1.0.0'], + ['>= 1', '1.0.0'], + ['<1.2', '1.1.1'], + ['< 1.2', '1.1.1'], + ['~v0.5.4-pre', '0.5.5'], + ['~v0.5.4-pre', '0.5.4'], + ['=0.7.x', '0.7.2'], + ['<=0.7.x', '0.7.2'], + ['>=0.7.x', '0.7.2'], + ['<=0.7.x', '0.6.2'], + ['~1.2.1 >=1.2.3', '1.2.3'], + ['~1.2.1 =1.2.3', '1.2.3'], + ['~1.2.1 1.2.3', '1.2.3'], + ['~1.2.1 >=1.2.3 1.2.3', '1.2.3'], + ['~1.2.1 1.2.3 >=1.2.3', '1.2.3'], + ['>=1.2.1 1.2.3', '1.2.3'], + ['1.2.3 >=1.2.1', '1.2.3'], + ['>=1.2.3 >=1.2.1', '1.2.3'], + ['>=1.2.1 >=1.2.3', '1.2.3'], + ['>=1.2', '1.2.8'], + ['^1.2.3', '1.8.1'], + ['^0.1.2', '0.1.2'], + ['^0.1', '0.1.2'], + ['^0.0.1', '0.0.1'], + ['^1.2', '1.4.2'], + ['^1.2 ^1', '1.4.2'], + ['^1.2.3-alpha', '1.2.3-pre'], + ['^1.2.0-alpha', '1.2.0-pre'], + ['^0.0.1-alpha', '0.0.1-beta'], + ['^0.0.1-alpha', '0.0.1'], + ['^0.1.1-alpha', '0.1.1-beta'], + ['^x', '1.2.3'], + ['x - 1.0.0', '0.9.7'], + ['x - 1.x', '0.9.7'], + ['1.0.0 - x', '1.9.7'], + ['1.x - x', '1.9.7'], + ['<=7.x', '7.9.9'], + ['2.x', '2.0.0-pre.0', { includePrerelease: true }], + ['2.x', '2.1.0-pre.0', { includePrerelease: true }], + ['1.1.x', '1.1.0-a', { includePrerelease: true }], + ['1.1.x', '1.1.1-a', { includePrerelease: true }], + ['*', '1.0.0-rc1', { includePrerelease: true }], + ['^1.0.0-0', '1.0.1-rc1', { includePrerelease: true }], + ['^1.0.0-rc2', '1.0.1-rc1', { includePrerelease: true }], + ['^1.0.0', '1.0.1-rc1', { includePrerelease: true }], + ['^1.0.0', '1.1.0-rc1', { includePrerelease: true }], + ['1 - 2', '2.0.0-pre', { includePrerelease: true }], + ['1 - 2', '1.0.0-pre', { includePrerelease: true }], + ['1.0 - 2', '1.0.0-pre', { includePrerelease: true }], + + ['=0.7.x', '0.7.0-asdf', { includePrerelease: true }], + ['>=0.7.x', '0.7.0-asdf', { includePrerelease: true }], + ['<=0.7.x', '0.7.0-asdf', { includePrerelease: true }], + + ['>=1.0.0 <=1.1.0', '1.1.0-pre', { includePrerelease: true }], +]; diff --git a/package-lock.json b/package-lock.json index 28ae3fd264d..7a80b690141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2277,7 +2277,6 @@ "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", "shimmer": "^1.2.1" }, "devDependencies": { @@ -34838,7 +34837,6 @@ "mocha": "10.8.2", "nyc": "17.1.0", "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", "shimmer": "^1.2.1", "sinon": "15.1.2", "ts-loader": "9.5.2",