reelkit-cli 0.5.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 +2 -1
- package/package.json +3 -2
- package/skill/SKILL.md +4 -4
- package/skill/reference/beat-sync.md +9 -0
- package/skill/reference/component-authoring.md +12 -1
- package/skill/reference/kit.md +1 -0
- package/src/cli.ts +8 -2
- package/src/commands/assets.ts +24 -6
- package/src/commands/build.ts +27 -4
- package/src/commands/components.ts +220 -0
- package/src/commands/init.ts +9 -3
- package/src/contract/index.ts +23 -3
- package/src/pipeline/review.ts +26 -0
- package/src/project/loudness.ts +68 -0
- package/src/project/manifest.ts +9 -0
- package/src/project/project.ts +4 -2
- package/src/project/refmeasure.ts +1 -1
- package/src/remotion/kit/Sfx.tsx +12 -6
- package/src/remotion/kit/docs.ts +1 -0
- package/src/remotion/kit/media.ts +5 -3
- package/src/render/master.ts +31 -0
- package/src/render/static-check.ts +156 -0
- package/src/render/validate.ts +3 -150
- package/src/testing/conformance.ts +49 -1
- package/src/testing/fake-api.ts +7 -1
|
@@ -166,7 +166,7 @@ export function runConformance(name: string, start: (opts: StartOptions) => Prom
|
|
|
166
166
|
["an image type for an sfx", { kind: "sfx", contentType: "image/png" }],
|
|
167
167
|
["zero bytes", { bytes: 0 }],
|
|
168
168
|
["more than 200 MB", { bytes: MAX_UPLOAD_BYTES + 1 }],
|
|
169
|
-
["a component", { kind: "component", filename: "X.tsx", contentType: "text/plain" }],
|
|
169
|
+
["a component without a description or an example", { kind: "component", filename: "X.tsx", contentType: "text/plain" }],
|
|
170
170
|
])("refuses %s with invalid_request", async (_what, over) => {
|
|
171
171
|
const e = await failure((await as("user-up00008"))("libraryUpload", upload(over) as never));
|
|
172
172
|
expect(e.code).toBe("invalid_request");
|
|
@@ -182,6 +182,54 @@ export function runConformance(name: string, start: (opts: StartOptions) => Prom
|
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
describe("component uploads", () => {
|
|
186
|
+
const SOURCE = new TextEncoder().encode('import React from "react";\nexport const ZipBadge: React.FC<{ label: string }> = ({ label }) => <b>{label}</b>;\n');
|
|
187
|
+
const component = (over: object = {}) => upload({
|
|
188
|
+
kind: "component", title: "ZipBadge", description: "A small bold badge that shows a label.", tags: ["badge"], meta: { example: '<ZipBadge label="New" />' },
|
|
189
|
+
filename: "ZipBadge.tsx", contentType: "text/plain", bytes: SOURCE.length, ...over,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("is committed into review, can be pulled by its owner, and is never in search or the public listing", async () => {
|
|
193
|
+
const mine = await as("user-cmp00001"), other = await as("user-cmp00002");
|
|
194
|
+
const up = await mine("libraryUpload", component());
|
|
195
|
+
expect((await put(up.uploadUrl, SOURCE, "text/plain")).ok).toBe(true);
|
|
196
|
+
const { item } = await mine("libraryCommit", { id: up.id });
|
|
197
|
+
expect(item).toMatchObject({ id: up.id, kind: "component", visibility: "review" });
|
|
198
|
+
expect((await mine("librarySearch", { q: "zipbadge badge", kind: "component" })).items.map((i) => i.id)).not.toContain(up.id);
|
|
199
|
+
expect((await other("librarySearch", { q: "zipbadge badge" })).items.map((i) => i.id)).not.toContain(up.id);
|
|
200
|
+
expect((await anon("publicLibrary", { q: "zipbadge badge" })).items.map((i) => i.id)).not.toContain(up.id);
|
|
201
|
+
expect((await mine("me", {})).contributions).toBe(0);
|
|
202
|
+
expect((await failure(other("libraryPull", { id: up.id }))).code).toBe("not_found");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("refuses at commit a source that fails the static check, with the first problem", async () => {
|
|
206
|
+
const mine = await as("user-cmp00003");
|
|
207
|
+
const bad = new TextEncoder().encode('import fs from "node:fs";\nexport const ZipBadge = () => null;\n');
|
|
208
|
+
const up = await mine("libraryUpload", component({ bytes: bad.length }));
|
|
209
|
+
await put(up.uploadUrl, bad, "text/plain");
|
|
210
|
+
const e = await failure(mine("libraryCommit", { id: up.id }));
|
|
211
|
+
expect(e.code).toBe("invalid_request");
|
|
212
|
+
expect(e.message).toContain("node:fs");
|
|
213
|
+
expect((await failure(mine("libraryPull", { id: up.id }))).code).toBe("not_found");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it.each([
|
|
217
|
+
["a file name that is not PascalCase .tsx", { filename: "zipBadge.tsx" }],
|
|
218
|
+
["a file name that is not .tsx", { filename: "ZipBadge.ts" }],
|
|
219
|
+
["a content type that is not text/plain", { contentType: "image/png" }],
|
|
220
|
+
["more than 64 KB", { bytes: 65_537 }],
|
|
221
|
+
["a description under 20 characters", { description: "a badge" }],
|
|
222
|
+
["no example", { meta: {} }],
|
|
223
|
+
["an example that does not start with the component's name", { meta: { example: '<Other label="x" />' } }],
|
|
224
|
+
["an example over 2000 characters", { meta: { example: `<ZipBadge label="${"x".repeat(2000)}" />` } }],
|
|
225
|
+
["a component that is not meant to be shared", { shareable: false }],
|
|
226
|
+
])("refuses %s with invalid_request", async (_what, over) => {
|
|
227
|
+
const e = await failure((await as("user-cmp00004"))("libraryUpload", component(over) as never));
|
|
228
|
+
expect(e.code).toBe("invalid_request");
|
|
229
|
+
expect(e.message.length).toBeGreaterThan(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
185
233
|
describe("the library", () => {
|
|
186
234
|
it("a seeded published item is found, pulled and downloaded byte for byte", async () => {
|
|
187
235
|
const bytes = new Uint8Array(Array.from({ length: 300 }, (_, i) => i % 251));
|
package/src/testing/fake-api.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { AddressInfo } from "node:net";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { MAX_UPLOAD_BYTES, routes, type ErrorCode, type LibraryItem, type RouteName, type Voice } from "../contract";
|
|
9
|
+
import { staticCheck } from "../render/validate";
|
|
9
10
|
import { PIXEL_PNG, silentWav } from "./fixtures";
|
|
10
11
|
|
|
11
12
|
type Blob = { filename: string; contentType: string; bytes: Uint8Array };
|
|
@@ -23,7 +24,7 @@ const WORD_SEC = 0.4;
|
|
|
23
24
|
// of long media (music, overlays, clips) and a component's source do not. Any kind with a preview of its own would use that instead
|
|
24
25
|
// (the fake has none).
|
|
25
26
|
const PREVIEW_OF_ORIGINAL: readonly string[] = ["image", "sfx"];
|
|
26
|
-
const MIME_PREFIX: Record<string, string> = { image: "image/", clip: "video/", overlay: "video/", sfx: "audio/", music: "audio/" };
|
|
27
|
+
const MIME_PREFIX: Record<string, string> = { component: "text/plain", image: "image/", clip: "video/", overlay: "video/", sfx: "audio/", music: "audio/" };
|
|
27
28
|
// The device-flow user of the fake is "tester"; anyone else is named from their id, as the API names a user with no profile.
|
|
28
29
|
const handleOf = (userId: string) => (userId === "u-test" ? "tester" : `user-${userId.slice(0, 8)}`);
|
|
29
30
|
const nextMonthStart = (now = new Date()) => new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
|
|
@@ -190,6 +191,11 @@ export async function startFakeApi(opts: { voiceoverCharLimit?: number; imageLim
|
|
|
190
191
|
const s = store.get(id);
|
|
191
192
|
if (!s || s.owner !== userId) throw new Fail(404, "not_found", `No upload with id ${id}.`);
|
|
192
193
|
if (!s.file) throw new Fail(400, "invalid_request", `Nothing was uploaded for ${id}. Send the file to the upload URL first.`);
|
|
194
|
+
// A user's component is checked like a pulled one would be, before it is anything but an upload. It stays in review for the owner.
|
|
195
|
+
if (!s.committed && s.item.kind === "component") {
|
|
196
|
+
const problem = staticCheck(new TextDecoder().decode(s.file.bytes), s.filename, [])[0];
|
|
197
|
+
if (problem) throw new Fail(400, "invalid_request", `The component was refused: ${problem}`);
|
|
198
|
+
}
|
|
193
199
|
// Committing again returns the same item and counts once.
|
|
194
200
|
if (!s.committed) { s.committed = true; usage.uploads++; if (s.item.visibility === "review") meter(userId).uploads++; }
|
|
195
201
|
return { item: s.item };
|