feat(cli,webapp): target notifications by minimum CLI version (#4407)

This commit is contained in:
Chris Arderne
2026-07-28 14:23:41 +01:00
committed by GitHub
parent 44eca4d166
commit 38bf82aebe
10 changed files with 429 additions and 229 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Send the running CLI version when checking for platform notifications so notices can be limited to compatible CLI releases.
+28 -2
View File
@@ -140,6 +140,8 @@ function parseNotificationFormData(formData: FormData) {
const cliShowEvery = formData.get("cliShowEvery")
? Number(formData.get("cliShowEvery"))
: undefined;
const minimumCliVersion =
(formData.get("minimumCliVersion") as string | null)?.trim() || undefined;
const discoveryFilePatterns = (formData.get("discoveryFilePatterns") as string) || "";
const discoveryContentPattern = (formData.get("discoveryContentPattern") as string) || undefined;
@@ -177,6 +179,7 @@ function parseNotificationFormData(formData: FormData) {
cliMaxShowCount,
cliMaxDaysAfterFirstSeen,
cliShowEvery,
minimumCliVersion,
discovery,
};
}
@@ -192,6 +195,9 @@ function buildPayloadInput(fields: ReturnType<typeof parseNotificationFormData>)
...(fields.image ? { image: fields.image } : {}),
...(fields.dismissOnAction ? { dismissOnAction: true } : {}),
...(fields.discovery ? { discovery: fields.discovery } : {}),
...(fields.surface === "CLI" && fields.minimumCliVersion
? { minimumCliVersion: fields.minimumCliVersion }
: {}),
},
};
}
@@ -669,6 +675,7 @@ type NotificationFormDefaults = {
contentPattern?: string;
matchBehavior: "show-if-found" | "show-if-not-found";
} | null;
payloadMinimumCliVersion?: string | null;
cliMaxShowCount?: number | null;
cliMaxDaysAfterFirstSeen?: number | null;
cliShowEvery?: number | null;
@@ -1024,7 +1031,19 @@ function NotificationForm({
<>
<input type="hidden" name="discoveryMatchBehavior" value={discoveryMatchBehavior} />
<div className="grid grid-cols-3 gap-3 rounded border border-grid-dimmed bg-background-deep p-3">
<div className="grid grid-cols-2 gap-3 rounded border border-grid-dimmed bg-background-deep p-3">
<div>
<Label variant="small">Minimum CLI version</Label>
<Input
name="minimumCliVersion"
variant="medium"
fullWidth
defaultValue={n?.payloadMinimumCliVersion ?? ""}
placeholder="e.g. 4.5.7"
className="mt-1"
/>
<Hint>Exact SemVer, inclusive</Hint>
</div>
<div>
<Label variant="small">Max show count</Label>
<Input
@@ -1188,6 +1207,7 @@ function NotificationDetailContent({
payloadDescription: string | null;
payloadActionUrl: string | null | undefined;
payloadImage: string | null | undefined;
payloadMinimumCliVersion: string | null;
cliMaxShowCount: number | null;
cliMaxDaysAfterFirstSeen: number | null;
cliShowEvery: number | null;
@@ -1239,10 +1259,16 @@ function NotificationDetailContent({
{/* CLI settings */}
{n.surface === "CLI" &&
(n.cliMaxShowCount || n.cliMaxDaysAfterFirstSeen || n.cliShowEvery) && (
(n.payloadMinimumCliVersion ||
n.cliMaxShowCount ||
n.cliMaxDaysAfterFirstSeen ||
n.cliShowEvery) && (
<div>
<p className="mb-1 text-xs font-medium text-text-dimmed">CLI Settings</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
{n.payloadMinimumCliVersion && (
<DetailRow label="Minimum CLI version" value={n.payloadMinimumCliVersion} />
)}
{n.cliMaxShowCount != null && (
<DetailRow label="Max show count" value={String(n.cliMaxShowCount)} />
)}
@@ -12,10 +12,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const projectRef = url.searchParams.get("projectRef") ?? undefined;
const cliVersion = request.headers.get("x-trigger-cli-version")?.trim() || undefined;
const notification = await getNextCliNotification({
userId: authenticationResult.userId,
projectRef,
cliVersion,
});
return json({ notification });
@@ -0,0 +1,228 @@
import { z } from "zod";
import { normalizeExactSemVer } from "./platformNotificationVersionTargeting";
const DiscoverySchema = z.object({
filePatterns: z.array(z.string().min(1)).min(1),
contentPattern: z
.string()
.max(200)
.optional()
.refine(
(val) => {
if (!val) return true;
try {
new RegExp(val);
return true;
} catch {
return false;
}
},
{ message: "contentPattern must be a valid regular expression" }
),
matchBehavior: z.enum(["show-if-found", "show-if-not-found"]),
});
// Constrain URL fields to http/https; `.url()` alone accepts other schemes
// that would be unsafe to render into an `<a href>`.
const httpUrl = z
.string()
.url()
.refine(
(v) => {
try {
const proto = new URL(v).protocol;
return proto === "http:" || proto === "https:";
} catch {
return false;
}
},
{ message: "URL must use http or https" }
);
const ExactSemVerSchema = z
.string()
.transform((value) => value.trim())
.refine((value) => normalizeExactSemVer(value) !== null, {
message: "minimumCliVersion must be a complete, exact SemVer",
})
.transform((value) => normalizeExactSemVer(value)!);
const CardDataV1Schema = z.object({
type: z.enum(["card", "info", "warn", "error", "success", "changelog"]),
title: z.string(),
description: z.string(),
image: httpUrl.optional(),
actionLabel: z.string().optional(),
actionUrl: httpUrl.optional(),
dismissOnAction: z.boolean().optional(),
discovery: DiscoverySchema.optional(),
minimumCliVersion: ExactSemVerSchema.optional(),
});
export const PayloadV1Schema = z.object({
version: z.literal("1"),
data: CardDataV1Schema,
});
export type PayloadV1 = z.infer<typeof PayloadV1Schema>;
const SCOPE_REQUIRED_FK: Record<string, "userId" | "organizationId" | "projectId"> = {
USER: "userId",
ORGANIZATION: "organizationId",
PROJECT: "projectId",
};
const ALL_FK_FIELDS = ["userId", "organizationId", "projectId"] as const;
const CLI_ONLY_FIELDS = ["cliMaxDaysAfterFirstSeen", "cliMaxShowCount", "cliShowEvery"] as const;
const NotificationBaseFields = {
title: z.string().min(1),
payload: PayloadV1Schema,
surface: z.enum(["WEBAPP", "CLI"]),
scope: z.enum(["USER", "PROJECT", "ORGANIZATION", "GLOBAL"]),
userId: z.string().optional(),
organizationId: z.string().optional(),
projectId: z.string().optional(),
endsAt: z
.string()
.datetime()
.transform((s) => new Date(s)),
priority: z.number().int().default(0),
cliMaxDaysAfterFirstSeen: z.number().int().positive().optional(),
cliMaxShowCount: z.number().int().positive().optional(),
cliShowEvery: z.number().int().min(2).optional(),
};
export const CreatePlatformNotificationSchema = z
.object({
...NotificationBaseFields,
startsAt: z
.string()
.datetime()
.transform((s) => new Date(s))
.optional(),
})
.superRefine((data, ctx) => {
validateScopeForeignKeys(data, ctx);
validateSurfaceFields(data, ctx);
validatePayloadTypeForSurface(data, ctx);
validateStartsAt(data, ctx);
validateEndsAt(data, ctx);
});
function validateScopeForeignKeys(
data: { scope: string; userId?: string; organizationId?: string; projectId?: string },
ctx: z.RefinementCtx
) {
const requiredFk = SCOPE_REQUIRED_FK[data.scope];
if (requiredFk && !data[requiredFk]) {
ctx.addIssue({
code: "custom",
message: `${requiredFk} is required when scope is ${data.scope}`,
path: [requiredFk],
});
}
const forbiddenFks = ALL_FK_FIELDS.filter((fk) => fk !== requiredFk);
for (const fk of forbiddenFks) {
if (data[fk]) {
ctx.addIssue({
code: "custom",
message: `${fk} must not be set when scope is ${data.scope}`,
path: [fk],
});
}
}
}
function validateSurfaceFields(
data: {
surface: string;
payload: PayloadV1;
cliMaxDaysAfterFirstSeen?: number;
cliMaxShowCount?: number;
cliShowEvery?: number;
},
ctx: z.RefinementCtx
) {
if (data.surface !== "WEBAPP") return;
for (const field of CLI_ONLY_FIELDS) {
if (data[field] !== undefined) {
ctx.addIssue({
code: "custom",
message: `${field} is not allowed for WEBAPP surface`,
path: [field],
});
}
}
if (data.payload.data.minimumCliVersion !== undefined) {
ctx.addIssue({
code: "custom",
message: "minimumCliVersion is not allowed for WEBAPP surface",
path: ["payload", "data", "minimumCliVersion"],
});
}
}
function validateStartsAt(data: { startsAt?: Date }, ctx: z.RefinementCtx) {
if (!data.startsAt) return;
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
if (data.startsAt < oneHourAgo) {
ctx.addIssue({
code: "custom",
message: "startsAt must be within the last hour or in the future",
path: ["startsAt"],
});
}
}
const CLI_TYPES = new Set(["info", "warn", "error", "success"]);
const WEBAPP_TYPES = new Set(["card", "changelog"]);
function validatePayloadTypeForSurface(
data: { surface: string; payload: PayloadV1 },
ctx: z.RefinementCtx
) {
const allowedTypes = data.surface === "CLI" ? CLI_TYPES : WEBAPP_TYPES;
if (!allowedTypes.has(data.payload.data.type)) {
ctx.addIssue({
code: "custom",
message: `payload.data.type "${data.payload.data.type}" is not allowed for ${data.surface} surface`,
path: ["payload", "data", "type"],
});
}
}
function validateEndsAt(data: { startsAt?: Date; endsAt: Date }, ctx: z.RefinementCtx) {
const effectiveStart = data.startsAt ?? new Date();
if (data.endsAt <= effectiveStart) {
ctx.addIssue({
code: "custom",
message: "endsAt must be after startsAt",
path: ["endsAt"],
});
}
}
export type CreatePlatformNotificationInput = z.input<typeof CreatePlatformNotificationSchema>;
export const UpdatePlatformNotificationSchema = z
.object({
...NotificationBaseFields,
id: z.string().min(1),
startsAt: z
.string()
.datetime()
.transform((s) => new Date(s)),
})
.superRefine((data, ctx) => {
validateScopeForeignKeys(data, ctx);
validateSurfaceFields(data, ctx);
validatePayloadTypeForSurface(data, ctx);
// Existing notifications may have a startsAt in the past.
validateEndsAt(data, ctx);
});
@@ -0,0 +1,29 @@
import * as semver from "semver";
export function normalizeExactSemVer(value: string): string | null {
const trimmed = value.trim();
const parsed = semver.parse(trimmed);
if (!parsed) return null;
const normalized = `${parsed.version}${
parsed.build.length > 0 ? `+${parsed.build.join(".")}` : ""
}`;
return normalized === trimmed ? normalized : null;
}
export function isCliVersionEligible(
minimumCliVersion: string | undefined,
cliVersion: string | undefined
): boolean {
if (minimumCliVersion === undefined) return true;
const normalizedMinimum = normalizeExactSemVer(minimumCliVersion);
if (!normalizedMinimum || cliVersion === undefined) return false;
const normalizedCliVersion = normalizeExactSemVer(cliVersion);
if (!normalizedCliVersion) return false;
return semver.gte(normalizedCliVersion, normalizedMinimum);
}
@@ -1,4 +1,4 @@
import { z } from "zod";
import type { z } from "zod";
import { errAsync, fromPromise, type ResultAsync } from "neverthrow";
import { prisma } from "~/db.server";
import {
@@ -6,64 +6,20 @@ import {
type PlatformNotificationSurface,
} from "@trigger.dev/database";
import { incrementCliRequestCounter } from "./platformNotificationCounter.server";
import {
CreatePlatformNotificationSchema,
type CreatePlatformNotificationInput,
type PayloadV1,
PayloadV1Schema,
UpdatePlatformNotificationSchema,
} from "./platformNotificationSchemas";
import { isCliVersionEligible } from "./platformNotificationVersionTargeting";
// --- Payload schema (spec v1) ---
const DiscoverySchema = z.object({
filePatterns: z.array(z.string().min(1)).min(1),
contentPattern: z
.string()
.max(200)
.optional()
.refine(
(val) => {
if (!val) return true;
try {
new RegExp(val);
return true;
} catch {
return false;
}
},
{ message: "contentPattern must be a valid regular expression" }
),
matchBehavior: z.enum(["show-if-found", "show-if-not-found"]),
});
// Constrain URL fields to http/https; `.url()` alone accepts other schemes
// that would be unsafe to render into an `<a href>`.
const httpUrl = z
.string()
.url()
.refine(
(v) => {
try {
const proto = new URL(v).protocol;
return proto === "http:" || proto === "https:";
} catch {
return false;
}
},
{ message: "URL must use http or https" }
);
const CardDataV1Schema = z.object({
type: z.enum(["card", "info", "warn", "error", "success", "changelog"]),
title: z.string(),
description: z.string(),
image: httpUrl.optional(),
actionLabel: z.string().optional(),
actionUrl: httpUrl.optional(),
dismissOnAction: z.boolean().optional(),
discovery: DiscoverySchema.optional(),
});
const PayloadV1Schema = z.object({
version: z.literal("1"),
data: CardDataV1Schema,
});
export type PayloadV1 = z.infer<typeof PayloadV1Schema>;
export {
CreatePlatformNotificationSchema,
UpdatePlatformNotificationSchema,
} from "./platformNotificationSchemas";
export type { CreatePlatformNotificationInput, PayloadV1 } from "./platformNotificationSchemas";
export type PlatformNotificationWithPayload = {
id: string;
@@ -136,6 +92,9 @@ export async function getAdminNotificationsList({
? (parsed.data.data.dismissOnAction ?? false)
: false,
payloadDiscovery: parsed.success ? (parsed.data.data.discovery ?? null) : null,
payloadMinimumCliVersion: parsed.success
? (parsed.data.data.minimumCliVersion ?? null)
: null,
cliMaxShowCount: n.cliMaxShowCount,
cliMaxDaysAfterFirstSeen: n.cliMaxDaysAfterFirstSeen,
cliShowEvery: n.cliShowEvery,
@@ -435,9 +394,11 @@ function isCliNotificationExpired(
export async function getNextCliNotification({
userId,
projectRef,
cliVersion,
}: {
userId: string;
projectRef?: string;
cliVersion?: string;
}): Promise<{
id: string;
payload: PayloadV1;
@@ -520,10 +481,11 @@ export async function getNextCliNotification({
const interaction = n.interactions[0] ?? null;
if (interaction?.cliDismissedAt) continue;
if (isCliNotificationExpired(interaction, n)) continue;
const parsed = PayloadV1Schema.safeParse(n.payload);
if (!parsed.success) continue;
if (!isCliVersionEligible(parsed.data.data.minimumCliVersion, cliVersion)) continue;
if (isCliNotificationExpired(interaction, n)) continue;
// Check cliShowEvery using the global request counter
if (n.cliShowEvery !== null && requestCounter % n.cliShowEvery !== 0) {
@@ -562,161 +524,7 @@ export async function getNextCliNotification({
return null;
}
// --- Create: admin endpoint support ---
const SCOPE_REQUIRED_FK: Record<string, "userId" | "organizationId" | "projectId"> = {
USER: "userId",
ORGANIZATION: "organizationId",
PROJECT: "projectId",
};
const ALL_FK_FIELDS = ["userId", "organizationId", "projectId"] as const;
const CLI_ONLY_FIELDS = ["cliMaxDaysAfterFirstSeen", "cliMaxShowCount", "cliShowEvery"] as const;
const NotificationBaseFields = {
title: z.string().min(1),
payload: PayloadV1Schema,
surface: z.enum(["WEBAPP", "CLI"]),
scope: z.enum(["USER", "PROJECT", "ORGANIZATION", "GLOBAL"]),
userId: z.string().optional(),
organizationId: z.string().optional(),
projectId: z.string().optional(),
endsAt: z
.string()
.datetime()
.transform((s) => new Date(s)),
priority: z.number().int().default(0),
cliMaxDaysAfterFirstSeen: z.number().int().positive().optional(),
cliMaxShowCount: z.number().int().positive().optional(),
cliShowEvery: z.number().int().min(2).optional(),
};
export const CreatePlatformNotificationSchema = z
.object({
...NotificationBaseFields,
startsAt: z
.string()
.datetime()
.transform((s) => new Date(s))
.optional(),
})
.superRefine((data, ctx) => {
validateScopeForeignKeys(data, ctx);
validateSurfaceFields(data, ctx);
validatePayloadTypeForSurface(data, ctx);
validateStartsAt(data, ctx);
validateEndsAt(data, ctx);
});
function validateScopeForeignKeys(
data: { scope: string; userId?: string; organizationId?: string; projectId?: string },
ctx: z.RefinementCtx
) {
const requiredFk = SCOPE_REQUIRED_FK[data.scope];
if (requiredFk && !data[requiredFk]) {
ctx.addIssue({
code: "custom",
message: `${requiredFk} is required when scope is ${data.scope}`,
path: [requiredFk],
});
}
const forbiddenFks = ALL_FK_FIELDS.filter((fk) => fk !== requiredFk);
for (const fk of forbiddenFks) {
if (data[fk]) {
ctx.addIssue({
code: "custom",
message: `${fk} must not be set when scope is ${data.scope}`,
path: [fk],
});
}
}
}
function validateSurfaceFields(
data: {
surface: string;
cliMaxDaysAfterFirstSeen?: number;
cliMaxShowCount?: number;
cliShowEvery?: number;
},
ctx: z.RefinementCtx
) {
if (data.surface !== "WEBAPP") return;
for (const field of CLI_ONLY_FIELDS) {
if (data[field] !== undefined) {
ctx.addIssue({
code: "custom",
message: `${field} is not allowed for WEBAPP surface`,
path: [field],
});
}
}
}
function validateStartsAt(data: { startsAt?: Date }, ctx: z.RefinementCtx) {
if (!data.startsAt) return;
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
if (data.startsAt < oneHourAgo) {
ctx.addIssue({
code: "custom",
message: "startsAt must be within the last hour or in the future",
path: ["startsAt"],
});
}
}
const CLI_TYPES = new Set(["info", "warn", "error", "success"]);
const WEBAPP_TYPES = new Set(["card", "changelog"]);
function validatePayloadTypeForSurface(
data: { surface: string; payload: PayloadV1 },
ctx: z.RefinementCtx
) {
const allowedTypes = data.surface === "CLI" ? CLI_TYPES : WEBAPP_TYPES;
if (!allowedTypes.has(data.payload.data.type)) {
ctx.addIssue({
code: "custom",
message: `payload.data.type "${data.payload.data.type}" is not allowed for ${data.surface} surface`,
path: ["payload", "data", "type"],
});
}
}
function validateEndsAt(data: { startsAt?: Date; endsAt: Date }, ctx: z.RefinementCtx) {
const effectiveStart = data.startsAt ?? new Date();
if (data.endsAt <= effectiveStart) {
ctx.addIssue({
code: "custom",
message: "endsAt must be after startsAt",
path: ["endsAt"],
});
}
}
export type CreatePlatformNotificationInput = z.input<typeof CreatePlatformNotificationSchema>;
// --- Update: admin endpoint support ---
export const UpdatePlatformNotificationSchema = z
.object({
...NotificationBaseFields,
id: z.string().min(1),
startsAt: z
.string()
.datetime()
.transform((s) => new Date(s)),
})
.superRefine((data, ctx) => {
validateScopeForeignKeys(data, ctx);
validateSurfaceFields(data, ctx);
validatePayloadTypeForSurface(data, ctx);
// NOTE: No validateStartsAt — existing notifications may have past startsAt
validateEndsAt(data, ctx);
});
// --- Create and update: admin endpoint support ---
type CreateError = { type: "validation"; issues: z.ZodIssue[] } | { type: "db"; message: string };
+2
View File
@@ -196,6 +196,7 @@
"remix-auth-google": "^2.0.0",
"remix-typedjson": "0.3.1",
"remix-utils": "^7.7.0",
"semver": "^7.5.0",
"simplur": "^3.0.1",
"slug": "^6.0.0",
"socket.io": "4.7.4",
@@ -242,6 +243,7 @@
"@types/react": "18.2.69",
"@types/react-dom": "18.2.7",
"@types/regression": "^2.0.6",
"@types/semver": "^7.5.0",
"@types/slug": "^5.0.3",
"@types/supertest": "^6.0.2",
"@types/tar": "^6.1.4",
@@ -0,0 +1,100 @@
import { describe, expect, it } from "vitest";
import { CreatePlatformNotificationSchema } from "~/services/platformNotificationSchemas";
import { isCliVersionEligible } from "~/services/platformNotificationVersionTargeting";
function createNotificationInput({
surface = "CLI",
minimumCliVersion,
}: {
surface?: "CLI" | "WEBAPP";
minimumCliVersion?: string;
} = {}) {
return {
title: "Notification",
surface,
scope: "GLOBAL" as const,
endsAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
payload: {
version: "1" as const,
data: {
type: surface === "CLI" ? ("info" as const) : ("card" as const),
title: "Update available",
description: "Update the CLI",
...(minimumCliVersion === undefined ? {} : { minimumCliVersion }),
},
},
};
}
describe("platform notification minimum CLI version schema", () => {
it("keeps existing unrestricted CLI payloads valid", () => {
const result = CreatePlatformNotificationSchema.safeParse(createNotificationInput());
expect(result.success).toBe(true);
});
it("trims and accepts a complete exact SemVer", () => {
const result = CreatePlatformNotificationSchema.parse(
createNotificationInput({ minimumCliVersion: " 4.5.7-beta.1+build.2 " })
);
expect(result.payload.data.minimumCliVersion).toBe("4.5.7-beta.1+build.2");
});
it.each(["4.5", ">=4.5.7", "4.5.x", "v4.5.7", "4.5.7 or newer", "01.2.3"])(
"rejects non-exact SemVer %s",
(minimumCliVersion) => {
const result = CreatePlatformNotificationSchema.safeParse(
createNotificationInput({ minimumCliVersion })
);
expect(result.success).toBe(false);
}
);
it("rejects minimumCliVersion for WEBAPP notifications", () => {
const result = CreatePlatformNotificationSchema.safeParse(
createNotificationInput({ surface: "WEBAPP", minimumCliVersion: "4.5.7" })
);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues).toContainEqual(
expect.objectContaining({ path: ["payload", "data", "minimumCliVersion"] })
);
}
});
});
describe("CLI notification version eligibility", () => {
it("allows unrestricted notifications without a request version", () => {
expect(isCliVersionEligible(undefined, undefined)).toBe(true);
});
it.each([undefined, "", "4.5", "not-a-version"])(
"rejects a targeted notification for missing or malformed request version %s",
(cliVersion) => {
expect(isCliVersionEligible("4.5.7", cliVersion)).toBe(false);
}
);
it("fails closed for an invalid stored minimum version", () => {
expect(isCliVersionEligible(">=4.5.7", "4.6.0")).toBe(false);
});
it.each([
["4.5.6", false],
["4.5.7-beta.1", false],
["4.5.7", true],
["4.5.7+build.9", true],
["4.6.0", true],
])("compares %s against inclusive minimum 4.5.7", (cliVersion, expected) => {
expect(isCliVersionEligible("4.5.7", cliVersion)).toBe(expected);
});
it("uses SemVer precedence for prerelease minimums", () => {
expect(isCliVersionEligible("4.5.7-beta.2", "4.5.7-beta.1")).toBe(false);
expect(isCliVersionEligible("4.5.7-beta.2", "4.5.7-beta.2+build.1")).toBe(true);
expect(isCliVersionEligible("4.5.7-beta.2", "4.5.7")).toBe(true);
});
});
+1
View File
@@ -606,6 +606,7 @@ export class CliApiClient {
headers: {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
"x-trigger-cli-version": VERSION,
},
signal,
});
+12 -13
View File
@@ -716,6 +716,9 @@ importers:
remix-utils:
specifier: ^7.7.0
version: 7.7.0(@remix-run/node@2.17.5(typescript@6.0.3))(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/router@1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9))(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.3.1)(zod@3.25.76)
semver:
specifier: ^7.5.0
version: 7.8.1
simplur:
specifier: ^3.0.1
version: 3.0.1
@@ -849,6 +852,9 @@ importers:
'@types/regression':
specifier: ^2.0.6
version: 2.0.6
'@types/semver':
specifier: ^7.5.0
version: 7.5.1
'@types/slug':
specifier: ^5.0.3
version: 5.0.3
@@ -13958,11 +13964,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
@@ -17525,7 +17526,7 @@ snapshots:
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.7.3
semver: 7.8.1
'@changesets/assemble-release-plan@5.2.4(patch_hash=a7a12643e8d89a00d5322f750c292e8567b5ee0fc9c613a238a1184c25533e4b)':
dependencies:
@@ -17534,7 +17535,7 @@ snapshots:
'@changesets/get-dependents-graph': 1.3.6
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
semver: 7.7.3
semver: 7.8.1
'@changesets/changelog-git@0.1.14':
dependencies:
@@ -17596,7 +17597,7 @@ snapshots:
'@manypkg/get-packages': 1.1.3
chalk: 2.4.2
fs-extra: 7.0.1
semver: 7.7.3
semver: 7.8.1
'@changesets/get-github-info@0.5.2(encoding@0.1.13)':
dependencies:
@@ -24149,7 +24150,7 @@ snapshots:
postcss-modules-scope: 3.1.1(postcss@8.5.10)
postcss-modules-values: 4.0.0(postcss@8.5.10)
postcss-value-parser: 4.2.0
semver: 7.6.3
semver: 7.8.1
optionalDependencies:
webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)
@@ -25570,7 +25571,7 @@ snapshots:
process-warning: 5.0.0
rfdc: 1.4.1
secure-json-parse: 4.0.0
semver: 7.7.3
semver: 7.8.1
toad-cache: 3.7.0
fastq@1.15.0:
@@ -28690,7 +28691,7 @@ snapshots:
cosmiconfig: 9.0.0(typescript@6.0.3)
jiti: 1.21.0
postcss: 8.5.15
semver: 7.6.3
semver: 7.8.1
optionalDependencies:
webpack: 5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)
transitivePeerDependencies:
@@ -29763,8 +29764,6 @@ snapshots:
semver@7.6.3: {}
semver@7.7.3: {}
semver@7.8.1: {}
send@0.18.0: