Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"project": ["./tsconfig.json", "./tsconfig.test.json"]
},
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["**/*.d.ts"],
"ignorePatterns": ["**/*.d.ts", "*.js", "**/*.js"],
"overrides": [
{
"files": ["src/**/*.ts"],
Expand Down
10 changes: 9 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,13 @@
"useTabs": true,
"tabWidth": 2,
"arrowParens": "avoid",
"printWidth": 150
"printWidth": 150,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false
}
}
]
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export default {
}
]
}
};
};
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@
"build:schema": "vite build --config vite.config.schema.ts",
"test": "npm run build && jest",
"test:watch": "jest --watch",
"test:package": "node test-package.js",
"typecheck": "tsc --noEmit && tsc --project tsconfig.test.json --noEmit",
"lint": "eslint --ignore-path .gitignore .",
"lint:fix": "eslint --ignore-path .gitignore --fix .",
"format": "prettier --ignore-path .gitignore --write .",
"prepublishOnly": "npm run build && npm run test"
"prepublishOnly": "npm run build && npm run test && npm run test:package"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
Expand All @@ -69,13 +71,14 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"jest": "^30.2.0",
"prettier": "^3.6.2",
"ts-jest": "^29.4.5",
"typescript": "^5.6.2",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@shotstack/shotstack-canvas": "^1.1.7",
"@shotstack/shotstack-canvas": "^1.3.0",
"fast-deep-equal": "^3.1.3",
"howler": "^2.2.4",
"mediabunny": "^1.11.2",
Expand Down
129 changes: 64 additions & 65 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ edit.stop(); // Stop and return to beginning

// Editing functions
edit.addClip(0, {
asset: {
type: "image",
src: "https://example.com/image.jpg"
},
start: 0,
length: 5
asset: {
type: "image",
src: "https://example.com/image.jpg"
},
start: 0,
length: 5
});

edit.addTrack(1, { clips: [] });
Expand All @@ -127,15 +127,15 @@ The Edit class provides an event system to listen for specific actions:
```typescript
// Listen for clip selection events
edit.events.on("clip:selected", data => {
console.log("Clip selected:", data.clip);
console.log("Track index:", data.trackIndex);
console.log("Clip index:", data.clipIndex);
console.log("Clip selected:", data.clip);
console.log("Track index:", data.trackIndex);
console.log("Clip index:", data.clipIndex);
});

// Listen for clip update events
edit.events.on("clip:updated", data => {
console.log("Previous state:", data.previous); // { clip, trackIndex, clipIndex }
console.log("Current state:", data.current); // { clip, trackIndex, clipIndex }
console.log("Previous state:", data.previous); // { clip, trackIndex, clipIndex }
console.log("Current state:", data.current); // { clip, trackIndex, clipIndex }
});
```

Expand Down Expand Up @@ -234,59 +234,59 @@ Create your own theme by defining colors and dimensions for each component:

```typescript
const customTheme = {
timeline: {
// Main timeline colors
background: "#1e1e1e",
divider: "#1a1a1a",
playhead: "#ff4444",
snapGuide: "#888888",
dropZone: "#00ff00",
trackInsertion: "#00ff00",

// Toolbar styling
toolbar: {
background: "#1a1a1a",
surface: "#2a2a2a", // Button backgrounds
hover: "#3a3a3a", // Button hover state
active: "#007acc", // Button active state
divider: "#3a3a3a", // Separator lines
icon: "#888888", // Icon colors
text: "#ffffff", // Text color
height: 36 // Toolbar height in pixels
},

// Ruler styling
ruler: {
background: "#404040",
text: "#ffffff", // Time labels
markers: "#666666", // Time marker dots
height: 40 // Ruler height in pixels
},

// Track styling
tracks: {
surface: "#2d2d2d", // Primary track color
surfaceAlt: "#252525", // Alternating track color
border: "#3a3a3a", // Track borders
height: 60 // Track height in pixels
},

// Clip colors by asset type
clips: {
video: "#4a9eff",
audio: "#00d4aa",
image: "#f5a623",
text: "#d0021b",
shape: "#9013fe",
html: "#50e3c2",
luma: "#b8e986",
default: "#8e8e93", // Unknown asset types
selected: "#007acc", // Selection border
radius: 4 // Corner radius in pixels
}
}
// Canvas theming will be available in future releases
// canvas: { ... }
timeline: {
// Main timeline colors
background: "#1e1e1e",
divider: "#1a1a1a",
playhead: "#ff4444",
snapGuide: "#888888",
dropZone: "#00ff00",
trackInsertion: "#00ff00",

// Toolbar styling
toolbar: {
background: "#1a1a1a",
surface: "#2a2a2a", // Button backgrounds
hover: "#3a3a3a", // Button hover state
active: "#007acc", // Button active state
divider: "#3a3a3a", // Separator lines
icon: "#888888", // Icon colors
text: "#ffffff", // Text color
height: 36 // Toolbar height in pixels
},

// Ruler styling
ruler: {
background: "#404040",
text: "#ffffff", // Time labels
markers: "#666666", // Time marker dots
height: 40 // Ruler height in pixels
},

// Track styling
tracks: {
surface: "#2d2d2d", // Primary track color
surfaceAlt: "#252525", // Alternating track color
border: "#3a3a3a", // Track borders
height: 60 // Track height in pixels
},

// Clip colors by asset type
clips: {
video: "#4a9eff",
audio: "#00d4aa",
image: "#f5a623",
text: "#d0021b",
shape: "#9013fe",
html: "#50e3c2",
luma: "#b8e986",
default: "#8e8e93", // Unknown asset types
selected: "#007acc", // Selection border
radius: 4 // Corner radius in pixels
}
}
// Canvas theming will be available in future releases
// canvas: { ... }
};

const timeline = new Timeline(edit, { width: 1280, height: 300 }, { theme: customTheme });
Expand All @@ -297,7 +297,6 @@ const timeline = new Timeline(edit, { width: 1280, height: 300 }, { theme: custo
Themes are organized by component, making it intuitive to customize specific parts of the interface:

- **Timeline**: Controls the appearance of the timeline interface

- `toolbar`: Playback controls and buttons
- `ruler`: Time markers and labels
- `tracks`: Track backgrounds and borders
Expand Down
169 changes: 169 additions & 0 deletions test-package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { existsSync, readFileSync, statSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log("🧪 Testing Shotstack Studio SDK Package Build\n");

console.log("📦 Test 1: Checking required package files exist...");
const requiredFiles = [
"dist/shotstack-studio.es.js",
"dist/shotstack-studio.umd.js",
"dist/index.d.ts",
"dist/schema/index.mjs",
"dist/schema/index.cjs",
"dist/schema/index.d.ts"
];

let allFilesExist = true;
requiredFiles.forEach(file => {
const fullPath = resolve(__dirname, file);
const exists = existsSync(fullPath);
if (exists) {
const size = statSync(fullPath).size;
const sizeKB = (size / 1024).toFixed(2);
console.log(` ✅ ${file} (${sizeKB} KB)`);
} else {
console.log(` ❌ ${file} - MISSING!`);
allFilesExist = false;
}
});

if (!allFilesExist) {
console.error("\n❌ Test 1 FAILED: Some required files are missing\n");
process.exit(1);
}
console.log(" ✅ Test 1 PASSED: All required files exist\n");

console.log("🔍 Test 2: Checking for unwanted chunk files...");
const distPath = resolve(__dirname, "dist");
const fs = await import("fs");
const distFiles = fs.readdirSync(distPath);
const chunkFiles = distFiles.filter(file => file.match(/^index-[a-zA-Z0-9]+\.js$/));

if (chunkFiles.length > 0) {
console.log(` ❌ Found ${chunkFiles.length} chunk file(s):`);
chunkFiles.forEach(file => console.log(` - ${file}`));
console.error("\n❌ Test 2 FAILED: Chunk files should not exist (should be inlined)\n");
process.exit(1);
}
console.log(" ✅ Test 2 PASSED: No chunk files found (all code is inlined)\n");

console.log("📝 Test 3: Checking ES module for chunk imports...");
const esModulePath = resolve(__dirname, "dist/shotstack-studio.es.js");
const esModuleContent = readFileSync(esModulePath, "utf-8");

const chunkImportPattern = /import\s+.*?from\s+['"]\.\/index-[a-zA-Z0-9]+\.js['"]/;
const hasChunkImports = chunkImportPattern.test(esModuleContent);

if (hasChunkImports) {
const matches = esModuleContent.match(chunkImportPattern);
console.log(` ❌ Found chunk import in ES module:`);
console.log(` ${matches[0]}`);
console.error("\n❌ Test 3 FAILED: ES module should not import chunk files\n");
process.exit(1);
}
console.log(" ✅ Test 3 PASSED: ES module is self-contained (no chunk imports)\n");

console.log("📊 Test 4: Checking file sizes are reasonable...");
const esModuleSize = statSync(esModulePath).size;
const esModuleSizeKB = (esModuleSize / 1024).toFixed(2);
const esModuleSizeMB = (esModuleSize / 1024 / 1024).toFixed(2);

if (esModuleSize < 100 * 1024) {
console.log(` ⚠️ ES module size is very small (${esModuleSizeKB} KB)`);
console.log(" This might indicate the code is not fully inlined");
console.error("\n❌ Test 4 FAILED: ES module size is suspiciously small\n");
process.exit(1);
}

console.log(` ✅ ES module size: ${esModuleSizeMB} MB (${esModuleSizeKB} KB)`);
console.log(" ✅ Test 4 PASSED: File size is reasonable\n");

console.log("📋 Test 5: Checking package.json exports configuration...");
const packageJsonPath = resolve(__dirname, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));

const requiredExports = {
".": {
types: "./dist/index.d.ts",
import: "./dist/shotstack-studio.es.js",
require: "./dist/shotstack-studio.umd.js"
},
"./schema": {
types: "./dist/schema/index.d.ts",
import: "./dist/schema/index.mjs",
require: "./dist/schema/index.cjs"
}
};

let exportsValid = true;
for (const [exportPath, exportConfig] of Object.entries(requiredExports)) {
if (!packageJson.exports[exportPath]) {
console.log(` ❌ Missing export: "${exportPath}"`);
exportsValid = false;
continue;
}

for (const [key, value] of Object.entries(exportConfig)) {
const actualValue = packageJson.exports[exportPath][key];
if (actualValue !== value) {
console.log(` ❌ Export "${exportPath}.${key}" is "${actualValue}", expected "${value}"`);
exportsValid = false;
}
}
}

if (!exportsValid) {
console.error("\n❌ Test 5 FAILED: package.json exports are not configured correctly\n");
process.exit(1);
}
console.log(" ✅ Test 5 PASSED: package.json exports are configured correctly\n");

console.log("🚀 Test 6: Testing dynamic import (simulates Next.js)...");
try {
const module = await import("./dist/shotstack-studio.es.js");

const expectedExports = ["Edit", "Canvas", "Controls", "VideoExporter", "Timeline"];
const missingExports = expectedExports.filter(exp => !module[exp]);

if (missingExports.length > 0) {
console.log(` ❌ Missing exports: ${missingExports.join(", ")}`);
console.error("\n❌ Test 6 FAILED: Some expected exports are missing\n");
process.exit(1);
}

console.log(" ✅ Successfully imported module");
console.log(` ✅ Found expected exports: ${expectedExports.join(", ")}`);
console.log(" ✅ Test 6 PASSED: Module can be imported successfully\n");
} catch (error) {
if (error.message.includes("index-") || error.code === "MODULE_NOT_FOUND") {
console.log(` ❌ Failed to import module: ${error.message}`);
console.log(" ⚠️ Error indicates missing chunk files - build configuration needs fixing!");
console.error("\n❌ Test 6 FAILED: Could not import the module\n");
process.exit(1);
}

const browserGlobals = ["self", "window", "document", "navigator", "HTMLCanvasElement"];
const isBrowserError = browserGlobals.some(global => error.message.includes(global));

if (isBrowserError) {
console.log(` ⚠️ Import failed due to browser-specific code: ${error.message}`);
console.log(" ℹ️ This is EXPECTED - the library requires a browser environment");
console.log(" ✅ No chunk file errors detected");
console.log(" ✅ Test 6 PASSED: Module structure is correct (browser-only limitation)\n");
} else {
console.log(` ❌ Failed to import module: ${error.message}`);
console.error("\n❌ Test 6 FAILED: Unexpected import error\n");
process.exit(1);
}
}

console.log("════════════════════════════════════════════════════════");
console.log("✅ ALL TESTS PASSED!");
console.log("════════════════════════════════════════════════════════");
console.log("\n📦 The package is ready for publishing and use in Next.js");
console.log(" No chunk file issues detected!");
console.log(" All imports are self-contained.\n");
Loading