Skip to content
85 changes: 65 additions & 20 deletions packages/open-next/src/build/patch/codePatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@ import logger from "../../logger.js";
import type { getManifests } from "../copyTracedFiles.js";
import * as buildHelper from "../helper.js";

// Either before or after should be provided, otherwise just use the field directly
type Versions =
| `>=${number}.${number}.${number} <=${number}.${number}.${number}`
| `>=${number}.${number}.${number}`
| `<=${number}.${number}.${number}`;
export interface VersionedField<T> {
/**
* The version before which the field should be used
* If the version is less than or equal to this, the field will be used
* The versions of Next.js that this field should be used for
* Should be in the format `">=16.0.0 <=17.0.0"` or `">=16.0.0"` or `"<=17.0.0"`
* **Be careful with spaces**
*/
before?:
| `${number}`
| `${number}.${number}`
| `${number}.${number}.${number}`;
/**
* The version after which the field should be used
* If the version is greater than this, the field will be used
*/
after?: `${number}` | `${number}.${number}` | `${number}.${number}.${number}`;
versions?: Versions;
field: T;
}

Expand Down Expand Up @@ -51,27 +47,76 @@ export interface CodePatcher {
patches: IndividualPatch | VersionedField<IndividualPatch>[];
}

export function parseVersions(versions?: Versions): {
before?: string;
after?: string;
} {
if (!versions) {
return {};
}
// We need to use regex to extract the versions
const versionRegex = /([<>]=?)([0-9]+\.[0-9]+\.[0-9]+)/g;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • = is not optional
  • using \d for digits
Suggested change
const versionRegex = /([<>]=?)([0-9]+\.[0-9]+\.[0-9]+)/g;
const versionRegex = /([<>]=)(\d+\.\d+\.\d+)/g;

const matches = Array.from(versions.matchAll(versionRegex));
if (matches.length === 0) {
throw new Error("Invalid version range, no matches found");
}
if (matches.length > 2) {
throw new Error("Invalid version range, too many matches found");
}
let after: string | undefined;
let before: string | undefined;
for (const match of matches) {
const [_, operator, version] = match;
if (operator === "<=") {
before = version;
} else if (operator === ">=") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (operator === ">=") {
} else {

after = version;
}
}
// Before returning we reconstruct the version string and compare it to the original
// If they don't match we throw an error
// We have to do this because template literal types here seems to allow for extra spaces
// that could easily break the version comparison and allow some patch to be applied on incorrect versions
// This might even go unnoticed
const reconstructedVersion = `${after ? `>=${after}` : ""}${
before ? `${after ? " " : ""}<=${before}` : ""
}`;
if (reconstructedVersion !== versions) {
throw new Error(
"Invalid version range, the reconstructed version does not match the original",
);
}
return {
before,
after,
};
}

export function extractVersionedField<T>(
fields: VersionedField<T>[],
version: string,
): T[] {
const result: T[] = [];

for (const field of fields) {
const { before, after } = parseVersions(field.versions);
if (
field.before &&
field.after &&
buildHelper.compareSemver(version, field.before) <= 0 &&
buildHelper.compareSemver(version, field.after) > 0
before &&
after &&
buildHelper.compareSemver(version, before) <= 0 &&
buildHelper.compareSemver(version, after) >= 0
) {
result.push(field.field);
} else if (
field.before &&
buildHelper.compareSemver(version, field.before) <= 0
before &&
!after &&
buildHelper.compareSemver(version, before) <= 0
) {
result.push(field.field);
} else if (
field.after &&
buildHelper.compareSemver(version, field.after) > 0
after &&
!before &&
buildHelper.compareSemver(version, after) >= 0
) {
result.push(field.field);
}
Expand Down
40 changes: 35 additions & 5 deletions packages/tests-unit/tests/build/patch/codePatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { extractVersionedField } from "@opennextjs/aws/build/patch/codePatcher.j
describe("extractVersionedField", () => {
it("should return the field if the version is between before and after", () => {
const result = extractVersionedField(
[{ before: "16.0.0", after: "15.0.0", field: 0 }],
[{ versions: ">=15.0.0 <=16.0.0", field: 0 }],
"15.5.0",
);

expect(result).toEqual([0]);
});

it("should return an empty array if the version is not between before and after", () => {
const result = extractVersionedField(
[{ versions: ">=15.0.0 <=16.0.0", field: 0 }],
"14.0.0",
);

expect(result).toEqual([]);
});

it("should return the field if the version is equal to before", () => {
const result = extractVersionedField(
[{ before: "15.0.0", after: "16.0.0", field: 0 }],
[{ versions: "<=15.0.0", field: 0 }],
"15.0.0",
);

Expand All @@ -21,7 +30,7 @@ describe("extractVersionedField", () => {

it("should return the field if the version is greater than after", () => {
const result = extractVersionedField(
[{ after: "16.0.0", field: 0 }],
[{ versions: ">=16.0.0", field: 0 }],
"16.5.0",
);

Expand All @@ -30,7 +39,7 @@ describe("extractVersionedField", () => {

it("should return the field if the version is less than before", () => {
const result = extractVersionedField(
[{ before: "15.0.0", field: 0 }],
[{ versions: "<=15.0.0", field: 0 }],
"14.0.0",
);

Expand All @@ -39,10 +48,31 @@ describe("extractVersionedField", () => {

it("should return an empty array if version is after before", () => {
const result = extractVersionedField(
[{ before: "15.0.0", field: 0 }],
[{ versions: "<=15.0.0", field: 0 }],
"15.1.0",
);

expect(result).toEqual([]);
});

it("should throw an error if a single version range is invalid because of a space before", () => {
expect(() =>
extractVersionedField([{ versions: "<= 15.0.0", field: 0 }], "15.0.0"),
).toThrow("Invalid version range");
});

it("should throw an error if a single version range is invalid because of a space inside version", () => {
expect(() =>
extractVersionedField([{ versions: ">=16. 0.0", field: 0 }], "15.0.0"),
).toThrow("Invalid version range");
});

it("should throw an error if one of the version range is invalid because of a space before the version", () => {
expect(() =>
extractVersionedField(
[{ versions: ">=16.0.0 <= 15.0.0", field: 0 }],
"15.0.0",
),
).toThrow("Invalid version range");
});
});