@effect-motion/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import { Video } from "@effect-motion/export";
2
+ import * as Console from "effect/Console";
3
+ import * as Effect from "effect/Effect";
4
+ import { FileSystem } from "effect/FileSystem";
5
+ import * as Option from "effect/Option";
6
+ import { Path } from "effect/Path";
7
+ import * as Result from "effect/Result";
8
+ import { Argument, Command, Flag } from "effect/unstable/cli";
9
+ import { DEFAULT_OUTPUT_DIR, resolveTarget, } from "../Config.js";
10
+ import { findConfig, loadConfig } from "../ConfigLoader.js";
11
+ import { MotionCliError } from "../MotionCliError.js";
12
+ import { makeViteLoader } from "../ViteLoader.js";
13
+ const opt = (o) => Option.getOrUndefined(o);
14
+ const renderFlags = {
15
+ config: Flag.optional(Flag.string("config").pipe(Flag.withDescription("Path to a motion.config.ts (tsc -p style)"))),
16
+ width: Flag.optional(Flag.integer("width")),
17
+ height: Flag.optional(Flag.integer("height")),
18
+ fps: Flag.optional(Flag.integer("fps").pipe(Flag.withDescription("Frame rate override"))),
19
+ dpr: Flag.optional(Flag.float("dpr").pipe(Flag.withDescription("Supersampling factor (output pixels = scene × dpr)"))),
20
+ seed: Flag.optional(Flag.string("seed")),
21
+ maxFrames: Flag.optional(Flag.integer("max-frames")),
22
+ frames: Flag.optional(Flag.integer("frames").pipe(Flag.withDescription("Cap encoded frames (required for infinite scenes)"))),
23
+ outDir: Flag.optional(Flag.string("out-dir").pipe(Flag.withDescription("Output directory override"))),
24
+ format: Flag.optional(Flag.choice("format", ["mp4"])),
25
+ targets: Argument.string("targets").pipe(Argument.withDescription("Target names from the config, or one scene file path"), Argument.variadic()),
26
+ };
27
+ const overridesFrom = (input) => {
28
+ const raw = {
29
+ width: opt(input.width),
30
+ height: opt(input.height),
31
+ frameRate: opt(input.fps),
32
+ dpr: opt(input.dpr),
33
+ seed: opt(input.seed),
34
+ maxFrames: opt(input.maxFrames),
35
+ frames: opt(input.frames),
36
+ outDir: opt(input.outDir),
37
+ format: opt(input.format),
38
+ };
39
+ return Object.fromEntries(Object.entries(raw).filter(([, v]) => v !== undefined));
40
+ };
41
+ // a positional is a scene file (configless mode) iff it looks like a module
42
+ // path — target names never carry an extension
43
+ const isSceneFile = (arg) => /\.(ts|tsx|mts|js|mjs)$/.test(arg);
44
+ const sceneBasename = (file) => {
45
+ const base = file.split("/").at(-1) ?? file;
46
+ return base.replace(/\.(ts|tsx|mts|js|mjs)$/, "");
47
+ };
48
+ /** Map a resolved target onto the export package's options shape. */
49
+ const toVideoOptions = (target) => {
50
+ const { dpr, ...settings } = target.settings;
51
+ return {
52
+ // ponytail: VideoSceneSettings doesn't name seed/backgroundColor, but
53
+ // Video.render passes settings straight through to Scene.stream — the
54
+ // cast is the CLI-side adaptation decided in design D2; collapse it if
55
+ // the export package ever widens its settings type
56
+ settings: settings,
57
+ ...(dpr !== undefined ? { dpr } : {}),
58
+ ...(target.frames !== undefined ? { frames: target.frames } : {}),
59
+ };
60
+ };
61
+ const renderOne = (loader, baseDir, target) => Effect.gen(function* () {
62
+ const path = yield* Path;
63
+ const fs = yield* FileSystem;
64
+ const sceneAbs = path.isAbsolute(target.scene)
65
+ ? target.scene
66
+ : path.resolve(baseDir, target.scene);
67
+ const module_ = yield* loader.load(sceneAbs);
68
+ const scene = module_.scene;
69
+ if (scene === undefined) {
70
+ return yield* new MotionCliError({
71
+ reason: "SceneLoadFailed",
72
+ message: `${sceneAbs} has no \`scene\` export`,
73
+ });
74
+ }
75
+ const outDirAbs = path.resolve(baseDir, target.outDir);
76
+ yield* fs.makeDirectory(outDirAbs, { recursive: true }).pipe(Effect.mapError((cause) => new MotionCliError({
77
+ reason: "RenderFailed",
78
+ message: `could not create output directory ${outDirAbs}`,
79
+ cause,
80
+ })));
81
+ const outFile = path.join(outDirAbs, target.fileName);
82
+ yield* Video.render(scene, outFile, toVideoOptions(target)).pipe(Effect.mapError((cause) => new MotionCliError({
83
+ reason: "RenderFailed",
84
+ message: `target "${target.name}" failed to render (${sceneAbs})`,
85
+ cause,
86
+ })));
87
+ return outFile;
88
+ });
89
+ const handler = (input) => Effect.gen(function* () {
90
+ const path = yield* Path;
91
+ const cwd = process.cwd();
92
+ const overrides = overridesFrom(input);
93
+ // resolve the target list and the directory paths are relative to
94
+ let baseDir;
95
+ let resolved;
96
+ const [first] = input.targets;
97
+ if (first !== undefined &&
98
+ input.targets.length === 1 &&
99
+ isSceneFile(first)) {
100
+ // configless mode: one scene file, library defaults + flags
101
+ baseDir = cwd;
102
+ resolved = [
103
+ resolveTarget({
104
+ name: sceneBasename(first),
105
+ scene: path.resolve(cwd, first),
106
+ output: DEFAULT_OUTPUT_DIR,
107
+ }, overrides),
108
+ ];
109
+ }
110
+ else {
111
+ const configPath = yield* findConfig(cwd, opt(input.config));
112
+ baseDir = path.dirname(configPath);
113
+ const loader = yield* makeViteLoader(baseDir);
114
+ const config = yield* loadConfig(loader, configPath);
115
+ if (input.targets.length === 0) {
116
+ resolved = config.targets.map((t) => resolveTarget(t, overrides));
117
+ }
118
+ else {
119
+ const known = new Map(config.targets.map((t) => [t.name, t]));
120
+ const unknown = input.targets.filter((name) => !known.has(name));
121
+ if (unknown.length > 0) {
122
+ return yield* new MotionCliError({
123
+ reason: "UnknownTarget",
124
+ message: `unknown target${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} — ` +
125
+ `known targets: ${[...known.keys()].join(", ") || "(none)"}`,
126
+ });
127
+ }
128
+ resolved = input.targets.map((name) =>
129
+ // biome-ignore lint/style/noNonNullAssertion: membership checked above
130
+ resolveTarget(known.get(name), overrides));
131
+ }
132
+ return yield* execute(loader, baseDir, resolved);
133
+ }
134
+ const loader = yield* makeViteLoader(baseDir);
135
+ return yield* execute(loader, baseDir, resolved);
136
+ }).pipe(Effect.scoped);
137
+ // render sequentially: ffmpeg already saturates the CPU per target
138
+ // (ponytail: parallelize only if profiling ever says otherwise)
139
+ const execute = (loader, baseDir, targets) => Effect.gen(function* () {
140
+ const failures = [];
141
+ for (const target of targets) {
142
+ const result = yield* Effect.result(renderOne(loader, baseDir, target));
143
+ if (Result.isSuccess(result)) {
144
+ yield* Console.log(`✓ ${target.name} → ${result.success}`);
145
+ }
146
+ else {
147
+ failures.push(result.failure);
148
+ yield* Console.error(`✗ ${target.name}: ${result.failure.message}`);
149
+ }
150
+ }
151
+ if (failures.length > 0) {
152
+ return yield* new MotionCliError({
153
+ reason: "RenderFailed",
154
+ message: `${failures.length} of ${targets.length} target${targets.length > 1 ? "s" : ""} failed`,
155
+ cause: failures[0],
156
+ });
157
+ }
158
+ });
159
+ export const renderCommand = Command.make("render", renderFlags, handler).pipe(Command.withDescription("Render targets from motion.config.ts (or one scene file) to video"));
@@ -0,0 +1,10 @@
1
+ import { FileSystem } from "effect/FileSystem";
2
+ import * as Option from "effect/Option";
3
+ import { Path } from "effect/Path";
4
+ import { Command } from "effect/unstable/cli";
5
+ import { MotionCliError } from "../MotionCliError.js";
6
+ export declare const studioCommand: Command.Command<"studio", {
7
+ readonly config: Option.Option<string>;
8
+ readonly port: Option.Option<number>;
9
+ readonly host: Option.Option<string>;
10
+ }, {}, MotionCliError, FileSystem | Path>;
@@ -0,0 +1,106 @@
1
+ import * as Console from "effect/Console";
2
+ import * as Effect from "effect/Effect";
3
+ import { FileSystem } from "effect/FileSystem";
4
+ import * as Option from "effect/Option";
5
+ import { Path } from "effect/Path";
6
+ import * as Result from "effect/Result";
7
+ import { Command, Flag } from "effect/unstable/cli";
8
+ import { findConfig } from "../ConfigLoader.js";
9
+ import { MotionCliError } from "../MotionCliError.js";
10
+ const studioFlags = {
11
+ config: Flag.optional(Flag.string("config")),
12
+ port: Flag.optional(Flag.integer("port")),
13
+ host: Flag.optional(Flag.string("host")),
14
+ };
15
+ /** Shipped studio app source (dist/commands/studio.js → ../../studio-app). */
16
+ const studioAppSource = (path) => path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "studio-app");
17
+ // The studio app is copied INTO the project (.motion/studio) so its bare
18
+ // imports (react, @effect-motion/react, the config's own imports) resolve
19
+ // against the project's node_modules — the same graph render uses. The
20
+ // generated project.ts pins the project root for /@fs imports.
21
+ const prepareStudioDir = (projectRoot) => Effect.gen(function* () {
22
+ const fs = yield* FileSystem;
23
+ const path = yield* Path;
24
+ const source = studioAppSource(path);
25
+ const studioDir = path.join(projectRoot, ".motion", "studio");
26
+ yield* fs.makeDirectory(studioDir, { recursive: true });
27
+ for (const entry of yield* fs.readDirectory(source)) {
28
+ yield* fs.copyFile(path.join(source, entry), path.join(studioDir, entry));
29
+ }
30
+ yield* fs.writeFileString(path.join(studioDir, "project.ts"), `// generated by \`motion studio\` — do not edit\nexport const projectRoot = ${JSON.stringify(projectRoot)};\n`);
31
+ return studioDir;
32
+ }).pipe(Effect.mapError((cause) => cause instanceof MotionCliError
33
+ ? cause
34
+ : new MotionCliError({
35
+ reason: "StudioFailed",
36
+ message: `could not prepare the studio app in ${projectRoot}/.motion/studio`,
37
+ cause,
38
+ })));
39
+ const handler = (input) => Effect.gen(function* () {
40
+ const path = yield* Path;
41
+ const cwd = process.cwd();
42
+ // a config is optional for studio: unregistered scenes are previewable,
43
+ // so "no config" just means "project root = cwd"
44
+ const found = yield* Effect.result(findConfig(cwd, Option.getOrUndefined(input.config)));
45
+ const projectRoot = Result.isSuccess(found)
46
+ ? path.dirname(found.success)
47
+ : cwd;
48
+ const studioDir = yield* prepareStudioDir(projectRoot);
49
+ const server = yield* Effect.acquireRelease(Effect.tryPromise({
50
+ try: async () => {
51
+ const { createServer } = await import("vite");
52
+ const server = await createServer({
53
+ root: studioDir,
54
+ configFile: false,
55
+ server: {
56
+ ...(Option.isSome(input.port) ? { port: input.port.value } : {}),
57
+ ...(Option.isSome(input.host) ? { host: input.host.value } : {}),
58
+ fs: { allow: [projectRoot] },
59
+ },
60
+ // one React/effect instance no matter where modules live
61
+ resolve: {
62
+ dedupe: ["react", "react-dom", "effect", "effect-motion"],
63
+ },
64
+ plugins: [
65
+ {
66
+ name: "motion:project-watch",
67
+ configureServer(server) {
68
+ // the project lives OUTSIDE the vite root
69
+ // (.motion/studio); vite's own add/unlink → glob
70
+ // invalidation doesn't reach out-of-root dirs, so
71
+ // scene add/remove refreshes the picker here
72
+ // (ponytail: invalidateAll + full reload — coarse,
73
+ // but studio-sized; narrow it if it ever matters)
74
+ const onAddUnlink = (file) => {
75
+ if (!file.startsWith(projectRoot))
76
+ return;
77
+ server.moduleGraph.invalidateAll();
78
+ server.ws.send({ type: "full-reload" });
79
+ };
80
+ server.watcher.on("add", onAddUnlink);
81
+ server.watcher.on("unlink", onAddUnlink);
82
+ },
83
+ },
84
+ ],
85
+ });
86
+ await server.listen();
87
+ // out-of-root project files aren't watched by default — without
88
+ // this, editing a scene or the config would not hot reload
89
+ server.watcher.add([
90
+ `${projectRoot}/src`,
91
+ `${projectRoot}/motion.config.ts`,
92
+ ]);
93
+ return server;
94
+ },
95
+ catch: (cause) => new MotionCliError({
96
+ reason: "StudioFailed",
97
+ message: `could not start the studio dev server for ${projectRoot}`,
98
+ cause,
99
+ }),
100
+ }), (server) => Effect.promise(() => server.close()));
101
+ yield* Console.log("motion studio running:");
102
+ server.printUrls();
103
+ // serve until interrupted (Ctrl-C) — teardown runs via the scope
104
+ yield* Effect.never;
105
+ }).pipe(Effect.scoped);
106
+ export const studioCommand = Command.make("studio", studioFlags, handler).pipe(Command.withDescription("Preview scenes in the browser with hot reload (Player + scene picker)"));
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_FORMAT, DEFAULT_OUTPUT_DIR, defineConfig, type MotionConfig, type MotionTarget, type OutputFormat, type RenderOverrides, type ResolvedTarget, resolveTarget, type TargetSettings, validateConfig, } from "./Config.js";
2
+ export { MotionCliError, type MotionCliReason } from "./MotionCliError.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Public API: the config contract (imported by user motion.config.ts files
2
+ // and the studio app, so this entry must stay browser-safe) and the error
3
+ // type. The commands live behind the `motion` bin, not this entry.
4
+ export { DEFAULT_FORMAT, DEFAULT_OUTPUT_DIR, defineConfig, resolveTarget, validateConfig, } from "./Config.js";
5
+ export { MotionCliError } from "./MotionCliError.js";
package/dist/pins.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Exact versions a scaffolded project is pinned to — the set this CLI
3
+ * release was built and tested against. The effect pin is a determinism
4
+ * invariant (upgrading effect can change seeded random sequences), so
5
+ * scaffolds never use ranges or `latest`. Updated by the CLI's own release
6
+ * process.
7
+ */
8
+ export declare const PINS: {
9
+ readonly effect: "4.0.0-beta.98";
10
+ readonly "effect-motion": "0.2.0";
11
+ readonly "@effect-motion/react": "0.2.0";
12
+ readonly "@effect-motion/export": "0.2.0";
13
+ readonly "@effect-motion/cli": "0.1.0";
14
+ };
15
+ /** Non-determinism-critical companions; ranges are fine here. */
16
+ export declare const COMPANIONS: {
17
+ readonly react: "^19.2.0";
18
+ readonly "react-dom": "^19.2.0";
19
+ readonly typescript: "^7.0.2";
20
+ readonly "@types/react": "^19.2.0";
21
+ readonly "@types/react-dom": "^19.2.0";
22
+ readonly "@types/node": "^26.1.1";
23
+ };
package/dist/pins.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Exact versions a scaffolded project is pinned to — the set this CLI
3
+ * release was built and tested against. The effect pin is a determinism
4
+ * invariant (upgrading effect can change seeded random sequences), so
5
+ * scaffolds never use ranges or `latest`. Updated by the CLI's own release
6
+ * process.
7
+ */
8
+ export const PINS = {
9
+ effect: "4.0.0-beta.98",
10
+ "effect-motion": "0.2.0",
11
+ "@effect-motion/react": "0.2.0",
12
+ "@effect-motion/export": "0.2.0",
13
+ "@effect-motion/cli": "0.1.0",
14
+ };
15
+ /** Non-determinism-critical companions; ranges are fine here. */
16
+ export const COMPANIONS = {
17
+ react: "^19.2.0",
18
+ "react-dom": "^19.2.0",
19
+ typescript: "^7.0.2",
20
+ "@types/react": "^19.2.0",
21
+ "@types/react-dom": "^19.2.0",
22
+ "@types/node": "^26.1.1",
23
+ };
@@ -0,0 +1,20 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { FileSystem } from "effect/FileSystem";
3
+ import { Path } from "effect/Path";
4
+ import { MotionCliError } from "./MotionCliError.js";
5
+ /**
6
+ * The non-interactive core of `motion init`: everything except the prompts,
7
+ * so tests can drive it directly. Copies templates/default into the target
8
+ * directory and generates package.json from the pinned versions.
9
+ */
10
+ /** Directory of the shipped templates (dist/scaffold.js → ../templates). */
11
+ export declare const templatesDir: (path: Path) => string;
12
+ /** `.` means "here"; the project is named after the resolved directory. */
13
+ export declare const resolveProjectDir: (path: Path, cwd: string, input: string) => {
14
+ dir: string;
15
+ name: string;
16
+ };
17
+ /** Non-empty means anything but dotfiles (a fresh `git init` is fine). */
18
+ export declare const ensureEmptyDir: (dir: string) => Effect.Effect<undefined, MotionCliError, FileSystem>;
19
+ /** Copy the template tree + write the generated package.json. */
20
+ export declare const scaffoldProject: (dir: string, name: string) => Effect.Effect<void, MotionCliError, FileSystem | Path>;
@@ -0,0 +1,86 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { FileSystem } from "effect/FileSystem";
3
+ import { Path } from "effect/Path";
4
+ import { MotionCliError } from "./MotionCliError.js";
5
+ import { COMPANIONS, PINS } from "./pins.js";
6
+ /**
7
+ * The non-interactive core of `motion init`: everything except the prompts,
8
+ * so tests can drive it directly. Copies templates/default into the target
9
+ * directory and generates package.json from the pinned versions.
10
+ */
11
+ /** Directory of the shipped templates (dist/scaffold.js → ../templates). */
12
+ export const templatesDir = (path) => path.join(path.dirname(new URL(import.meta.url).pathname), "..", "templates", "default");
13
+ /** `.` means "here"; the project is named after the resolved directory. */
14
+ export const resolveProjectDir = (path, cwd, input) => {
15
+ const dir = path.resolve(cwd, input);
16
+ return { dir, name: path.basename(dir) };
17
+ };
18
+ /** Non-empty means anything but dotfiles (a fresh `git init` is fine). */
19
+ export const ensureEmptyDir = (dir) => Effect.gen(function* () {
20
+ const fs = yield* FileSystem;
21
+ if (!(yield* fs.exists(dir)))
22
+ return;
23
+ const entries = yield* fs.readDirectory(dir);
24
+ const meaningful = entries.filter((entry) => !entry.startsWith("."));
25
+ if (meaningful.length > 0) {
26
+ return yield* new MotionCliError({
27
+ reason: "ScaffoldTargetNotEmpty",
28
+ message: `${dir} is not empty (found ${meaningful.slice(0, 3).join(", ")}${meaningful.length > 3 ? ", …" : ""}) — choose an empty or new directory`,
29
+ });
30
+ }
31
+ }).pipe(wrapFsError("ScaffoldFailed", `could not inspect ${dir}`));
32
+ const wrapFsError = (reason, message) => (effect) => Effect.mapError(effect, (cause) => cause instanceof MotionCliError
33
+ ? cause
34
+ : new MotionCliError({ reason, message, cause }));
35
+ const packageJson = (name) => `${JSON.stringify({
36
+ name,
37
+ private: true,
38
+ version: "0.0.0",
39
+ type: "module",
40
+ scripts: {
41
+ studio: "motion studio",
42
+ render: "motion render",
43
+ },
44
+ dependencies: {
45
+ "@effect-motion/export": PINS["@effect-motion/export"],
46
+ "@effect-motion/react": PINS["@effect-motion/react"],
47
+ effect: PINS.effect,
48
+ "effect-motion": PINS["effect-motion"],
49
+ react: COMPANIONS.react,
50
+ "react-dom": COMPANIONS["react-dom"],
51
+ },
52
+ devDependencies: {
53
+ "@effect-motion/cli": PINS["@effect-motion/cli"],
54
+ "@types/node": COMPANIONS["@types/node"],
55
+ "@types/react": COMPANIONS["@types/react"],
56
+ "@types/react-dom": COMPANIONS["@types/react-dom"],
57
+ typescript: COMPANIONS.typescript,
58
+ },
59
+ }, null, "\t")}\n`;
60
+ /** Copy the template tree + write the generated package.json. */
61
+ export const scaffoldProject = (dir, name) => Effect.gen(function* () {
62
+ const fs = yield* FileSystem;
63
+ const path = yield* Path;
64
+ const templates = templatesDir(path);
65
+ yield* fs.makeDirectory(dir, { recursive: true });
66
+ yield* copyTree(fs, path, templates, dir);
67
+ // npm mangles nested .gitignore/package.json files in published
68
+ // tarballs, so both ship outside the template tree
69
+ yield* fs.writeFileString(path.join(dir, "package.json"), packageJson(name));
70
+ yield* fs.rename(path.join(dir, "_gitignore"), path.join(dir, ".gitignore"));
71
+ }).pipe(wrapFsError("ScaffoldFailed", `could not scaffold ${dir}`));
72
+ const copyTree = (fs, path, from, to) => Effect.gen(function* () {
73
+ const entries = yield* fs.readDirectory(from);
74
+ for (const entry of entries) {
75
+ const src = path.join(from, entry);
76
+ const dst = path.join(to, entry);
77
+ const info = yield* fs.stat(src);
78
+ if (info.type === "Directory") {
79
+ yield* fs.makeDirectory(dst, { recursive: true });
80
+ yield* copyTree(fs, path, src, dst);
81
+ }
82
+ else {
83
+ yield* fs.copyFile(src, dst);
84
+ }
85
+ }
86
+ });
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@effect-motion/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command line for effect-motion: scaffold projects (init), preview scenes with hot reload (studio), and render videos from motion.config.ts (render)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/julia-script/effect-motion.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://github.com/julia-script/effect-motion#readme",
13
+ "bugs": "https://github.com/julia-script/effect-motion/issues",
14
+ "keywords": [
15
+ "effect",
16
+ "motion",
17
+ "cli",
18
+ "scaffold",
19
+ "studio",
20
+ "render",
21
+ "video"
22
+ ],
23
+ "bin": {
24
+ "motion": "./dist/bin.js"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "templates",
35
+ "studio-app"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "@effect/platform-node": "4.0.0-beta.98",
42
+ "effect": "4.0.0-beta.98",
43
+ "vite": "^7.0.0",
44
+ "@effect-motion/export": "^0.2.0",
45
+ "effect-motion": "^0.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^26.1.1",
49
+ "@types/react": "^19.2.0",
50
+ "@types/react-dom": "^19.2.0",
51
+ "react": "^19.2.0",
52
+ "react-dom": "^19.2.0",
53
+ "typescript": "^7.0.2",
54
+ "vitest": "^4.1.10",
55
+ "@effect-motion/react": "^0.2.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc -p tsconfig.build.json",
59
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
60
+ "test": "vitest run",
61
+ "check": "tsc --noEmit"
62
+ }
63
+ }