@effect-motion/cli 0.3.2 → 0.5.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 +3 -21
- package/dist/MotionCliError.d.ts +1 -1
- package/dist/StudioConfig.d.ts +71 -0
- package/dist/StudioConfig.js +76 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -4
- package/dist/commands/render.d.ts +2 -12
- package/dist/commands/render.js +44 -142
- package/dist/commands/studio.d.ts +1 -1
- package/dist/commands/studio.js +57 -45
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/package.json +5 -6
- package/studio-app/App.tsx +80 -158
- package/studio-app/StudioConfig.ts +5 -0
- package/studio-app/entry.ts +4 -0
- package/dist/Config.d.ts +0 -78
- package/dist/Config.js +0 -67
- package/dist/ConfigLoader.d.ts +0 -15
- package/dist/ConfigLoader.js +0 -58
- package/dist/commands/init.d.ts +0 -9
- package/dist/commands/init.js +0 -94
- package/dist/pins.d.ts +0 -23
- package/dist/pins.js +0 -23
- package/dist/scaffold.d.ts +0 -20
- package/dist/scaffold.js +0 -86
- package/studio-app/project.ts +0 -3
- package/templates/default/AGENTS.md +0 -59
- package/templates/default/_gitignore +0 -3
- package/templates/default/motion.config.ts +0 -20
- package/templates/default/src/assets/.gitkeep +0 -0
- package/templates/default/src/main.ts +0 -12
- package/templates/default/src/scenes/hello-world.ts +0 -15
- package/templates/default/tsconfig.json +0 -15
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-motion/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command line for effect-motion:
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Command line for effect-motion: preview scenes with hot reload (studio) and render videos from motion.config.ts (render). Scaffold new projects with `pnpm create effect-motion`",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
|
-
"templates",
|
|
35
34
|
"studio-app"
|
|
36
35
|
],
|
|
37
36
|
"publishConfig": {
|
|
@@ -41,8 +40,8 @@
|
|
|
41
40
|
"@effect/platform-node": "4.0.0-beta.98",
|
|
42
41
|
"effect": "4.0.0-beta.98",
|
|
43
42
|
"vite": "^7.0.0",
|
|
44
|
-
"@effect-motion/export": "^0.
|
|
45
|
-
"effect-motion": "^0.
|
|
43
|
+
"@effect-motion/export": "^0.5.0",
|
|
44
|
+
"effect-motion": "^0.5.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@types/node": "^26.1.1",
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
"react-dom": "^19.2.0",
|
|
53
52
|
"typescript": "^7.0.2",
|
|
54
53
|
"vitest": "^4.1.10",
|
|
55
|
-
"@effect-motion/react": "^0.
|
|
54
|
+
"@effect-motion/react": "^0.5.0"
|
|
56
55
|
},
|
|
57
56
|
"scripts": {
|
|
58
57
|
"build": "tsc -p tsconfig.build.json",
|
package/studio-app/App.tsx
CHANGED
|
@@ -1,146 +1,68 @@
|
|
|
1
1
|
import { Player, type PlayerProps } from "@effect-motion/react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
// generated by `motion studio`: statically re-exports the user's studio.ts,
|
|
4
|
+
// which places the entire scene graph in Vite's module graph (ordinary HMR
|
|
5
|
+
// covers scene edits/additions/removals — no bespoke watchers)
|
|
6
|
+
import studioModule, { studioPath } from "./entry";
|
|
7
|
+
// the CLI's own contract module, copied beside the app at prepare time —
|
|
8
|
+
// one brand, one entry-normalization implementation, shared with `motion`
|
|
9
|
+
import {
|
|
10
|
+
type ResolvedEntry,
|
|
11
|
+
resolveEntries,
|
|
12
|
+
type StudioConfig,
|
|
13
|
+
StudioConfigTypeId,
|
|
14
|
+
} from "./StudioConfig";
|
|
4
15
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type TargetLike = {
|
|
9
|
-
readonly name?: string;
|
|
10
|
-
readonly scene?: string;
|
|
11
|
-
readonly settings?: Record<string, unknown>;
|
|
12
|
-
};
|
|
13
|
-
type ConfigLike = { readonly targets?: ReadonlyArray<TargetLike> };
|
|
14
|
-
|
|
15
|
-
type SceneEntry = {
|
|
16
|
-
/** picker identity: `target:<name>` for config targets, `file:<path>` otherwise */
|
|
17
|
-
readonly key: string;
|
|
18
|
-
readonly label: string;
|
|
19
|
-
/** project-relative module path */
|
|
20
|
-
readonly path: string;
|
|
21
|
-
readonly registered: boolean;
|
|
22
|
-
readonly settings: PlayerProps["settings"] | undefined;
|
|
23
|
-
readonly load: () => Promise<unknown>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// every scene in src/scenes is previewable without registration; vite keeps
|
|
27
|
-
// this list current as files appear and disappear
|
|
28
|
-
const globbed = import.meta.glob("../../src/scenes/*.ts");
|
|
29
|
-
|
|
30
|
-
const normalize = (p: string) => p.replace(/^\.\//, "").replace(/^\//, "");
|
|
31
|
-
const globKey = (p: string) => normalize(p.replace(/^(\.\.\/)+/, ""));
|
|
32
|
-
const fileLabel = (key: string) =>
|
|
33
|
-
key
|
|
34
|
-
.split("/")
|
|
35
|
-
.at(-1)
|
|
36
|
-
?.replace(/\.(ts|tsx|mts|js|mjs)$/, "") ?? key;
|
|
16
|
+
// selection survives the full-page reloads that scene edits trigger
|
|
17
|
+
const selectionFromHash = () =>
|
|
18
|
+
decodeURIComponent(window.location.hash.slice(1)) || null;
|
|
37
19
|
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
type Resolved =
|
|
21
|
+
| { readonly _tag: "ready"; readonly entries: ReadonlyArray<ResolvedEntry> }
|
|
22
|
+
| { readonly _tag: "error"; readonly message: string };
|
|
40
23
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const path = normalize(target.scene);
|
|
53
|
-
referenced.add(path);
|
|
54
|
-
entries.push({
|
|
55
|
-
key: `target:${target.name}`,
|
|
56
|
-
label: target.name,
|
|
57
|
-
path,
|
|
58
|
-
registered: true,
|
|
59
|
-
// a registered scene previews with its target settings so the
|
|
60
|
-
// preview aspect matches the export
|
|
61
|
-
settings: target.settings as PlayerProps["settings"],
|
|
62
|
-
load: globLoaders.get(path) ?? loadByAbsolutePath(path),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
for (const [path, load] of globLoaders) {
|
|
66
|
-
if (referenced.has(path)) continue;
|
|
67
|
-
entries.push({
|
|
68
|
-
key: `file:${path}`,
|
|
69
|
-
label: fileLabel(path),
|
|
70
|
-
path,
|
|
71
|
-
registered: false,
|
|
72
|
-
settings: undefined,
|
|
73
|
-
load,
|
|
74
|
-
});
|
|
24
|
+
const resolve = (): Resolved => {
|
|
25
|
+
try {
|
|
26
|
+
return {
|
|
27
|
+
_tag: "ready",
|
|
28
|
+
entries: resolveEntries(studioModule, studioPath),
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
_tag: "error",
|
|
33
|
+
message: error instanceof Error ? error.message : String(error),
|
|
34
|
+
};
|
|
75
35
|
}
|
|
76
|
-
return entries.sort((a, b) => a.label.localeCompare(b.label));
|
|
77
36
|
};
|
|
78
37
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
readonly _tag: "ready";
|
|
89
|
-
readonly key: string;
|
|
90
|
-
readonly scene: PlayerProps["scene"];
|
|
91
|
-
};
|
|
38
|
+
// the config's layers cover the union of every registered scene's
|
|
39
|
+
// resources (typed in the user's studio.ts); the dynamic boundary here is
|
|
40
|
+
// backed by that authoring-time check
|
|
41
|
+
const configLayers =
|
|
42
|
+
typeof studioModule === "object" &&
|
|
43
|
+
studioModule !== null &&
|
|
44
|
+
StudioConfigTypeId in studioModule
|
|
45
|
+
? (studioModule as StudioConfig).layers
|
|
46
|
+
: undefined;
|
|
92
47
|
|
|
93
48
|
export const App = () => {
|
|
94
|
-
const [config, setConfig] = useState<ConfigLike | null>(null);
|
|
95
|
-
const [configError, setConfigError] = useState<string | null>(null);
|
|
96
49
|
const [selected, setSelected] = useState<string | null>(selectionFromHash);
|
|
97
|
-
const
|
|
50
|
+
const resolved = useMemo(resolve, []);
|
|
98
51
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
52
|
+
if (resolved._tag === "error") {
|
|
53
|
+
return (
|
|
54
|
+
<main className="studio-main">
|
|
55
|
+
<div className="studio-error">
|
|
56
|
+
<h2>Studio entrypoint failed to load</h2>
|
|
57
|
+
<pre>{resolved.message}</pre>
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
105
60
|
);
|
|
106
|
-
}
|
|
61
|
+
}
|
|
107
62
|
|
|
108
|
-
const entries =
|
|
63
|
+
const entries = resolved.entries;
|
|
109
64
|
const entry = entries.find((e) => e.key === selected) ?? entries[0];
|
|
110
65
|
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
if (entry === undefined) return;
|
|
113
|
-
let cancelled = false;
|
|
114
|
-
setState({ _tag: "loading" });
|
|
115
|
-
setConfigError(null);
|
|
116
|
-
entry.load().then(
|
|
117
|
-
(module_) => {
|
|
118
|
-
if (cancelled) return;
|
|
119
|
-
const scene = (module_ as { scene?: PlayerProps["scene"] }).scene;
|
|
120
|
-
if (scene === undefined) {
|
|
121
|
-
setState({
|
|
122
|
-
_tag: "error",
|
|
123
|
-
key: entry.key,
|
|
124
|
-
message: `${entry.path} has no \`scene\` export`,
|
|
125
|
-
});
|
|
126
|
-
} else {
|
|
127
|
-
setState({ _tag: "ready", key: entry.key, scene });
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
(error) => {
|
|
131
|
-
if (cancelled) return;
|
|
132
|
-
setState({
|
|
133
|
-
_tag: "error",
|
|
134
|
-
key: entry.key,
|
|
135
|
-
message: `failed to load ${entry.path}\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
136
|
-
});
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
return () => {
|
|
140
|
-
cancelled = true;
|
|
141
|
-
};
|
|
142
|
-
}, [entry]);
|
|
143
|
-
|
|
144
66
|
return (
|
|
145
67
|
<>
|
|
146
68
|
<nav className="studio-sidebar">
|
|
@@ -157,40 +79,40 @@ export const App = () => {
|
|
|
157
79
|
}}
|
|
158
80
|
>
|
|
159
81
|
{e.label}
|
|
160
|
-
|
|
82
|
+
{e.label !== e.key && <small>{e.key}</small>}
|
|
161
83
|
</button>
|
|
162
84
|
))}
|
|
163
|
-
{entries.length === 0 && (
|
|
164
|
-
<p className="studio-empty">
|
|
165
|
-
No scenes found — add one in src/scenes/
|
|
166
|
-
</p>
|
|
167
|
-
)}
|
|
168
|
-
{configError !== null && <p className="studio-empty">{configError}</p>}
|
|
169
85
|
</nav>
|
|
170
86
|
<main className="studio-main">
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
87
|
+
{entry !== undefined &&
|
|
88
|
+
(() => {
|
|
89
|
+
// options.fps is a preview override of the scene's own rate,
|
|
90
|
+
// so it lands in settings.frameRate (the Player's one clock,
|
|
91
|
+
// which wins over its fps prop); everything else is a plain
|
|
92
|
+
// Player prop and wins over the studio defaults
|
|
93
|
+
const {
|
|
94
|
+
fps,
|
|
95
|
+
settings: entrySettings,
|
|
96
|
+
...playerProps
|
|
97
|
+
} = entry.options;
|
|
98
|
+
const settings =
|
|
99
|
+
fps !== undefined
|
|
100
|
+
? { ...entrySettings, frameRate: fps }
|
|
101
|
+
: entrySettings;
|
|
102
|
+
return (
|
|
103
|
+
<Player
|
|
104
|
+
key={entry.key}
|
|
105
|
+
scene={entry.scene as PlayerProps["scene"] as never}
|
|
106
|
+
autoPlay
|
|
107
|
+
defaultRepeatMode
|
|
108
|
+
{...(settings !== undefined ? { settings } : {})}
|
|
109
|
+
{...(configLayers !== undefined
|
|
110
|
+
? { renderLayers: configLayers as never }
|
|
111
|
+
: {})}
|
|
112
|
+
{...playerProps}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
})()}
|
|
194
116
|
</main>
|
|
195
117
|
</>
|
|
196
118
|
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// In-repo typecheck stub: re-exports the CLI's contract module. At prepare
|
|
2
|
+
// time this file is NOT copied — the built dist/StudioConfig.js is placed
|
|
3
|
+
// next to the app instead, so the app and CLI share one implementation
|
|
4
|
+
// resolved against the user's project node_modules.
|
|
5
|
+
export * from "../src/StudioConfig.js";
|
package/dist/Config.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type * as Runner from "effect-motion/Runner";
|
|
2
|
-
/**
|
|
3
|
-
* The `motion.config.ts` contract. This module is intentionally
|
|
4
|
-
* browser-safe (the studio app imports the user's config directly), so it
|
|
5
|
-
* must stay free of Node-only imports — loading/discovery live in
|
|
6
|
-
* ConfigLoader.ts.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Per-target render settings: the Runner `Settings` subset an export can
|
|
10
|
-
* honor, plus `dpr`. From the author's perspective dpr is a rendering
|
|
11
|
-
* setting like width/height; the render command maps it to the export
|
|
12
|
-
* package's supersampling option.
|
|
13
|
-
*/
|
|
14
|
-
export interface TargetSettings {
|
|
15
|
-
readonly width?: number;
|
|
16
|
-
readonly height?: number;
|
|
17
|
-
readonly frameRate?: number;
|
|
18
|
-
readonly seed?: Runner.Seed;
|
|
19
|
-
readonly maxFrames?: number;
|
|
20
|
-
readonly backgroundColor?: Runner.Settings["backgroundColor"];
|
|
21
|
-
readonly dpr?: number;
|
|
22
|
-
}
|
|
23
|
-
/** v1 ships MP4 only; the field exists so more containers slot in later. */
|
|
24
|
-
export type OutputFormat = "mp4";
|
|
25
|
-
export interface MotionTarget {
|
|
26
|
-
/** Unique per config — doubles as the output file basename. */
|
|
27
|
-
readonly name: string;
|
|
28
|
-
/** Path to a module exporting `scene`, relative to the config file. */
|
|
29
|
-
readonly scene: string;
|
|
30
|
-
readonly settings?: TargetSettings;
|
|
31
|
-
/** Output DIRECTORY (never a file), relative to the config file. */
|
|
32
|
-
readonly output?: string;
|
|
33
|
-
readonly format?: OutputFormat;
|
|
34
|
-
/** Frame cap — required in practice for an infinite scene. */
|
|
35
|
-
readonly frames?: number;
|
|
36
|
-
}
|
|
37
|
-
export interface MotionConfig {
|
|
38
|
-
readonly targets: ReadonlyArray<MotionTarget>;
|
|
39
|
-
}
|
|
40
|
-
/** Identity helper that types `motion.config.ts` (vite/vitest convention). */
|
|
41
|
-
export declare const defineConfig: (config: MotionConfig) => MotionConfig;
|
|
42
|
-
export declare const DEFAULT_OUTPUT_DIR = "./output";
|
|
43
|
-
export declare const DEFAULT_FORMAT: OutputFormat;
|
|
44
|
-
/**
|
|
45
|
-
* Validate the default export of a loaded config module. Plain structural
|
|
46
|
-
* checks (not Schema): the config is authored in TS against `MotionConfig`,
|
|
47
|
-
* so this only guards the untyped escape hatches (JS configs, `as any`).
|
|
48
|
-
*/
|
|
49
|
-
export declare const validateConfig: (value: unknown, configPath: string) => MotionConfig;
|
|
50
|
-
/** Flag values a `motion render` invocation can lay over a target. */
|
|
51
|
-
export interface RenderOverrides {
|
|
52
|
-
readonly width?: number;
|
|
53
|
-
readonly height?: number;
|
|
54
|
-
readonly frameRate?: number;
|
|
55
|
-
readonly dpr?: number;
|
|
56
|
-
readonly seed?: Runner.Seed;
|
|
57
|
-
readonly maxFrames?: number;
|
|
58
|
-
readonly frames?: number;
|
|
59
|
-
readonly outDir?: string;
|
|
60
|
-
readonly format?: OutputFormat;
|
|
61
|
-
}
|
|
62
|
-
/** A target with overrides applied and output location derived. */
|
|
63
|
-
export interface ResolvedTarget {
|
|
64
|
-
readonly name: string;
|
|
65
|
-
readonly scene: string;
|
|
66
|
-
readonly settings: TargetSettings;
|
|
67
|
-
readonly frames: number | undefined;
|
|
68
|
-
/** Output directory (still config-relative — the caller resolves). */
|
|
69
|
-
readonly outDir: string;
|
|
70
|
-
/** Derived file name: `<name>.<format>` — never user-specified. */
|
|
71
|
-
readonly fileName: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Precedence, highest wins: CLI flags → target config → library defaults.
|
|
75
|
-
* Library defaults are NOT materialized here — leaving fields undefined
|
|
76
|
-
* lets the Runner's own defaults apply, so there is one source of truth.
|
|
77
|
-
*/
|
|
78
|
-
export declare const resolveTarget: (target: MotionTarget, overrides?: RenderOverrides) => ResolvedTarget;
|
package/dist/Config.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { MotionCliError } from "./MotionCliError.js";
|
|
2
|
-
/** Identity helper that types `motion.config.ts` (vite/vitest convention). */
|
|
3
|
-
export const defineConfig = (config) => config;
|
|
4
|
-
export const DEFAULT_OUTPUT_DIR = "./output";
|
|
5
|
-
export const DEFAULT_FORMAT = "mp4";
|
|
6
|
-
/**
|
|
7
|
-
* Validate the default export of a loaded config module. Plain structural
|
|
8
|
-
* checks (not Schema): the config is authored in TS against `MotionConfig`,
|
|
9
|
-
* so this only guards the untyped escape hatches (JS configs, `as any`).
|
|
10
|
-
*/
|
|
11
|
-
export const validateConfig = (value, configPath) => {
|
|
12
|
-
const fail = (problem) => {
|
|
13
|
-
throw new MotionCliError({
|
|
14
|
-
reason: "ConfigInvalid",
|
|
15
|
-
message: `${configPath}: ${problem}`,
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
if (typeof value !== "object" || value === null) {
|
|
19
|
-
return fail("default export is not a config object (did you forget `export default defineConfig({...})`?)");
|
|
20
|
-
}
|
|
21
|
-
const config = value;
|
|
22
|
-
if (!Array.isArray(config.targets)) {
|
|
23
|
-
return fail("config has no `targets` array");
|
|
24
|
-
}
|
|
25
|
-
const seen = new Set();
|
|
26
|
-
for (const [i, target] of config.targets.entries()) {
|
|
27
|
-
if (typeof target !== "object" || target === null) {
|
|
28
|
-
return fail(`targets[${i}] is not an object`);
|
|
29
|
-
}
|
|
30
|
-
if (typeof target.name !== "string" || target.name.length === 0) {
|
|
31
|
-
return fail(`targets[${i}] is missing a \`name\``);
|
|
32
|
-
}
|
|
33
|
-
if (typeof target.scene !== "string" || target.scene.length === 0) {
|
|
34
|
-
return fail(`target "${target.name}" is missing a \`scene\` path`);
|
|
35
|
-
}
|
|
36
|
-
if (seen.has(target.name)) {
|
|
37
|
-
return fail(`duplicate target name "${target.name}" (names double as output filenames, so they must be unique)`);
|
|
38
|
-
}
|
|
39
|
-
seen.add(target.name);
|
|
40
|
-
if (target.format !== undefined && target.format !== "mp4") {
|
|
41
|
-
return fail(`target "${target.name}" has unsupported format "${target.format}" (v1 supports "mp4")`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return config;
|
|
45
|
-
};
|
|
46
|
-
const definedEntries = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
47
|
-
/**
|
|
48
|
-
* Precedence, highest wins: CLI flags → target config → library defaults.
|
|
49
|
-
* Library defaults are NOT materialized here — leaving fields undefined
|
|
50
|
-
* lets the Runner's own defaults apply, so there is one source of truth.
|
|
51
|
-
*/
|
|
52
|
-
export const resolveTarget = (target, overrides = {}) => {
|
|
53
|
-
const { frames: overrideFrames, outDir, format, ...settingsOverrides } = overrides;
|
|
54
|
-
const settings = {
|
|
55
|
-
...definedEntries(target.settings ?? {}),
|
|
56
|
-
...definedEntries(settingsOverrides),
|
|
57
|
-
};
|
|
58
|
-
const resolvedFormat = format ?? target.format ?? DEFAULT_FORMAT;
|
|
59
|
-
return {
|
|
60
|
-
name: target.name,
|
|
61
|
-
scene: target.scene,
|
|
62
|
-
settings,
|
|
63
|
-
frames: overrideFrames ?? target.frames,
|
|
64
|
-
outDir: outDir ?? target.output ?? DEFAULT_OUTPUT_DIR,
|
|
65
|
-
fileName: `${target.name}.${resolvedFormat}`,
|
|
66
|
-
};
|
|
67
|
-
};
|
package/dist/ConfigLoader.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
|
-
import { FileSystem } from "effect/FileSystem";
|
|
3
|
-
import { Path } from "effect/Path";
|
|
4
|
-
import { type MotionConfig } from "./Config.js";
|
|
5
|
-
import { MotionCliError } from "./MotionCliError.js";
|
|
6
|
-
import type { ViteLoader } from "./ViteLoader.js";
|
|
7
|
-
export declare const CONFIG_FILE = "motion.config.ts";
|
|
8
|
-
/**
|
|
9
|
-
* Resolve the config path: an explicit `--config` wins; otherwise walk up
|
|
10
|
-
* from `cwd` to the nearest motion.config.ts (tsc `-p` semantics). Fails
|
|
11
|
-
* with ConfigNotFound naming both escape hatches.
|
|
12
|
-
*/
|
|
13
|
-
export declare const findConfig: (cwd: string, explicit?: string) => Effect.Effect<string, MotionCliError, FileSystem | Path>;
|
|
14
|
-
/** Load + validate a config module through the shared Vite loader. */
|
|
15
|
-
export declare const loadConfig: (loader: ViteLoader, configPath: string) => Effect.Effect<MotionConfig, MotionCliError>;
|
package/dist/ConfigLoader.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect";
|
|
2
|
-
import { FileSystem } from "effect/FileSystem";
|
|
3
|
-
import { Path } from "effect/Path";
|
|
4
|
-
import { validateConfig } from "./Config.js";
|
|
5
|
-
import { MotionCliError } from "./MotionCliError.js";
|
|
6
|
-
export const CONFIG_FILE = "motion.config.ts";
|
|
7
|
-
/**
|
|
8
|
-
* Resolve the config path: an explicit `--config` wins; otherwise walk up
|
|
9
|
-
* from `cwd` to the nearest motion.config.ts (tsc `-p` semantics). Fails
|
|
10
|
-
* with ConfigNotFound naming both escape hatches.
|
|
11
|
-
*/
|
|
12
|
-
export const findConfig = (cwd, explicit) => Effect.gen(function* () {
|
|
13
|
-
const fs = yield* FileSystem;
|
|
14
|
-
const path = yield* Path;
|
|
15
|
-
if (explicit !== undefined) {
|
|
16
|
-
const resolved = path.resolve(cwd, explicit);
|
|
17
|
-
const exists = yield* orFalse(fs.exists(resolved));
|
|
18
|
-
if (!exists) {
|
|
19
|
-
return yield* new MotionCliError({
|
|
20
|
-
reason: "ConfigNotFound",
|
|
21
|
-
message: `config file not found: ${resolved}`,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
return resolved;
|
|
25
|
-
}
|
|
26
|
-
let dir = path.resolve(cwd);
|
|
27
|
-
while (true) {
|
|
28
|
-
const candidate = path.join(dir, CONFIG_FILE);
|
|
29
|
-
if (yield* orFalse(fs.exists(candidate)))
|
|
30
|
-
return candidate;
|
|
31
|
-
const parent = path.dirname(dir);
|
|
32
|
-
if (parent === dir)
|
|
33
|
-
break;
|
|
34
|
-
dir = parent;
|
|
35
|
-
}
|
|
36
|
-
return yield* new MotionCliError({
|
|
37
|
-
reason: "ConfigNotFound",
|
|
38
|
-
message: `no ${CONFIG_FILE} found from ${cwd} upward — ` +
|
|
39
|
-
`create one (export default defineConfig({ targets: [...] })), pass --config <path>, ` +
|
|
40
|
-
`or pass a scene file directly (motion render ./src/scenes/foo.ts)`,
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
// fs.exists fails on permission errors etc. — treat those as "not here"
|
|
44
|
-
const orFalse = (effect) => Effect.catchCause(effect, () => Effect.succeed(false));
|
|
45
|
-
/** Load + validate a config module through the shared Vite loader. */
|
|
46
|
-
export const loadConfig = (loader, configPath) => Effect.gen(function* () {
|
|
47
|
-
const module_ = yield* loader.load(configPath);
|
|
48
|
-
return yield* Effect.try({
|
|
49
|
-
try: () => validateConfig(module_.default, configPath),
|
|
50
|
-
catch: (error) => error instanceof MotionCliError
|
|
51
|
-
? error
|
|
52
|
-
: new MotionCliError({
|
|
53
|
-
reason: "ConfigInvalid",
|
|
54
|
-
message: `${configPath}: config validation crashed`,
|
|
55
|
-
cause: error,
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
});
|
package/dist/commands/init.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import * as Option from "effect/Option";
|
|
2
|
-
import { Command, Prompt } from "effect/unstable/cli";
|
|
3
|
-
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
|
-
import { MotionCliError } from "../MotionCliError.js";
|
|
5
|
-
export declare const initCommand: Command.Command<"init", {
|
|
6
|
-
readonly directory: Option.Option<string>;
|
|
7
|
-
readonly pm: Option.Option<"bun" | "npm" | "pnpm" | "yarn">;
|
|
8
|
-
readonly noInstall: boolean;
|
|
9
|
-
}, {}, MotionCliError, ChildProcessSpawner.ChildProcessSpawner | Prompt.Environment>;
|
package/dist/commands/init.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import * as Console from "effect/Console";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as Option from "effect/Option";
|
|
4
|
-
import { Path } from "effect/Path";
|
|
5
|
-
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
6
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
7
|
-
import { MotionCliError } from "../MotionCliError.js";
|
|
8
|
-
import { ensureEmptyDir, resolveProjectDir, scaffoldProject, } from "../scaffold.js";
|
|
9
|
-
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
10
|
-
/** The manager that invoked us (`pnpm create …` etc.), if detectable. */
|
|
11
|
-
const detectPackageManager = () => {
|
|
12
|
-
const agent = process.env.npm_config_user_agent ?? "";
|
|
13
|
-
return PACKAGE_MANAGERS.find((pm) => agent.startsWith(pm));
|
|
14
|
-
};
|
|
15
|
-
const initFlags = {
|
|
16
|
-
directory: Argument.string("directory").pipe(Argument.withDescription('Target directory ("." scaffolds into the current directory)'), Argument.optional),
|
|
17
|
-
pm: Flag.optional(Flag.choice("pm", PACKAGE_MANAGERS).pipe(Flag.withDescription("Package manager (skips the prompt)"))),
|
|
18
|
-
noInstall: Flag.boolean("no-install").pipe(Flag.withDescription("Skip dependency installation")),
|
|
19
|
-
};
|
|
20
|
-
const promptDirectory = Prompt.text({
|
|
21
|
-
message: 'Where should the project be created? ("." for the current directory)',
|
|
22
|
-
default: "my-motion-project",
|
|
23
|
-
});
|
|
24
|
-
const promptPackageManager = Effect.suspend(() => {
|
|
25
|
-
const detected = detectPackageManager();
|
|
26
|
-
// detected manager listed first so plain Enter picks it
|
|
27
|
-
const ordered = [
|
|
28
|
-
...(detected ? [detected] : []),
|
|
29
|
-
...PACKAGE_MANAGERS.filter((pm) => pm !== detected),
|
|
30
|
-
];
|
|
31
|
-
return Prompt.select({
|
|
32
|
-
message: "Which package manager?",
|
|
33
|
-
choices: ordered.map((pm) => ({ title: pm, value: pm })),
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
const runInstall = (pm, dir) => Effect.gen(function* () {
|
|
37
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
38
|
-
const command = ChildProcess.make(pm, ["install"], {
|
|
39
|
-
cwd: dir,
|
|
40
|
-
stdin: "inherit",
|
|
41
|
-
stdout: "inherit",
|
|
42
|
-
stderr: "inherit",
|
|
43
|
-
});
|
|
44
|
-
yield* Effect.scoped(Effect.gen(function* () {
|
|
45
|
-
const handle = yield* spawner.spawn(command);
|
|
46
|
-
const code = yield* handle.exitCode;
|
|
47
|
-
if (code !== 0) {
|
|
48
|
-
return yield* new MotionCliError({
|
|
49
|
-
reason: "InstallFailed",
|
|
50
|
-
message: `${pm} install exited with code ${code} in ${dir} — run it manually`,
|
|
51
|
-
cause: code,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
})).pipe(Effect.catchTag("PlatformError", (cause) => Effect.fail(new MotionCliError({
|
|
55
|
-
reason: "InstallFailed",
|
|
56
|
-
message: `could not run "${pm} install" in ${dir} — is ${pm} installed?`,
|
|
57
|
-
cause,
|
|
58
|
-
}))));
|
|
59
|
-
});
|
|
60
|
-
const handler = (input) => Effect.gen(function* () {
|
|
61
|
-
const path = yield* Path;
|
|
62
|
-
const cwd = process.cwd();
|
|
63
|
-
const dirInput = Option.getOrUndefined(input.directory) ?? (yield* promptDirectory);
|
|
64
|
-
const { dir, name } = resolveProjectDir(path, cwd, dirInput);
|
|
65
|
-
yield* ensureEmptyDir(dir);
|
|
66
|
-
const pm = Option.getOrUndefined(input.pm) ?? (yield* promptPackageManager);
|
|
67
|
-
yield* scaffoldProject(dir, name);
|
|
68
|
-
yield* Console.log(`Scaffolded ${name} in ${dir}`);
|
|
69
|
-
if (input.noInstall) {
|
|
70
|
-
yield* Console.log([
|
|
71
|
-
"",
|
|
72
|
-
"Next steps:",
|
|
73
|
-
dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
|
|
74
|
-
` ${pm} install`,
|
|
75
|
-
` ${pm === "npm" ? "npm run" : pm} studio`,
|
|
76
|
-
]
|
|
77
|
-
.filter((line) => line !== "")
|
|
78
|
-
.join("\n"));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
yield* runInstall(pm, dir);
|
|
82
|
-
yield* Console.log([
|
|
83
|
-
"",
|
|
84
|
-
`${name} is ready.`,
|
|
85
|
-
dir === cwd ? "" : ` cd ${path.relative(cwd, dir)}`,
|
|
86
|
-
` ${pm === "npm" ? "npm run" : pm} studio # preview scenes with hot reload`,
|
|
87
|
-
` ${pm === "npm" ? "npm run" : pm} render # render targets from motion.config.ts`,
|
|
88
|
-
]
|
|
89
|
-
.filter((line) => line !== "")
|
|
90
|
-
.join("\n"));
|
|
91
|
-
}).pipe(
|
|
92
|
-
// ctrl-c in a prompt is an interruption, not a failure
|
|
93
|
-
Effect.catchTag("QuitError", () => Effect.interrupt));
|
|
94
|
-
export const initCommand = Command.make("init", initFlags, handler).pipe(Command.withDescription("Scaffold a new effect-motion project"));
|