@scenar/cli 0.1.18 → 0.1.20-rc.1
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/package.json +17 -5
- package/src/__tests__/deploy-command.test.ts +45 -0
- package/src/__tests__/deploy-flow.test.ts +125 -0
- package/src/__tests__/deploy-upload.test.ts +39 -0
- package/src/__tests__/edge-tts-provider.test.ts +6 -6
- package/src/__tests__/embed-snippet.test.ts +56 -0
- package/src/__tests__/pack-bundle-contract.test.ts +135 -0
- package/src/__tests__/pack-command.test.ts +51 -0
- package/src/__tests__/pack-generate-embed-entry.test.ts +90 -0
- package/src/__tests__/pack-manifest.test.ts +138 -0
- package/src/__tests__/viewport.test.ts +33 -0
- package/src/commands/deploy.d.ts +3 -0
- package/src/commands/deploy.d.ts.map +1 -0
- package/src/commands/deploy.js +166 -0
- package/src/commands/deploy.js.map +1 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/pack.d.ts +3 -0
- package/src/commands/pack.d.ts.map +1 -0
- package/src/commands/pack.js +156 -0
- package/src/commands/pack.js.map +1 -0
- package/src/commands/pack.ts +189 -0
- package/src/deploy/client.d.ts +20 -0
- package/src/deploy/client.d.ts.map +1 -0
- package/src/deploy/client.js +22 -0
- package/src/deploy/client.js.map +1 -0
- package/src/deploy/client.ts +28 -0
- package/src/deploy/deploy-flow.d.ts +78 -0
- package/src/deploy/deploy-flow.d.ts.map +1 -0
- package/src/deploy/deploy-flow.js +53 -0
- package/src/deploy/deploy-flow.js.map +1 -0
- package/src/deploy/deploy-flow.ts +115 -0
- package/src/deploy/embed-snippet.d.ts +27 -0
- package/src/deploy/embed-snippet.d.ts.map +1 -0
- package/src/deploy/embed-snippet.js +40 -0
- package/src/deploy/embed-snippet.js.map +1 -0
- package/src/deploy/embed-snippet.ts +52 -0
- package/src/deploy/upload.d.ts +10 -0
- package/src/deploy/upload.d.ts.map +1 -0
- package/src/deploy/upload.js +23 -0
- package/src/deploy/upload.js.map +1 -0
- package/src/deploy/upload.ts +29 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/index.ts +5 -1
- package/src/pack/build.d.ts +17 -0
- package/src/pack/build.d.ts.map +1 -0
- package/src/pack/build.js +42 -0
- package/src/pack/build.js.map +1 -0
- package/src/pack/build.ts +59 -0
- package/src/pack/bundle-contract.d.ts +61 -0
- package/src/pack/bundle-contract.d.ts.map +1 -0
- package/src/pack/bundle-contract.js +116 -0
- package/src/pack/bundle-contract.js.map +1 -0
- package/src/pack/bundle-contract.ts +128 -0
- package/src/pack/generate-embed-entry.d.ts +42 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -0
- package/src/pack/generate-embed-entry.js +125 -0
- package/src/pack/generate-embed-entry.js.map +1 -0
- package/src/pack/generate-embed-entry.ts +162 -0
- package/src/pack/pack-manifest.d.ts +45 -0
- package/src/pack/pack-manifest.d.ts.map +1 -0
- package/src/pack/pack-manifest.js +95 -0
- package/src/pack/pack-manifest.js.map +1 -0
- package/src/pack/pack-manifest.ts +146 -0
- package/src/pack/viewport.d.ts +30 -0
- package/src/pack/viewport.d.ts.map +1 -0
- package/src/pack/viewport.js +19 -0
- package/src/pack/viewport.js.map +1 -0
- package/src/pack/viewport.ts +40 -0
- package/src/tts/edge-tts.d.ts +8 -0
- package/src/tts/edge-tts.d.ts.map +1 -1
- package/src/tts/edge-tts.js +11 -3
- package/src/tts/edge-tts.js.map +1 -1
- package/src/tts/edge-tts.ts +11 -3
- package/src/util/load-yaml.d.ts +1 -1
- package/src/util/load-yaml.js +1 -1
- package/src/util/load-yaml.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle contract — the exact rules the Scenar Cloud backend enforces when
|
|
3
|
+
* it accepts a deploy. `scenar pack` validates its own output against these
|
|
4
|
+
* rules so a malformed bundle fails locally with a clear message rather than
|
|
5
|
+
* mid-upload, and a conformance test asserts pack output satisfies them so pack
|
|
6
|
+
* and the server can never drift.
|
|
7
|
+
*
|
|
8
|
+
* Source of truth (kept in lockstep):
|
|
9
|
+
* - DeployContentTypes.java — extension allowlist + canonical content type
|
|
10
|
+
* - DeployManifestValidator.java — path pattern, depth, dupes, content-type check
|
|
11
|
+
* - CompleteDeployUploadSessionHandler.java — sha256/size/content-type shape
|
|
12
|
+
* - ScenarioJsonValidator.java — scenario.json rules
|
|
13
|
+
*
|
|
14
|
+
* The backend verifies each file's declared content type equals the canonical
|
|
15
|
+
* type for its extension (the same declared-then-verified contract as sha256 and
|
|
16
|
+
* size), so `CONTENT_TYPE_BY_EXTENSION` below must match the server's table
|
|
17
|
+
* exactly. svg is intentionally absent: it is active content (it can run script
|
|
18
|
+
* on direct same-origin navigation), so scenarios inline SVG (as a React
|
|
19
|
+
* component or a data URI) rather than emitting it as a served file.
|
|
20
|
+
*/
|
|
21
|
+
/** Conventional content type per allowed extension. Matches the backend exactly. */
|
|
22
|
+
export const CONTENT_TYPE_BY_EXTENSION = {
|
|
23
|
+
// The application shell, its scenario descriptor, and narration audio.
|
|
24
|
+
html: "text/html",
|
|
25
|
+
js: "text/javascript",
|
|
26
|
+
css: "text/css",
|
|
27
|
+
json: "application/json",
|
|
28
|
+
mp3: "audio/mpeg",
|
|
29
|
+
// Raster web images.
|
|
30
|
+
png: "image/png",
|
|
31
|
+
jpg: "image/jpeg",
|
|
32
|
+
jpeg: "image/jpeg",
|
|
33
|
+
gif: "image/gif",
|
|
34
|
+
webp: "image/webp",
|
|
35
|
+
avif: "image/avif",
|
|
36
|
+
// Modern web fonts (RFC 8081 top-level "font" types).
|
|
37
|
+
woff2: "font/woff2",
|
|
38
|
+
woff: "font/woff",
|
|
39
|
+
};
|
|
40
|
+
/** Final-extension allowlist (lowercase, no dot). Derived from the content-type table. */
|
|
41
|
+
export const ALLOWED_EXTENSIONS = Object.keys(CONTENT_TYPE_BY_EXTENSION);
|
|
42
|
+
/** A bundle-relative path: clean POSIX segments, no leading/trailing/double slash. */
|
|
43
|
+
export const RELATIVE_PATH_PATTERN = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
44
|
+
/** Maximum number of path segments (directory depth) the backend allows. */
|
|
45
|
+
export const MAX_PATH_DEPTH = 12;
|
|
46
|
+
/** Lowercase hex SHA-256 (exactly 64 chars) — the form the presign binds. */
|
|
47
|
+
export const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
48
|
+
/** A `type/subtype` content type — the minimal shape the backend requires. */
|
|
49
|
+
export const CONTENT_TYPE_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
50
|
+
/** The file every bundle must contain at its root. */
|
|
51
|
+
export const REQUIRED_FILE = "scenario.json";
|
|
52
|
+
/** Keys forbidden in scenario.json to block prototype-pollution payloads. */
|
|
53
|
+
const FORBIDDEN_JSON_KEYS = ["__proto__", "constructor", "prototype"];
|
|
54
|
+
/** scenario.json must be at most 1 MiB. */
|
|
55
|
+
const MAX_SCENARIO_JSON_BYTES = 1024 * 1024;
|
|
56
|
+
/** Returns the lowercase final extension of a path (no dot), or "" if none. */
|
|
57
|
+
export function finalExtension(path) {
|
|
58
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
59
|
+
const dot = base.lastIndexOf(".");
|
|
60
|
+
return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate one bundle-relative file path against the backend rules. Returns an
|
|
64
|
+
* error message, or null if the path is acceptable.
|
|
65
|
+
*/
|
|
66
|
+
export function validateRelativePath(path) {
|
|
67
|
+
if (!RELATIVE_PATH_PATTERN.test(path)) {
|
|
68
|
+
return `path "${path}" is not a clean relative path (allowed: A-Z a-z 0-9 . _ - and /)`;
|
|
69
|
+
}
|
|
70
|
+
const segments = path.split("/");
|
|
71
|
+
if (segments.length > MAX_PATH_DEPTH) {
|
|
72
|
+
return `path "${path}" exceeds the maximum depth of ${MAX_PATH_DEPTH}`;
|
|
73
|
+
}
|
|
74
|
+
// The charset regex still admits "." and ".." segments (the dot is allowed);
|
|
75
|
+
// the backend rejects them as traversal, so we must too.
|
|
76
|
+
for (const segment of segments) {
|
|
77
|
+
if (segment === "." || segment === "..") {
|
|
78
|
+
return `path "${path}" must not contain "." or ".." segments`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const ext = finalExtension(path);
|
|
82
|
+
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
|
83
|
+
return `file "${path}" has a disallowed extension; allowed: ${ALLOWED_EXTENSIONS.join(", ")}`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate the parsed scenario.json content. Returns an error message, or null
|
|
89
|
+
* if it satisfies the backend's ScenarioJsonValidator rules.
|
|
90
|
+
*/
|
|
91
|
+
export function validateScenarioJson(raw) {
|
|
92
|
+
if (Buffer.byteLength(raw, "utf-8") > MAX_SCENARIO_JSON_BYTES) {
|
|
93
|
+
return "scenario.json exceeds the 1 MiB limit";
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(raw);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return "scenario.json is not valid JSON";
|
|
101
|
+
}
|
|
102
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
103
|
+
return "scenario.json must be a JSON object";
|
|
104
|
+
}
|
|
105
|
+
const obj = parsed;
|
|
106
|
+
if (typeof obj.schemaVersion !== "string" || obj.schemaVersion.length === 0) {
|
|
107
|
+
return 'scenario.json must have a non-empty string "schemaVersion"';
|
|
108
|
+
}
|
|
109
|
+
for (const key of FORBIDDEN_JSON_KEYS) {
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
111
|
+
return `scenario.json must not contain the forbidden key "${key}"`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=bundle-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-contract.js","sourceRoot":"","sources":["../../../src/pack/bundle-contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundle contract — the exact rules the Scenar Cloud backend enforces when
|
|
3
|
+
* it accepts a deploy. `scenar pack` validates its own output against these
|
|
4
|
+
* rules so a malformed bundle fails locally with a clear message rather than
|
|
5
|
+
* mid-upload, and a conformance test asserts pack output satisfies them so pack
|
|
6
|
+
* and the server can never drift.
|
|
7
|
+
*
|
|
8
|
+
* Source of truth (kept in lockstep):
|
|
9
|
+
* - DeployContentTypes.java — extension allowlist + canonical content type
|
|
10
|
+
* - DeployManifestValidator.java — path pattern, depth, dupes, content-type check
|
|
11
|
+
* - CompleteDeployUploadSessionHandler.java — sha256/size/content-type shape
|
|
12
|
+
* - ScenarioJsonValidator.java — scenario.json rules
|
|
13
|
+
*
|
|
14
|
+
* The backend verifies each file's declared content type equals the canonical
|
|
15
|
+
* type for its extension (the same declared-then-verified contract as sha256 and
|
|
16
|
+
* size), so `CONTENT_TYPE_BY_EXTENSION` below must match the server's table
|
|
17
|
+
* exactly. svg is intentionally absent: it is active content (it can run script
|
|
18
|
+
* on direct same-origin navigation), so scenarios inline SVG (as a React
|
|
19
|
+
* component or a data URI) rather than emitting it as a served file.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Conventional content type per allowed extension. Matches the backend exactly. */
|
|
23
|
+
export const CONTENT_TYPE_BY_EXTENSION = {
|
|
24
|
+
// The application shell, its scenario descriptor, and narration audio.
|
|
25
|
+
html: "text/html",
|
|
26
|
+
js: "text/javascript",
|
|
27
|
+
css: "text/css",
|
|
28
|
+
json: "application/json",
|
|
29
|
+
mp3: "audio/mpeg",
|
|
30
|
+
// Raster web images.
|
|
31
|
+
png: "image/png",
|
|
32
|
+
jpg: "image/jpeg",
|
|
33
|
+
jpeg: "image/jpeg",
|
|
34
|
+
gif: "image/gif",
|
|
35
|
+
webp: "image/webp",
|
|
36
|
+
avif: "image/avif",
|
|
37
|
+
// Modern web fonts (RFC 8081 top-level "font" types).
|
|
38
|
+
woff2: "font/woff2",
|
|
39
|
+
woff: "font/woff",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
/** Final-extension allowlist (lowercase, no dot). Derived from the content-type table. */
|
|
43
|
+
export const ALLOWED_EXTENSIONS = Object.keys(
|
|
44
|
+
CONTENT_TYPE_BY_EXTENSION,
|
|
45
|
+
) as (keyof typeof CONTENT_TYPE_BY_EXTENSION)[];
|
|
46
|
+
|
|
47
|
+
/** A bundle-relative path: clean POSIX segments, no leading/trailing/double slash. */
|
|
48
|
+
export const RELATIVE_PATH_PATTERN = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
49
|
+
|
|
50
|
+
/** Maximum number of path segments (directory depth) the backend allows. */
|
|
51
|
+
export const MAX_PATH_DEPTH = 12;
|
|
52
|
+
|
|
53
|
+
/** Lowercase hex SHA-256 (exactly 64 chars) — the form the presign binds. */
|
|
54
|
+
export const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
55
|
+
|
|
56
|
+
/** A `type/subtype` content type — the minimal shape the backend requires. */
|
|
57
|
+
export const CONTENT_TYPE_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
58
|
+
|
|
59
|
+
/** The file every bundle must contain at its root. */
|
|
60
|
+
export const REQUIRED_FILE = "scenario.json";
|
|
61
|
+
|
|
62
|
+
/** Keys forbidden in scenario.json to block prototype-pollution payloads. */
|
|
63
|
+
const FORBIDDEN_JSON_KEYS = ["__proto__", "constructor", "prototype"];
|
|
64
|
+
|
|
65
|
+
/** scenario.json must be at most 1 MiB. */
|
|
66
|
+
const MAX_SCENARIO_JSON_BYTES = 1024 * 1024;
|
|
67
|
+
|
|
68
|
+
/** Returns the lowercase final extension of a path (no dot), or "" if none. */
|
|
69
|
+
export function finalExtension(path: string): string {
|
|
70
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
71
|
+
const dot = base.lastIndexOf(".");
|
|
72
|
+
return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate one bundle-relative file path against the backend rules. Returns an
|
|
77
|
+
* error message, or null if the path is acceptable.
|
|
78
|
+
*/
|
|
79
|
+
export function validateRelativePath(path: string): string | null {
|
|
80
|
+
if (!RELATIVE_PATH_PATTERN.test(path)) {
|
|
81
|
+
return `path "${path}" is not a clean relative path (allowed: A-Z a-z 0-9 . _ - and /)`;
|
|
82
|
+
}
|
|
83
|
+
const segments = path.split("/");
|
|
84
|
+
if (segments.length > MAX_PATH_DEPTH) {
|
|
85
|
+
return `path "${path}" exceeds the maximum depth of ${MAX_PATH_DEPTH}`;
|
|
86
|
+
}
|
|
87
|
+
// The charset regex still admits "." and ".." segments (the dot is allowed);
|
|
88
|
+
// the backend rejects them as traversal, so we must too.
|
|
89
|
+
for (const segment of segments) {
|
|
90
|
+
if (segment === "." || segment === "..") {
|
|
91
|
+
return `path "${path}" must not contain "." or ".." segments`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ext = finalExtension(path);
|
|
95
|
+
if (!(ALLOWED_EXTENSIONS as readonly string[]).includes(ext)) {
|
|
96
|
+
return `file "${path}" has a disallowed extension; allowed: ${ALLOWED_EXTENSIONS.join(", ")}`;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate the parsed scenario.json content. Returns an error message, or null
|
|
103
|
+
* if it satisfies the backend's ScenarioJsonValidator rules.
|
|
104
|
+
*/
|
|
105
|
+
export function validateScenarioJson(raw: string): string | null {
|
|
106
|
+
if (Buffer.byteLength(raw, "utf-8") > MAX_SCENARIO_JSON_BYTES) {
|
|
107
|
+
return "scenario.json exceeds the 1 MiB limit";
|
|
108
|
+
}
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
return "scenario.json is not valid JSON";
|
|
114
|
+
}
|
|
115
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
116
|
+
return "scenario.json must be a JSON object";
|
|
117
|
+
}
|
|
118
|
+
const obj = parsed as Record<string, unknown>;
|
|
119
|
+
if (typeof obj.schemaVersion !== "string" || obj.schemaVersion.length === 0) {
|
|
120
|
+
return 'scenario.json must have a non-empty string "schemaVersion"';
|
|
121
|
+
}
|
|
122
|
+
for (const key of FORBIDDEN_JSON_KEYS) {
|
|
123
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
124
|
+
return `scenario.json must not contain the forbidden key "${key}"`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inputs required to generate the web entry for a packed scenario.
|
|
3
|
+
*
|
|
4
|
+
* All paths are absolute filesystem paths; the generator converts them to
|
|
5
|
+
* posix-style forward-slash specifiers suitable for Vite/Rollup imports.
|
|
6
|
+
*/
|
|
7
|
+
export interface EmbedEntryInput {
|
|
8
|
+
/** Absolute path to the scenario directory (contains steps.ts). */
|
|
9
|
+
scenarioDir: string;
|
|
10
|
+
/** Absolute path to the file that exports renderStep. */
|
|
11
|
+
renderFilePath: string;
|
|
12
|
+
/** Scenario ID (directory basename) — used as the bundle id. */
|
|
13
|
+
scenarioId: string;
|
|
14
|
+
/** Whether the scenario has a narration manifest (+ audio). */
|
|
15
|
+
hasNarration: boolean;
|
|
16
|
+
/** Absolute path to .scenar/providers.tsx, or null if none. */
|
|
17
|
+
providersPath: string | null;
|
|
18
|
+
/** Canonical viewport width in px (children render at this width). */
|
|
19
|
+
canonicalWidth: number;
|
|
20
|
+
/** Shell height in px, exposed as --scenar-shell-height. */
|
|
21
|
+
shellHeight: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Produce the TypeScript source for the browser entry that mounts a scenario as
|
|
25
|
+
* a live, hosted embed.
|
|
26
|
+
*
|
|
27
|
+
* This is the web sibling of the Remotion entry generator: instead of
|
|
28
|
+
* registering a Remotion composition, it mounts {@link ScenarioPlayer} into
|
|
29
|
+
* `#root` with the same `bundle` + `renderStep` contract. It imports the
|
|
30
|
+
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
31
|
+
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
32
|
+
*
|
|
33
|
+
* Pure function, no side effects — the caller writes the result to disk.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateEmbedEntry(input: EmbedEntryInput): string;
|
|
36
|
+
/**
|
|
37
|
+
* Produce the index.html that boots the embed. The entry is referenced as an
|
|
38
|
+
* external module script (`<script type="module" src>`) — never inline — so the
|
|
39
|
+
* server CSP (`script-src 'self'`) is satisfied without loosening.
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateEmbedHtml(scenarioId: string, entryFileName: string): string;
|
|
42
|
+
//# sourceMappingURL=generate-embed-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-embed-entry.d.ts","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,YAAY,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAqFjE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAgBnF"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Produce the TypeScript source for the browser entry that mounts a scenario as
|
|
4
|
+
* a live, hosted embed.
|
|
5
|
+
*
|
|
6
|
+
* This is the web sibling of the Remotion entry generator: instead of
|
|
7
|
+
* registering a Remotion composition, it mounts {@link ScenarioPlayer} into
|
|
8
|
+
* `#root` with the same `bundle` + `renderStep` contract. It imports the
|
|
9
|
+
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
10
|
+
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
11
|
+
*
|
|
12
|
+
* Pure function, no side effects — the caller writes the result to disk.
|
|
13
|
+
*/
|
|
14
|
+
export function generateEmbedEntry(input) {
|
|
15
|
+
const scenarioPath = toPosix(input.scenarioDir);
|
|
16
|
+
const stepsImport = `${scenarioPath}/steps`;
|
|
17
|
+
const renderImport = toPosix(input.renderFilePath).replace(/\.[^.]+$/, "");
|
|
18
|
+
const lines = [];
|
|
19
|
+
// --- Imports ---
|
|
20
|
+
lines.push(`import { createRoot } from "react-dom/client";`);
|
|
21
|
+
const reactImports = input.hasNarration
|
|
22
|
+
? `ScenarioPlayer, DemoViewport, SCENAR_CLASS, useNarrationManifest`
|
|
23
|
+
: `ScenarioPlayer, DemoViewport, SCENAR_CLASS`;
|
|
24
|
+
lines.push(`import { ${reactImports} } from "@scenar/react";`);
|
|
25
|
+
lines.push(`import "@scenar/react/theme.css";`);
|
|
26
|
+
lines.push(`import "@scenar/react/styles.css";`);
|
|
27
|
+
lines.push(`import { renderStep } from ${JSON.stringify(renderImport)};`);
|
|
28
|
+
lines.push(`import * as _stepsModule from ${JSON.stringify(stepsImport)};`);
|
|
29
|
+
if (input.providersPath) {
|
|
30
|
+
const providersImport = toPosix(input.providersPath).replace(/\.[^.]+$/, "");
|
|
31
|
+
lines.push(`import { PreviewProviders as _Providers } from ${JSON.stringify(providersImport)};`);
|
|
32
|
+
}
|
|
33
|
+
// --- Steps resolution (duck-type the first array of objects with delayMs) ---
|
|
34
|
+
lines.push(``);
|
|
35
|
+
lines.push(`function _findSteps(mod: Record<string, unknown>): unknown[] {`);
|
|
36
|
+
lines.push(` for (const val of Object.values(mod)) {`);
|
|
37
|
+
lines.push(` if (`);
|
|
38
|
+
lines.push(` Array.isArray(val) &&`);
|
|
39
|
+
lines.push(` val.length > 0 &&`);
|
|
40
|
+
lines.push(` typeof val[0] === "object" &&`);
|
|
41
|
+
lines.push(` val[0] !== null &&`);
|
|
42
|
+
lines.push(` "delayMs" in val[0]`);
|
|
43
|
+
lines.push(` ) return val;`);
|
|
44
|
+
lines.push(` }`);
|
|
45
|
+
lines.push(` throw new Error("No steps array found in ${escapeForString(stepsImport)}");`);
|
|
46
|
+
lines.push(`}`);
|
|
47
|
+
lines.push(``);
|
|
48
|
+
lines.push(`const _steps: any = _findSteps(_stepsModule as unknown as Record<string, unknown>);`);
|
|
49
|
+
// --- Narration manifest URL resolver (stable module-level reference) ---
|
|
50
|
+
// The manifest is fetched at runtime from its own relative location so that
|
|
51
|
+
// useNarrationManifest resolves each clip src against it (audio lives under
|
|
52
|
+
// ./narration/). A stable resolver avoids refetch-on-every-render.
|
|
53
|
+
if (input.hasNarration) {
|
|
54
|
+
lines.push(``);
|
|
55
|
+
lines.push(`const _resolveManifestUrl = () => "./narration/manifest.json";`);
|
|
56
|
+
}
|
|
57
|
+
// --- App tree ---
|
|
58
|
+
lines.push(``);
|
|
59
|
+
lines.push(`function _App() {`);
|
|
60
|
+
if (input.hasNarration) {
|
|
61
|
+
lines.push(` const _manifest = useNarrationManifest(${JSON.stringify(input.scenarioId)}, _resolveManifestUrl);`);
|
|
62
|
+
}
|
|
63
|
+
lines.push(` const _bundle = {`);
|
|
64
|
+
lines.push(` id: ${JSON.stringify(input.scenarioId)},`);
|
|
65
|
+
lines.push(` steps: _steps,`);
|
|
66
|
+
lines.push(` narrationManifest: ${input.hasNarration ? "_manifest" : "undefined"},`);
|
|
67
|
+
lines.push(` };`);
|
|
68
|
+
const open = input.providersPath ? `<_Providers>` : ``;
|
|
69
|
+
const close = input.providersPath ? `</_Providers>` : ``;
|
|
70
|
+
lines.push(` return (`);
|
|
71
|
+
lines.push(` <div className={SCENAR_CLASS}>`);
|
|
72
|
+
lines.push(` ${open}`);
|
|
73
|
+
lines.push(` <DemoViewport canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
74
|
+
lines.push(` <ScenarioPlayer bundle={_bundle} embed>`);
|
|
75
|
+
lines.push(` {(data: any, stepIndex: number) => renderStep(data, stepIndex)}`);
|
|
76
|
+
lines.push(` </ScenarioPlayer>`);
|
|
77
|
+
lines.push(` </DemoViewport>`);
|
|
78
|
+
lines.push(` ${close}`);
|
|
79
|
+
lines.push(` </div>`);
|
|
80
|
+
lines.push(` );`);
|
|
81
|
+
lines.push(`}`);
|
|
82
|
+
// --- Mount ---
|
|
83
|
+
lines.push(``);
|
|
84
|
+
lines.push(`const _rootEl = document.getElementById("root");`);
|
|
85
|
+
lines.push(`if (!_rootEl) throw new Error("Embed root element #root not found");`);
|
|
86
|
+
lines.push(`createRoot(_rootEl).render(<_App />);`);
|
|
87
|
+
lines.push(``);
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Produce the index.html that boots the embed. The entry is referenced as an
|
|
92
|
+
* external module script (`<script type="module" src>`) — never inline — so the
|
|
93
|
+
* server CSP (`script-src 'self'`) is satisfied without loosening.
|
|
94
|
+
*/
|
|
95
|
+
export function generateEmbedHtml(scenarioId, entryFileName) {
|
|
96
|
+
return [
|
|
97
|
+
`<!doctype html>`,
|
|
98
|
+
`<html lang="en">`,
|
|
99
|
+
` <head>`,
|
|
100
|
+
` <meta charset="utf-8" />`,
|
|
101
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
|
|
102
|
+
` <title>${escapeHtml(scenarioId)}</title>`,
|
|
103
|
+
` </head>`,
|
|
104
|
+
` <body>`,
|
|
105
|
+
` <div id="root"></div>`,
|
|
106
|
+
` <script type="module" src=${JSON.stringify("./" + entryFileName)}></script>`,
|
|
107
|
+
` </body>`,
|
|
108
|
+
`</html>`,
|
|
109
|
+
``,
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
/** Convert a filesystem path to posix separators (for bundler imports). */
|
|
113
|
+
function toPosix(fsPath) {
|
|
114
|
+
return fsPath.split(posix.sep === "/" ? /\\/ : /[\\/]/).join("/");
|
|
115
|
+
}
|
|
116
|
+
function escapeForString(value) {
|
|
117
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
118
|
+
}
|
|
119
|
+
function escapeHtml(value) {
|
|
120
|
+
return value
|
|
121
|
+
.replace(/&/g, "&")
|
|
122
|
+
.replace(/</g, "<")
|
|
123
|
+
.replace(/>/g, ">");
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=generate-embed-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-embed-entry.js","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inputs required to generate the web entry for a packed scenario.
|
|
5
|
+
*
|
|
6
|
+
* All paths are absolute filesystem paths; the generator converts them to
|
|
7
|
+
* posix-style forward-slash specifiers suitable for Vite/Rollup imports.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmbedEntryInput {
|
|
10
|
+
/** Absolute path to the scenario directory (contains steps.ts). */
|
|
11
|
+
scenarioDir: string;
|
|
12
|
+
/** Absolute path to the file that exports renderStep. */
|
|
13
|
+
renderFilePath: string;
|
|
14
|
+
/** Scenario ID (directory basename) — used as the bundle id. */
|
|
15
|
+
scenarioId: string;
|
|
16
|
+
/** Whether the scenario has a narration manifest (+ audio). */
|
|
17
|
+
hasNarration: boolean;
|
|
18
|
+
/** Absolute path to .scenar/providers.tsx, or null if none. */
|
|
19
|
+
providersPath: string | null;
|
|
20
|
+
/** Canonical viewport width in px (children render at this width). */
|
|
21
|
+
canonicalWidth: number;
|
|
22
|
+
/** Shell height in px, exposed as --scenar-shell-height. */
|
|
23
|
+
shellHeight: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Produce the TypeScript source for the browser entry that mounts a scenario as
|
|
28
|
+
* a live, hosted embed.
|
|
29
|
+
*
|
|
30
|
+
* This is the web sibling of the Remotion entry generator: instead of
|
|
31
|
+
* registering a Remotion composition, it mounts {@link ScenarioPlayer} into
|
|
32
|
+
* `#root` with the same `bundle` + `renderStep` contract. It imports the
|
|
33
|
+
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
34
|
+
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
35
|
+
*
|
|
36
|
+
* Pure function, no side effects — the caller writes the result to disk.
|
|
37
|
+
*/
|
|
38
|
+
export function generateEmbedEntry(input: EmbedEntryInput): string {
|
|
39
|
+
const scenarioPath = toPosix(input.scenarioDir);
|
|
40
|
+
const stepsImport = `${scenarioPath}/steps`;
|
|
41
|
+
const renderImport = toPosix(input.renderFilePath).replace(/\.[^.]+$/, "");
|
|
42
|
+
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
|
|
45
|
+
// --- Imports ---
|
|
46
|
+
lines.push(`import { createRoot } from "react-dom/client";`);
|
|
47
|
+
const reactImports = input.hasNarration
|
|
48
|
+
? `ScenarioPlayer, DemoViewport, SCENAR_CLASS, useNarrationManifest`
|
|
49
|
+
: `ScenarioPlayer, DemoViewport, SCENAR_CLASS`;
|
|
50
|
+
lines.push(`import { ${reactImports} } from "@scenar/react";`);
|
|
51
|
+
lines.push(`import "@scenar/react/theme.css";`);
|
|
52
|
+
lines.push(`import "@scenar/react/styles.css";`);
|
|
53
|
+
lines.push(`import { renderStep } from ${JSON.stringify(renderImport)};`);
|
|
54
|
+
lines.push(`import * as _stepsModule from ${JSON.stringify(stepsImport)};`);
|
|
55
|
+
|
|
56
|
+
if (input.providersPath) {
|
|
57
|
+
const providersImport = toPosix(input.providersPath).replace(/\.[^.]+$/, "");
|
|
58
|
+
lines.push(
|
|
59
|
+
`import { PreviewProviders as _Providers } from ${JSON.stringify(providersImport)};`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Steps resolution (duck-type the first array of objects with delayMs) ---
|
|
64
|
+
lines.push(``);
|
|
65
|
+
lines.push(`function _findSteps(mod: Record<string, unknown>): unknown[] {`);
|
|
66
|
+
lines.push(` for (const val of Object.values(mod)) {`);
|
|
67
|
+
lines.push(` if (`);
|
|
68
|
+
lines.push(` Array.isArray(val) &&`);
|
|
69
|
+
lines.push(` val.length > 0 &&`);
|
|
70
|
+
lines.push(` typeof val[0] === "object" &&`);
|
|
71
|
+
lines.push(` val[0] !== null &&`);
|
|
72
|
+
lines.push(` "delayMs" in val[0]`);
|
|
73
|
+
lines.push(` ) return val;`);
|
|
74
|
+
lines.push(` }`);
|
|
75
|
+
lines.push(` throw new Error("No steps array found in ${escapeForString(stepsImport)}");`);
|
|
76
|
+
lines.push(`}`);
|
|
77
|
+
lines.push(``);
|
|
78
|
+
lines.push(`const _steps: any = _findSteps(_stepsModule as unknown as Record<string, unknown>);`);
|
|
79
|
+
|
|
80
|
+
// --- Narration manifest URL resolver (stable module-level reference) ---
|
|
81
|
+
// The manifest is fetched at runtime from its own relative location so that
|
|
82
|
+
// useNarrationManifest resolves each clip src against it (audio lives under
|
|
83
|
+
// ./narration/). A stable resolver avoids refetch-on-every-render.
|
|
84
|
+
if (input.hasNarration) {
|
|
85
|
+
lines.push(``);
|
|
86
|
+
lines.push(`const _resolveManifestUrl = () => "./narration/manifest.json";`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- App tree ---
|
|
90
|
+
lines.push(``);
|
|
91
|
+
lines.push(`function _App() {`);
|
|
92
|
+
if (input.hasNarration) {
|
|
93
|
+
lines.push(` const _manifest = useNarrationManifest(${JSON.stringify(input.scenarioId)}, _resolveManifestUrl);`);
|
|
94
|
+
}
|
|
95
|
+
lines.push(` const _bundle = {`);
|
|
96
|
+
lines.push(` id: ${JSON.stringify(input.scenarioId)},`);
|
|
97
|
+
lines.push(` steps: _steps,`);
|
|
98
|
+
lines.push(` narrationManifest: ${input.hasNarration ? "_manifest" : "undefined"},`);
|
|
99
|
+
lines.push(` };`);
|
|
100
|
+
const open = input.providersPath ? `<_Providers>` : ``;
|
|
101
|
+
const close = input.providersPath ? `</_Providers>` : ``;
|
|
102
|
+
lines.push(` return (`);
|
|
103
|
+
lines.push(` <div className={SCENAR_CLASS}>`);
|
|
104
|
+
lines.push(` ${open}`);
|
|
105
|
+
lines.push(` <DemoViewport canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
106
|
+
lines.push(` <ScenarioPlayer bundle={_bundle} embed>`);
|
|
107
|
+
lines.push(` {(data: any, stepIndex: number) => renderStep(data, stepIndex)}`);
|
|
108
|
+
lines.push(` </ScenarioPlayer>`);
|
|
109
|
+
lines.push(` </DemoViewport>`);
|
|
110
|
+
lines.push(` ${close}`);
|
|
111
|
+
lines.push(` </div>`);
|
|
112
|
+
lines.push(` );`);
|
|
113
|
+
lines.push(`}`);
|
|
114
|
+
|
|
115
|
+
// --- Mount ---
|
|
116
|
+
lines.push(``);
|
|
117
|
+
lines.push(`const _rootEl = document.getElementById("root");`);
|
|
118
|
+
lines.push(`if (!_rootEl) throw new Error("Embed root element #root not found");`);
|
|
119
|
+
lines.push(`createRoot(_rootEl).render(<_App />);`);
|
|
120
|
+
lines.push(``);
|
|
121
|
+
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Produce the index.html that boots the embed. The entry is referenced as an
|
|
127
|
+
* external module script (`<script type="module" src>`) — never inline — so the
|
|
128
|
+
* server CSP (`script-src 'self'`) is satisfied without loosening.
|
|
129
|
+
*/
|
|
130
|
+
export function generateEmbedHtml(scenarioId: string, entryFileName: string): string {
|
|
131
|
+
return [
|
|
132
|
+
`<!doctype html>`,
|
|
133
|
+
`<html lang="en">`,
|
|
134
|
+
` <head>`,
|
|
135
|
+
` <meta charset="utf-8" />`,
|
|
136
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
|
|
137
|
+
` <title>${escapeHtml(scenarioId)}</title>`,
|
|
138
|
+
` </head>`,
|
|
139
|
+
` <body>`,
|
|
140
|
+
` <div id="root"></div>`,
|
|
141
|
+
` <script type="module" src=${JSON.stringify("./" + entryFileName)}></script>`,
|
|
142
|
+
` </body>`,
|
|
143
|
+
`</html>`,
|
|
144
|
+
``,
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Convert a filesystem path to posix separators (for bundler imports). */
|
|
149
|
+
function toPosix(fsPath: string): string {
|
|
150
|
+
return fsPath.split(posix.sep === "/" ? /\\/ : /[\\/]/).join("/");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function escapeForString(value: string): string {
|
|
154
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function escapeHtml(value: string): string {
|
|
158
|
+
return value
|
|
159
|
+
.replace(/&/g, "&")
|
|
160
|
+
.replace(/</g, "<")
|
|
161
|
+
.replace(/>/g, ">");
|
|
162
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Viewport } from "./viewport.js";
|
|
2
|
+
/** One uploadable file, described exactly as the deploy session needs it. */
|
|
3
|
+
export interface PackFile {
|
|
4
|
+
/** Bundle-relative POSIX path (e.g. "assets/index-abc.js"). */
|
|
5
|
+
readonly path: string;
|
|
6
|
+
/** Lowercase-hex SHA-256 of the bytes (64 chars). */
|
|
7
|
+
readonly sha256: string;
|
|
8
|
+
/** Byte length (> 0). */
|
|
9
|
+
readonly sizeBytes: number;
|
|
10
|
+
/** Content type the server binds into the presign and the edge serves. */
|
|
11
|
+
readonly contentType: string;
|
|
12
|
+
}
|
|
13
|
+
/** The deploy instruction file `scenar deploy` consumes. */
|
|
14
|
+
export interface PackManifest {
|
|
15
|
+
readonly schemaVersion: number;
|
|
16
|
+
readonly scenarioId: string;
|
|
17
|
+
/** Files to upload — excludes the manifest itself. */
|
|
18
|
+
readonly files: PackFile[];
|
|
19
|
+
}
|
|
20
|
+
/** Name of the manifest file written at the bundle root (not itself uploaded). */
|
|
21
|
+
export declare const PACK_MANIFEST_FILE = "pack-manifest.json";
|
|
22
|
+
/** Name of the required scenario descriptor at the bundle root. */
|
|
23
|
+
export declare const SCENARIO_JSON_FILE = "scenario.json";
|
|
24
|
+
/**
|
|
25
|
+
* Write the bundle's scenario.json — the required-at-root descriptor. Kept
|
|
26
|
+
* intentionally minimal (a validated metadata header) plus the canonical
|
|
27
|
+
* `viewport` baked into the bundle: the player reads its steps from the bundled
|
|
28
|
+
* JS, so the full serialized spec is still a follow-up, but `deploy` needs the
|
|
29
|
+
* viewport to derive a correctly-proportioned embed snippet (DD-004). The
|
|
30
|
+
* viewport mirrors `ViewportConfig { width, height }`. Validated against the
|
|
31
|
+
* backend rules before writing (which accept the extra `viewport` key).
|
|
32
|
+
*/
|
|
33
|
+
export declare function writeScenarioJson(outDir: string, scenarioId: string, generatorVersion: string, viewport: Viewport): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Walk the build output and produce the pack manifest: one validated entry per
|
|
36
|
+
* file (excluding the manifest itself), with a lowercase-hex SHA-256, byte size,
|
|
37
|
+
* and content type. Throws with a clear message if any file violates the bundle
|
|
38
|
+
* contract (e.g. a disallowed extension), so the failure is local and obvious.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildPackManifest(outDir: string, scenarioId: string): Promise<PackManifest>;
|
|
41
|
+
/** Write pack-manifest.json at the bundle root. */
|
|
42
|
+
export declare function writePackManifest(outDir: string, manifest: PackManifest): Promise<void>;
|
|
43
|
+
/** True if every listed file exists on disk under outDir with the recorded size. */
|
|
44
|
+
export declare function verifyManifestFilesExist(outDir: string, manifest: PackManifest): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=pack-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack-manifest.d.ts","sourceRoot":"","sources":["../../../src/pack/pack-manifest.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yBAAyB;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC;CAC5B;AAED,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,mEAAmE;AACnE,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA+BjG;AAED,mDAAmD;AACnD,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7F;AAiBD,oFAAoF;AACpF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQpG"}
|