Skip to content
Open
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
93 changes: 93 additions & 0 deletions .github/workflows/release-compositor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Release Compositor

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-linux
platform: webreel-linux-x86_64
- os: ubuntu-latest
target: aarch64-linux
platform: webreel-linux-aarch64
- os: macos-latest
target: x86_64-macos
platform: webreel-darwin-x86_64
- os: macos-latest
target: aarch64-macos
platform: webreel-darwin-aarch64
- os: windows-latest
target: x86_64-windows
platform: webreel-windows-x86_64

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2
with:
version: 0.15.2

- name: Fetch deps
working-directory: packages/@webreel/compositor
run: bash scripts/fetch-deps.sh

- name: Build
working-directory: packages/@webreel/compositor
run: |
zig build \
-Doptimize=ReleaseFast \
-Dtarget=${{ matrix.target }} \
-Dexe-name=webreel

- name: Package archive
working-directory: packages/@webreel/compositor
shell: bash
run: |
mkdir -p dist
if [ -f zig-out/bin/webreel.exe ]; then
cp zig-out/bin/webreel.exe dist/webreel.exe
tar czf dist/${{ matrix.platform }}.tar.gz -C dist webreel.exe
rm dist/webreel.exe
else
cp zig-out/bin/webreel dist/webreel
tar czf dist/${{ matrix.platform }}.tar.gz -C dist webreel
rm dist/webreel
fi

- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ matrix.platform }}
path: packages/@webreel/compositor/dist/${{ matrix.platform }}.tar.gz

release:
needs: build
runs-on: ubuntu-latest

steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
merge-multiple: true

- name: Generate checksums
run: |
cd artifacts
sha256sum *.tar.gz > checksums.txt

- name: Create release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
with:
files: artifacts/*
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dist
!.env.example
coverage
*.tsbuildinfo
next-env.d.ts
*.log
package-lock.json
demo-reel.mp4
2 changes: 1 addition & 1 deletion apps/docs/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
25 changes: 25 additions & 0 deletions apps/studio/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
7 changes: 7 additions & 0 deletions apps/studio/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const config: NextConfig = {
devIndicators: false,
};

export default config;
44 changes: 44 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@webreel/studio",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "portless run --name studio.webreel next dev",
"build": "next build",
"start": "next start",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@codemirror/autocomplete": "^6",
"@codemirror/commands": "^6",
"@codemirror/lang-json": "^6",
"@codemirror/language": "^6",
"@codemirror/state": "^6",
"@codemirror/theme-one-dark": "^6",
"@codemirror/view": "^6",
"class-variance-authority": "^0.7",
"clsx": "^2",
"cmdk": "^1.1.1",
"codemirror": "^6",
"geist": "^1",
"jotai": "^2",
"lucide-react": "^0.575.0",
"next": "~16.1.6",
"next-themes": "^0.4",
"radix-ui": "^1",
"react": "~19.2.4",
"react-dom": "~19.2.4",
"react-resizable-panels": "^4",
"tailwind-merge": "^3",
"tw-animate-css": "^1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"typescript": "^5"
}
}
7 changes: 7 additions & 0 deletions apps/studio/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
118 changes: 118 additions & 0 deletions apps/studio/src/app/api/cors/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from "next/server";
import { isBlockedUrl } from "@/lib/url-validation";

const STRIPPED_REQUEST_HEADERS = new Set([
"host",
"origin",
"referer",
"cookie",
"authorization",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
]);

const STRIPPED_RESPONSE_HEADERS = new Set([
"content-encoding",
"content-security-policy",
"content-security-policy-report-only",
"x-frame-options",
"strict-transport-security",
"set-cookie",
"transfer-encoding",
]);

function resolveUrl(raw: string, base?: string): string {
if (/^https?:\/\//i.test(raw)) return raw;
if (!base) return raw;
try {
return new URL(raw, base).href;
} catch {
return raw;
}
}

function rewriteCssUrls(css: string, baseUrl: string): string {
return css.replace(/url\(\s*(['"]?)(\/?[^)'"]+)\1\s*\)/g, (_match, quote, rawUrl) => {
if (rawUrl.startsWith("data:") || rawUrl.startsWith("#")) return _match;
const absolute = resolveUrl(rawUrl, baseUrl);
if (!/^https?:\/\//i.test(absolute)) return _match;
return `url(${quote}/api/cors?url=${encodeURIComponent(absolute)}${quote})`;
});
}

export async function GET(request: NextRequest) {
const url = request.nextUrl.searchParams.get("url");
if (!url) {
return NextResponse.json({ error: "Missing url parameter" }, { status: 400 });
}

if (isBlockedUrl(url)) {
return NextResponse.json({ error: "URL not allowed" }, { status: 403 });
}

try {
const headers: Record<string, string> = {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "*/*",
};

for (const [key, value] of request.headers.entries()) {
if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) {
headers[key] = value;
}
}

const response = await fetch(url, {
headers,
redirect: "follow",
signal: AbortSignal.timeout(10_000),
});

const contentType = response.headers.get("content-type") || "";
const responseHeaders = new Headers();

for (const [key, value] of response.headers.entries()) {
if (!STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase())) {
responseHeaders.set(key, value);
}
}
responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set("Access-Control-Allow-Methods", "GET, OPTIONS");
responseHeaders.set("Access-Control-Allow-Headers", "*");

if (contentType.includes("text/css")) {
const css = await response.text();
const baseUrl = url.replace(/[?#].*$/, "").replace(/\/[^/]*$/, "/");
const rewritten = rewriteCssUrls(css, baseUrl);
responseHeaders.set("Content-Type", contentType);
return new NextResponse(rewritten, {
status: response.status,
headers: responseHeaders,
});
}

responseHeaders.delete("content-encoding");

return new NextResponse(response.body, {
status: response.status,
headers: responseHeaders,
});
} catch (e) {
const message = e instanceof Error ? e.message : "Failed to fetch";
return NextResponse.json({ error: message }, { status: 502 });
}
}

export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Max-Age": "86400",
},
});
}
Loading
Loading