From c45d3fe7845f45ec84d804c2659a8bcf907a8c9f Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Wed, 25 Mar 2026 19:14:36 +0500 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20IIFE=20crash=20fixed=20=E2=80=94=208?= =?UTF-8?q?=20pixi.js=20submodule=20globals=20now=20mapped=20to=20PIXI.=20?= =?UTF-8?q?pixi-filters=20externalized=20=E2=80=94=20no=20longer=20bundled?= =?UTF-8?q?=20with=20mixed=20boundary;=20mapped=20to=20PIXI=20global.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test-package.js | 81 ++++++++++++++++++++++++++++++++++++++++- vite.config.internal.ts | 42 ++------------------- vite.config.ts | 42 ++------------------- vite.shared.ts | 61 +++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 77 deletions(-) create mode 100644 vite.shared.ts diff --git a/test-package.js b/test-package.js index fb36e34..9dddcaa 100644 --- a/test-package.js +++ b/test-package.js @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, readdirSync } from "fs"; +import { existsSync, readFileSync, readdirSync, statSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; @@ -133,6 +133,13 @@ const CONTRACT = { { className: "UIController", tokens: ["registerButton(config: ToolbarButtonConfig): this;"] }, { className: "Timeline", tokens: ["load(): Promise;"] } ], + bundleSizeLimits: { + "dist/shotstack-studio.umd.js": 3 * 1024 * 1024, + "dist/shotstack-studio.es.js": 4 * 1024 * 1024, + "dist/internal.umd.js": 2.5 * 1024 * 1024, + "dist/internal.es.js": 3 * 1024 * 1024 + }, + umdBundles: ["dist/shotstack-studio.umd.js", "dist/internal.umd.js"] }; const BROWSER_GLOBALS = ["self", "window", "document", "navigator", "HTMLCanvasElement"]; @@ -291,6 +298,76 @@ const checkPackageExports = () => { printResult("package.json exports contract", true); }; +const checkUmdGlobalMappings = () => { + const errors = []; + + for (const bundlePath of CONTRACT.umdBundles) { + const fullPath = resolve(__dirname, bundlePath); + const fd = readFileSync(fullPath, "utf-8").slice(0, 2048); + + const requirePattern = /require\("([^"]+)"\)/g; + const requiredModules = []; + let match; + while ((match = requirePattern.exec(fd)) !== null) { + requiredModules.push(match[1]); + } + + const iifeMatch = fd.match(/\((\w+)=typeof globalThis[^)]+,(\w+)\(([^)]+)\)\)/); + if (!iifeMatch) { + errors.push(`${bundlePath}: Could not parse IIFE global branch from UMD wrapper.`); + continue; + } + + const iifeArgs = iifeMatch[3]; + + const iifeArgCount = iifeArgs.split(",").length; + + const factoryMatch = fd.match(/\}\)\([^,]+,function\(([^)]*)\)/); + if (!factoryMatch) { + errors.push(`${bundlePath}: Could not parse factory function signature.`); + continue; + } + const factoryParamCount = factoryMatch[1].split(",").length; + + if (iifeArgCount !== factoryParamCount) { + errors.push( + `${bundlePath}: IIFE branch passes ${iifeArgCount} args but factory expects ${factoryParamCount} params. ` + + `Mismatch means some dependencies will be undefined at runtime. ` + + `Check that every non-side-effect external has a global mapping in vite.shared.ts.` + ); + } + } + + if (errors.length > 0) { + failWithDetails("UMD global mappings", errors); + } + printResult("UMD global mappings", true); +}; + +const checkBundleSizes = () => { + const errors = []; + const details = []; + + for (const [file, maxBytes] of Object.entries(CONTRACT.bundleSizeLimits)) { + const fullPath = resolve(__dirname, file); + if (!existsSync(fullPath)) continue; + + const actualBytes = statSync(fullPath).size; + const actualMB = (actualBytes / (1024 * 1024)).toFixed(2); + const maxMB = (maxBytes / (1024 * 1024)).toFixed(1); + details.push(`${file}: ${actualMB} MB (limit: ${maxMB} MB)`); + + if (actualBytes > maxBytes) { + errors.push(`${file} is ${actualMB} MB, exceeds limit of ${maxMB} MB`); + } + } + + if (errors.length > 0) { + failWithDetails("Bundle size thresholds", errors); + } + printResult("Bundle size thresholds", true, details); +}; + const runRuntimeExportSmokeTest = async (name, modulePath, expectedExports) => { try { const module = await import(modulePath); @@ -327,6 +404,8 @@ checkDeclarationSurface(); checkInternalDeclarationSurface(); checkNoChunkArtifactsOrImports(); checkPackageExports(); +checkUmdGlobalMappings(); +checkBundleSizes(); await runRuntimeExportSmokeTest("Runtime export smoke test", "./dist/shotstack-studio.es.js", CONTRACT.runtimeExports); await runRuntimeExportSmokeTest("Internal runtime export smoke test", "./dist/internal.es.js", CONTRACT.internalRuntimeExports); diff --git a/vite.config.internal.ts b/vite.config.internal.ts index db53c16..461f320 100644 --- a/vite.config.internal.ts +++ b/vite.config.internal.ts @@ -2,24 +2,13 @@ import { defineConfig } from "vite"; import { resolve } from "path"; import dts from "vite-plugin-dts"; +import { globals, external, sharedConfig } from "./vite.shared"; -const globals = { - "pixi.js": "PIXI", - howler: "Howler", - "opentype.js": "opentype", - "@ffmpeg/ffmpeg": "FFmpeg", - harfbuzzjs: "createHarfBuzz", - "@napi-rs/canvas": "Canvas" -}; +const shared = sharedConfig(__dirname); const INTERNAL_TYPES_ENTRY_STUB = "export * from './internal'"; export default defineConfig({ - define: { - "process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] || "development") - }, - worker: { - format: "es" - }, + ...shared, plugins: [ dts({ rollupTypes: true, @@ -43,25 +32,6 @@ export default defineConfig({ } }) ], - resolve: { - alias: { - "@core": resolve(__dirname, "src/core"), - "@canvas": resolve(__dirname, "src/components/canvas"), - "@timeline": resolve(__dirname, "src/components/timeline"), - "@shared": resolve(__dirname, "src/core/shared"), - "@schemas": resolve(__dirname, "src/core/schemas"), - "@timing": resolve(__dirname, "src/core/timing"), - "@layouts": resolve(__dirname, "src/core/layouts"), - "@animations": resolve(__dirname, "src/core/animations"), - "@events": resolve(__dirname, "src/core/events"), - "@inputs": resolve(__dirname, "src/core/inputs"), - "@loaders": resolve(__dirname, "src/core/loaders"), - "@export": resolve(__dirname, "src/core/export"), - "@styles": resolve(__dirname, "src/styles"), - "@templates": resolve(__dirname, "src/templates"), - "@shotstack/shotstack-canvas": resolve(__dirname, "node_modules/@shotstack/shotstack-canvas/dist/entry.web.js") - } - }, build: { target: "esnext", emptyOutDir: false, @@ -72,11 +42,7 @@ export default defineConfig({ formats: ["es", "umd"] }, rollupOptions: { - external: id => { - if (id === "pixi.js" || id.startsWith("pixi.js/")) return true; - if (id.startsWith("@napi-rs/")) return true; - return ["harfbuzzjs", "opentype.js", "howler", "canvas"].includes(id); - }, + external, output: { globals, inlineDynamicImports: true diff --git a/vite.config.ts b/vite.config.ts index 1884ce5..8dd9529 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,23 +2,12 @@ import { defineConfig } from "vite"; import { resolve } from "path"; import dts from "vite-plugin-dts"; +import { globals, external, sharedConfig } from "./vite.shared"; -const globals = { - "pixi.js": "PIXI", - howler: "Howler", - "opentype.js": "opentype", - "@ffmpeg/ffmpeg": "FFmpeg", - harfbuzzjs: "createHarfBuzz", - "@napi-rs/canvas": "Canvas" -}; +const shared = sharedConfig(__dirname); export default defineConfig({ - define: { - "process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] || "development") - }, - worker: { - format: "es" - }, + ...shared, plugins: [ dts({ rollupTypes: true, @@ -27,25 +16,6 @@ export default defineConfig({ pathsToAliases: true }) ], - resolve: { - alias: { - "@core": resolve(__dirname, "src/core"), - "@canvas": resolve(__dirname, "src/components/canvas"), - "@timeline": resolve(__dirname, "src/components/timeline"), - "@shared": resolve(__dirname, "src/core/shared"), - "@schemas": resolve(__dirname, "src/core/schemas"), - "@timing": resolve(__dirname, "src/core/timing"), - "@layouts": resolve(__dirname, "src/core/layouts"), - "@animations": resolve(__dirname, "src/core/animations"), - "@events": resolve(__dirname, "src/core/events"), - "@inputs": resolve(__dirname, "src/core/inputs"), - "@loaders": resolve(__dirname, "src/core/loaders"), - "@export": resolve(__dirname, "src/core/export"), - "@styles": resolve(__dirname, "src/styles"), - "@templates": resolve(__dirname, "src/templates"), - "@shotstack/shotstack-canvas": resolve(__dirname, "node_modules/@shotstack/shotstack-canvas/dist/entry.web.js") - } - }, build: { target: "esnext", lib: { @@ -55,11 +25,7 @@ export default defineConfig({ formats: ["es", "umd"] }, rollupOptions: { - external: id => { - if (id === "pixi.js" || id.startsWith("pixi.js/")) return true; - if (id.startsWith("@napi-rs/")) return true; - return ["harfbuzzjs", "opentype.js", "howler", "canvas"].includes(id); - }, + external, output: { globals, inlineDynamicImports: true diff --git a/vite.shared.ts b/vite.shared.ts new file mode 100644 index 0000000..47fcf33 --- /dev/null +++ b/vite.shared.ts @@ -0,0 +1,61 @@ +import { resolve } from "path"; +import type { UserConfig } from "vite"; + +export const globals: Record = { + "pixi.js": "PIXI", + "pixi.js/app": "PIXI", + "pixi.js/events": "PIXI", + "pixi.js/graphics": "PIXI", + "pixi.js/text": "PIXI", + "pixi.js/text-html": "PIXI", + "pixi.js/sprite-tiling": "PIXI", + "pixi.js/filters": "PIXI", + "pixi.js/mesh": "PIXI", + "pixi-filters": "PIXI", + howler: "Howler", + "opentype.js": "opentype", + "@ffmpeg/ffmpeg": "FFmpeg", + harfbuzzjs: "createHarfBuzz", + "@napi-rs/canvas": "Canvas" +}; + +export function external(id: string): boolean { + if (id === "pixi.js" || id.startsWith("pixi.js/")) return true; + if (id === "pixi-filters" || id.startsWith("pixi-filters/")) return true; + if (id.startsWith("@napi-rs/")) return true; + return ["harfbuzzjs", "opentype.js", "howler", "canvas"].includes(id); +} + +export function aliases(dirname: string): Record { + return { + "@core": resolve(dirname, "src/core"), + "@canvas": resolve(dirname, "src/components/canvas"), + "@timeline": resolve(dirname, "src/components/timeline"), + "@shared": resolve(dirname, "src/core/shared"), + "@schemas": resolve(dirname, "src/core/schemas"), + "@timing": resolve(dirname, "src/core/timing"), + "@layouts": resolve(dirname, "src/core/layouts"), + "@animations": resolve(dirname, "src/core/animations"), + "@events": resolve(dirname, "src/core/events"), + "@inputs": resolve(dirname, "src/core/inputs"), + "@loaders": resolve(dirname, "src/core/loaders"), + "@export": resolve(dirname, "src/core/export"), + "@styles": resolve(dirname, "src/styles"), + "@templates": resolve(dirname, "src/templates"), + "@shotstack/shotstack-canvas": resolve(dirname, "node_modules/@shotstack/shotstack-canvas/dist/entry.web.js") + }; +} + +export function sharedConfig(dirname: string): Partial { + return { + define: { + "process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] || "development") + }, + worker: { + format: "es" as const + }, + resolve: { + alias: aliases(dirname) + } + }; +} From 4b120f5d914663a065f21c4cd310a9fe91ff49d3 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 26 Mar 2026 16:00:19 +0500 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20future=20typewriter=20words=20have?= =?UTF-8?q?=20opacity:=200=20=E2=80=94=20completely=20invisible=20until=20?= =?UTF-8?q?the=20typewriter=20begins=20revealing=20their=20characters.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- .../canvas/players/rich-caption-player.ts | 2 +- src/core/ui/rich-caption-toolbar.ts | 10 +++++----- tests/rich-caption-player.test.ts | 2 +- tests/rich-caption-toolbar.test.ts | 20 +++++++++---------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 91947e1..9c10061 100644 --- a/package.json +++ b/package.json @@ -96,8 +96,8 @@ "vite-plugin-dts": "^4.5.4" }, "dependencies": { - "@shotstack/schemas": "1.9.3", - "@shotstack/shotstack-canvas": "^2.1.10", + "@shotstack/schemas": "1.9.5", + "@shotstack/shotstack-canvas": "2.1.14", "howler": "^2.2.4", "mediabunny": "^1.11.2", "opentype.js": "^1.3.4", diff --git a/src/components/canvas/players/rich-caption-player.ts b/src/components/canvas/players/rich-caption-player.ts index a0befb1..6ffc95c 100644 --- a/src/components/canvas/players/rich-caption-player.ts +++ b/src/components/canvas/players/rich-caption-player.ts @@ -482,7 +482,7 @@ export class RichCaptionPlayer extends Player { border: asset.border, padding: asset.padding, style: asset.style, - wordAnimation: asset.wordAnimation, + animation: asset.animation, align: asset.align, pauseThreshold: this.resolvedPauseThreshold }; diff --git a/src/core/ui/rich-caption-toolbar.ts b/src/core/ui/rich-caption-toolbar.ts index d09d22c..900b031 100644 --- a/src/core/ui/rich-caption-toolbar.ts +++ b/src/core/ui/rich-caption-toolbar.ts @@ -162,8 +162,8 @@ export class RichCaptionToolbar extends RichTextToolbar { if (!asset) return; // ─── Word Animation ──────────────────────────────── - const wordAnim = asset.wordAnimation; - const animStyle = wordAnim?.style ?? "karaoke"; + const anim = asset.animation; + const animStyle = anim?.style ?? "karaoke"; this.container?.querySelectorAll("[data-caption-word-style]").forEach(btn => { this.setButtonActive(btn, btn.dataset["captionWordStyle"] === animStyle); }); @@ -172,7 +172,7 @@ export class RichCaptionToolbar extends RichTextToolbar { this.wordAnimDirectionSection.style.display = animStyle === "slide" ? "" : "none"; } - const direction = wordAnim?.direction ?? "up"; + const direction = anim?.direction ?? "up"; this.container?.querySelectorAll("[data-caption-word-direction]").forEach(btn => { this.setButtonActive(btn, btn.dataset["captionWordDirection"] === direction); }); @@ -586,7 +586,7 @@ export class RichCaptionToolbar extends RichTextToolbar { const style = btn.dataset["captionWordStyle"]; if (!style) return; const asset = this.getCaptionAsset(); - this.updateClipProperty({ wordAnimation: { ...(asset?.wordAnimation ?? {}), style } }); + this.updateClipProperty({ animation: { ...(asset?.animation ?? {}), style } }); }); }); @@ -596,7 +596,7 @@ export class RichCaptionToolbar extends RichTextToolbar { const direction = btn.dataset["captionWordDirection"]; if (!direction) return; const asset = this.getCaptionAsset(); - this.updateClipProperty({ wordAnimation: { style: "slide" as const, ...(asset?.wordAnimation ?? {}), direction } }); + this.updateClipProperty({ animation: { style: "slide" as const, ...(asset?.animation ?? {}), direction } }); }); }); } diff --git a/tests/rich-caption-player.test.ts b/tests/rich-caption-player.test.ts index 55564c9..42d2d8d 100644 --- a/tests/rich-caption-player.test.ts +++ b/tests/rich-caption-player.test.ts @@ -483,7 +483,7 @@ describe("RichCaptionPlayer", () => { it("renders karaoke animation on every update", async () => { const asset = createAsset({ - wordAnimation: { style: "karaoke", speed: 1, direction: "up" } + animation: { style: "karaoke", speed: 1, direction: "up" } } as Partial); const edit = createMockEdit(); const player = new RichCaptionPlayer(edit, createClip(asset)); diff --git a/tests/rich-caption-toolbar.test.ts b/tests/rich-caption-toolbar.test.ts index 6a5eedf..b0ed52a 100644 --- a/tests/rich-caption-toolbar.test.ts +++ b/tests/rich-caption-toolbar.test.ts @@ -134,7 +134,7 @@ function createMockEdit(overrides: Record = {}) { function createCaptionAsset(overrides: Record = {}) { return { type: "rich-caption", - wordAnimation: { style: "karaoke", direction: "up" }, + animation: { style: "karaoke", direction: "up" }, active: { font: { color: "#ffff00", opacity: 1 }, stroke: { width: 2, color: "#000000", opacity: 1 } @@ -281,7 +281,7 @@ describe("RichCaptionToolbar", () => { describe("syncState", () => { it("should sync word animation style buttons", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "pop" } }); + setupCaptionClip(mockEdit, { animation: { style: "pop" } }); toolbar.mount(container); toolbar.show(0, 0); @@ -293,7 +293,7 @@ describe("RichCaptionToolbar", () => { }); it("should show direction section only for 'slide' animation", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "slide", direction: "left" } }); + setupCaptionClip(mockEdit, { animation: { style: "slide", direction: "left" } }); toolbar.mount(container); toolbar.show(0, 0); @@ -302,7 +302,7 @@ describe("RichCaptionToolbar", () => { }); it("should hide direction section for non-slide animations", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "karaoke" } }); + setupCaptionClip(mockEdit, { animation: { style: "karaoke" } }); toolbar.mount(container); toolbar.show(0, 0); @@ -347,7 +347,7 @@ describe("RichCaptionToolbar", () => { }); it("should show scale section when word animation is 'pop'", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "pop" }, active: { scale: 1.5 } }); + setupCaptionClip(mockEdit, { animation: { style: "pop" }, active: { scale: 1.5 } }); toolbar.mount(container); toolbar.show(0, 0); @@ -359,7 +359,7 @@ describe("RichCaptionToolbar", () => { }); it("should hide scale section when word animation is not 'pop'", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "karaoke" } }); + setupCaptionClip(mockEdit, { animation: { style: "karaoke" } }); toolbar.mount(container); toolbar.show(0, 0); @@ -386,14 +386,14 @@ describe("RichCaptionToolbar", () => { 0, 0, expect.objectContaining({ asset: expect.objectContaining({ - wordAnimation: expect.objectContaining({ style: "pop" }) + animation: expect.objectContaining({ style: "pop" }) }) }) ); }); it("should call updateClip when direction button is clicked", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "slide", direction: "up" } }); + setupCaptionClip(mockEdit, { animation: { style: "slide", direction: "up" } }); toolbar.mount(container); toolbar.show(0, 0); @@ -404,7 +404,7 @@ describe("RichCaptionToolbar", () => { 0, 0, expect.objectContaining({ asset: expect.objectContaining({ - wordAnimation: expect.objectContaining({ direction: "left" }) + animation: expect.objectContaining({ direction: "left" }) }) }) ); @@ -474,7 +474,7 @@ describe("RichCaptionToolbar", () => { }); it("should call updateClip when scale slider changes", () => { - setupCaptionClip(mockEdit, { wordAnimation: { style: "pop" }, active: { scale: 1 } }); + setupCaptionClip(mockEdit, { animation: { style: "pop" }, active: { scale: 1 } }); toolbar.mount(container); toolbar.show(0, 0); From a360d301d847291ece791fa29b9b78bff9817ab5 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Thu, 26 Mar 2026 16:18:22 +0500 Subject: [PATCH 3/3] chore: bump canvas version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 350285e..2746b7d 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ }, "dependencies": { "@shotstack/schemas": "1.9.5", - "@shotstack/shotstack-canvas": "2.1.14", + "@shotstack/shotstack-canvas": "2.1.15", "howler": "^2.2.4", "mediabunny": "^1.11.2", "opentype.js": "^1.3.4",