From bce3d3aee3e066f869ed950a1c9d2dea480b58af Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Tue, 30 Dec 2025 23:31:46 +0500 Subject: [PATCH 1/9] added svg schema for canvas --- api.oas3.yaml | 63 ++++++ schemas/asset.yaml | 2 + schemas/svgasset.yaml | 120 +++++++++++ schemas/svgproperties.yaml | 311 +++++++++++++++++++++++++++ schemas/svgshapes.yaml | 385 ++++++++++++++++++++++++++++++++++ scripts/fix-discriminator.cjs | 88 ++++++++ 6 files changed, 969 insertions(+) create mode 100644 schemas/svgasset.yaml create mode 100644 schemas/svgproperties.yaml create mode 100644 schemas/svgshapes.yaml diff --git a/api.oas3.yaml b/api.oas3.yaml index 85d8f46..c91d474 100644 --- a/api.oas3.yaml +++ b/api.oas3.yaml @@ -199,6 +199,69 @@ components: TitleAsset: $ref: "./schemas/titleasset.yaml#/TitleAsset" + SvgAsset: + $ref: "./schemas/svgasset.yaml#/SvgAsset" + + SvgShape: + $ref: "./schemas/svgshapes.yaml#/SvgShape" + + SvgRectangleShape: + $ref: "./schemas/svgshapes.yaml#/SvgRectangleShape" + + SvgCircleShape: + $ref: "./schemas/svgshapes.yaml#/SvgCircleShape" + + SvgEllipseShape: + $ref: "./schemas/svgshapes.yaml#/SvgEllipseShape" + + SvgLineShape: + $ref: "./schemas/svgshapes.yaml#/SvgLineShape" + + SvgPolygonShape: + $ref: "./schemas/svgshapes.yaml#/SvgPolygonShape" + + SvgStarShape: + $ref: "./schemas/svgshapes.yaml#/SvgStarShape" + + SvgArrowShape: + $ref: "./schemas/svgshapes.yaml#/SvgArrowShape" + + SvgHeartShape: + $ref: "./schemas/svgshapes.yaml#/SvgHeartShape" + + SvgCrossShape: + $ref: "./schemas/svgshapes.yaml#/SvgCrossShape" + + SvgRingShape: + $ref: "./schemas/svgshapes.yaml#/SvgRingShape" + + SvgPathShape: + $ref: "./schemas/svgshapes.yaml#/SvgPathShape" + + SvgFill: + $ref: "./schemas/svgproperties.yaml#/SvgFill" + + SvgSolidFill: + $ref: "./schemas/svgproperties.yaml#/SvgSolidFill" + + SvgLinearGradientFill: + $ref: "./schemas/svgproperties.yaml#/SvgLinearGradientFill" + + SvgRadialGradientFill: + $ref: "./schemas/svgproperties.yaml#/SvgRadialGradientFill" + + SvgGradientStop: + $ref: "./schemas/svgproperties.yaml#/SvgGradientStop" + + SvgStroke: + $ref: "./schemas/svgproperties.yaml#/SvgStroke" + + SvgShadow: + $ref: "./schemas/svgproperties.yaml#/SvgShadow" + + SvgTransform: + $ref: "./schemas/svgproperties.yaml#/SvgTransform" + Transition: $ref: "./schemas/transition.yaml#/Transition" diff --git a/schemas/asset.yaml b/schemas/asset.yaml index adbae25..364ee10 100644 --- a/schemas/asset.yaml +++ b/schemas/asset.yaml @@ -16,6 +16,7 @@ Asset: html: '#/components/schemas/HtmlAsset' title: '#/components/schemas/TitleAsset' shape: '#/components/schemas/ShapeAsset' + svg: '#/components/schemas/SvgAsset' text-to-image: '#/components/schemas/TextToImageAsset' image-to-video: '#/components/schemas/ImageToVideoAsset' oneOf: @@ -29,6 +30,7 @@ Asset: - $ref: './htmlasset.yaml#/HtmlAsset' - $ref: './titleasset.yaml#/TitleAsset' - $ref: './shapeasset.yaml#/ShapeAsset' + - $ref: './svgasset.yaml#/SvgAsset' - $ref: './texttoimageasset.yaml#/TextToImageAsset' - $ref: './imagetovideoasset.yaml#/ImageToVideoAsset' additionalProperties: false diff --git a/schemas/svgasset.yaml b/schemas/svgasset.yaml new file mode 100644 index 0000000..fca1164 --- /dev/null +++ b/schemas/svgasset.yaml @@ -0,0 +1,120 @@ +SvgAsset: + description: | + The SvgAsset is used to add scalable vector graphics (SVG) shapes to a video. + It provides a comprehensive set of primitive shapes and custom paths with full + styling support including fills, strokes, gradients, and shadows. + + **Available Shapes:** + - `rectangle` - Rectangles with optional rounded corners + - `circle` - Perfect circles + - `ellipse` - Ellipses/ovals with separate x and y radii + - `line` - Straight lines with configurable thickness + - `polygon` - Regular polygons (triangle, pentagon, hexagon, etc.) + - `star` - Multi-pointed stars + - `arrow` - Directional arrows + - `heart` - Heart shapes + - `cross` - Plus/cross shapes + - `ring` - Donut/ring shapes + - `path` - Custom shapes using SVG path data + + **Styling Options:** + - Fill with solid colors or linear/radial gradients + - Stroke with configurable width, color, dash patterns, and line caps/joins + - Drop shadows with blur, offset, and opacity + - Transform properties for positioning, rotation, and scaling + + See [W3C SVG 2 Specification](https://www.w3.org/TR/SVG2/) for path data syntax. + type: object + properties: + type: + description: The asset type - set to `svg` for SVG shapes. + type: string + enum: + - svg + default: svg + example: svg + shape: + description: | + The shape definition. The `type` property within determines the shape kind + and its specific properties. See individual shape documentation for details. + $ref: "./svgshapes.yaml#/SvgShape" + fill: + description: | + Fill properties for the shape interior. + Can be a solid color or a gradient (linear/radial). + If omitted, the shape will have no fill (transparent interior). + $ref: "./svgproperties.yaml#/SvgFill" + stroke: + description: | + Stroke (outline) properties for the shape. + If omitted, the shape will have no stroke (no outline). + $ref: "./svgproperties.yaml#/SvgStroke" + shadow: + description: | + Drop shadow properties for the shape. + Creates a shadow effect behind the shape. + $ref: "./svgproperties.yaml#/SvgShadow" + transform: + description: | + Transform properties for positioning, rotating, and scaling the shape. + The transform is applied relative to the transformation origin. + $ref: "./svgproperties.yaml#/SvgTransform" + opacity: + description: | + The overall opacity of the entire shape (including fill, stroke, and shadow). + `1` is fully opaque, `0` is fully transparent. + This is applied on top of individual fill/stroke/shadow opacity values. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + width: + description: | + The width of the bounding box in pixels. + If specified, the shape may be scaled to fit within this width. + If omitted, the shape uses its natural dimensions. + type: integer + minimum: 1 + maximum: 4096 + example: 400 + height: + description: | + The height of the bounding box in pixels. + If specified, the shape may be scaled to fit within this height. + If omitted, the shape uses its natural dimensions. + type: integer + minimum: 1 + maximum: 4096 + example: 300 + required: + - type + - shape + example: + type: svg + shape: + type: star + points: 5 + outerRadius: 100 + innerRadius: 50 + fill: + type: linear + angle: 45 + stops: + - offset: 0 + color: "#FFD700" + - offset: 1 + color: "#FF6B6B" + opacity: 1 + stroke: + color: "#2C3E50" + width: 3 + opacity: 1 + lineCap: round + lineJoin: round + transform: + x: 200 + y: 150 + rotation: 0 + scale: 1 + opacity: 1 diff --git a/schemas/svgproperties.yaml b/schemas/svgproperties.yaml new file mode 100644 index 0000000..1c70760 --- /dev/null +++ b/schemas/svgproperties.yaml @@ -0,0 +1,311 @@ +SvgFill: + description: | + Fill properties for SVG shapes. Supports solid colors and gradients. + The fill defines how the interior of a shape is painted. + oneOf: + - $ref: "#/SvgSolidFill" + - $ref: "#/SvgLinearGradientFill" + - $ref: "#/SvgRadialGradientFill" + discriminator: + propertyName: type + mapping: + solid: "#/components/schemas/SvgSolidFill" + linear: "#/components/schemas/SvgLinearGradientFill" + radial: "#/components/schemas/SvgRadialGradientFill" + +SvgSolidFill: + description: A solid color fill for SVG shapes. + type: object + properties: + type: + description: The fill type - set to `solid` for a single color fill. + type: string + enum: + - solid + default: solid + color: + description: | + The fill color using hexadecimal color notation (e.g., `#FF0000` for red). + Must be a 6-digit hex color code prefixed with `#`. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#3498db" + opacity: + description: | + The opacity of the fill where `1` is fully opaque and `0` is fully transparent. + Values between 0 and 1 create semi-transparent fills. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 0.8 + required: + - type + - color + +SvgGradientStop: + description: | + A color stop in a gradient. Each stop defines a color at a specific position + along the gradient vector. Gradients require at least 2 stops. + type: object + properties: + offset: + description: | + Position of the color stop along the gradient vector. + `0` represents the start and `1` represents the end of the gradient. + type: number + minimum: 0 + maximum: 1 + example: 0.5 + color: + description: The color at this stop using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + example: "#e74c3c" + required: + - offset + - color + +SvgLinearGradientFill: + description: | + A linear gradient fill that transitions colors along a straight line. + The gradient direction is controlled by the `angle` property. + type: object + properties: + type: + description: The fill type - set to `linear` for a linear gradient fill. + type: string + enum: + - linear + angle: + description: | + The angle of the gradient in degrees. `0` is horizontal (left to right), + `90` is vertical (bottom to top), `180` is right to left, etc. + type: number + minimum: 0 + maximum: 360 + default: 0 + example: 45 + stops: + description: | + Array of color stops that define the gradient colors and their positions. + Must have at least 2 stops. Offsets should increase from 0 to 1. + type: array + minItems: 2 + items: + $ref: "#/SvgGradientStop" + opacity: + description: | + The overall opacity of the gradient where `1` is fully opaque and `0` is fully transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + required: + - type + - stops + +SvgRadialGradientFill: + description: | + A radial gradient fill that transitions colors radiating outward from a center point. + The gradient creates a circular or elliptical color transition. + type: object + properties: + type: + description: The fill type - set to `radial` for a radial gradient fill. + type: string + enum: + - radial + stops: + description: | + Array of color stops that define the gradient colors and their positions. + Must have at least 2 stops. Offset `0` is the center, `1` is the outer edge. + type: array + minItems: 2 + items: + $ref: "#/SvgGradientStop" + opacity: + description: | + The overall opacity of the gradient where `1` is fully opaque and `0` is fully transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + required: + - type + - stops + +SvgStroke: + description: | + Stroke (outline) properties for SVG shapes. The stroke defines how the outline + of a shape is painted, including its color, width, and line style. + type: object + properties: + color: + description: The stroke color using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#2c3e50" + width: + description: | + The width of the stroke in pixels. Must be greater than 0. + Larger values create thicker outlines. + type: number + minimum: 0 + maximum: 100 + default: 1 + example: 2 + opacity: + description: The opacity of the stroke where `1` is opaque and `0` is transparent. + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + lineCap: + description: | + The shape at the end of open paths (lines, polylines, unclosed paths). + + type: string + enum: + - butt + - round + - square + default: butt + example: round + lineJoin: + description: | + The shape at the corners where two lines meet. + + type: string + enum: + - miter + - round + - bevel + default: miter + example: round + dashArray: + description: | + Pattern of dashes and gaps for the stroke. An array of numbers where + odd indices are dash lengths and even indices are gap lengths. + For example, `[10, 5]` creates 10px dashes with 5px gaps. + `[10, 5, 2, 5]` creates alternating 10px and 2px dashes with 5px gaps. + type: array + items: + type: number + minimum: 0 + example: [10, 5] + dashOffset: + description: | + Offset for the dash pattern. Positive values shift the pattern + forward along the path, negative values shift it backward. + type: number + default: 0 + example: 5 + +SvgShadow: + description: | + Drop shadow properties for SVG shapes. Creates a shadow effect behind the shape. + type: object + properties: + offsetX: + description: | + Horizontal offset of the shadow in pixels. + Positive values move the shadow to the right, negative to the left. + type: number + default: 0 + example: 4 + offsetY: + description: | + Vertical offset of the shadow in pixels. + Positive values move the shadow down, negative values move it up. + type: number + default: 0 + example: 4 + blur: + description: The blur radius of the shadow in pixels. Must be 0 or greater. + type: number + minimum: 0 + default: 0 + example: 8 + color: + description: The shadow color using hexadecimal color notation. + type: string + pattern: "^#[A-Fa-f0-9]{6}$" + default: "#000000" + example: "#000000" + opacity: + description: The opacity of the shadow where `1` is opaque and `0` is transparent. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.3 + +SvgTransform: + description: | + Transformation properties for positioning, rotating, and scaling SVG shapes. + type: object + properties: + x: + description: | + The x-coordinate position of the shape in pixels. + Relative to the top-left corner of the viewport. + type: number + default: 0 + example: 100 + y: + description: | + The y-coordinate position of the shape in pixels. + Relative to the top-left corner of the viewport. + type: number + default: 0 + example: 100 + rotation: + description: | + Rotation angle in degrees. Positive values rotate clockwise, + negative values rotate counter-clockwise. Range: -360 to 360. + type: number + minimum: -360 + maximum: 360 + default: 0 + example: 45 + scale: + description: | + Scale factor for the shape. `1` is original size, `2` is double size, + `0.5` is half size. Must be greater than 0. + type: number + minimum: 0.01 + maximum: 100 + default: 1 + example: 1.5 + originX: + description: | + The x-coordinate of the transformation origin as a value from 0 to 1. + `0` is the left edge, `0.5` is the center, `1` is the right edge. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.5 + originY: + description: | + The y-coordinate of the transformation origin as a value from 0 to 1. + `0` is the top edge, `0.5` is the center, `1` is the bottom edge. + type: number + minimum: 0 + maximum: 1 + default: 0.5 + example: 0.5 diff --git a/schemas/svgshapes.yaml b/schemas/svgshapes.yaml new file mode 100644 index 0000000..2e2978e --- /dev/null +++ b/schemas/svgshapes.yaml @@ -0,0 +1,385 @@ +SvgShape: + description: | + The shape definition for an SVG asset. Each shape type has its own specific + properties. The `type` field determines which shape is rendered. + oneOf: + - $ref: "#/SvgRectangleShape" + - $ref: "#/SvgCircleShape" + - $ref: "#/SvgEllipseShape" + - $ref: "#/SvgLineShape" + - $ref: "#/SvgPolygonShape" + - $ref: "#/SvgStarShape" + - $ref: "#/SvgArrowShape" + - $ref: "#/SvgHeartShape" + - $ref: "#/SvgCrossShape" + - $ref: "#/SvgRingShape" + - $ref: "#/SvgPathShape" + discriminator: + propertyName: type + mapping: + rectangle: "#/components/schemas/SvgRectangleShape" + circle: "#/components/schemas/SvgCircleShape" + ellipse: "#/components/schemas/SvgEllipseShape" + line: "#/components/schemas/SvgLineShape" + polygon: "#/components/schemas/SvgPolygonShape" + star: "#/components/schemas/SvgStarShape" + arrow: "#/components/schemas/SvgArrowShape" + heart: "#/components/schemas/SvgHeartShape" + cross: "#/components/schemas/SvgCrossShape" + ring: "#/components/schemas/SvgRingShape" + path: "#/components/schemas/SvgPathShape" + +SvgRectangleShape: + description: | + A rectangle shape with optional rounded corners. + The rectangle is defined by its width and height dimensions. + type: object + properties: + type: + description: The shape type - set to `rectangle`. + type: string + enum: + - rectangle + width: + description: The width of the rectangle in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 200 + height: + description: The height of the rectangle in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + cornerRadius: + description: | + The corner radius for rounded corners in pixels. + Set to `0` for sharp corners. The radius is automatically clamped + to half of the smallest dimension. + type: number + minimum: 0 + maximum: 2048 + default: 0 + example: 10 + required: + - type + - width + - height + +SvgCircleShape: + description: | + A perfect circle shape defined by its radius. + The circle is centered at the shape's position. + type: object + properties: + type: + description: The shape type - set to `circle`. + type: string + enum: + - circle + radius: + description: The radius of the circle in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - radius + +SvgEllipseShape: + description: | + An ellipse (oval) shape with separate horizontal and vertical radii. + The ellipse is centered at the shape's position. + type: object + properties: + type: + description: The shape type - set to `ellipse`. + type: string + enum: + - ellipse + radiusX: + description: The horizontal radius (semi-major axis) in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 80 + radiusY: + description: The vertical radius (semi-minor axis) in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - radiusX + - radiusY + +SvgLineShape: + description: | + A straight line shape with a specified length and thickness. + The line is drawn horizontally by default and can be rotated using transform. + type: object + properties: + type: + description: The shape type - set to `line`. + type: string + enum: + - line + length: + description: The length of the line in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + thickness: + description: The thickness of the line in pixels. + type: number + minimum: 1 + maximum: 500 + example: 4 + required: + - type + - length + - thickness + +SvgPolygonShape: + description: | + A regular polygon shape with a specified number of sides. + Examples: triangle (3), square (4), pentagon (5), hexagon (6), etc. + The polygon is inscribed in a circle of the given radius. + type: object + properties: + type: + description: The shape type - set to `polygon`. + type: string + enum: + - polygon + sides: + description: | + The number of sides of the polygon. + Minimum 3 (triangle), maximum 100 for practical use. + type: integer + minimum: 3 + maximum: 100 + example: 6 + radius: + description: | + The radius of the circumscribed circle in pixels. + This determines the size of the polygon. + type: number + minimum: 1 + maximum: 2048 + example: 50 + required: + - type + - sides + - radius + +SvgStarShape: + description: | + A star shape with a specified number of points. + The star is defined by outer and inner radii, creating the characteristic + pointed appearance. + type: object + properties: + type: + description: The shape type - set to `star`. + type: string + enum: + - star + points: + description: | + The number of points on the star. + Minimum 3 for a triangle-like star, typically 5 for a classic star. + type: integer + minimum: 3 + maximum: 100 + example: 5 + outerRadius: + description: | + The outer radius in pixels - the distance from center to the tips of the points. + type: number + minimum: 1 + maximum: 2048 + example: 50 + innerRadius: + description: | + The inner radius in pixels - the distance from center to the inner vertices. + Should be smaller than outerRadius for a star effect. + type: number + minimum: 1 + maximum: 2048 + example: 25 + required: + - type + - points + - outerRadius + - innerRadius + +SvgArrowShape: + description: | + An arrow shape pointing to the right by default. + Use transform rotation to change direction. + type: object + properties: + type: + description: The shape type - set to `arrow`. + type: string + enum: + - arrow + length: + description: The total length of the arrow from tail to tip in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + headWidth: + description: The width of the arrow head (the widest part) in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 40 + headLength: + description: The length of the arrow head portion in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 30 + shaftWidth: + description: The width of the arrow shaft (body) in pixels. + type: number + minimum: 1 + maximum: 1000 + example: 20 + required: + - type + - length + - headWidth + - headLength + - shaftWidth + +SvgHeartShape: + description: | + A heart shape commonly used for love/like icons. + The heart is defined by a single size parameter. + type: object + properties: + type: + description: The shape type - set to `heart`. + type: string + enum: + - heart + size: + description: | + The size of the heart in pixels. + This determines both the width and height proportionally. + type: number + minimum: 1 + maximum: 4096 + example: 100 + required: + - type + - size + +SvgCrossShape: + description: | + A cross or plus shape with equal or different arm lengths. + Can be styled as a plus sign (+) or a cross (x with rotation). + type: object + properties: + type: + description: The shape type - set to `cross`. + type: string + enum: + - cross + width: + description: The total width of the cross in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + height: + description: The total height of the cross in pixels. + type: number + minimum: 1 + maximum: 4096 + example: 100 + thickness: + description: The thickness of the cross arms in pixels. + type: number + minimum: 1 + maximum: 500 + example: 20 + required: + - type + - width + - height + - thickness + +SvgRingShape: + description: | + A ring (donut/annulus) shape - a circle with a circular hole in the center. + The ring is defined by outer and inner radii. + type: object + properties: + type: + description: The shape type - set to `ring`. + type: string + enum: + - ring + outerRadius: + description: The outer radius of the ring in pixels. + type: number + minimum: 1 + maximum: 2048 + example: 50 + innerRadius: + description: | + The inner radius (hole) of the ring in pixels. + Must be smaller than outerRadius. + type: number + minimum: 0 + maximum: 2048 + example: 30 + required: + - type + - outerRadius + - innerRadius + +SvgPathShape: + description: | + A custom shape defined by SVG path data. + Supports all standard SVG path commands for creating complex shapes. + + **Path Commands:** + - `M x y` / `m dx dy` - Move to (absolute/relative) + - `L x y` / `l dx dy` - Line to + - `H x` / `h dx` - Horizontal line to + - `V y` / `v dy` - Vertical line to + - `C x1 y1 x2 y2 x y` / `c` - Cubic Bezier curve + - `S x2 y2 x y` / `s` - Smooth cubic Bezier + - `Q x1 y1 x y` / `q` - Quadratic Bezier curve + - `T x y` / `t` - Smooth quadratic Bezier + - `A rx ry angle large-arc sweep x y` / `a` - Elliptical arc + - `Z` / `z` - Close path + type: object + properties: + type: + description: The shape type - set to `path`. + type: string + enum: + - path + d: + description: | + The SVG path data string defining the shape. + Uses standard SVG path commands (M, L, C, Q, A, Z, etc.). + Example: `M 0 0 L 100 0 L 100 100 L 0 100 Z` draws a square. + type: string + minLength: 1 + maxLength: 100000 + example: "M 0 0 L 100 0 L 50 86.6 Z" + required: + - type + - d diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 0bc421c..055559d 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -23,6 +23,7 @@ const newAssetSchema = `export const assetAssetSchema = z.discriminatedUnion("ty htmlassetHtmlAssetSchema, titleassetTitleAssetSchema, shapeassetShapeAssetSchema, + svgassetSvgAssetSchema, texttoimageassetTextToImageAssetSchema, imagetovideoassetImageToVideoAssetSchema, ]);`; @@ -34,6 +35,46 @@ if (assetUnionPattern.test(content)) { console.log("⚠ Could not find assetAssetSchema to replace"); } +const svgShapeUnionPattern = + /export const svgshapesSvgShapeSchema = z\.union\(\[[\s\S]*?\]\);/; + +const newSvgShapeSchema = `export const svgshapesSvgShapeSchema = z.discriminatedUnion("type", [ + svgshapesSvgRectangleShapeSchema, + svgshapesSvgCircleShapeSchema, + svgshapesSvgEllipseShapeSchema, + svgshapesSvgLineShapeSchema, + svgshapesSvgPolygonShapeSchema, + svgshapesSvgStarShapeSchema, + svgshapesSvgArrowShapeSchema, + svgshapesSvgHeartShapeSchema, + svgshapesSvgCrossShapeSchema, + svgshapesSvgRingShapeSchema, + svgshapesSvgPathShapeSchema, +]);`; + +if (svgShapeUnionPattern.test(content)) { + content = content.replace(svgShapeUnionPattern, newSvgShapeSchema); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator"); +} else { + console.log("⚠ Could not find svgshapesSvgShapeSchema to replace"); +} + +const svgFillUnionPattern = + /export const svgpropertiesSvgFillSchema = z\.union\(\[[\s\S]*?\]\);/; + +const newSvgFillSchema = `export const svgpropertiesSvgFillSchema = z.discriminatedUnion("type", [ + svgpropertiesSvgSolidFillSchema, + svgpropertiesSvgLinearGradientFillSchema, + svgpropertiesSvgRadialGradientFillSchema, +]);`; + +if (svgFillUnionPattern.test(content)) { + content = content.replace(svgFillUnionPattern, newSvgFillSchema); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator"); +} else { + console.log("⚠ Could not find svgpropertiesSvgFillSchema to replace"); +} + const clipStartPattern = /start: z\.union\(\[z\.number\(\), z\.enum\(\["auto"\]\)\]\)/g; const clipStartReplacement = `start: z.union([z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()), z.enum(["auto"])])`; @@ -114,6 +155,7 @@ if (fs.existsSync(zodGenCjsPath)) { exports.htmlassetHtmlAssetSchema, exports.titleassetTitleAssetSchema, exports.shapeassetShapeAssetSchema, + exports.svgassetSvgAssetSchema, exports.texttoimageassetTextToImageAssetSchema, exports.imagetovideoassetImageToVideoAssetSchema, ]);`; @@ -123,6 +165,42 @@ if (fs.existsSync(zodGenCjsPath)) { console.log("✓ Fixed assetAssetSchema discriminator in CJS"); } + const cjsSvgShapeUnionPattern = + /exports\.svgshapesSvgShapeSchema = zod_1\.z\.union\(\[[\s\S]*?\]\);/; + + const newCjsSvgShapeSchema = `exports.svgshapesSvgShapeSchema = zod_1.z.discriminatedUnion("type", [ + exports.svgshapesSvgRectangleShapeSchema, + exports.svgshapesSvgCircleShapeSchema, + exports.svgshapesSvgEllipseShapeSchema, + exports.svgshapesSvgLineShapeSchema, + exports.svgshapesSvgPolygonShapeSchema, + exports.svgshapesSvgStarShapeSchema, + exports.svgshapesSvgArrowShapeSchema, + exports.svgshapesSvgHeartShapeSchema, + exports.svgshapesSvgCrossShapeSchema, + exports.svgshapesSvgRingShapeSchema, + exports.svgshapesSvgPathShapeSchema, +]);`; + + if (cjsSvgShapeUnionPattern.test(cjsContent)) { + cjsContent = cjsContent.replace(cjsSvgShapeUnionPattern, newCjsSvgShapeSchema); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator in CJS"); + } + + const cjsSvgFillUnionPattern = + /exports\.svgpropertiesSvgFillSchema = zod_1\.z\.union\(\[[\s\S]*?\]\);/; + + const newCjsSvgFillSchema = `exports.svgpropertiesSvgFillSchema = zod_1.z.discriminatedUnion("type", [ + exports.svgpropertiesSvgSolidFillSchema, + exports.svgpropertiesSvgLinearGradientFillSchema, + exports.svgpropertiesSvgRadialGradientFillSchema, +]);`; + + if (cjsSvgFillUnionPattern.test(cjsContent)) { + cjsContent = cjsContent.replace(cjsSvgFillUnionPattern, newCjsSvgFillSchema); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in CJS"); + } + const cjsCoercionPatterns = [ { pattern: @@ -183,6 +261,16 @@ if (fs.existsSync(zodGenJsPath)) { console.log("✓ Fixed assetAssetSchema discriminator in ESM JS"); } + if (svgShapeUnionPattern.test(jsContent)) { + jsContent = jsContent.replace(svgShapeUnionPattern, newSvgShapeSchema); + console.log("✓ Fixed svgshapesSvgShapeSchema discriminator in ESM JS"); + } + + if (svgFillUnionPattern.test(jsContent)) { + jsContent = jsContent.replace(svgFillUnionPattern, newSvgFillSchema); + console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in ESM JS"); + } + const esmCoercionPatterns = [ { pattern: clipStartPattern, From f1225dd591774a59b57095537f6196ded3e433f4 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Wed, 31 Dec 2025 11:35:04 +1100 Subject: [PATCH 2/9] feat: add global string-to-number coercion for z.number() and z.int() schemas --- scripts/fix-discriminator.cjs | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 0bc421c..6c3d78e 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -94,6 +94,54 @@ if (scaleCount > 0) { console.log(`✓ Added coercion for scale (${scaleCount} occurrences)`); } +// Global coercion for ALL remaining z.number() patterns not already handled +// This catches font.size, opacity, width, height, etc. +const coerceHelper = `(val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val`; + +// Pattern: z.number() without any chained methods (standalone) +const plainNumberPattern = /z\.number\(\)(?!\.)/g; +const plainNumberReplacement = `z.preprocess(${coerceHelper}, z.number())`; +const plainNumberCount = (content.match(plainNumberPattern) || []).length; +if (plainNumberCount > 0) { + content = content.replace(plainNumberPattern, plainNumberReplacement); + console.log(`✓ Added coercion for plain z.number() (${plainNumberCount} occurrences)`); +} + +// Pattern: z.number() with chained methods (e.g., .gte(), .lte(), .min(), .max(), .int()) +// Match z.number() followed by method chains +const chainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; +let chainedCount = 0; +content = content.replace(chainedNumberPattern, (match, chain) => { + // Skip if already preprocessed (from specific patterns above) + if (match.includes('preprocess')) return match; + chainedCount++; + return `z.preprocess(${coerceHelper}, z.number()${chain})`; +}); +if (chainedCount > 0) { + console.log(`✓ Added coercion for chained z.number() (${chainedCount} occurrences)`); +} + +// Pattern: z.int() - integers also need coercion from strings +const plainIntPattern = /z\.int\(\)(?!\.)/g; +const plainIntReplacement = `z.preprocess(${coerceHelper}, z.int())`; +const plainIntCount = (content.match(plainIntPattern) || []).length; +if (plainIntCount > 0) { + content = content.replace(plainIntPattern, plainIntReplacement); + console.log(`✓ Added coercion for plain z.int() (${plainIntCount} occurrences)`); +} + +// Pattern: z.int() with chained methods (e.g., .gte(), .lte()) +const chainedIntPattern = /z\.int\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; +let chainedIntCount = 0; +content = content.replace(chainedIntPattern, (match, chain) => { + if (match.includes('preprocess')) return match; + chainedIntCount++; + return `z.preprocess(${coerceHelper}, z.int()${chain})`; +}); +if (chainedIntCount > 0) { + console.log(`✓ Added coercion for chained z.int() (${chainedIntCount} occurrences)`); +} + fs.writeFileSync(zodGenPath, content); const zodGenCjsPath = path.join(__dirname, "..", "dist", "zod", "zod.gen.cjs"); @@ -168,6 +216,28 @@ if (fs.existsSync(zodGenCjsPath)) { } } + // Global coercion for ALL remaining zod_1.z.number() patterns in CJS + const cjsCoerceHelper = `(val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val`; + + const cjsPlainNumberPattern = /zod_1\.z\.number\(\)(?!\.)/g; + const cjsPlainNumberReplacement = `zod_1.z.preprocess(${cjsCoerceHelper}, zod_1.z.number())`; + const cjsPlainNumberCount = (cjsContent.match(cjsPlainNumberPattern) || []).length; + if (cjsPlainNumberCount > 0) { + cjsContent = cjsContent.replace(cjsPlainNumberPattern, cjsPlainNumberReplacement); + console.log(`✓ Added coercion for plain z.number() in CJS (${cjsPlainNumberCount} occurrences)`); + } + + const cjsChainedNumberPattern = /zod_1\.z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; + let cjsChainedCount = 0; + cjsContent = cjsContent.replace(cjsChainedNumberPattern, (match, chain) => { + if (match.includes('preprocess')) return match; + cjsChainedCount++; + return `zod_1.z.preprocess(${cjsCoerceHelper}, zod_1.z.number()${chain})`; + }); + if (cjsChainedCount > 0) { + console.log(`✓ Added coercion for chained z.number() in CJS (${cjsChainedCount} occurrences)`); + } + fs.writeFileSync(zodGenCjsPath, cjsContent); } @@ -210,6 +280,23 @@ if (fs.existsSync(zodGenJsPath)) { } } + // Global coercion for ALL remaining z.number() patterns in ESM JS + const esmPlainNumberCount = (jsContent.match(plainNumberPattern) || []).length; + if (esmPlainNumberCount > 0) { + jsContent = jsContent.replace(plainNumberPattern, plainNumberReplacement); + console.log(`✓ Added coercion for plain z.number() in ESM JS (${esmPlainNumberCount} occurrences)`); + } + + let esmChainedCount = 0; + jsContent = jsContent.replace(chainedNumberPattern, (match, chain) => { + if (match.includes('preprocess')) return match; + esmChainedCount++; + return `z.preprocess(${coerceHelper}, z.number()${chain})`; + }); + if (esmChainedCount > 0) { + console.log(`✓ Added coercion for chained z.number() in ESM JS (${esmChainedCount} occurrences)`); + } + fs.writeFileSync(zodGenJsPath, jsContent); } From 6f0785a8187c532a9d5cf84ddd3aa7f98f0fd497 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Wed, 31 Dec 2025 15:06:29 +0500 Subject: [PATCH 3/9] updated coerce script --- package.json | 2 +- scripts/fix-discriminator.cjs | 240 +++++++++------------------------- 2 files changed, 66 insertions(+), 176 deletions(-) diff --git a/package.json b/package.json index b7ca647..08362d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shotstack/schemas", - "version": "1.3.5", + "version": "1.3.8", "description": "Centralized OpenAPI schemas and TypeScript types for Shotstack API", "type": "module", "main": "dist/index.js", diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 37bd336..5f30f53 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -3,9 +3,7 @@ const path = require("path"); const zodGenPath = path.join(__dirname, "..", "dist", "zod", "zod.gen.ts"); -console.log( - "Fixing discriminator and adding coercion in generated Zod schemas..." -); +console.log("Fixing discriminator and adding z.coerce for number fields..."); let content = fs.readFileSync(zodGenPath, "utf8"); @@ -75,112 +73,54 @@ if (svgFillUnionPattern.test(content)) { console.log("⚠ Could not find svgpropertiesSvgFillSchema to replace"); } -const clipStartPattern = - /start: z\.union\(\[z\.number\(\), z\.enum\(\["auto"\]\)\]\)/g; -const clipStartReplacement = `start: z.union([z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()), z.enum(["auto"])])`; +const rejectInvalid = `((v: unknown) => v === '' || Array.isArray(v) ? NaN : v)`; -if (clipStartPattern.test(content)) { - content = content.replace(clipStartPattern, clipStartReplacement); - console.log("✓ Added coercion for clip start"); -} else { - console.log("⚠ Could not find clip start pattern to add coercion"); -} - -const clipLengthPattern = - /length: z\.union\(\[z\.number\(\), z\.literal\("auto"\), z\.literal\("end"\)\]\)/g; -const clipLengthReplacement = `length: z.union([z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()), z.literal("auto"), z.literal("end")])`; - -if (clipLengthPattern.test(content)) { - content = content.replace(clipLengthPattern, clipLengthReplacement); - console.log("✓ Added coercion for clip length"); -} else { - console.log("⚠ Could not find clip length pattern to add coercion"); -} - -const trimPattern = /trim: z\.optional\(z\.number\(\)\)/g; -const trimReplacement = `trim: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()))`; - -const trimCount = (content.match(trimPattern) || []).length; -if (trimCount > 0) { - content = content.replace(trimPattern, trimReplacement); - console.log(`✓ Added coercion for trim (${trimCount} occurrences)`); -} - -const volumePattern = - /volume: z\.optional\(z\.number\(\)\.gte\(0\)\.lte\(1\)\)/g; -const volumeReplacement = `volume: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number().gte(0).lte(1)))`; - -const volumeCount = (content.match(volumePattern) || []).length; -if (volumeCount > 0) { - content = content.replace(volumePattern, volumeReplacement); - console.log(`✓ Added coercion for volume (${volumeCount} occurrences)`); -} - -const speedPattern = - /speed: z\.optional\(z\.number\(\)\.gte\(0\)\.lte\(10\)\)/g; -const speedReplacement = `speed: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number().gte(0).lte(10)))`; - -const speedCount = (content.match(speedPattern) || []).length; -if (speedCount > 0) { - content = content.replace(speedPattern, speedReplacement); - console.log(`✓ Added coercion for speed (${speedCount} occurrences)`); -} - -const scalePattern = /scale: z\.optional\(z\.number\(\)\)/g; -const scaleReplacement = `scale: z.optional(z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, z.number()))`; - -const scaleCount = (content.match(scalePattern) || []).length; -if (scaleCount > 0) { - content = content.replace(scalePattern, scaleReplacement); - console.log(`✓ Added coercion for scale (${scaleCount} occurrences)`); -} - -// Global coercion for ALL remaining z.number() patterns not already handled -// This catches font.size, opacity, width, height, etc. -const coerceHelper = `(val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val`; - -// Pattern: z.number() without any chained methods (standalone) const plainNumberPattern = /z\.number\(\)(?!\.)/g; -const plainNumberReplacement = `z.preprocess(${coerceHelper}, z.number())`; const plainNumberCount = (content.match(plainNumberPattern) || []).length; if (plainNumberCount > 0) { - content = content.replace(plainNumberPattern, plainNumberReplacement); - console.log(`✓ Added coercion for plain z.number() (${plainNumberCount} occurrences)`); + content = content.replace( + plainNumberPattern, + `z.preprocess(${rejectInvalid}, z.coerce.number())` + ); + console.log( + `✓ Added z.coerce.number() for plain z.number() (${plainNumberCount} occurrences)` + ); } -// Pattern: z.number() with chained methods (e.g., .gte(), .lte(), .min(), .max(), .int()) -// Match z.number() followed by method chains const chainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let chainedCount = 0; content = content.replace(chainedNumberPattern, (match, chain) => { - // Skip if already preprocessed (from specific patterns above) - if (match.includes('preprocess')) return match; chainedCount++; - return `z.preprocess(${coerceHelper}, z.number()${chain})`; + return `z.preprocess(${rejectInvalid}, z.coerce.number()${chain})`; }); if (chainedCount > 0) { - console.log(`✓ Added coercion for chained z.number() (${chainedCount} occurrences)`); + console.log( + `✓ Added z.coerce.number() for chained z.number() (${chainedCount} occurrences)` + ); } -// Pattern: z.int() - integers also need coercion from strings const plainIntPattern = /z\.int\(\)(?!\.)/g; -const plainIntReplacement = `z.preprocess(${coerceHelper}, z.int())`; const plainIntCount = (content.match(plainIntPattern) || []).length; if (plainIntCount > 0) { - content = content.replace(plainIntPattern, plainIntReplacement); - console.log(`✓ Added coercion for plain z.int() (${plainIntCount} occurrences)`); + content = content.replace( + plainIntPattern, + `z.preprocess(${rejectInvalid}, z.coerce.number().int())` + ); + console.log( + `✓ Added z.coerce.number().int() for plain z.int() (${plainIntCount} occurrences)` + ); } -// Pattern: z.int() with chained methods (e.g., .gte(), .lte()) const chainedIntPattern = /z\.int\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let chainedIntCount = 0; content = content.replace(chainedIntPattern, (match, chain) => { - if (match.includes('preprocess')) return match; chainedIntCount++; - return `z.preprocess(${coerceHelper}, z.int()${chain})`; + return `z.preprocess(${rejectInvalid}, z.coerce.number().int()${chain})`; }); if (chainedIntCount > 0) { - console.log(`✓ Added coercion for chained z.int() (${chainedIntCount} occurrences)`); + console.log( + `✓ Added z.coerce.number().int() for chained z.int() (${chainedIntCount} occurrences)` + ); } fs.writeFileSync(zodGenPath, content); @@ -231,7 +171,10 @@ if (fs.existsSync(zodGenCjsPath)) { ]);`; if (cjsSvgShapeUnionPattern.test(cjsContent)) { - cjsContent = cjsContent.replace(cjsSvgShapeUnionPattern, newCjsSvgShapeSchema); + cjsContent = cjsContent.replace( + cjsSvgShapeUnionPattern, + newCjsSvgShapeSchema + ); console.log("✓ Fixed svgshapesSvgShapeSchema discriminator in CJS"); } @@ -245,75 +188,39 @@ if (fs.existsSync(zodGenCjsPath)) { ]);`; if (cjsSvgFillUnionPattern.test(cjsContent)) { - cjsContent = cjsContent.replace(cjsSvgFillUnionPattern, newCjsSvgFillSchema); + cjsContent = cjsContent.replace( + cjsSvgFillUnionPattern, + newCjsSvgFillSchema + ); console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in CJS"); } - const cjsCoercionPatterns = [ - { - pattern: - /start: zod_1\.z\.union\(\[zod_1\.z\.number\(\), zod_1\.z\.enum\(\["auto"\]\)\]\)/g, - replacement: `start: zod_1.z.union([zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()), zod_1.z.enum(["auto"])])`, - name: "clip start", - }, - { - pattern: - /length: zod_1\.z\.union\(\[zod_1\.z\.number\(\), zod_1\.z\.literal\("auto"\), zod_1\.z\.literal\("end"\)\]\)/g, - replacement: `length: zod_1.z.union([zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()), zod_1.z.literal("auto"), zod_1.z.literal("end")])`, - name: "clip length", - }, - { - pattern: /trim: zod_1\.z\.optional\(zod_1\.z\.number\(\)\)/g, - replacement: `trim: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()))`, - name: "trim", - }, - { - pattern: - /volume: zod_1\.z\.optional\(zod_1\.z\.number\(\)\.gte\(0\)\.lte\(1\)\)/g, - replacement: `volume: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number().gte(0).lte(1)))`, - name: "volume", - }, - { - pattern: - /speed: zod_1\.z\.optional\(zod_1\.z\.number\(\)\.gte\(0\)\.lte\(10\)\)/g, - replacement: `speed: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number().gte(0).lte(10)))`, - name: "speed", - }, - { - pattern: /scale: zod_1\.z\.optional\(zod_1\.z\.number\(\)\)/g, - replacement: `scale: zod_1.z.optional(zod_1.z.preprocess((val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val, zod_1.z.number()))`, - name: "scale", - }, - ]; - - for (const { pattern, replacement, name } of cjsCoercionPatterns) { - const count = (cjsContent.match(pattern) || []).length; - if (count > 0) { - cjsContent = cjsContent.replace(pattern, replacement); - console.log(`✓ Added coercion for ${name} in CJS (${count} occurrences)`); - } - } - - // Global coercion for ALL remaining zod_1.z.number() patterns in CJS - const cjsCoerceHelper = `(val) => typeof val === 'string' && val !== '' && !isNaN(Number(val)) ? Number(val) : val`; + const cjsRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; const cjsPlainNumberPattern = /zod_1\.z\.number\(\)(?!\.)/g; - const cjsPlainNumberReplacement = `zod_1.z.preprocess(${cjsCoerceHelper}, zod_1.z.number())`; - const cjsPlainNumberCount = (cjsContent.match(cjsPlainNumberPattern) || []).length; + const cjsPlainNumberCount = (cjsContent.match(cjsPlainNumberPattern) || []) + .length; if (cjsPlainNumberCount > 0) { - cjsContent = cjsContent.replace(cjsPlainNumberPattern, cjsPlainNumberReplacement); - console.log(`✓ Added coercion for plain z.number() in CJS (${cjsPlainNumberCount} occurrences)`); + cjsContent = cjsContent.replace( + cjsPlainNumberPattern, + `zod_1.z.preprocess(${cjsRejectInvalid}, zod_1.z.coerce.number())` + ); + console.log( + `✓ Added z.coerce.number() in CJS (${cjsPlainNumberCount} occurrences)` + ); } - const cjsChainedNumberPattern = /zod_1\.z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; + const cjsChainedNumberPattern = + /zod_1\.z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let cjsChainedCount = 0; cjsContent = cjsContent.replace(cjsChainedNumberPattern, (match, chain) => { - if (match.includes('preprocess')) return match; cjsChainedCount++; - return `zod_1.z.preprocess(${cjsCoerceHelper}, zod_1.z.number()${chain})`; + return `zod_1.z.preprocess(${cjsRejectInvalid}, zod_1.z.coerce.number()${chain})`; }); if (cjsChainedCount > 0) { - console.log(`✓ Added coercion for chained z.number() in CJS (${cjsChainedCount} occurrences)`); + console.log( + `✓ Added z.coerce.number() chains in CJS (${cjsChainedCount} occurrences)` + ); } fs.writeFileSync(zodGenCjsPath, cjsContent); @@ -341,48 +248,31 @@ if (fs.existsSync(zodGenJsPath)) { console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in ESM JS"); } - const esmCoercionPatterns = [ - { - pattern: clipStartPattern, - replacement: clipStartReplacement, - name: "clip start", - }, - { - pattern: clipLengthPattern, - replacement: clipLengthReplacement, - name: "clip length", - }, - { pattern: trimPattern, replacement: trimReplacement, name: "trim" }, - { pattern: volumePattern, replacement: volumeReplacement, name: "volume" }, - { pattern: speedPattern, replacement: speedReplacement, name: "speed" }, - { pattern: scalePattern, replacement: scaleReplacement, name: "scale" }, - ]; - - for (const { pattern, replacement, name } of esmCoercionPatterns) { - const count = (jsContent.match(pattern) || []).length; - if (count > 0) { - jsContent = jsContent.replace(pattern, replacement); - console.log( - `✓ Added coercion for ${name} in ESM JS (${count} occurrences)` - ); - } - } + const esmRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; - // Global coercion for ALL remaining z.number() patterns in ESM JS - const esmPlainNumberCount = (jsContent.match(plainNumberPattern) || []).length; + const esmPlainNumberPattern = /z\.number\(\)(?!\.)/g; + const esmPlainNumberCount = (jsContent.match(esmPlainNumberPattern) || []) + .length; if (esmPlainNumberCount > 0) { - jsContent = jsContent.replace(plainNumberPattern, plainNumberReplacement); - console.log(`✓ Added coercion for plain z.number() in ESM JS (${esmPlainNumberCount} occurrences)`); + jsContent = jsContent.replace( + esmPlainNumberPattern, + `z.preprocess(${esmRejectInvalid}, z.coerce.number())` + ); + console.log( + `✓ Added z.coerce.number() in ESM JS (${esmPlainNumberCount} occurrences)` + ); } + const esmChainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let esmChainedCount = 0; - jsContent = jsContent.replace(chainedNumberPattern, (match, chain) => { - if (match.includes('preprocess')) return match; + jsContent = jsContent.replace(esmChainedNumberPattern, (match, chain) => { esmChainedCount++; - return `z.preprocess(${coerceHelper}, z.number()${chain})`; + return `z.preprocess(${esmRejectInvalid}, z.coerce.number()${chain})`; }); if (esmChainedCount > 0) { - console.log(`✓ Added coercion for chained z.number() in ESM JS (${esmChainedCount} occurrences)`); + console.log( + `✓ Added z.coerce.number() chains in ESM JS (${esmChainedCount} occurrences)` + ); } fs.writeFileSync(zodGenJsPath, jsContent); From 2c249696ad773369fe84d48e79c33c7c6f994ddf Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 1 Jan 2026 00:50:21 +0500 Subject: [PATCH 4/9] fixed the float values and bump the version --- package.json | 2 +- schemas/ingest/responses/sourceresponseattributes.yaml | 2 +- schemas/rotatetransformation.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 08362d0..f5ab041 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shotstack/schemas", - "version": "1.3.8", + "version": "1.3.9", "description": "Centralized OpenAPI schemas and TypeScript types for Shotstack API", "type": "module", "main": "dist/index.js", diff --git a/schemas/ingest/responses/sourceresponseattributes.yaml b/schemas/ingest/responses/sourceresponseattributes.yaml index e525865..5745453 100644 --- a/schemas/ingest/responses/sourceresponseattributes.yaml +++ b/schemas/ingest/responses/sourceresponseattributes.yaml @@ -51,7 +51,7 @@ example: 1920 height: description: The height in pixels of the ingested source file, if a video or image. - type: string + type: integer example: 1080 duration: description: The duration in seconds of the ingested source file, if a video or audio file. diff --git a/schemas/rotatetransformation.yaml b/schemas/rotatetransformation.yaml index 85a68ab..3fcfbd8 100644 --- a/schemas/rotatetransformation.yaml +++ b/schemas/rotatetransformation.yaml @@ -9,7 +9,7 @@ Rotate a clip by the specified angle in degrees. Use a number or an array of [Tween](./#tocs_tween) objects to create a custom animation. oneOf: - - type: integer + - type: number description: >- The angle to rotate the clip. Can be 0 to 360, or 0 to -360. Using a positive number rotates the clip clockwise, negative numbers From b24818ed8efd7febe6e2131ba8ea4ac2e6765a33 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 1 Jan 2026 12:10:04 +0500 Subject: [PATCH 5/9] added svg import schema --- schemas/svgasset.yaml | 77 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/schemas/svgasset.yaml b/schemas/svgasset.yaml index fca1164..eeccc15 100644 --- a/schemas/svgasset.yaml +++ b/schemas/svgasset.yaml @@ -1,10 +1,33 @@ SvgAsset: description: | The SvgAsset is used to add scalable vector graphics (SVG) shapes to a video. - It provides a comprehensive set of primitive shapes and custom paths with full - styling support including fills, strokes, gradients, and shadows. + It provides two mutually exclusive ways to define shapes: - **Available Shapes:** + **Option 1: Import SVG markup using `src`** + ```json + { + "type": "svg", + "src": "" + } + ``` + When using `src`, no other properties are allowed. The fill, stroke, and dimensions + are automatically extracted from the SVG markup. + + **Option 2: Define shapes programmatically using `shape`** + ```json + { + "type": "svg", + "shape": { "type": "circle", "radius": 50 }, + "fill": { "type": "solid", "color": "#FF0000" } + } + ``` + When using `shape`, you can customize fill, stroke, shadow, transform, and other properties. + The `src` property is not allowed in this mode. + + **Important:** You must provide either `src` OR `shape`, but not both. + These two modes are mutually exclusive. + + **Available Shapes (Option 2 only):** - `rectangle` - Rectangles with optional rounded corners - `circle` - Perfect circles - `ellipse` - Ellipses/ovals with separate x and y radii @@ -17,12 +40,6 @@ SvgAsset: - `ring` - Donut/ring shapes - `path` - Custom shapes using SVG path data - **Styling Options:** - - Fill with solid colors or linear/radial gradients - - Stroke with configurable width, color, dash patterns, and line caps/joins - - Drop shadows with blur, offset, and opacity - - Transform properties for positioning, rotation, and scaling - See [W3C SVG 2 Specification](https://www.w3.org/TR/SVG2/) for path data syntax. type: object properties: @@ -33,37 +50,70 @@ SvgAsset: - svg default: svg example: svg + src: + description: | + Raw SVG markup string to import. When provided, the shape is extracted + automatically from the SVG content. + + **Supported elements:** ``, ``, ``, ``, + ``, ``, `` + + **Automatically extracted:** + - Path data (converted to a single combined path) + - Fill color (from `fill` attribute or `style`) + - Stroke color and width (from attributes or `style`) + - Dimensions (from `width`/`height` or `viewBox`) + - Opacity (from `opacity` attribute) + + **Important:** When using `src`, no other properties (shape, fill, stroke, etc.) + are allowed. All styling must be defined within the SVG markup itself. + type: string + minLength: 1 + maxLength: 500000 + example: '' shape: description: | - The shape definition. The `type` property within determines the shape kind - and its specific properties. See individual shape documentation for details. + The shape definition using primitives. The `type` property within determines + the shape kind and its specific properties. + + **Important:** When using `shape`, the `src` property is not allowed. $ref: "./svgshapes.yaml#/SvgShape" fill: description: | Fill properties for the shape interior. Can be a solid color or a gradient (linear/radial). If omitted, the shape will have no fill (transparent interior). + + **Note:** Only allowed when using `shape`, not with `src`. $ref: "./svgproperties.yaml#/SvgFill" stroke: description: | Stroke (outline) properties for the shape. If omitted, the shape will have no stroke (no outline). + + **Note:** Only allowed when using `shape`, not with `src`. $ref: "./svgproperties.yaml#/SvgStroke" shadow: description: | Drop shadow properties for the shape. Creates a shadow effect behind the shape. + + **Note:** Only allowed when using `shape`, not with `src`. $ref: "./svgproperties.yaml#/SvgShadow" transform: description: | Transform properties for positioning, rotating, and scaling the shape. The transform is applied relative to the transformation origin. + + **Note:** Only allowed when using `shape`, not with `src`. $ref: "./svgproperties.yaml#/SvgTransform" opacity: description: | The overall opacity of the entire shape (including fill, stroke, and shadow). `1` is fully opaque, `0` is fully transparent. This is applied on top of individual fill/stroke/shadow opacity values. + + **Note:** Only allowed when using `shape`, not with `src`. type: number minimum: 0 maximum: 1 @@ -74,6 +124,8 @@ SvgAsset: The width of the bounding box in pixels. If specified, the shape may be scaled to fit within this width. If omitted, the shape uses its natural dimensions. + + **Note:** Only allowed when using `shape`, not with `src`. type: integer minimum: 1 maximum: 4096 @@ -83,13 +135,14 @@ SvgAsset: The height of the bounding box in pixels. If specified, the shape may be scaled to fit within this height. If omitted, the shape uses its natural dimensions. + + **Note:** Only allowed when using `shape`, not with `src`. type: integer minimum: 1 maximum: 4096 example: 300 required: - type - - shape example: type: svg shape: From a04270a1908d889769dcb6f3e3e2196dcaa8cb2c Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 1 Jan 2026 12:40:46 +0500 Subject: [PATCH 6/9] updated validation for svg --- scripts/fix-discriminator.cjs | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 5f30f53..009d90d 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -73,6 +73,61 @@ if (svgFillUnionPattern.test(content)) { console.log("⚠ Could not find svgpropertiesSvgFillSchema to replace"); } +const svgAssetPattern = + /export const svgassetSvgAssetSchema = z\.object\(\{[\s\S]*?\}\);/; + +const svgAssetSuperRefine = `export const svgassetSvgAssetSchema = z.object({ + type: z.enum(["svg"]), + src: z.optional(z.string().min(1).max(500000)), + shape: z.optional(svgshapesSvgShapeSchema), + fill: z.optional(svgpropertiesSvgFillSchema), + stroke: z.optional(svgpropertiesSvgStrokeSchema), + shadow: z.optional(svgpropertiesSvgShadowSchema), + transform: z.optional(svgpropertiesSvgTransformSchema), + opacity: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().gte(0).lte(1))).default(1), + width: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().int().gte(1).lte(4096))), + height: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().int().gte(1).lte(4096))), +}).superRefine((data, ctx) => { + const hasShape = data.shape !== undefined; + const hasSrc = data.src !== undefined && data.src.trim() !== ""; + + if (!hasShape && !hasSrc) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Either 'src' or 'shape' must be provided", + path: [], + }); + } + + if (hasShape && hasSrc) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provide either 'src' or 'shape', not both", + path: ["src"], + }); + } + + if (hasSrc) { + const disallowedProps = ["shape", "fill", "stroke", "shadow", "transform", "width", "height"]; + for (const prop of disallowedProps) { + if (data[prop] !== undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: \`'\${prop}' is not allowed when using 'src'. Only 'type' and 'src' are allowed in import mode\`, + path: [prop], + }); + } + } + } +});`; + +if (svgAssetPattern.test(content)) { + content = content.replace(svgAssetPattern, svgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema for mutual exclusivity"); +} else { + console.log("⚠ Could not find svgassetSvgAssetSchema to add superRefine validation"); +} + const rejectInvalid = `((v: unknown) => v === '' || Array.isArray(v) ? NaN : v)`; const plainNumberPattern = /z\.number\(\)(?!\.)/g; @@ -195,6 +250,59 @@ if (fs.existsSync(zodGenCjsPath)) { console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in CJS"); } + const cjsSvgAssetPattern = + /exports\.svgassetSvgAssetSchema = zod_1\.z\.object\(\{[\s\S]*?\}\);/; + + const cjsSvgAssetSuperRefine = `exports.svgassetSvgAssetSchema = zod_1.z.object({ + type: zod_1.z.enum(["svg"]), + src: zod_1.z.optional(zod_1.z.string().min(1).max(500000)), + shape: zod_1.z.optional(exports.svgshapesSvgShapeSchema), + fill: zod_1.z.optional(exports.svgpropertiesSvgFillSchema), + stroke: zod_1.z.optional(exports.svgpropertiesSvgStrokeSchema), + shadow: zod_1.z.optional(exports.svgpropertiesSvgShadowSchema), + transform: zod_1.z.optional(exports.svgpropertiesSvgTransformSchema), + opacity: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().gte(0).lte(1))).default(1), + width: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().int().gte(1).lte(4096))), + height: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().int().gte(1).lte(4096))), +}).superRefine((data, ctx) => { + const hasShape = data.shape !== undefined; + const hasSrc = data.src !== undefined && data.src.trim() !== ""; + + if (!hasShape && !hasSrc) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "Either 'src' or 'shape' must be provided", + path: [], + }); + } + + if (hasShape && hasSrc) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "Provide either 'src' or 'shape', not both", + path: ["src"], + }); + } + + if (hasSrc) { + const disallowedProps = ["shape", "fill", "stroke", "shadow", "transform", "width", "height"]; + for (const prop of disallowedProps) { + if (data[prop] !== undefined) { + ctx.addIssue({ + code: zod_1.z.ZodIssueCode.custom, + message: "'" + prop + "' is not allowed when using 'src'. Only 'type' and 'src' are allowed in import mode", + path: [prop], + }); + } + } + } +});`; + + if (cjsSvgAssetPattern.test(cjsContent)) { + cjsContent = cjsContent.replace(cjsSvgAssetPattern, cjsSvgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in CJS"); + } + const cjsRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; const cjsPlainNumberPattern = /zod_1\.z\.number\(\)(?!\.)/g; @@ -248,6 +356,11 @@ if (fs.existsSync(zodGenJsPath)) { console.log("✓ Fixed svgpropertiesSvgFillSchema discriminator in ESM JS"); } + if (svgAssetPattern.test(jsContent)) { + jsContent = jsContent.replace(svgAssetPattern, svgAssetSuperRefine); + console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in ESM JS"); + } + const esmRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; const esmPlainNumberPattern = /z\.number\(\)(?!\.)/g; From 9d820bf15f8eeb42a333af8d9b08088ebeade606 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 1 Jan 2026 12:44:25 +0500 Subject: [PATCH 7/9] updated package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5ab041..07e0764 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shotstack/schemas", - "version": "1.3.9", + "version": "1.4.0", "description": "Centralized OpenAPI schemas and TypeScript types for Shotstack API", "type": "module", "main": "dist/index.js", From ef6131d5a4474f0783b583c6a5aa3120b102a27d Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Fri, 2 Jan 2026 14:40:35 +1100 Subject: [PATCH 8/9] fix: improve number coercion in schema preprocessing with proper null/undefined handling --- scripts/fix-discriminator.cjs | 53 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/scripts/fix-discriminator.cjs b/scripts/fix-discriminator.cjs index 009d90d..1d4b225 100644 --- a/scripts/fix-discriminator.cjs +++ b/scripts/fix-discriminator.cjs @@ -84,9 +84,9 @@ const svgAssetSuperRefine = `export const svgassetSvgAssetSchema = z.object({ stroke: z.optional(svgpropertiesSvgStrokeSchema), shadow: z.optional(svgpropertiesSvgShadowSchema), transform: z.optional(svgpropertiesSvgTransformSchema), - opacity: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().gte(0).lte(1))).default(1), - width: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().int().gte(1).lte(4096))), - height: z.optional(z.preprocess(((v: unknown) => v === '' || Array.isArray(v) ? NaN : v), z.coerce.number().int().gte(1).lte(4096))), + opacity: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().gte(0).lte(1))).default(1), + width: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().int().gte(1).lte(4096))), + height: z.optional(z.preprocess(((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), z.number().int().gte(1).lte(4096))), }).superRefine((data, ctx) => { const hasShape = data.shape !== undefined; const hasSrc = data.src !== undefined && data.src.trim() !== ""; @@ -128,17 +128,18 @@ if (svgAssetPattern.test(content)) { console.log("⚠ Could not find svgassetSvgAssetSchema to add superRefine validation"); } -const rejectInvalid = `((v: unknown) => v === '' || Array.isArray(v) ? NaN : v)`; +// Coercion function that converts strings to numbers inside preprocess (doesn't rely on z.coerce) +const coerceNumber = `((v: unknown) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; const plainNumberPattern = /z\.number\(\)(?!\.)/g; const plainNumberCount = (content.match(plainNumberPattern) || []).length; if (plainNumberCount > 0) { content = content.replace( plainNumberPattern, - `z.preprocess(${rejectInvalid}, z.coerce.number())` + `z.preprocess(${coerceNumber}, z.number())` ); console.log( - `✓ Added z.coerce.number() for plain z.number() (${plainNumberCount} occurrences)` + `✓ Added number coercion for plain z.number() (${plainNumberCount} occurrences)` ); } @@ -146,11 +147,11 @@ const chainedNumberPattern = /z\.number\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let chainedCount = 0; content = content.replace(chainedNumberPattern, (match, chain) => { chainedCount++; - return `z.preprocess(${rejectInvalid}, z.coerce.number()${chain})`; + return `z.preprocess(${coerceNumber}, z.number()${chain})`; }); if (chainedCount > 0) { console.log( - `✓ Added z.coerce.number() for chained z.number() (${chainedCount} occurrences)` + `✓ Added number coercion for chained z.number() (${chainedCount} occurrences)` ); } @@ -159,10 +160,10 @@ const plainIntCount = (content.match(plainIntPattern) || []).length; if (plainIntCount > 0) { content = content.replace( plainIntPattern, - `z.preprocess(${rejectInvalid}, z.coerce.number().int())` + `z.preprocess(${coerceNumber}, z.number().int())` ); console.log( - `✓ Added z.coerce.number().int() for plain z.int() (${plainIntCount} occurrences)` + `✓ Added number coercion for plain z.int() (${plainIntCount} occurrences)` ); } @@ -170,11 +171,11 @@ const chainedIntPattern = /z\.int\(\)((?:\.[a-zA-Z]+\([^)]*\))+)/g; let chainedIntCount = 0; content = content.replace(chainedIntPattern, (match, chain) => { chainedIntCount++; - return `z.preprocess(${rejectInvalid}, z.coerce.number().int()${chain})`; + return `z.preprocess(${coerceNumber}, z.number().int()${chain})`; }); if (chainedIntCount > 0) { console.log( - `✓ Added z.coerce.number().int() for chained z.int() (${chainedIntCount} occurrences)` + `✓ Added number coercion for chained z.int() (${chainedIntCount} occurrences)` ); } @@ -261,9 +262,9 @@ if (fs.existsSync(zodGenCjsPath)) { stroke: zod_1.z.optional(exports.svgpropertiesSvgStrokeSchema), shadow: zod_1.z.optional(exports.svgpropertiesSvgShadowSchema), transform: zod_1.z.optional(exports.svgpropertiesSvgTransformSchema), - opacity: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().gte(0).lte(1))).default(1), - width: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().int().gte(1).lte(4096))), - height: zod_1.z.optional(zod_1.z.preprocess(((v) => v === '' || Array.isArray(v) ? NaN : v), zod_1.z.coerce.number().int().gte(1).lte(4096))), + opacity: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().gte(0).lte(1))).default(1), + width: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().int().gte(1).lte(4096))), + height: zod_1.z.optional(zod_1.z.preprocess(((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; }), zod_1.z.number().int().gte(1).lte(4096))), }).superRefine((data, ctx) => { const hasShape = data.shape !== undefined; const hasSrc = data.src !== undefined && data.src.trim() !== ""; @@ -303,7 +304,8 @@ if (fs.existsSync(zodGenCjsPath)) { console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in CJS"); } - const cjsRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; + // Coercion function for CJS (without TypeScript type annotation) + const cjsCoerceNumber = `((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; const cjsPlainNumberPattern = /zod_1\.z\.number\(\)(?!\.)/g; const cjsPlainNumberCount = (cjsContent.match(cjsPlainNumberPattern) || []) @@ -311,10 +313,10 @@ if (fs.existsSync(zodGenCjsPath)) { if (cjsPlainNumberCount > 0) { cjsContent = cjsContent.replace( cjsPlainNumberPattern, - `zod_1.z.preprocess(${cjsRejectInvalid}, zod_1.z.coerce.number())` + `zod_1.z.preprocess(${cjsCoerceNumber}, zod_1.z.number())` ); console.log( - `✓ Added z.coerce.number() in CJS (${cjsPlainNumberCount} occurrences)` + `✓ Added number coercion in CJS (${cjsPlainNumberCount} occurrences)` ); } @@ -323,11 +325,11 @@ if (fs.existsSync(zodGenCjsPath)) { let cjsChainedCount = 0; cjsContent = cjsContent.replace(cjsChainedNumberPattern, (match, chain) => { cjsChainedCount++; - return `zod_1.z.preprocess(${cjsRejectInvalid}, zod_1.z.coerce.number()${chain})`; + return `zod_1.z.preprocess(${cjsCoerceNumber}, zod_1.z.number()${chain})`; }); if (cjsChainedCount > 0) { console.log( - `✓ Added z.coerce.number() chains in CJS (${cjsChainedCount} occurrences)` + `✓ Added number coercion chains in CJS (${cjsChainedCount} occurrences)` ); } @@ -361,7 +363,8 @@ if (fs.existsSync(zodGenJsPath)) { console.log("✓ Added superRefine validation to svgassetSvgAssetSchema in ESM JS"); } - const esmRejectInvalid = `((v) => v === '' || Array.isArray(v) ? NaN : v)`; + // Coercion function for ESM JS (without TypeScript type annotation) + const esmCoerceNumber = `((v) => { if (v === '' || v === null || v === undefined || Array.isArray(v)) return undefined; if (typeof v === 'string') return Number(v); return v; })`; const esmPlainNumberPattern = /z\.number\(\)(?!\.)/g; const esmPlainNumberCount = (jsContent.match(esmPlainNumberPattern) || []) @@ -369,10 +372,10 @@ if (fs.existsSync(zodGenJsPath)) { if (esmPlainNumberCount > 0) { jsContent = jsContent.replace( esmPlainNumberPattern, - `z.preprocess(${esmRejectInvalid}, z.coerce.number())` + `z.preprocess(${esmCoerceNumber}, z.number())` ); console.log( - `✓ Added z.coerce.number() in ESM JS (${esmPlainNumberCount} occurrences)` + `✓ Added number coercion in ESM JS (${esmPlainNumberCount} occurrences)` ); } @@ -380,11 +383,11 @@ if (fs.existsSync(zodGenJsPath)) { let esmChainedCount = 0; jsContent = jsContent.replace(esmChainedNumberPattern, (match, chain) => { esmChainedCount++; - return `z.preprocess(${esmRejectInvalid}, z.coerce.number()${chain})`; + return `z.preprocess(${esmCoerceNumber}, z.number()${chain})`; }); if (esmChainedCount > 0) { console.log( - `✓ Added z.coerce.number() chains in ESM JS (${esmChainedCount} occurrences)` + `✓ Added number coercion chains in ESM JS (${esmChainedCount} occurrences)` ); } From c665a1c4bc61aec480fc6deecbb26ef601929f5d Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Fri, 2 Jan 2026 08:55:50 +0500 Subject: [PATCH 9/9] package updated --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07e0764..d8d5c61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shotstack/schemas", - "version": "1.4.0", + "version": "1.4.1", "description": "Centralized OpenAPI schemas and TypeScript types for Shotstack API", "type": "module", "main": "dist/index.js",