reelkit-cli 0.12.2 → 0.12.3
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/src/project/scene-components.ts +97 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
What changed in each published version of `reelkit-cli`, newest first. A push to `main` publishes the version in
|
|
4
4
|
`package.json` when it is not on npm yet, and it must have an entry here.
|
|
5
5
|
|
|
6
|
+
## 0.12.3 - 2026-10-09
|
|
7
|
+
|
|
8
|
+
- A template's components are tied to scenes in more of the ways compositions are written: a project's own helper that is handed a scene's id, a lookup by id, a list that pairs ids with components, a branch on a scene's id, and components the file declares for itself.
|
|
9
|
+
|
|
6
10
|
## 0.12.2 - 2026-10-09
|
|
7
11
|
|
|
8
12
|
- You can keep 50 templates (it was 20).
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ For image or video cutouts, editable Blender product shots, custom 3D materials,
|
|
|
15
15
|
|
|
16
16
|
To implement a chosen style, read its row in `reference/style-recipes.md` for action, timing, sound and proof. `reference/style-components.md` maps the shared components and built-in kit to shot roles; inspect the selected source before using its props.
|
|
17
17
|
|
|
18
|
-
This skill targets `reelkit-cli` 0.12.
|
|
18
|
+
This skill targets `reelkit-cli` 0.12.3 and its HyperFrames kit. Check the installed version with `reelkit --version`; in a source checkout, invoke its `bin/reelkit.mjs` with Node. Use the matching CLI before authoring with `reelkit/frame`.
|
|
19
19
|
|
|
20
20
|
## What decides whether the film is accepted
|
|
21
21
|
|
|
@@ -3,10 +3,12 @@ import { sourceFile } from "../render/static-check";
|
|
|
3
3
|
|
|
4
4
|
// Which components a composition uses, and in which scenes, read from its source without running it.
|
|
5
5
|
//
|
|
6
|
-
// A composition is one tree, not a file per scene, so "this component belongs to that scene" is read from how it is
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// A composition is one tree, not a file per scene, so "this component belongs to that scene" is read from how it is placed and timed.
|
|
7
|
+
// A call that is handed a scene's id as a string (`sceneById(manifest, "ask")`, a project's own `scene("ask", <>...</>)` or
|
|
8
|
+
// `at("ask")`), or a lookup by it (`s["ask"]`), is about that scene. A variable made from such an expression stands for the scene, and
|
|
9
|
+
// so does one computed from it. A component is counted for a scene when it is written inside such a call, or when its props (or its
|
|
10
|
+
// enclosing element's props) mention one. A component placed by plain numbers, or by nothing, is counted for no scene. It is a
|
|
11
|
+
// reading of the code, not a measurement.
|
|
10
12
|
export type UsedComponent = { name: string; from: "project" | "kit" | "library"; uses: number; scenes: string[] };
|
|
11
13
|
|
|
12
14
|
// Tags that are plumbing rather than something the film shows.
|
|
@@ -32,13 +34,20 @@ export function componentsOf(source: string, opts: { sceneIds: string[]; pulled?
|
|
|
32
34
|
|
|
33
35
|
// Variables that stand for a scene: `const grow = sceneById(manifest, "grow")`, then anything computed from one, in source order.
|
|
34
36
|
const sceneOf = new Map<string, Set<string>>();
|
|
37
|
+
// The scene ids this one node names outright: a string argument of a call, or the key of a lookup. With the plan's ids known, any
|
|
38
|
+
// call counts; without them only `sceneById`, so an ordinary string is never taken for a scene.
|
|
39
|
+
const named = (n: ts.Node): string[] => {
|
|
40
|
+
if (ts.isCallExpression(n)) {
|
|
41
|
+
if (!known.size) { const arg = ts.isIdentifier(n.expression) && n.expression.text === "sceneById" ? n.arguments[1] : undefined; return arg && ts.isStringLiteralLike(arg) ? [arg.text] : []; }
|
|
42
|
+
return n.arguments.filter((a): a is ts.StringLiteralLike => ts.isStringLiteralLike(a) && known.has(a.text)).map((a) => a.text);
|
|
43
|
+
}
|
|
44
|
+
if (ts.isElementAccessExpression(n) && ts.isStringLiteralLike(n.argumentExpression) && known.has(n.argumentExpression.text)) return [n.argumentExpression.text];
|
|
45
|
+
return [];
|
|
46
|
+
};
|
|
35
47
|
const idsIn = (node: ts.Node): Set<string> => {
|
|
36
48
|
const found = new Set<string>();
|
|
37
49
|
const walk = (n: ts.Node) => {
|
|
38
|
-
|
|
39
|
-
const arg = n.arguments[1];
|
|
40
|
-
if (arg && ts.isStringLiteralLike(arg) && (!known.size || known.has(arg.text))) found.add(arg.text);
|
|
41
|
-
}
|
|
50
|
+
for (const id of named(n)) found.add(id);
|
|
42
51
|
if (ts.isIdentifier(n)) for (const id of sceneOf.get(n.text) ?? []) found.add(id);
|
|
43
52
|
ts.forEachChild(n, walk);
|
|
44
53
|
};
|
|
@@ -61,28 +70,96 @@ export function componentsOf(source: string, opts: { sceneIds: string[]; pulled?
|
|
|
61
70
|
// Twice, so a variable computed from one declared further down (a helper above its data) is still followed.
|
|
62
71
|
declare(sf); declare(sf);
|
|
63
72
|
|
|
64
|
-
const
|
|
65
|
-
|
|
73
|
+
// The components the file declares for itself (a `const HookShot = () => ...`): what is written inside one belongs to the scenes that
|
|
74
|
+
// one is used in. The composition itself (`Video`) is not one of them.
|
|
75
|
+
const local = new Map<string, ts.Node>();
|
|
76
|
+
const collect = (node: ts.Node) => {
|
|
77
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && /^[A-Z]/.test(node.name.text) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) local.set(node.name.text, node.initializer);
|
|
78
|
+
if (ts.isFunctionDeclaration(node) && node.name && /^[A-Z]/.test(node.name.text)) local.set(node.name.text, node);
|
|
79
|
+
ts.forEachChild(node, collect);
|
|
80
|
+
};
|
|
81
|
+
collect(sf);
|
|
82
|
+
local.delete("Video");
|
|
83
|
+
|
|
84
|
+
type Use = { scenes: Set<string>; owner: string };
|
|
85
|
+
const uses = new Map<string, Use[]>(); // imported components
|
|
86
|
+
const localUses = new Map<string, Use[]>(); // the file's own components
|
|
87
|
+
const note = (map: Map<string, Use[]>, name: string, scenes: Set<string>, owner: string) => map.set(name, [...(map.get(name) ?? []), { scenes: new Set(scenes), owner }]);
|
|
88
|
+
// The ids a condition compares against: `scene.id === "hook"`.
|
|
89
|
+
const compared = (node: ts.Node): Set<string> => {
|
|
90
|
+
const found = new Set<string>();
|
|
91
|
+
const walk = (n: ts.Node) => {
|
|
92
|
+
if (ts.isBinaryExpression(n) && (n.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || n.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken)) {
|
|
93
|
+
for (const side of [n.left, n.right]) if (ts.isStringLiteralLike(side) && known.has(side.text)) found.add(side.text);
|
|
94
|
+
}
|
|
95
|
+
ts.forEachChild(n, walk);
|
|
96
|
+
};
|
|
97
|
+
walk(node);
|
|
98
|
+
return found;
|
|
99
|
+
};
|
|
100
|
+
const visit = (node: ts.Node, inherited: Set<string>, owner: string) => {
|
|
66
101
|
let here = inherited;
|
|
102
|
+
let inside = owner;
|
|
103
|
+
for (const [name, body] of local) if (body === node) { inside = name; here = new Set(); }
|
|
67
104
|
const opening = ts.isJsxSelfClosingElement(node) ? node : ts.isJsxElement(node) ? node.openingElement : undefined;
|
|
68
105
|
if (opening) {
|
|
69
106
|
const own = idsIn(opening.attributes);
|
|
70
|
-
here = own.size ? own :
|
|
107
|
+
here = own.size ? own : here;
|
|
71
108
|
const tag = opening.tagName.getText(sf);
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
entry.uses += 1;
|
|
76
|
-
for (const id of here) entry.scenes.add(id);
|
|
77
|
-
used.set(tag, entry);
|
|
78
|
-
}
|
|
109
|
+
const root = tag.split(".")[0]!;
|
|
110
|
+
if (local.has(root)) note(localUses, root, here, inside);
|
|
111
|
+
else if (origin.get(root) && !PLUMBING.has(tag) && /^[A-Z]/.test(tag)) note(uses, tag, here, inside);
|
|
79
112
|
}
|
|
80
113
|
// A block of JSX produced inside an expression that mentions a scene (a `.map` over that scene's items, a condition on its frame)
|
|
81
114
|
// belongs to that scene too.
|
|
82
115
|
if (ts.isJsxExpression(node) && node.expression) { const own = idsIn(node.expression); if (own.size && !ts.isJsxElement(node.expression)) here = new Set([...own]); }
|
|
83
|
-
|
|
116
|
+
// What is written inside a call that names a scene (`scene("ask", <>...</>)`) is that scene's.
|
|
117
|
+
const direct = named(node);
|
|
118
|
+
if (direct.length) here = new Set(direct);
|
|
119
|
+
// A list that pairs a scene's id with a component (`["hook", HookShot]`), or an object keyed by scene ids, places that component.
|
|
120
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
121
|
+
const ids = node.elements.filter((e): e is ts.StringLiteralLike => ts.isStringLiteralLike(e) && known.has(e.text)).map((e) => e.text);
|
|
122
|
+
if (ids.length) { here = new Set(ids); for (const e of node.elements) if (ts.isIdentifier(e) && local.has(e.text)) note(localUses, e.text, here, inside); }
|
|
123
|
+
}
|
|
124
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name)) && known.has(node.name.text)) {
|
|
125
|
+
here = new Set([node.name.text]);
|
|
126
|
+
if (ts.isIdentifier(node.initializer) && local.has(node.initializer.text)) note(localUses, node.initializer.text, here, inside);
|
|
127
|
+
}
|
|
128
|
+
// The branch taken when a scene's id is the one compared against is that scene's.
|
|
129
|
+
if (ts.isConditionalExpression(node) || ts.isIfStatement(node)) {
|
|
130
|
+
const cond = ts.isConditionalExpression(node) ? node.condition : node.expression;
|
|
131
|
+
const then = ts.isConditionalExpression(node) ? node.whenTrue : node.thenStatement;
|
|
132
|
+
const ids = compared(cond);
|
|
133
|
+
if (ids.size) {
|
|
134
|
+
ts.forEachChild(node, (child) => visit(child, child === then ? ids : here, inside));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
139
|
+
const ids = compared(node.left);
|
|
140
|
+
if (ids.size) { visit(node.left, here, inside); visit(node.right, ids, inside); return; }
|
|
141
|
+
}
|
|
142
|
+
if (ts.isCaseClause(node) && ts.isStringLiteralLike(node.expression) && known.has(node.expression.text)) here = new Set([node.expression.text]);
|
|
143
|
+
ts.forEachChild(node, (child) => visit(child, here, inside));
|
|
84
144
|
};
|
|
85
|
-
visit(sf, new Set());
|
|
145
|
+
visit(sf, new Set(), "");
|
|
146
|
+
|
|
147
|
+
// The scenes of each of the file's own components: where it is placed, or, when it is placed inside another of them with no scene
|
|
148
|
+
// of its own, wherever that one is. Repeated until nothing changes, so nesting of any depth is followed.
|
|
149
|
+
const scenesOfLocal = new Map<string, Set<string>>([...local.keys()].map((name) => [name, new Set<string>()]));
|
|
150
|
+
for (let changed = true, rounds = 0; changed && rounds < 12; rounds++) {
|
|
151
|
+
changed = false;
|
|
152
|
+
for (const [name, list] of localUses) {
|
|
153
|
+
const mine = scenesOfLocal.get(name)!;
|
|
154
|
+
for (const use of list) for (const id of use.scenes.size ? use.scenes : (scenesOfLocal.get(use.owner) ?? [])) if (!mine.has(id)) { mine.add(id); changed = true; }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const used = new Map<string, { uses: number; scenes: Set<string> }>();
|
|
158
|
+
for (const [tag, list] of uses) {
|
|
159
|
+
const scenes = new Set<string>();
|
|
160
|
+
for (const use of list) for (const id of use.scenes.size ? use.scenes : (scenesOfLocal.get(use.owner) ?? [])) scenes.add(id);
|
|
161
|
+
used.set(tag, { uses: list.length, scenes });
|
|
162
|
+
}
|
|
86
163
|
|
|
87
164
|
const order = opts.sceneIds;
|
|
88
165
|
return [...used].map(([name, u]) => ({ name, from: origin.get(name.split(".")[0]!)!, uses: u.uses, scenes: [...u.scenes].sort((a, b) => order.indexOf(a) - order.indexOf(b)) }))
|