create-effect-motion 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  Scaffold a new [effect-motion](https://github.com/julia-script/effect-motion) project:
4
4
 
5
5
  ```sh
6
- pnpm create effect-motion
7
- # or: npm create effect-motion / yarn create effect-motion / bun create effect-motion
6
+ bun create effect-motion
7
+ # or: npm create effect-motion / pnpm create effect-motion / yarn create effect-motion
8
8
  ```
9
9
 
10
10
  The prompts ask for a target directory, a package manager, and whether to set up [Biome](https://biomejs.dev) for linting/formatting. The generated project:
@@ -25,8 +25,8 @@ my-motion-project/
25
25
  Answering `.` scaffolds into the current directory and names the project after it. A `git init` runs automatically unless the directory already sits inside a repository. Then:
26
26
 
27
27
  ```sh
28
- pnpm studio # preview scenes with hot reload
29
- pnpm render # render targets from motion.config.ts to MP4
28
+ bun run studio # preview scenes with hot reload
29
+ bun run render # render targets from motion.config.ts to MP4
30
30
  ```
31
31
 
32
32
  ## Flags
@@ -34,7 +34,7 @@ pnpm render # render targets from motion.config.ts to MP4
34
34
  Every prompt has a flag twin, so the scaffolder runs non-interactively in scripts and CI:
35
35
 
36
36
  ```sh
37
- create-effect-motion my-app --pm pnpm --no-biome --no-install
37
+ create-effect-motion my-app --pm bun --no-biome --no-install
38
38
  create-effect-motion --yes # accept every default (-y)
39
39
  ```
40
40
 
package/dist/bin.js CHANGED
File without changes
package/dist/create.js CHANGED
@@ -8,24 +8,24 @@ import { MotionCliError, renderForTerminal } from "./MotionCliError.js";
8
8
  import { VERSION } from "./pins.js";
9
9
  import { ensureEmptyDir, resolveProjectDir, scaffoldProject, } from "./scaffold.js";
10
10
  const DEFAULT_DIRECTORY = "my-motion-project";
11
- const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
12
- /** The manager that invoked us (`pnpm create …` etc.), if detectable. */
11
+ const PACKAGE_MANAGERS = ["bun", "pnpm", "npm", "yarn"];
12
+ /** The manager that invoked us (`bun create …` etc.), if detectable. */
13
13
  const detectPackageManager = () => {
14
14
  const agent = process.env.npm_config_user_agent ?? "";
15
15
  return PACKAGE_MANAGERS.find((pm) => agent.startsWith(pm));
16
16
  };
17
17
  const flags = {
18
- directory: Argument.string("directory").pipe(Argument.withDescription('Target directory ("." scaffolds into the current directory)'), Argument.optional),
19
- pm: Flag.optional(Flag.choice("pm", PACKAGE_MANAGERS).pipe(Flag.withDescription("Package manager (skips the prompt)"))),
20
- biome: Flag.boolean("biome").pipe(Flag.withDescription("Set up Biome for linting/formatting (skips the prompt)")),
21
- noBiome: Flag.boolean("no-biome").pipe(Flag.withDescription("Skip the Biome setup (skips the prompt)")),
22
- noInstall: Flag.boolean("no-install").pipe(Flag.withDescription("Skip dependency installation")),
23
- yes: Flag.boolean("yes").pipe(Flag.withAlias("y"), Flag.withDescription("Accept the default answer for every prompt not answered by a flag")),
18
+ directory: Argument.String("directory").pipe(Argument.withDescription('Target directory ("." scaffolds into the current directory)'), Argument.optional),
19
+ pm: Flag.optional(Flag.Literals("pm", PACKAGE_MANAGERS).pipe(Flag.withDescription("Package manager (skips the prompt)"))),
20
+ biome: Flag.Boolean("biome").pipe(Flag.withDefault(false), Flag.withDescription("Set up Biome for linting/formatting (skips the prompt)")),
21
+ noBiome: Flag.Boolean("no-biome").pipe(Flag.withDefault(false), Flag.withDescription("Skip the Biome setup (skips the prompt)")),
22
+ noInstall: Flag.Boolean("no-install").pipe(Flag.withDefault(false), Flag.withDescription("Skip dependency installation")),
23
+ yes: Flag.Boolean("yes").pipe(Flag.withDefault(false), Flag.withAlias("y"), Flag.withDescription("Accept the default answer for every prompt not answered by a flag")),
24
24
  };
25
- const promptDirectory = Prompt.text({
25
+ const promptDirectory = Prompt.run(Prompt.String({
26
26
  message: 'Where should the project be created? ("." for the current directory)',
27
27
  default: DEFAULT_DIRECTORY,
28
- });
28
+ }));
29
29
  const promptPackageManager = Effect.suspend(() => {
30
30
  const detected = detectPackageManager();
31
31
  // detected manager listed first so plain Enter picks it
@@ -33,15 +33,15 @@ const promptPackageManager = Effect.suspend(() => {
33
33
  ...(detected ? [detected] : []),
34
34
  ...PACKAGE_MANAGERS.filter((pm) => pm !== detected),
35
35
  ];
36
- return Prompt.select({
36
+ return Prompt.run(Prompt.Select({
37
37
  message: "Which package manager?",
38
38
  choices: ordered.map((pm) => ({ title: pm, value: pm })),
39
- });
39
+ }));
40
40
  });
41
- const promptBiome = Prompt.confirm({
41
+ const promptBiome = Prompt.run(Prompt.Confirm({
42
42
  message: "Add Biome for linting/formatting?",
43
43
  initial: true,
44
- });
44
+ }));
45
45
  const runInstall = (pm, dir) => Effect.gen(function* () {
46
46
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
47
47
  const command = ChildProcess.make(pm, ["install"], {
@@ -100,7 +100,7 @@ const handler = (input) => Effect.gen(function* () {
100
100
  yield* ensureEmptyDir(dir);
101
101
  const pm = Option.getOrUndefined(input.pm) ??
102
102
  (input.yes
103
- ? (detectPackageManager() ?? "npm")
103
+ ? (detectPackageManager() ?? "bun")
104
104
  : yield* promptPackageManager);
105
105
  // explicit flags win over --yes; --no-biome beats --biome if both are passed
106
106
  const biome = input.noBiome
@@ -127,7 +127,7 @@ const handler = (input) => Effect.gen(function* () {
127
127
  `${name} is ready.`,
128
128
  dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
129
129
  ` ${pm === "npm" ? "npm run" : pm} studio # preview scenes with hot reload`,
130
- ` ${pm === "npm" ? "npm run" : pm} render # render targets from motion.config.ts`,
130
+ ` ${pm === "npm" ? "npm run" : pm} render # execute render.ts (the render program)`,
131
131
  ]
132
132
  .filter((line) => line !== "")
133
133
  .join("\n"));
@@ -136,8 +136,8 @@ const handler = (input) => Effect.gen(function* () {
136
136
  Effect.catchTag("QuitError", () => Effect.interrupt));
137
137
  // registered globally so `--verbose` parses anywhere on the command line;
138
138
  // the reporter reads argv directly because it sits outside handler context
139
- const verboseFlag = GlobalFlag.setting("verbose")({
140
- flag: Flag.boolean("verbose").pipe(Flag.withDescription("Print full error cause chains")),
139
+ const verboseFlag = GlobalFlag.Setting("verbose")({
140
+ flag: Flag.Boolean("verbose").pipe(Flag.withDefault(false), Flag.withDescription("Print full error cause chains")),
141
141
  });
142
142
  export const rootCommand = Command.make("create-effect-motion", flags, handler).pipe(Command.withDescription("Scaffold a new effect-motion project"), Command.withGlobalFlags([verboseFlag]));
143
143
  export const CLI_VERSION = VERSION;
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
1
  {
2
- "name": "create-effect-motion",
3
- "version": "0.4.0",
4
- "description": "Scaffold a new effect-motion project — run with `pnpm create effect-motion` (or npm/yarn/bun create)",
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/create-effect-motion"
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
- "create",
18
- "scaffold",
19
- "template",
20
- "motion-graphics"
21
- ],
22
- "bin": {
23
- "create-effect-motion": "./dist/bin.js"
24
- },
25
- "files": [
26
- "dist",
27
- "templates"
28
- ],
29
- "dependencies": {
30
- "@effect/platform-node": "4.0.0-beta.98",
31
- "effect": "4.0.0-beta.98"
32
- },
33
- "devDependencies": {
34
- "@biomejs/biome": "^2.5.3",
35
- "@types/node": "^26.1.1",
36
- "typescript": "^7.0.2",
37
- "vitest": "^4.1.10"
38
- },
39
- "scripts": {
40
- "build": "tsc -p tsconfig.build.json",
41
- "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
42
- "test": "vitest run",
43
- "check": "tsc --noEmit"
44
- }
45
- }
2
+ "name": "create-effect-motion",
3
+ "version": "0.6.0",
4
+ "description": "Scaffold a new effect-motion project — run with `bun create effect-motion` (or npm/pnpm/yarn create)",
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/create-effect-motion"
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
+ "create",
18
+ "scaffold",
19
+ "template",
20
+ "motion-graphics"
21
+ ],
22
+ "bin": {
23
+ "create-effect-motion": "./dist/bin.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "templates"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.build.json",
31
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
32
+ "test": "vitest run",
33
+ "check": "tsc --noEmit"
34
+ },
35
+ "dependencies": {
36
+ "@effect/platform-node": "4.0.0-rc.115",
37
+ "effect": "4.0.0-rc.115"
38
+ },
39
+ "devDependencies": {
40
+ "@biomejs/biome": "^2.5.3",
41
+ "@types/node": "^26.1.1",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^4.1.10"
44
+ }
45
+ }
@@ -4,14 +4,15 @@ This is an [effect-motion](https://github.com/julia-script/effect-motion) projec
4
4
 
5
5
  ## Layout and commands
6
6
 
7
- - `src/scenes/*.ts` — one scene per module, each exporting `scene`. Any file here is previewable without registration.
7
+ - `src/scenes/*.ts` — one scene per module, each exporting `scene`. `Scene.make("Display Name", gen, meta?)` optionally names a scene for the studio picker.
8
8
  - `src/main.ts` — the movie: an ordinary scene that sequences the others (`Scene.play` + `handle.finished`). Nothing is special about it.
9
- - `motion.config.ts` — render targets. Output is always `<output>/<name>.mp4`; never write output paths by hand.
9
+ - `studio.ts` — the studio registration: `studioConfig({ scenes, layers })`. Record keys are unique identifiers; ONLY registered scenes appear in the picker, so add an import + entry for every new scene. Scenes with typed resources (fonts, images) need their loaders in `layers` — the file will not compile until every registered scene is covered.
10
+ - `render.ts` — an ordinary program default-exporting a `Video.render(...)` effect. More outputs are more calls; loader layers are provided here with `Effect.provide` (compile-checked). Knobs (paths, fps, seed) live in this code — there are no CLI flags.
10
11
  - `src/assets/` — static files (images, fonts).
11
- - `motion studio` — browser preview with hot reload (scene picker lists config targets plus unregistered scenes).
12
- - `motion render [name...]` — render targets; `motion render ./src/scenes/foo.ts` renders one file with defaults. Flags beat config beat library defaults. `--verbose` prints full error cause chains.
12
+ - `motion studio [file]` — browser preview with hot reload of `studio.ts` (or the given entrypoint).
13
+ - `motion render [file]` — execute `render.ts` (or the given entrypoint) with the platform provided. `--verbose` prints full error cause chains. The same file runs standalone via `tsx render.ts` by piping through `NodeServices` from `@effect/platform-node`.
13
14
 
14
- Verify a scene change by rendering it (`motion render <target>`) or checking it in the running studio — not by reading code alone.
15
+ Verify a scene change by rendering it (`motion render`) or checking it in the running studio — not by reading code alone.
15
16
 
16
17
  ## Writing scenes
17
18
 
@@ -39,21 +40,26 @@ export const scene = Scene.make(function* () {
39
40
 
40
41
  - **Never** use `Math.random()`, `Date.now()`, or any wall-clock/OS state in a scene — every run must be byte-identical. Use the provided seeded random (`Effect.random`, seeded from `settings.seed`).
41
42
  - Durations land exactly on target on the final frame; springs snap on settle. Don't add "fudge" frames.
42
- - Scene coordinates are the `settings.width`/`height` of the target that renders them (this template: 1920×1080). `dpr` scales output pixels, not coordinates.
43
+ - Scene coordinates are the scene's OWN comp config — `Scene.make(gen, { width, height, backgroundColor })` (this template: 1920×1080). `dpr` (a `Video.render` option) scales output pixels, not coordinates.
43
44
  - The `effect` dependency is pinned **exactly** — upgrading it can change seeded-random sequences. Never bump it casually; upgrade `effect` and `effect-motion` together, deliberately.
44
45
 
45
- ## Config
46
+ ## Entrypoints
46
47
 
47
48
  ```ts
48
- export default defineConfig({
49
- targets: [{
50
- name: "intro", // unique — doubles as the output basename
51
- scene: "./src/scenes/intro.ts",
52
- settings: { width: 1920, height: 1080, frameRate: 60, dpr: 1 },
53
- output: "./output", // a DIRECTORY; file name is derived
54
- // frames: 600 // REQUIRED if the scene is infinite
55
- }],
49
+ // studio.ts — what the studio previews
50
+ export default studioConfig({
51
+ scenes: {
52
+ intro, // key = identifier, label = scene name ?? key
53
+ fancy: { scene: fancy, fps: 30 }, // per-entry player options
54
+ },
55
+ // layers: Layer.mergeAll(Font.layer(...), …) // REQUIRED once a scene declares resources
56
+ });
57
+
58
+ // render.ts — what `motion render` executes
59
+ export default Effect.gen(function* () {
60
+ yield* Video.render(intro, "./output/intro.mp4", { settings: { frameRate: 60 } });
61
+ // yield* Video.render(intro, "./output/intro-hd.mp4", { dpr: 2 }); // more outputs = more calls
56
62
  });
57
63
  ```
58
64
 
59
- A scene used by several targets renders once per target (e.g. different resolutions). An infinite scene (one that never finishes) must set `frames`, or rendering would never end.
65
+ Render the same scene several times for variants (resolutions, dpr). An infinite scene (one that never finishes) must pass `frames` in its `Video.render` options, or rendering would never end.
@@ -0,0 +1,14 @@
1
+ import { Video } from "@effect-motion/export";
2
+ import * as Effect from "effect/Effect";
3
+ import { scene as main } from "./src/main";
4
+
5
+ // An ordinary program: `motion render` executes this default export with
6
+ // the platform provided (it also runs standalone via `tsx render.ts` by
7
+ // piping through NodeServices from @effect/platform-node). More outputs
8
+ // are more Video.render calls; scenes with typed resources provide their
9
+ // loader layers right here with Effect.provide — checked at compile time.
10
+ export default Effect.gen(function* () {
11
+ yield* Video.render(main, "./output/main.mp4", {
12
+ settings: { frameRate: 60 },
13
+ });
14
+ });
@@ -2,8 +2,8 @@ import { Scene } from "effect-motion";
2
2
  import { scene as helloWorld } from "./scenes/hello-world";
3
3
 
4
4
  // The movie: an ordinary scene that sequences the scenes in src/scenes.
5
- // Nothing is special about this file — it is one more target in
6
- // motion.config.ts. Add scenes and chain them here.
5
+ // Nothing is special about this file — studio.ts registers it and
6
+ // render.ts renders it like any other scene. Add scenes and chain them here.
7
7
  export const scene = Scene.make(function* () {
8
8
  const hello = yield* Scene.play(helloWorld);
9
9
  yield* hello.finished;
@@ -1,15 +1,17 @@
1
1
  import { Color, Motion, Scene, Shapes } from "effect-motion";
2
2
 
3
3
  // A scene is a generator: instantiate entities, then yield animations.
4
- // Preview it with `motion studio`, render it with `motion render`.
5
- export const scene = Scene.make(function* () {
4
+ // The leading string is a DISPLAY name (the studio picker label); the
5
+ // studio.ts record key is its identifier. Preview with `motion studio`,
6
+ // render with `motion render`.
7
+ export const scene = Scene.make("Hello World", function* () {
6
8
  const circle = yield* Scene.instantiate(Shapes.Circle, {
7
- x: 300,
8
- y: 540,
9
+ x: -660,
10
+ y: 0,
9
11
  radius: 80,
10
12
  fill: Color.hex("#7f5af0"),
11
13
  });
12
14
 
13
- yield* Motion.tweenTo(circle, { x: 1620 }, "1200 millis", "easeInOutCubic");
15
+ yield* Motion.tweenTo(circle, { x: 660 }, "1200 millis", "easeInOutCubic");
14
16
  yield* Motion.fadeTo(circle, 0, "400 millis");
15
17
  });
@@ -0,0 +1,16 @@
1
+ import { studioConfig } from "@effect-motion/cli";
2
+ import { scene as main } from "./src/main";
3
+ import { scene as helloWorld } from "./src/scenes/hello-world";
4
+
5
+ // The studio's registration: keys are the picker's unique identifiers
6
+ // (labels come from Scene.make's display name when set). Register every
7
+ // scene you want to preview — `motion studio` serves exactly this record.
8
+ // When a scene declares typed resources (fonts, images), provide their
9
+ // loaders here as one `layers` (Layer.mergeAll(Font.layer(...), ...)) —
10
+ // the file will not compile until every registered scene is covered.
11
+ export default studioConfig({
12
+ scenes: {
13
+ "hello-world": helloWorld,
14
+ main,
15
+ },
16
+ });
@@ -11,5 +11,5 @@
11
11
  "lib": ["ES2022"],
12
12
  "types": ["node"]
13
13
  },
14
- "include": ["src", "motion.config.ts"]
14
+ "include": ["src", "studio.ts", "render.ts"]
15
15
  }
@@ -1,20 +0,0 @@
1
- import { defineConfig } from "@effect-motion/cli";
2
-
3
- // Each target is one rendered video: <output>/<name>.mp4
4
- // `motion render` renders all of them; `motion render <name>` picks one.
5
- export default defineConfig({
6
- targets: [
7
- {
8
- name: "hello-world",
9
- scene: "./src/scenes/hello-world.ts",
10
- settings: { width: 1920, height: 1080, frameRate: 60 },
11
- output: "./output",
12
- },
13
- {
14
- name: "main",
15
- scene: "./src/main.ts",
16
- settings: { width: 1920, height: 1080, frameRate: 60 },
17
- output: "./output",
18
- },
19
- ],
20
- });