diff --git a/.claude/commands/upgrade-mediabunny.md b/.claude/commands/upgrade-mediabunny.md index c5be55afdf0..61064bbb6c0 100644 --- a/.claude/commands/upgrade-mediabunny.md +++ b/.claude/commands/upgrade-mediabunny.md @@ -2,5 +2,7 @@ - Look in the root package.json and update the version of Mediabunny to that version. - Also upgrade @mediabunny/mp3-encoder to the same version. - Look in packages/template-\*/package.json and update the version of Mediabunny to the desired version. +- Update `packages/cli/src/extra-packages.ts` with the new Mediabunny version. +- Update `packages/studio-shared/src/package-info.ts` with the new Mediabunny version in `extraPackages`. - Update `packages/docs/docs/mediabunny/version.mdx` compatiblity table. To find the next version this upgrade is going to be applied, look in the root package.json for the version and increment the patch version by one - Run `bun i` in the end. diff --git a/packages/cli/src/add.ts b/packages/cli/src/add.ts index b55cbc8ac2e..cd4549374eb 100644 --- a/packages/cli/src/add.ts +++ b/packages/cli/src/add.ts @@ -1,9 +1,26 @@ import {RenderInternals, type LogLevel} from '@remotion/renderer'; import {StudioServerInternals} from '@remotion/studio-server'; import {spawn} from 'node:child_process'; +import fs from 'node:fs'; import {chalk} from './chalk'; +import {EXTRA_PACKAGES} from './extra-packages'; import {listOfRemotionPackages} from './list-of-remotion-packages'; import {Log} from './log'; +import {resolveFrom} from './resolve-from'; + +const getInstalledVersion = ( + remotionRoot: string, + pkg: string, +): string | null => { + try { + const pkgJsonPath = resolveFrom(remotionRoot, `${pkg}/package.json`); + const file = fs.readFileSync(pkgJsonPath, 'utf-8'); + const packageJson = JSON.parse(file); + return packageJson.version; + } catch { + return null; + } +}; export const addCommand = async ({ remotionRoot, @@ -20,11 +37,11 @@ export const addCommand = async ({ }) => { // Validate that all package names are Remotion packages const invalidPackages = packageNames.filter( - (pkg) => !listOfRemotionPackages.includes(pkg), + (pkg) => !listOfRemotionPackages.includes(pkg) && !EXTRA_PACKAGES[pkg], ); if (invalidPackages.length > 0) { throw new Error( - `The following packages are not Remotion packages: ${invalidPackages.join(', ')}. Must be one of the Remotion packages.`, + `The following packages are not Remotion packages: ${invalidPackages.join(', ')}. Must be one of the Remotion packages or one of the supported extra packages: ${Object.keys(EXTRA_PACKAGES).join(', ')}.`, ); } @@ -43,8 +60,33 @@ export const addCommand = async ({ ...peerDependencies, ]; - const alreadyInstalled = packageNames.filter((pkg) => allDeps.includes(pkg)); - const toInstall = packageNames.filter((pkg) => !allDeps.includes(pkg)); + const alreadyInstalled: string[] = []; + const toInstall: string[] = []; + const toUpgrade: {pkg: string; from: string; to: string}[] = []; + + for (const pkg of packageNames) { + const isInstalled = allDeps.includes(pkg); + const requiredVersion = EXTRA_PACKAGES[pkg]; + + if (!isInstalled) { + toInstall.push(pkg); + } else if (requiredVersion) { + // For extra packages, check if the version is correct + const installedVersion = getInstalledVersion(remotionRoot, pkg); + if (installedVersion !== requiredVersion) { + toUpgrade.push({ + pkg, + from: installedVersion ?? 'unknown', + to: requiredVersion, + }); + toInstall.push(pkg); + } else { + alreadyInstalled.push(pkg); + } + } else { + alreadyInstalled.push(pkg); + } + } // Log already installed packages for (const pkg of alreadyInstalled) { @@ -54,41 +96,49 @@ export const addCommand = async ({ ); } + // Log packages that will be upgraded + for (const {pkg, from, to} of toUpgrade) { + Log.info( + {indent: false, logLevel}, + `↑ ${pkg} ${chalk.yellow(`${from} → ${to}`)}`, + ); + } + // If nothing to install, return early if (toInstall.length === 0) { return; } - // Find the version of installed Remotion packages const installedRemotionPackages = listOfRemotionPackages.filter((pkg) => allDeps.includes(pkg), ); - if (installedRemotionPackages.length === 0) { - throw new Error( - 'No Remotion packages found in your project. Install Remotion first.', - ); - } - // Get the version from the first installed Remotion package const packageJsonPath = `${remotionRoot}/node_modules/${installedRemotionPackages[0]}/package.json`; - let targetVersion: string; - - try { - const packageJson = require(packageJsonPath); - targetVersion = packageJson.version; - const packageList = - toInstall.length === 1 - ? toInstall[0] - : `${toInstall.length} packages (${toInstall.join(', ')})`; - Log.info( - {indent: false, logLevel}, - `Installing ${packageList}@${targetVersion} to match your other Remotion packages`, - ); - } catch (err) { - throw new Error( - `Could not determine version of installed Remotion packages: ${(err as Error).message}`, - ); + let targetVersion: string | null = null; + + if (installedRemotionPackages.length > 0) { + try { + const packageJson = require(packageJsonPath); + targetVersion = packageJson.version; + const packageList = + toInstall.length === 1 + ? toInstall[0] + : `${toInstall.length} packages (${toInstall.join(', ')})`; + Log.info({indent: false, logLevel}, `Installing ${packageList}`); + } catch (err) { + throw new Error( + `Could not determine version of installed Remotion packages: ${(err as Error).message}`, + ); + } + } else { + // If no Remotion packages are installed, we can only install extra packages + const notExtraPackages = toInstall.filter((pkg) => !EXTRA_PACKAGES[pkg]); + if (notExtraPackages.length > 0) { + throw new Error( + 'No Remotion packages found in your project. Install Remotion first.', + ); + } } const manager = StudioServerInternals.getPackageManager( @@ -105,10 +155,18 @@ export const addCommand = async ({ ); } + const packagesWithVersions = toInstall.map((pkg) => { + if (EXTRA_PACKAGES[pkg]) { + return `${pkg}@${EXTRA_PACKAGES[pkg]}`; + } + + return `${pkg}@${targetVersion}`; + }); + const command = StudioServerInternals.getInstallCommand({ manager: manager.manager, - packages: toInstall, - version: targetVersion, + packages: packagesWithVersions, + version: '', additionalArgs: args, }); @@ -122,6 +180,7 @@ export const addCommand = async ({ ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1', + npm_config_loglevel: 'error', }, stdio: RenderInternals.isEqualOrBelowLogLevel(logLevel, 'info') ? 'inherit' @@ -142,14 +201,18 @@ export const addCommand = async ({ }); }); - for (const pkg of alreadyInstalled) { - Log.info( - {indent: false, logLevel}, - `○ ${pkg}@${targetVersion} ${chalk.gray('(already installed)')}`, - ); - } + const upgradedPkgs = new Set(toUpgrade.map((u) => u.pkg)); for (const pkg of toInstall) { - Log.info({indent: false, logLevel}, `+ ${pkg}@${targetVersion}`); + if (upgradedPkgs.has(pkg)) { + // Already logged as upgrade + continue; + } + + if (EXTRA_PACKAGES[pkg]) { + Log.info({indent: false, logLevel}, `+ ${pkg}@${EXTRA_PACKAGES[pkg]}`); + } else { + Log.info({indent: false, logLevel}, `+ ${pkg}@${targetVersion}`); + } } }; diff --git a/packages/cli/src/extra-packages.ts b/packages/cli/src/extra-packages.ts new file mode 100644 index 00000000000..218eb8a410c --- /dev/null +++ b/packages/cli/src/extra-packages.ts @@ -0,0 +1,9 @@ +export const EXTRA_PACKAGES: Record = { + zod: '3.22.3', + mediabunny: '1.29.0', +}; + +export const EXTRA_PACKAGES_DOCS: Record = { + zod: 'https://www.remotion.dev/docs/schemas#prerequisites', + mediabunny: 'https://www.remotion.dev/docs/mediabunny/version', +}; diff --git a/packages/cli/src/upgrade.ts b/packages/cli/src/upgrade.ts index e2f79c3c873..90f80e03f9d 100644 --- a/packages/cli/src/upgrade.ts +++ b/packages/cli/src/upgrade.ts @@ -1,10 +1,35 @@ import {RenderInternals, type LogLevel} from '@remotion/renderer'; import {StudioServerInternals} from '@remotion/studio-server'; -import {spawn} from 'node:child_process'; +import {execSync, spawn} from 'node:child_process'; import {chalk} from './chalk'; +import {EXTRA_PACKAGES} from './extra-packages'; import {listOfRemotionPackages} from './list-of-remotion-packages'; import {Log} from './log'; +const getExtraPackageVersionsForRemotionVersion = ( + remotionVersion: string, +): Record => { + try { + const output = execSync( + `npm view @remotion/studio@${remotionVersion} dependencies --json`, + {encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']}, + ); + const dependencies = JSON.parse(output) as Record; + + const extraVersions: Record = {}; + for (const pkg of Object.keys(EXTRA_PACKAGES)) { + if (dependencies[pkg]) { + extraVersions[pkg] = dependencies[pkg]; + } + } + + return extraVersions; + } catch { + // If we can't fetch the versions, return the default versions from EXTRA_PACKAGES + return EXTRA_PACKAGES; + } +}; + export const upgradeCommand = async ({ remotionRoot, packageManager, @@ -55,18 +80,45 @@ export const upgradeCommand = async ({ ); } - const toUpgrade = listOfRemotionPackages.filter( - (u) => - dependencies.includes(u) || - devDependencies.includes(u) || - optionalDependencies.includes(u) || - peerDependencies.includes(u), + const allDeps = [ + ...dependencies, + ...devDependencies, + ...optionalDependencies, + ...peerDependencies, + ]; + + const remotionToUpgrade = listOfRemotionPackages.filter((u) => + allDeps.includes(u), ); + // Check if extra packages (zod, mediabunny) are installed + const installedExtraPackages = Object.keys(EXTRA_PACKAGES).filter((pkg) => + allDeps.includes(pkg), + ); + + // Get the correct versions for extra packages for this Remotion version + const extraPackageVersions = + getExtraPackageVersionsForRemotionVersion(targetVersion); + + // Build the list of packages to upgrade + const packagesWithVersions = [ + ...remotionToUpgrade.map((pkg) => `${pkg}@${targetVersion}`), + ...installedExtraPackages.map( + (pkg) => `${pkg}@${extraPackageVersions[pkg]}`, + ), + ]; + + if (installedExtraPackages.length > 0) { + Log.info( + {indent: false, logLevel}, + `Also upgrading extra packages: ${installedExtraPackages.map((pkg) => `${pkg}@${extraPackageVersions[pkg]}`).join(', ')}`, + ); + } + const command = StudioServerInternals.getInstallCommand({ manager: manager.manager, - packages: toUpgrade, - version: targetVersion, + packages: packagesWithVersions, + version: '', additionalArgs: args, }); diff --git a/packages/cli/src/versions.ts b/packages/cli/src/versions.ts index 56421a324ac..8e27cc7a091 100644 --- a/packages/cli/src/versions.ts +++ b/packages/cli/src/versions.ts @@ -3,6 +3,7 @@ import {RenderInternals} from '@remotion/renderer'; import fs from 'node:fs'; import path from 'node:path'; import {chalk} from './chalk'; +import {EXTRA_PACKAGES, EXTRA_PACKAGES_DOCS} from './extra-packages'; import {listOfRemotionPackages} from './list-of-remotion-packages'; import {Log} from './log'; import {parseCommandLine} from './parse-command-line'; @@ -58,6 +59,44 @@ const getAllVersions = async ( ).filter(([, version]) => version); }; +type ExtraPackageStatus = { + pkg: string; + requiredVersion: string; + installedVersion: string | null; + path: string | null; + isCorrect: boolean; +}; + +const getExtraPackagesStatus = async ( + remotionRoot: string, +): Promise => { + const results: ExtraPackageStatus[] = []; + + for (const [pkg, requiredVersion] of Object.entries(EXTRA_PACKAGES)) { + const versionAndPath = await getVersion(remotionRoot, pkg); + + if (versionAndPath) { + results.push({ + pkg, + requiredVersion, + installedVersion: versionAndPath.version, + path: versionAndPath.path, + isCorrect: versionAndPath.version === requiredVersion, + }); + } else { + results.push({ + pkg, + requiredVersion, + installedVersion: null, + path: null, + isCorrect: true, // Not installed is fine - only validate if installed + }); + } + } + + return results; +}; + export const VERSIONS_COMMAND = 'versions'; export const validateVersionsBeforeCommand = async ( @@ -70,26 +109,54 @@ export const validateVersionsBeforeCommand = async ( const installedVersions = Object.keys(grouped); - if (installedVersions.length === 1) { + const hasRemotionMismatch = + installedVersions.length > 1 && installedVersions.length !== 0; + + // Check extra packages + const extraPackagesStatus = await getExtraPackagesStatus(remotionRoot); + const incorrectExtraPackages = extraPackagesStatus.filter( + (status) => !status.isCorrect, + ); + + if (!hasRemotionMismatch && incorrectExtraPackages.length === 0) { return; } // Could be a global install of @remotion/cli. // If you render a bundle with a different version, it will give a warning accordingly. - if (installedVersions.length === 0) { + if (installedVersions.length === 0 && incorrectExtraPackages.length === 0) { return; } const logOptions: LogOptions = {indent: false, logLevel}; Log.warn(logOptions, '-------------'); Log.warn(logOptions, 'Version mismatch:'); - for (const version of installedVersions) { - Log.warn(logOptions, `- On version: ${version}`); - for (const pkg of grouped[version] ?? []) { + + if (hasRemotionMismatch) { + for (const version of installedVersions) { + Log.warn(logOptions, `- On version: ${version}`); + for (const pkg of grouped[version] ?? []) { + Log.warn( + logOptions, + ` - ${pkg.pkg} ${chalk.gray(path.relative(remotionRoot, pkg.versionAndPath.path))}`, + ); + } + + Log.info({indent: false, logLevel}); + } + } + + if (incorrectExtraPackages.length > 0) { + Log.warn(logOptions, 'Extra packages with wrong versions:'); + for (const status of incorrectExtraPackages) { + const docLink = EXTRA_PACKAGES_DOCS[status.pkg]; Log.warn( logOptions, - ` - ${pkg.pkg} ${chalk.gray(path.relative(remotionRoot, pkg.versionAndPath.path))}`, + ` - ${status.pkg}: installed ${status.installedVersion}, required ${status.requiredVersion}`, ); + if (docLink) { + Log.warn(logOptions, ` See: ${docLink}`); + } } Log.info({indent: false, logLevel}); @@ -109,6 +176,13 @@ export const validateVersionsBeforeCommand = async ( logOptions, '- Remove the `^` character in front of a version to pin a package.', ); + for (const incorrectPkg of incorrectExtraPackages) { + Log.warn( + logOptions, + `- For ${incorrectPkg.pkg}, install exact version ${incorrectPkg.requiredVersion} (run: npx remotion add ${incorrectPkg.pkg}).`, + ); + } + if (!RenderInternals.isEqualOrBelowLogLevel(logLevel, 'verbose')) { Log.warn( logOptions, @@ -149,6 +223,29 @@ export const versionsCommand = async ( Log.info({indent: false, logLevel}); } + // Check extra packages + const extraPackagesStatus = await getExtraPackagesStatus(remotionRoot); + const installedExtraPackages = extraPackagesStatus.filter( + (status) => status.installedVersion !== null, + ); + + if (installedExtraPackages.length > 0) { + Log.info({indent: false, logLevel}, 'Extra packages:'); + for (const status of installedExtraPackages) { + const versionStatus = status.isCorrect + ? chalk.green(`${status.installedVersion}`) + : chalk.red( + `${status.installedVersion} (required: ${status.requiredVersion})`, + ); + Log.info({indent: false, logLevel}, `- ${status.pkg}@${versionStatus}`); + if (status.path) { + Log.verbose({indent: false, logLevel}, ` ${status.path}`); + } + } + + Log.info({indent: false, logLevel}); + } + if (installedVersions.length === 0) { Log.info({indent: false, logLevel}, 'No Remotion packages found.'); Log.info( @@ -163,24 +260,53 @@ export const versionsCommand = async ( process.exit(1); } - if (installedVersions.length === 1) { + const incorrectExtraPackages = extraPackagesStatus.filter( + (status) => !status.isCorrect, + ); + + if (installedVersions.length === 1 && incorrectExtraPackages.length === 0) { Log.info( {indent: false, logLevel}, - `✅ Great! All packages have the same version.`, + `All packages have the correct version.`, ); } else { - Log.error( - {indent: false, logLevel}, - 'Version mismatch: Not all Remotion packages have the same version.', - ); - Log.info( - {indent: false, logLevel}, - '- Make sure your package.json has all Remotion packages pointing to the same version.', - ); - Log.info( - {indent: false, logLevel}, - '- Remove the `^` character in front of a version to pin a package.', - ); + if (installedVersions.length !== 1) { + Log.error( + {indent: false, logLevel}, + 'Version mismatch: Not all Remotion packages have the same version.', + ); + Log.info( + {indent: false, logLevel}, + '- Make sure your package.json has all Remotion packages pointing to the same version.', + ); + Log.info( + {indent: false, logLevel}, + '- Remove the `^` character in front of a version to pin a package.', + ); + } + + if (incorrectExtraPackages.length > 0) { + Log.error( + {indent: false, logLevel}, + 'Extra packages have incorrect versions:', + ); + for (const status of incorrectExtraPackages) { + const docLink = EXTRA_PACKAGES_DOCS[status.pkg]; + Log.info( + {indent: false, logLevel}, + `- ${status.pkg}: installed ${status.installedVersion}, required ${status.requiredVersion}`, + ); + if (docLink) { + Log.info({indent: false, logLevel}, ` See: ${docLink}`); + } + } + + Log.info( + {indent: false, logLevel}, + `To fix, run: npx remotion add ${incorrectExtraPackages.map((s) => s.pkg).join(' ')}`, + ); + } + if (!RenderInternals.isEqualOrBelowLogLevel(logLevel, 'verbose')) { Log.info( {indent: false, logLevel}, diff --git a/packages/docs/docs/cli/add.mdx b/packages/docs/docs/cli/add.mdx index f8c335cbdaf..dd9948b215a 100644 --- a/packages/docs/docs/cli/add.mdx +++ b/packages/docs/docs/cli/add.mdx @@ -7,6 +7,8 @@ crumb: CLI Reference Adds one or more Remotion packages to your project with the same version as your other Remotion packages. +Also supports adding `zod` and `mediabunny` with the correct version for your Remotion installation. + ``` npx remotion add ``` @@ -27,7 +29,7 @@ npx remotion add @remotion/transitions @remotion/three @remotion/lottie This command will: -1. Verify that all package names are valid Remotion packages +1. Verify that all package names are valid Remotion packages (or supported extra packages: `zod`, `mediabunny`) 2. Check which packages are already installed (and skip them) 3. Detect the version of your currently installed Remotion packages 4. Install the specified packages with the matching version diff --git a/packages/docs/docs/cli/upgrade.mdx b/packages/docs/docs/cli/upgrade.mdx index 3a9b53813d8..4e821bdc503 100644 --- a/packages/docs/docs/cli/upgrade.mdx +++ b/packages/docs/docs/cli/upgrade.mdx @@ -7,6 +7,8 @@ crumb: CLI Reference Upgrades all Remotion-related packages. +If `mediabunny` is installed, it will also be upgraded to the version [compatible with the new Remotion version](/docs/mediabunny/version). + ``` npx remotion upgrade ``` diff --git a/packages/docs/docs/cli/versions.mdx b/packages/docs/docs/cli/versions.mdx index e058aad0b9b..3eb86618efa 100644 --- a/packages/docs/docs/cli/versions.mdx +++ b/packages/docs/docs/cli/versions.mdx @@ -9,6 +9,8 @@ crumb: CLI Reference Prints out which versions of Remotion packages are installed. +Also checks if `zod` and [`mediabunny`](/docs/mediabunny/version) are installed with the correct versions. + ``` npx remotion versions ``` diff --git a/packages/docs/docs/mediabunny/version.mdx b/packages/docs/docs/mediabunny/version.mdx index 698ccb803c4..4744f8561e5 100644 --- a/packages/docs/docs/mediabunny/version.mdx +++ b/packages/docs/docs/mediabunny/version.mdx @@ -12,7 +12,11 @@ The following package of Remotion is using [Mediabunny](https://mediabunny.dev): The current version of Remotion uses version [`1.29.0`](https://github.com/Vanilagy/mediabunny/releases/tag/v1.29.0) of Mediabunny. -If you use Mediabunny as a direct dependency in your project, you can use this version number to not have two versions of Mediabunny installed. +If you use Mediabunny as a direct dependency in your project, you can install it with the correct version using: + +```bash +npx remotion add mediabunny +``` ## History diff --git a/packages/docs/docs/schemas.mdx b/packages/docs/docs/schemas.mdx index ada5dd85e37..00a3e80b226 100644 --- a/packages/docs/docs/schemas.mdx +++ b/packages/docs/docs/schemas.mdx @@ -12,7 +12,9 @@ As an alternative to [using TypeScript types](/docs/parameterized-rendering) to If you want to use this feature, install `zod@3.22.3` and [`@remotion/zod-types`](/docs/zod-types) for Remotion-specific types: - +```bash +npx remotion add @remotion/zod-types zod +``` ## Defining a schema diff --git a/packages/it-tests/src/monorepo/cli-versions.test.ts b/packages/it-tests/src/monorepo/cli-versions.test.ts index 795b755250a..bdf69449602 100644 --- a/packages/it-tests/src/monorepo/cli-versions.test.ts +++ b/packages/it-tests/src/monorepo/cli-versions.test.ts @@ -12,5 +12,5 @@ test('should return list of versions', async () => { expect(text).toInclude(`On version: ${VERSION}`); expect(text).toInclude(`- @remotion/three`); - expect(text).toInclude(`Great! All packages have the same version`); + expect(text).toInclude(`All packages have the correct version`); }); diff --git a/packages/it-tests/src/monorepo/extra-packages.test.ts b/packages/it-tests/src/monorepo/extra-packages.test.ts new file mode 100644 index 00000000000..e094b1eb2d5 --- /dev/null +++ b/packages/it-tests/src/monorepo/extra-packages.test.ts @@ -0,0 +1,22 @@ +import {expect, test} from 'bun:test'; +import {readFileSync} from 'fs'; +import path from 'path'; + +test('@remotion/studio should have zod and mediabunny in dependencies', () => { + const studioPackageJsonPath = path.resolve( + __dirname, + '..', + '..', + '..', + 'studio', + 'package.json', + ); + + const json = readFileSync(studioPackageJsonPath, 'utf-8'); + const packageJson = JSON.parse(json); + const {dependencies} = packageJson; + + expect(dependencies).toBeDefined(); + expect(dependencies.zod).toBeDefined(); + expect(dependencies.mediabunny).toBeDefined(); +}); diff --git a/packages/studio-server/src/helpers/get-installed-installable-packages.ts b/packages/studio-server/src/helpers/get-installed-installable-packages.ts index 1b27137610b..0b6ee264807 100644 --- a/packages/studio-server/src/helpers/get-installed-installable-packages.ts +++ b/packages/studio-server/src/helpers/get-installed-installable-packages.ts @@ -1,4 +1,4 @@ -import {installableMap} from '@remotion/studio-shared'; +import {extraPackages, installableMap} from '@remotion/studio-shared'; import {getInstalledDependencies} from './get-installed-dependencies'; export const getInstalledInstallablePackages = ( @@ -12,8 +12,14 @@ export const getInstalledInstallablePackages = ( ...optionalDependencies, ]; - return Object.entries(installableMap) + const remotionPackages = Object.entries(installableMap) .filter(([, _installable]) => _installable) .map(([pkg]) => (pkg === 'core' ? 'remotion' : `@remotion/${pkg}`)) .filter((pkg) => installablePackages.includes(pkg)); + + const installedExtraPackages = extraPackages + .map((pkg) => pkg.name) + .filter((pkg) => installablePackages.includes(pkg)); + + return [...remotionPackages, ...installedExtraPackages]; }; diff --git a/packages/studio-server/src/helpers/install-command.ts b/packages/studio-server/src/helpers/install-command.ts index 2c4b698c152..691f22a3427 100644 --- a/packages/studio-server/src/helpers/install-command.ts +++ b/packages/studio-server/src/helpers/install-command.ts @@ -11,7 +11,7 @@ export const getInstallCommand = ({ version: string; additionalArgs: string[]; }): string[] => { - const pkgList = packages.map((p) => `${p}@${version}`); + const pkgList = packages.map((p) => (version ? `${p}@${version}` : p)); const commands: {[key in PackageManager]: string[]} = { npm: [ diff --git a/packages/studio-server/src/preview-server/routes/install-dependency.ts b/packages/studio-server/src/preview-server/routes/install-dependency.ts index 49dc78bd93f..000acd26d8a 100644 --- a/packages/studio-server/src/preview-server/routes/install-dependency.ts +++ b/packages/studio-server/src/preview-server/routes/install-dependency.ts @@ -1,5 +1,6 @@ import {RenderInternals} from '@remotion/renderer'; import { + extraPackages, type InstallPackageRequest, type InstallPackageResponse, } from '@remotion/studio-shared'; @@ -9,12 +10,23 @@ import {getInstallCommand} from '../../helpers/install-command'; import type {ApiHandler} from '../api-types'; import {getPackageManager, lockFilePaths} from '../get-package-manager'; +const extraPackageNames = extraPackages.map((pkg) => pkg.name); + +const isExtraPackage = (packageName: string): boolean => { + return extraPackageNames.includes(packageName); +}; + +const getExtraPackageVersion = (packageName: string): string | null => { + const pkg = extraPackages.find((p) => p.name === packageName); + return pkg ? pkg.version : null; +}; + export const handleInstallPackage: ApiHandler< InstallPackageRequest, InstallPackageResponse > = async ({logLevel, remotionRoot, input: {packageNames}}) => { for (const packageName of packageNames) { - if (!packageName.startsWith('@remotion/')) { + if (!packageName.startsWith('@remotion/') && !isExtraPackage(packageName)) { return Promise.reject( new Error(`Package ${packageName} is not allowed to be installed.`), ); @@ -30,10 +42,20 @@ export const handleInstallPackage: ApiHandler< ); } + // Build packages with appropriate versions + const packagesWithVersions = packageNames.map((pkg) => { + const extraVersion = getExtraPackageVersion(pkg); + if (extraVersion) { + return `${pkg}@${extraVersion}`; + } + + return `${pkg}@${VERSION}`; + }); + const command = getInstallCommand({ manager: manager.manager, - packages: packageNames, - version: VERSION, + packages: packagesWithVersions, + version: '', additionalArgs: [], }); diff --git a/packages/studio-shared/src/index.ts b/packages/studio-shared/src/index.ts index 19f90d0bf64..f4ed7b804f2 100644 --- a/packages/studio-shared/src/index.ts +++ b/packages/studio-shared/src/index.ts @@ -52,8 +52,10 @@ export { Pkgs, apiDocs, descriptions, + extraPackages, installableMap, packages, + type ExtraPackage, } from './package-info'; export {PackageManager} from './package-manager'; export {ProjectInfo} from './project-info'; diff --git a/packages/studio-shared/src/package-info.ts b/packages/studio-shared/src/package-info.ts index 287fe548f29..1d1bab9c74a 100644 --- a/packages/studio-shared/src/package-info.ts +++ b/packages/studio-shared/src/package-info.ts @@ -86,6 +86,28 @@ export const packages = [ export type Pkgs = (typeof packages)[number]; +export type ExtraPackage = { + name: string; + version: string; + description: string; + docsUrl: string; +}; + +export const extraPackages: ExtraPackage[] = [ + { + name: 'zod', + version: '3.22.3', + description: 'Schema validation library for defining component props', + docsUrl: 'https://www.remotion.dev/docs/schemas', + }, + { + name: 'mediabunny', + version: '1.29.0', + description: 'Multimedia library used by Remotion', + docsUrl: 'https://www.remotion.dev/docs/mediabunny/version', + }, +]; + export const descriptions: {[key in Pkgs]: string | null} = { compositor: 'Rust binary for Remotion', player: 'React component for embedding a Remotion preview into your app', diff --git a/packages/studio/src/components/InstallPackage.tsx b/packages/studio/src/components/InstallPackage.tsx index b4b01fd5d88..4a84e73975b 100644 --- a/packages/studio/src/components/InstallPackage.tsx +++ b/packages/studio/src/components/InstallPackage.tsx @@ -1,5 +1,10 @@ -import type {PackageManager, Pkgs} from '@remotion/studio-shared'; -import {apiDocs, descriptions, installableMap} from '@remotion/studio-shared'; +import type {ExtraPackage, PackageManager, Pkgs} from '@remotion/studio-shared'; +import { + apiDocs, + descriptions, + extraPackages, + installableMap, +} from '@remotion/studio-shared'; import React, {useCallback, useContext, useEffect} from 'react'; import {VERSION} from 'remotion'; import {installPackages} from '../api/install-package'; @@ -155,6 +160,34 @@ export const InstallPackageModal: React.FC<{ ); })} + {extraPackages.map((extraPkg: ExtraPackage) => { + const isInstalled = + window.remotion_installedPackages?.includes(extraPkg.name) ?? + false; + + return ( + + { + setMap((prev) => ({ + ...prev, + [extraPkg.name]: !prev[extraPkg.name], + })); + }} + disabled={!canSelectPackages || isInstalled} + /> + + + + ); + })} )}