@sequio/runtime 0.1.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/dist/assets.d.ts +46 -0
- package/dist/compile.d.ts +16 -0
- package/dist/composer.d.ts +74 -0
- package/dist/composition.d.ts +49 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +397 -0
- package/dist/module-runtime.d.ts +41 -0
- package/dist/node-fs.cjs +2 -0
- package/dist/node-fs.d.ts +9 -0
- package/dist/node-fs.js +48 -0
- package/dist/runtime.d.ts +93 -0
- package/dist/vfs.d.ts +44 -0
- package/package.json +62 -0
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Local media assets** for a composition.
|
|
3
|
+
*
|
|
4
|
+
* A composition run by the runtime describes its video imperatively and can pull
|
|
5
|
+
* in local media — an `image` / `video` sitting next to it — through a single
|
|
6
|
+
* host-provided hook. The composition asks for a file by a project-relative path
|
|
7
|
+
* and gets back a `Blob`, which both `ImageSource` and `VideoSource` accept:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineComposition, loadAsset } from '@sequio/runtime';
|
|
11
|
+
* export default defineComposition(async () => {
|
|
12
|
+
* const video = new VideoSource({ src: await loadAsset('./clip.mp4') });
|
|
13
|
+
* const image = new ImageSource({ src: await loadAsset('./assets/photo.jpg') });
|
|
14
|
+
* // …
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The runtime owns the *contract* (the `loadAsset` symbol on `@sequio/runtime`
|
|
19
|
+
* and the path rules here); the **host** supplies the actual bytes via
|
|
20
|
+
* {@link RuntimeOptions.loadAsset}, so the same `loadAsset('./clip.mp4')` resolves
|
|
21
|
+
* identically wherever the composition runs (contract #3): the browser preview
|
|
22
|
+
* fetches it from the dev server, the Node render reads it off disk, an editor
|
|
23
|
+
* serves it from an upload store. Assets stay **out of the source bundle** (which
|
|
24
|
+
* is text-only), so a large local `.mp4` never bloats what's transferred or
|
|
25
|
+
* committed.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A host's binary-asset resolver: map a project-relative media path (already
|
|
29
|
+
* cleaned by {@link resolveAssetPath}) to its bytes as a `Blob`.
|
|
30
|
+
*/
|
|
31
|
+
export type AssetLoader = (path: string) => Promise<Blob>;
|
|
32
|
+
/**
|
|
33
|
+
* Canonicalise a composition's asset reference to a clean project-relative path.
|
|
34
|
+
* Accepts `./clip.mp4`, `clip.mp4` or `/clip.mp4` and returns `clip.mp4`;
|
|
35
|
+
* `./assets/x.png` → `assets/x.png`. Paths resolve against the **project root**
|
|
36
|
+
* (the bundle's virtual `/`). Rejects anything that escapes it (`..`), so a host
|
|
37
|
+
* loader can never be steered outside the project directory.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveAssetPath(path: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* The default `loadAsset` when a host wires no {@link AssetLoader}: fail loudly
|
|
42
|
+
* (per the repo's "throw, don't render silent black" convention) so a composition
|
|
43
|
+
* that references a local file isn't silently handed nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare const NO_ASSET_LOADER: AssetLoader;
|
|
46
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Which language a file is written in, inferred from its extension. */
|
|
2
|
+
export type SourceLang = 'ts' | 'tsx' | 'js' | 'jsx' | 'json';
|
|
3
|
+
/** Infer the language from a file path's extension (defaults to `ts`). */
|
|
4
|
+
export declare function langOf(path: string): SourceLang;
|
|
5
|
+
export interface CompileResult {
|
|
6
|
+
/** The emitted CommonJS code. */
|
|
7
|
+
code: string;
|
|
8
|
+
/** Syntactic diagnostics as human-readable strings (empty when clean). */
|
|
9
|
+
diagnostics: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Transpile `code` (a TS/JS source) to CommonJS. `.json` is wrapped as a module
|
|
13
|
+
* exporting the parsed value. Throws on a hard parse failure with the file name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compileModule(code: string, fileName: string): CompileResult;
|
|
16
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AudioEngine, Compositor, ExportOptions, RealtimeClock } from '@sequio/engine';
|
|
2
|
+
import { Composition, CompositionEnv } from './composition';
|
|
3
|
+
/** A portable snapshot of the program: its source files and entry path. */
|
|
4
|
+
export interface RuntimeBundle {
|
|
5
|
+
/** Absolute virtual path → source text. */
|
|
6
|
+
files: Record<string, string>;
|
|
7
|
+
/** Entry module path (e.g. `/index.ts`). */
|
|
8
|
+
entry: string;
|
|
9
|
+
}
|
|
10
|
+
/** A built, initialized composition graph plus a teardown. */
|
|
11
|
+
export interface BuiltComposition {
|
|
12
|
+
compositor: Compositor;
|
|
13
|
+
audioEngine: AudioEngine;
|
|
14
|
+
/** Timeline duration in seconds (builder value, else derived from clip ends). */
|
|
15
|
+
duration: number;
|
|
16
|
+
/** Dispose the compositor and its audio engine. */
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
/** A live preview: the built graph, its clock, and media-element-style controls. */
|
|
20
|
+
export interface PreviewHandle {
|
|
21
|
+
readonly built: BuiltComposition;
|
|
22
|
+
readonly clock: RealtimeClock;
|
|
23
|
+
readonly view: HTMLCanvasElement;
|
|
24
|
+
readonly duration: number;
|
|
25
|
+
play(): void;
|
|
26
|
+
pause(): void;
|
|
27
|
+
seek(t: number): void;
|
|
28
|
+
readonly playing: boolean;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Re-links + runs the program for a given environment and returns its
|
|
33
|
+
* {@link Composition}. The runtime supplies this so each build gets a fresh graph
|
|
34
|
+
* whose engine `Compositor` already has the environment's options folded in —
|
|
35
|
+
* which is why user code just writes `new Compositor({ width, height, timebase })`
|
|
36
|
+
* with no env plumbing.
|
|
37
|
+
*/
|
|
38
|
+
export type CompositionLinker = (env: CompositionEnv) => Composition;
|
|
39
|
+
export declare class Composer {
|
|
40
|
+
private readonly link;
|
|
41
|
+
private readonly bundle;
|
|
42
|
+
constructor(link: CompositionLinker, bundle: RuntimeBundle);
|
|
43
|
+
/** The entry module path the program ran from. */
|
|
44
|
+
get entry(): string;
|
|
45
|
+
/** The source files, as a fresh copy (edit freely; the Composer keeps its own). */
|
|
46
|
+
get files(): Record<string, string>;
|
|
47
|
+
/**
|
|
48
|
+
* The portable code bundle to hand to server-side rendering: the same files +
|
|
49
|
+
* entry, run by a runtime on the server. This replaces "serialize a spec" —
|
|
50
|
+
* the code itself is what ships, so there's no schema to drift.
|
|
51
|
+
*/
|
|
52
|
+
toBundle(): RuntimeBundle;
|
|
53
|
+
/** `JSON.stringify(composer)` yields the portable bundle. */
|
|
54
|
+
toJSON(): RuntimeBundle;
|
|
55
|
+
/**
|
|
56
|
+
* Run the builder to a fresh, initialized graph. The caller owns the result and
|
|
57
|
+
* must {@link BuiltComposition.dispose} it. `env` lets a host inject a renderer
|
|
58
|
+
* (Node) or an output scale; it defaults to the browser (`{}`).
|
|
59
|
+
*/
|
|
60
|
+
build(env?: Partial<CompositionEnv>): Promise<BuiltComposition>;
|
|
61
|
+
/**
|
|
62
|
+
* Build the graph and start a preview loop. If `container` is given the
|
|
63
|
+
* compositor's canvas is appended to it. Returns a {@link PreviewHandle} with
|
|
64
|
+
* play/pause/seek; the first frame renders immediately so the canvas is never
|
|
65
|
+
* blank before playback starts.
|
|
66
|
+
*/
|
|
67
|
+
preview(container?: HTMLElement, env?: Partial<CompositionEnv>): Promise<PreviewHandle>;
|
|
68
|
+
/**
|
|
69
|
+
* Render the composition to a video `Blob` in the client. Builds a fresh graph,
|
|
70
|
+
* runs the engine's {@link Exporter} over `[0, duration]`, then disposes it.
|
|
71
|
+
*/
|
|
72
|
+
export(options?: Partial<ExportOptions>, onProgress?: (progress: number) => void): Promise<Blob>;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AudioEngine, Compositor, CompositorOptions } from '@sequio/engine';
|
|
2
|
+
/** Brand marking a value produced by {@link defineComposition}. */
|
|
3
|
+
export declare const COMPOSITION_TAG: "@sequio/runtime:composition";
|
|
4
|
+
/**
|
|
5
|
+
* The live object graph a builder returns: an initialized {@link Compositor} plus
|
|
6
|
+
* (optionally) the {@link AudioEngine} driving its audio, and the timeline length.
|
|
7
|
+
*/
|
|
8
|
+
export interface CompositionResult {
|
|
9
|
+
/** The built + `init()`-ed compositor to preview / export / server-render. */
|
|
10
|
+
compositor: Compositor;
|
|
11
|
+
/** The audio engine for preview playback and the export mix, if any. */
|
|
12
|
+
audioEngine?: AudioEngine;
|
|
13
|
+
/**
|
|
14
|
+
* Timeline duration in seconds. Optional — when omitted it's derived from the
|
|
15
|
+
* largest clip end across the compositor's tracks (see {@link deriveDuration}).
|
|
16
|
+
*/
|
|
17
|
+
duration?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The environment a composition is built in. The same builder runs for client
|
|
21
|
+
* preview, client export and server render; `compositorOptions` is how the host
|
|
22
|
+
* injects what differs (a Node WebGPU renderer, an output scale), so portable
|
|
23
|
+
* code spreads it into `new Compositor({ ... })`.
|
|
24
|
+
*/
|
|
25
|
+
export interface CompositionEnv {
|
|
26
|
+
/** Merge into the `Compositor` options. Browser: `{}`; Node: `{ createRenderer, resolution }`. */
|
|
27
|
+
readonly compositorOptions: Partial<CompositorOptions>;
|
|
28
|
+
/** Where this build runs, in case user code wants to branch (e.g. output scale). */
|
|
29
|
+
readonly target: 'preview' | 'export' | 'server';
|
|
30
|
+
}
|
|
31
|
+
/** A function that builds a live composition. May be async (it `await`s `init()`). */
|
|
32
|
+
export type CompositionBuilder = (env: CompositionEnv) => CompositionResult | Promise<CompositionResult>;
|
|
33
|
+
/** A tagged builder returned by {@link defineComposition}. */
|
|
34
|
+
export interface Composition {
|
|
35
|
+
readonly __tag: typeof COMPOSITION_TAG;
|
|
36
|
+
readonly build: CompositionBuilder;
|
|
37
|
+
}
|
|
38
|
+
/** Whether `value` is a {@link Composition} produced by {@link defineComposition}. */
|
|
39
|
+
export declare function isComposition(value: unknown): value is Composition;
|
|
40
|
+
/**
|
|
41
|
+
* Tag a builder function as a composition. Kept intentionally thin — all the
|
|
42
|
+
* power is in the builder body (real engine classes, real control flow); this
|
|
43
|
+
* just marks the value so the runtime can tell "the user returned a composition"
|
|
44
|
+
* from "the user returned something else", and fails loudly if handed a non-function.
|
|
45
|
+
*/
|
|
46
|
+
export declare function defineComposition(build: CompositionBuilder): Composition;
|
|
47
|
+
/** Largest clip end across every track of a compositor (the timeline duration). */
|
|
48
|
+
export declare function deriveDuration(compositor: Compositor): number;
|
|
49
|
+
//# sourceMappingURL=composition.d.ts.map
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("typescript"),f=require("@sequio/engine");function R(s){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(s){for(const t in s)if(t!=="default"){const n=Object.getOwnPropertyDescriptor(s,t);Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>s[t]})}}return e.default=s,Object.freeze(e)}const x=R(f);function u(s){const e=s.startsWith("/"),t=[];for(const n of s.split("/"))n===""||n==="."||(n===".."?t.length>0&&t[t.length-1]!==".."?t.pop():e||t.push(".."):t.push(n));return(e?"/":"")+t.join("/")}function y(s){const e=u(s),t=e.lastIndexOf("/");return t<=0?"/":e.slice(0,t)}function T(s,e){return e.startsWith("/")?u(e):u(`${s}/${e}`)}class m{files=new Map;constructor(e={}){for(const[t,n]of Object.entries(e))this.writeFile(t,n)}writeFile(e,t){this.files.set(u(e.startsWith("/")?e:`/${e}`),t)}deleteFile(e){return this.files.delete(u(e.startsWith("/")?e:`/${e}`))}readFile(e){const t=this.files.get(u(e));return t===void 0?null:t}exists(e){return this.files.has(u(e))}listFiles(){return[...this.files.keys()].sort()}}function S(s){return s.endsWith(".tsx")?"tsx":s.endsWith(".ts")?"ts":s.endsWith(".jsx")?"jsx":s.endsWith(".json")?"json":s.endsWith(".js")||s.endsWith(".mjs")||s.endsWith(".cjs")?"js":"ts"}function O(s,e){const t=S(e);if(t==="json"){let r;try{r=JSON.parse(s)}catch(l){throw new Error(`Failed to parse JSON module ${e}: ${l.message}`)}return{code:`module.exports = ${JSON.stringify(r)};`,diagnostics:[]}}const n=d.transpileModule(s,{fileName:e,reportDiagnostics:!0,compilerOptions:{module:d.ModuleKind.CommonJS,target:d.ScriptTarget.ES2020,esModuleInterop:!0,isolatedModules:!0,jsx:t==="tsx"||t==="jsx"?d.JsxEmit.Preserve:d.JsxEmit.None,allowJs:!0}}),o=(n.diagnostics??[]).map(r=>d.flattenDiagnosticMessageText(r.messageText,`
|
|
2
|
+
`));return{code:n.outputText,diagnostics:o}}const M=[".ts",".tsx",".js",".jsx",".mjs",".cjs",".json"];class p extends Error{constructor(e,t){super(`Cannot resolve module '${e}' from '${t}'`),this.specifier=e,this.from=t,this.name="ModuleResolutionError"}}class C{fs;externals;compile;cache=new Map;constructor(e){this.fs=e.fs,this.externals=e.externals??{},this.compile=e.compile??((t,n)=>O(t,n).code)}resolve(e,t){const n=T(t,e),o=[n,...M.map(r=>n+r),...M.map(r=>u(`${n}/index${r}`))];for(const r of o)if(this.fs.exists(r))return r;return null}isBare(e){return!e.startsWith("./")&&!e.startsWith("../")&&!e.startsWith("/")}require(e,t){if(this.isBare(e)){if(e in this.externals)return this.externals[e];throw new p(e,t)}const n=this.resolve(e,y(t));if(!n)throw new p(e,t);return this.load(n)}load(e){const t=u(e),n=this.cache.get(t);if(n)return n.exports;const o=this.fs.readFile(t);if(o===null)throw new Error(`File not found: ${t}`);const r={exports:{}};this.cache.set(t,r);const l=this.compile(o,t),h=c=>this.require(c,t),i=y(t),a=new Function("exports","require","module","__filename","__dirname",l);try{a(r.exports,h,r,t,i)}catch(c){if(this.cache.delete(t),c instanceof p)throw c;const g=c instanceof Error?c.message:String(c);throw new Error(`Error executing module '${t}': ${g}`)}return r.exports}run(e){const t=this.fs.exists(e)?u(e):this.resolve(e.startsWith(".")?e:`./${e}`,"/");if(!t)throw new Error(`Entry module not found: ${e}`);return this.load(t)}}function I(s){const e=[];for(const t of s.replace(/\\/g,"/").split("/"))if(!(t===""||t===".")){if(t==="..")throw new Error(`asset path escapes the project root: ${s}`);e.push(t)}if(e.length===0)throw new Error(`empty asset path: ${s}`);return e.join("/")}const b=s=>{throw new Error(`loadAsset('${s}'): this runtime has no asset loader. The host must pass RuntimeOptions.loadAsset (the sequio CLI does this for both preview and render). To avoid a local file entirely, reference a network URL instead.`)},E="@sequio/runtime:composition";function j(s){return typeof s=="object"&&s!==null&&s.__tag===E&&typeof s.build=="function"}function v(s){if(typeof s!="function")throw new Error("defineComposition expects a builder function (env) => { compositor, duration }.");return{__tag:E,build:s}}function $(s){let e=0;for(const t of s.getTracks())for(const n of t.clips)n.end>e&&(e=n.end);return e}const W={compositorOptions:{},target:"export"};class k{constructor(e,t){this.link=e,this.bundle=t}get entry(){return this.bundle.entry}get files(){return{...this.bundle.files}}toBundle(){return{files:{...this.bundle.files},entry:this.bundle.entry}}toJSON(){return this.toBundle()}async build(e={}){const t={...W,...e},n=await this.link(t).build(t),o=n.compositor,r=n.duration??$(o),l=n.audioEngine??new f.AudioEngine(new f.Timebase(30));return{compositor:o,audioEngine:l,duration:r,dispose(){l.dispose(),o.dispose()}}}async preview(e,t={}){const n=await this.build({target:"preview",...t}),{compositor:o,audioEngine:r,duration:l}=n,h=o.view;e&&e.append(h);const i=new f.RealtimeClock;i.duration=l;let a=!1;const c=i.onTick(w=>o.renderPreview(w)),g=i.onEnded(()=>{a=!1,r.pause()});return i.seek(0),o.renderPreview(0),{built:n,clock:i,view:h,duration:l,get playing(){return a},play(){a=!0,i.play(),r.play(i.currentTime)},pause(){a=!1,i.pause(),r.pause()},seek(w){i.seek(w),o.renderPreview(i.currentTime),a&&r.seek(i.currentTime)},dispose(){a=!1,c.unsubscribe(),g.unsubscribe(),i.pause(),n.dispose()}}}async export(e={},t){const n=await this.build({target:"export"});try{return await new f.Exporter(n.compositor,n.audioEngine).export({fps:30,range:[0,n.duration],audio:!1,...e},t)}finally{n.dispose()}}}const A={defineComposition:v,isComposition:j,COMPOSITION_TAG:E,loadAsset:b},D="@sequio/runtime",F="@sequio/engine",_=["/index.ts","/index.tsx","/index.js","/main.ts","/main.js"];function L(s){if(Object.keys(s).length===0)return x;class e extends x.Compositor{constructor(n){super({...n,...s})}}return{...x,Compositor:e}}function U(s){return s?s instanceof m||typeof s.readFile=="function"?s:new m(s):new m}function P(s){let e=s;if(e&&typeof e=="object"&&"default"in e&&(e=e.default),j(e))return e;if(typeof e=="function")return v(e);throw new Error("Entry module must export (as default) a Composition from defineComposition(builder) or a builder function.")}class N{fs;entry;hostExternals;compileCache=new Map;constructor(e={}){this.fs=U(e.files),this.entry=e.entry;const t=e.loadAsset??b,n={...A,loadAsset:o=>t(I(o))};this.hostExternals={[D]:n,...e.externals}}get fileSystem(){return this.fs}resolveEntry(){if(this.entry)return this.entry;for(const e of _)if(this.fs.exists(e))return e;throw new Error(`No entry module. Provide one, or add ${_.join(" / ")}. Have: ${this.fs.listFiles().join(", ")||"(no files)"}`)}toBundle(e){const t={};for(const n of this.fs.listFiles()){const o=this.fs.readFile(n);o!==null&&(t[n]=o)}return{files:t,entry:e}}compile=(e,t)=>{const n=`${t}\0${e}`;let o=this.compileCache.get(n);return o===void 0&&(o=O(e,t).code,this.compileCache.set(n,o)),o};link(e){const t={[F]:L(e.compositorOptions),...this.hostExternals},n=new C({fs:this.fs,externals:t,compile:this.compile});return P(n.run(this.resolveEntry()))}runToComposition(){return this.link({compositorOptions:{},target:"preview"})}async run(){const e=this.resolveEntry();return this.runToComposition(),new k(t=>this.link(t),this.toBundle(e))}}async function q(s,e={}){return new N({files:s,...e}).run()}exports.COMPOSITION_TAG=E;exports.Composer=k;exports.ENGINE_MODULE_ID=F;exports.InMemoryFileSystem=m;exports.ModuleResolutionError=p;exports.ModuleRuntime=C;exports.NO_ASSET_LOADER=b;exports.RUNTIME_MODULE_API=A;exports.RUNTIME_MODULE_ID=D;exports.Runtime=N;exports.compileModule=O;exports.defineComposition=v;exports.deriveDuration=$;exports.dirname=y;exports.isComposition=j;exports.joinPath=T;exports.langOf=S;exports.normalizePath=u;exports.resolveAssetPath=I;exports.runComposition=q;
|
|
3
|
+
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sequio/runtime` — compile and run multi-file TS/JS into a
|
|
3
|
+
* {@link Composer}.
|
|
4
|
+
*
|
|
5
|
+
* The runtime takes a set of source files (an in-memory {@link InMemoryFileSystem}
|
|
6
|
+
* or an injected real one), transpiles each with the TypeScript compiler, links
|
|
7
|
+
* them with a tiny CommonJS loader, and runs the entry. The program describes a
|
|
8
|
+
* video **imperatively** — it `new`s the engine's own classes inside a
|
|
9
|
+
* `defineComposition(builder)` (same style as the `example/` demos), so a user
|
|
10
|
+
* can bring their own `Clip` / `Effect` subclasses with no schema to keep in
|
|
11
|
+
* sync. The result is a {@link Composer} that previews and exports in the browser
|
|
12
|
+
* and whose `toBundle()` (the source files themselves) is what feeds server-side
|
|
13
|
+
* rendering — one object, three destinations.
|
|
14
|
+
*
|
|
15
|
+
* Everything here is browser-safe. A Node/real-filesystem adapter lives in
|
|
16
|
+
* `@sequio/runtime/node-fs` (imports `node:fs`, kept out of this
|
|
17
|
+
* barrel) so importing the runtime never drags Node built-ins into a bundle.
|
|
18
|
+
*/
|
|
19
|
+
export { type FileSystem, InMemoryFileSystem, normalizePath, dirname, joinPath, } from './vfs';
|
|
20
|
+
export { compileModule, langOf, type CompileResult, type SourceLang, } from './compile';
|
|
21
|
+
export { ModuleRuntime, ModuleResolutionError, type Externals, type ModuleRuntimeOptions, } from './module-runtime';
|
|
22
|
+
export { type AssetLoader, resolveAssetPath, NO_ASSET_LOADER, } from './assets';
|
|
23
|
+
export { defineComposition, isComposition, deriveDuration, COMPOSITION_TAG, type Composition, type CompositionBuilder, type CompositionEnv, type CompositionResult, } from './composition';
|
|
24
|
+
export { Runtime, runComposition, RUNTIME_MODULE_API, RUNTIME_MODULE_ID, ENGINE_MODULE_ID, type RuntimeOptions, } from './runtime';
|
|
25
|
+
export { Composer, type PreviewHandle, type RuntimeBundle, type BuiltComposition, } from './composer';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import d from "typescript";
|
|
2
|
+
import * as m from "@sequio/engine";
|
|
3
|
+
import { AudioEngine as $, Timebase as C, RealtimeClock as M, Exporter as _ } from "@sequio/engine";
|
|
4
|
+
function u(s) {
|
|
5
|
+
const t = s.startsWith("/"), e = [];
|
|
6
|
+
for (const n of s.split("/"))
|
|
7
|
+
n === "" || n === "." || (n === ".." ? e.length > 0 && e[e.length - 1] !== ".." ? e.pop() : t || e.push("..") : e.push(n));
|
|
8
|
+
return (t ? "/" : "") + e.join("/");
|
|
9
|
+
}
|
|
10
|
+
function g(s) {
|
|
11
|
+
const t = u(s), e = t.lastIndexOf("/");
|
|
12
|
+
return e <= 0 ? "/" : t.slice(0, e);
|
|
13
|
+
}
|
|
14
|
+
function k(s, t) {
|
|
15
|
+
return t.startsWith("/") ? u(t) : u(`${s}/${t}`);
|
|
16
|
+
}
|
|
17
|
+
class w {
|
|
18
|
+
files = /* @__PURE__ */ new Map();
|
|
19
|
+
constructor(t = {}) {
|
|
20
|
+
for (const [e, n] of Object.entries(t)) this.writeFile(e, n);
|
|
21
|
+
}
|
|
22
|
+
/** Add/replace a file. Keys are normalized to absolute paths. */
|
|
23
|
+
writeFile(t, e) {
|
|
24
|
+
this.files.set(u(t.startsWith("/") ? t : `/${t}`), e);
|
|
25
|
+
}
|
|
26
|
+
/** Remove a file; returns whether it existed. */
|
|
27
|
+
deleteFile(t) {
|
|
28
|
+
return this.files.delete(u(t.startsWith("/") ? t : `/${t}`));
|
|
29
|
+
}
|
|
30
|
+
readFile(t) {
|
|
31
|
+
const e = this.files.get(u(t));
|
|
32
|
+
return e === void 0 ? null : e;
|
|
33
|
+
}
|
|
34
|
+
exists(t) {
|
|
35
|
+
return this.files.has(u(t));
|
|
36
|
+
}
|
|
37
|
+
listFiles() {
|
|
38
|
+
return [...this.files.keys()].sort();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function F(s) {
|
|
42
|
+
return s.endsWith(".tsx") ? "tsx" : s.endsWith(".ts") ? "ts" : s.endsWith(".jsx") ? "jsx" : s.endsWith(".json") ? "json" : s.endsWith(".js") || s.endsWith(".mjs") || s.endsWith(".cjs") ? "js" : "ts";
|
|
43
|
+
}
|
|
44
|
+
function v(s, t) {
|
|
45
|
+
const e = F(t);
|
|
46
|
+
if (e === "json") {
|
|
47
|
+
let r;
|
|
48
|
+
try {
|
|
49
|
+
r = JSON.parse(s);
|
|
50
|
+
} catch (l) {
|
|
51
|
+
throw new Error(`Failed to parse JSON module ${t}: ${l.message}`);
|
|
52
|
+
}
|
|
53
|
+
return { code: `module.exports = ${JSON.stringify(r)};`, diagnostics: [] };
|
|
54
|
+
}
|
|
55
|
+
const n = d.transpileModule(s, {
|
|
56
|
+
fileName: t,
|
|
57
|
+
reportDiagnostics: !0,
|
|
58
|
+
compilerOptions: {
|
|
59
|
+
module: d.ModuleKind.CommonJS,
|
|
60
|
+
target: d.ScriptTarget.ES2020,
|
|
61
|
+
esModuleInterop: !0,
|
|
62
|
+
// Keep the transform isolated & fast — no cross-file inference. Matches how
|
|
63
|
+
// the linker treats each file independently.
|
|
64
|
+
isolatedModules: !0,
|
|
65
|
+
jsx: e === "tsx" || e === "jsx" ? d.JsxEmit.Preserve : d.JsxEmit.None,
|
|
66
|
+
// `require`/`module`/`exports` are provided by the linker's wrapper.
|
|
67
|
+
allowJs: !0
|
|
68
|
+
}
|
|
69
|
+
}), o = (n.diagnostics ?? []).map(
|
|
70
|
+
(r) => d.flattenDiagnosticMessageText(r.messageText, `
|
|
71
|
+
`)
|
|
72
|
+
);
|
|
73
|
+
return { code: n.outputText, diagnostics: o };
|
|
74
|
+
}
|
|
75
|
+
const y = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
|
|
76
|
+
class x extends Error {
|
|
77
|
+
constructor(t, e) {
|
|
78
|
+
super(`Cannot resolve module '${t}' from '${e}'`), this.specifier = t, this.from = e, this.name = "ModuleResolutionError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
class S {
|
|
82
|
+
fs;
|
|
83
|
+
externals;
|
|
84
|
+
compile;
|
|
85
|
+
cache = /* @__PURE__ */ new Map();
|
|
86
|
+
constructor(t) {
|
|
87
|
+
this.fs = t.fs, this.externals = t.externals ?? {}, this.compile = t.compile ?? ((e, n) => v(e, n).code);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a relative specifier against the VFS to a concrete file path, or
|
|
91
|
+
* `null` if nothing matches. Bare specifiers are handled by {@link require}
|
|
92
|
+
* against `externals` and are not resolved here.
|
|
93
|
+
*/
|
|
94
|
+
resolve(t, e) {
|
|
95
|
+
const n = k(e, t), o = [
|
|
96
|
+
n,
|
|
97
|
+
...y.map((r) => n + r),
|
|
98
|
+
...y.map((r) => u(`${n}/index${r}`))
|
|
99
|
+
];
|
|
100
|
+
for (const r of o)
|
|
101
|
+
if (this.fs.exists(r)) return r;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/** Whether a specifier is bare (a package name, not a path). */
|
|
105
|
+
isBare(t) {
|
|
106
|
+
return !t.startsWith("./") && !t.startsWith("../") && !t.startsWith("/");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `require` as seen from a module located at `fromPath`. Returns the resolved
|
|
110
|
+
* module's `exports` (bare specifiers return the injected external verbatim).
|
|
111
|
+
*/
|
|
112
|
+
require(t, e) {
|
|
113
|
+
if (this.isBare(t)) {
|
|
114
|
+
if (t in this.externals) return this.externals[t];
|
|
115
|
+
throw new x(t, e);
|
|
116
|
+
}
|
|
117
|
+
const n = this.resolve(t, g(e));
|
|
118
|
+
if (!n) throw new x(t, e);
|
|
119
|
+
return this.load(n);
|
|
120
|
+
}
|
|
121
|
+
/** Load (and cache) the module at an already-resolved absolute path. */
|
|
122
|
+
load(t) {
|
|
123
|
+
const e = u(t), n = this.cache.get(e);
|
|
124
|
+
if (n) return n.exports;
|
|
125
|
+
const o = this.fs.readFile(e);
|
|
126
|
+
if (o === null) throw new Error(`File not found: ${e}`);
|
|
127
|
+
const r = { exports: {} };
|
|
128
|
+
this.cache.set(e, r);
|
|
129
|
+
const l = this.compile(o, e), f = (c) => this.require(c, e), i = g(e), a = new Function(
|
|
130
|
+
"exports",
|
|
131
|
+
"require",
|
|
132
|
+
"module",
|
|
133
|
+
"__filename",
|
|
134
|
+
"__dirname",
|
|
135
|
+
l
|
|
136
|
+
);
|
|
137
|
+
try {
|
|
138
|
+
a(r.exports, f, r, e, i);
|
|
139
|
+
} catch (c) {
|
|
140
|
+
if (this.cache.delete(e), c instanceof x) throw c;
|
|
141
|
+
const h = c instanceof Error ? c.message : String(c);
|
|
142
|
+
throw new Error(`Error executing module '${e}': ${h}`);
|
|
143
|
+
}
|
|
144
|
+
return r.exports;
|
|
145
|
+
}
|
|
146
|
+
/** Load the entry module and return its `exports`. */
|
|
147
|
+
run(t) {
|
|
148
|
+
const e = this.fs.exists(t) ? u(t) : this.resolve(t.startsWith(".") ? t : `./${t}`, "/");
|
|
149
|
+
if (!e) throw new Error(`Entry module not found: ${t}`);
|
|
150
|
+
return this.load(e);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function A(s) {
|
|
154
|
+
const t = [];
|
|
155
|
+
for (const e of s.replace(/\\/g, "/").split("/"))
|
|
156
|
+
if (!(e === "" || e === ".")) {
|
|
157
|
+
if (e === "..") throw new Error(`asset path escapes the project root: ${s}`);
|
|
158
|
+
t.push(e);
|
|
159
|
+
}
|
|
160
|
+
if (t.length === 0) throw new Error(`empty asset path: ${s}`);
|
|
161
|
+
return t.join("/");
|
|
162
|
+
}
|
|
163
|
+
const b = (s) => {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`loadAsset('${s}'): this runtime has no asset loader. The host must pass RuntimeOptions.loadAsset (the sequio CLI does this for both preview and render). To avoid a local file entirely, reference a network URL instead.`
|
|
166
|
+
);
|
|
167
|
+
}, E = "@sequio/runtime:composition";
|
|
168
|
+
function O(s) {
|
|
169
|
+
return typeof s == "object" && s !== null && s.__tag === E && typeof s.build == "function";
|
|
170
|
+
}
|
|
171
|
+
function T(s) {
|
|
172
|
+
if (typeof s != "function")
|
|
173
|
+
throw new Error("defineComposition expects a builder function (env) => { compositor, duration }.");
|
|
174
|
+
return { __tag: E, build: s };
|
|
175
|
+
}
|
|
176
|
+
function W(s) {
|
|
177
|
+
let t = 0;
|
|
178
|
+
for (const e of s.getTracks())
|
|
179
|
+
for (const n of e.clips) n.end > t && (t = n.end);
|
|
180
|
+
return t;
|
|
181
|
+
}
|
|
182
|
+
const I = { compositorOptions: {}, target: "export" };
|
|
183
|
+
class R {
|
|
184
|
+
constructor(t, e) {
|
|
185
|
+
this.link = t, this.bundle = e;
|
|
186
|
+
}
|
|
187
|
+
/** The entry module path the program ran from. */
|
|
188
|
+
get entry() {
|
|
189
|
+
return this.bundle.entry;
|
|
190
|
+
}
|
|
191
|
+
/** The source files, as a fresh copy (edit freely; the Composer keeps its own). */
|
|
192
|
+
get files() {
|
|
193
|
+
return { ...this.bundle.files };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The portable code bundle to hand to server-side rendering: the same files +
|
|
197
|
+
* entry, run by a runtime on the server. This replaces "serialize a spec" —
|
|
198
|
+
* the code itself is what ships, so there's no schema to drift.
|
|
199
|
+
*/
|
|
200
|
+
toBundle() {
|
|
201
|
+
return { files: { ...this.bundle.files }, entry: this.bundle.entry };
|
|
202
|
+
}
|
|
203
|
+
/** `JSON.stringify(composer)` yields the portable bundle. */
|
|
204
|
+
toJSON() {
|
|
205
|
+
return this.toBundle();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Run the builder to a fresh, initialized graph. The caller owns the result and
|
|
209
|
+
* must {@link BuiltComposition.dispose} it. `env` lets a host inject a renderer
|
|
210
|
+
* (Node) or an output scale; it defaults to the browser (`{}`).
|
|
211
|
+
*/
|
|
212
|
+
async build(t = {}) {
|
|
213
|
+
const e = { ...I, ...t }, n = await this.link(e).build(e), o = n.compositor, r = n.duration ?? W(o), l = n.audioEngine ?? new $(new C(30));
|
|
214
|
+
return {
|
|
215
|
+
compositor: o,
|
|
216
|
+
audioEngine: l,
|
|
217
|
+
duration: r,
|
|
218
|
+
dispose() {
|
|
219
|
+
l.dispose(), o.dispose();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Build the graph and start a preview loop. If `container` is given the
|
|
225
|
+
* compositor's canvas is appended to it. Returns a {@link PreviewHandle} with
|
|
226
|
+
* play/pause/seek; the first frame renders immediately so the canvas is never
|
|
227
|
+
* blank before playback starts.
|
|
228
|
+
*/
|
|
229
|
+
async preview(t, e = {}) {
|
|
230
|
+
const n = await this.build({ target: "preview", ...e }), { compositor: o, audioEngine: r, duration: l } = n, f = o.view;
|
|
231
|
+
t && t.append(f);
|
|
232
|
+
const i = new M();
|
|
233
|
+
i.duration = l;
|
|
234
|
+
let a = !1;
|
|
235
|
+
const c = i.onTick((p) => o.renderPreview(p)), h = i.onEnded(() => {
|
|
236
|
+
a = !1, r.pause();
|
|
237
|
+
});
|
|
238
|
+
return i.seek(0), o.renderPreview(0), {
|
|
239
|
+
built: n,
|
|
240
|
+
clock: i,
|
|
241
|
+
view: f,
|
|
242
|
+
duration: l,
|
|
243
|
+
get playing() {
|
|
244
|
+
return a;
|
|
245
|
+
},
|
|
246
|
+
play() {
|
|
247
|
+
a = !0, i.play(), r.play(i.currentTime);
|
|
248
|
+
},
|
|
249
|
+
pause() {
|
|
250
|
+
a = !1, i.pause(), r.pause();
|
|
251
|
+
},
|
|
252
|
+
seek(p) {
|
|
253
|
+
i.seek(p), o.renderPreview(i.currentTime), a && r.seek(i.currentTime);
|
|
254
|
+
},
|
|
255
|
+
dispose() {
|
|
256
|
+
a = !1, c.unsubscribe(), h.unsubscribe(), i.pause(), n.dispose();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Render the composition to a video `Blob` in the client. Builds a fresh graph,
|
|
262
|
+
* runs the engine's {@link Exporter} over `[0, duration]`, then disposes it.
|
|
263
|
+
*/
|
|
264
|
+
async export(t = {}, e) {
|
|
265
|
+
const n = await this.build({ target: "export" });
|
|
266
|
+
try {
|
|
267
|
+
return await new _(n.compositor, n.audioEngine).export(
|
|
268
|
+
{ fps: 30, range: [0, n.duration], audio: !1, ...t },
|
|
269
|
+
e
|
|
270
|
+
);
|
|
271
|
+
} finally {
|
|
272
|
+
n.dispose();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const N = {
|
|
277
|
+
defineComposition: T,
|
|
278
|
+
isComposition: O,
|
|
279
|
+
COMPOSITION_TAG: E,
|
|
280
|
+
loadAsset: b
|
|
281
|
+
}, D = "@sequio/runtime", L = "@sequio/engine", j = ["/index.ts", "/index.tsx", "/index.js", "/main.ts", "/main.js"];
|
|
282
|
+
function q(s) {
|
|
283
|
+
if (Object.keys(s).length === 0) return m;
|
|
284
|
+
class t extends m.Compositor {
|
|
285
|
+
constructor(n) {
|
|
286
|
+
super({ ...n, ...s });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { ...m, Compositor: t };
|
|
290
|
+
}
|
|
291
|
+
function J(s) {
|
|
292
|
+
return s ? s instanceof w || typeof s.readFile == "function" ? s : new w(s) : new w();
|
|
293
|
+
}
|
|
294
|
+
function U(s) {
|
|
295
|
+
let t = s;
|
|
296
|
+
if (t && typeof t == "object" && "default" in t && (t = t.default), O(t)) return t;
|
|
297
|
+
if (typeof t == "function") return T(t);
|
|
298
|
+
throw new Error(
|
|
299
|
+
"Entry module must export (as default) a Composition from defineComposition(builder) or a builder function."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
class B {
|
|
303
|
+
fs;
|
|
304
|
+
entry;
|
|
305
|
+
hostExternals;
|
|
306
|
+
/** Transpiled output cached across re-links (linking runs once per build). */
|
|
307
|
+
compileCache = /* @__PURE__ */ new Map();
|
|
308
|
+
constructor(t = {}) {
|
|
309
|
+
this.fs = J(t.files), this.entry = t.entry;
|
|
310
|
+
const e = t.loadAsset ?? b, n = {
|
|
311
|
+
...N,
|
|
312
|
+
loadAsset: (o) => e(A(o))
|
|
313
|
+
};
|
|
314
|
+
this.hostExternals = {
|
|
315
|
+
[D]: n,
|
|
316
|
+
...t.externals
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** The filesystem backing this runtime (add/edit files then re-`run`). */
|
|
320
|
+
get fileSystem() {
|
|
321
|
+
return this.fs;
|
|
322
|
+
}
|
|
323
|
+
resolveEntry() {
|
|
324
|
+
if (this.entry) return this.entry;
|
|
325
|
+
for (const t of j) if (this.fs.exists(t)) return t;
|
|
326
|
+
throw new Error(
|
|
327
|
+
`No entry module. Provide one, or add ${j.join(" / ")}. Have: ${this.fs.listFiles().join(", ") || "(no files)"}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
/** Snapshot the current files into a portable {@link RuntimeBundle}. */
|
|
331
|
+
toBundle(t) {
|
|
332
|
+
const e = {};
|
|
333
|
+
for (const n of this.fs.listFiles()) {
|
|
334
|
+
const o = this.fs.readFile(n);
|
|
335
|
+
o !== null && (e[n] = o);
|
|
336
|
+
}
|
|
337
|
+
return { files: e, entry: t };
|
|
338
|
+
}
|
|
339
|
+
compile = (t, e) => {
|
|
340
|
+
const n = `${e}\0${t}`;
|
|
341
|
+
let o = this.compileCache.get(n);
|
|
342
|
+
return o === void 0 && (o = v(t, e).code, this.compileCache.set(n, o)), o;
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Compile + link + run the entry for a given environment and return the
|
|
346
|
+
* {@link Composition} it produced. Called once per build so the injected engine
|
|
347
|
+
* `Compositor` carries that environment's options (implicit `compositorOptions`)
|
|
348
|
+
* and each build gets independent module state. Transpilation is cached, so
|
|
349
|
+
* re-linking only re-executes the (cheap) module bodies.
|
|
350
|
+
*/
|
|
351
|
+
link(t) {
|
|
352
|
+
const e = {
|
|
353
|
+
[L]: q(t.compositorOptions),
|
|
354
|
+
...this.hostExternals
|
|
355
|
+
}, n = new S({ fs: this.fs, externals: e, compile: this.compile });
|
|
356
|
+
return U(n.run(this.resolveEntry()));
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Compile + run the program and return the {@link Composition} its entry
|
|
360
|
+
* produced (the tagged builder), without wrapping it in a `Composer`. Uses the
|
|
361
|
+
* plain browser environment (no injected renderer).
|
|
362
|
+
*/
|
|
363
|
+
runToComposition() {
|
|
364
|
+
return this.link({ compositorOptions: {}, target: "preview" });
|
|
365
|
+
}
|
|
366
|
+
/** Compile + run the program and wrap its result in a {@link Composer}. */
|
|
367
|
+
async run() {
|
|
368
|
+
const t = this.resolveEntry();
|
|
369
|
+
return this.runToComposition(), new R((e) => this.link(e), this.toBundle(t));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function G(s, t = {}) {
|
|
373
|
+
return new B({ files: s, ...t }).run();
|
|
374
|
+
}
|
|
375
|
+
export {
|
|
376
|
+
E as COMPOSITION_TAG,
|
|
377
|
+
R as Composer,
|
|
378
|
+
L as ENGINE_MODULE_ID,
|
|
379
|
+
w as InMemoryFileSystem,
|
|
380
|
+
x as ModuleResolutionError,
|
|
381
|
+
S as ModuleRuntime,
|
|
382
|
+
b as NO_ASSET_LOADER,
|
|
383
|
+
N as RUNTIME_MODULE_API,
|
|
384
|
+
D as RUNTIME_MODULE_ID,
|
|
385
|
+
B as Runtime,
|
|
386
|
+
v as compileModule,
|
|
387
|
+
T as defineComposition,
|
|
388
|
+
W as deriveDuration,
|
|
389
|
+
g as dirname,
|
|
390
|
+
O as isComposition,
|
|
391
|
+
k as joinPath,
|
|
392
|
+
F as langOf,
|
|
393
|
+
u as normalizePath,
|
|
394
|
+
A as resolveAssetPath,
|
|
395
|
+
G as runComposition
|
|
396
|
+
};
|
|
397
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FileSystem } from './vfs';
|
|
2
|
+
/** A module value keyed by a bare specifier the sandbox can `require`. */
|
|
3
|
+
export type Externals = Record<string, unknown>;
|
|
4
|
+
export interface ModuleRuntimeOptions {
|
|
5
|
+
fs: FileSystem;
|
|
6
|
+
/** Bare-specifier → module value (e.g. the runtime API, the engine). */
|
|
7
|
+
externals?: Externals;
|
|
8
|
+
/** Transform a source file to CommonJS. Defaults to {@link compileModule}. */
|
|
9
|
+
compile?: (code: string, fileName: string) => string;
|
|
10
|
+
}
|
|
11
|
+
/** A resolution failure that names the specifier and the importer. */
|
|
12
|
+
export declare class ModuleResolutionError extends Error {
|
|
13
|
+
readonly specifier: string;
|
|
14
|
+
readonly from: string;
|
|
15
|
+
constructor(specifier: string, from: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class ModuleRuntime {
|
|
18
|
+
private readonly fs;
|
|
19
|
+
private readonly externals;
|
|
20
|
+
private readonly compile;
|
|
21
|
+
private readonly cache;
|
|
22
|
+
constructor(options: ModuleRuntimeOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a relative specifier against the VFS to a concrete file path, or
|
|
25
|
+
* `null` if nothing matches. Bare specifiers are handled by {@link require}
|
|
26
|
+
* against `externals` and are not resolved here.
|
|
27
|
+
*/
|
|
28
|
+
resolve(specifier: string, fromDir: string): string | null;
|
|
29
|
+
/** Whether a specifier is bare (a package name, not a path). */
|
|
30
|
+
private isBare;
|
|
31
|
+
/**
|
|
32
|
+
* `require` as seen from a module located at `fromPath`. Returns the resolved
|
|
33
|
+
* module's `exports` (bare specifiers return the injected external verbatim).
|
|
34
|
+
*/
|
|
35
|
+
require(specifier: string, fromPath: string): unknown;
|
|
36
|
+
/** Load (and cache) the module at an already-resolved absolute path. */
|
|
37
|
+
load(path: string): unknown;
|
|
38
|
+
/** Load the entry module and return its `exports`. */
|
|
39
|
+
run(entryPath: string): unknown;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=module-runtime.d.ts.map
|
package/dist/node-fs.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("node:fs"),o=require("node:path");function l(s,t){const e=t.replace(/^\/+/,"");return o.resolve(s,e)}function u(s,t){return"/"+o.relative(s,t).split(o.sep).join("/")}class a{constructor(t){this.rootDir=t}readFile(t){const e=l(this.rootDir,t);try{return!r.existsSync(e)||!r.statSync(e).isFile()?null:r.readFileSync(e,"utf8")}catch{return null}}exists(t){const e=l(this.rootDir,t);try{return r.existsSync(e)&&r.statSync(e).isFile()}catch{return!1}}listFiles(){const t=[],e=n=>{for(const i of r.readdirSync(n,{withFileTypes:!0})){if(i.name==="node_modules"||i.name.startsWith("."))continue;const c=o.join(n,i.name);i.isDirectory()?e(c):i.isFile()&&t.push(u(this.rootDir,c))}};try{e(this.rootDir)}catch{}return t.sort()}}exports.NodeFileSystem=a;
|
|
2
|
+
//# sourceMappingURL=node-fs.cjs.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FileSystem } from './vfs';
|
|
2
|
+
export declare class NodeFileSystem implements FileSystem {
|
|
3
|
+
private readonly rootDir;
|
|
4
|
+
constructor(rootDir: string);
|
|
5
|
+
readFile(path: string): string | null;
|
|
6
|
+
exists(path: string): boolean;
|
|
7
|
+
listFiles(): string[];
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=node-fs.d.ts.map
|
package/dist/node-fs.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync as n, statSync as l, readFileSync as u, readdirSync as a } from "node:fs";
|
|
2
|
+
import { join as f, resolve as h, relative as y, sep as m } from "node:path";
|
|
3
|
+
function c(i, t) {
|
|
4
|
+
const r = t.replace(/^\/+/, "");
|
|
5
|
+
return h(i, r);
|
|
6
|
+
}
|
|
7
|
+
function d(i, t) {
|
|
8
|
+
return "/" + y(i, t).split(m).join("/");
|
|
9
|
+
}
|
|
10
|
+
class D {
|
|
11
|
+
constructor(t) {
|
|
12
|
+
this.rootDir = t;
|
|
13
|
+
}
|
|
14
|
+
readFile(t) {
|
|
15
|
+
const r = c(this.rootDir, t);
|
|
16
|
+
try {
|
|
17
|
+
return !n(r) || !l(r).isFile() ? null : u(r, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exists(t) {
|
|
23
|
+
const r = c(this.rootDir, t);
|
|
24
|
+
try {
|
|
25
|
+
return n(r) && l(r).isFile();
|
|
26
|
+
} catch {
|
|
27
|
+
return !1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
listFiles() {
|
|
31
|
+
const t = [], r = (s) => {
|
|
32
|
+
for (const e of a(s, { withFileTypes: !0 })) {
|
|
33
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
34
|
+
const o = f(s, e.name);
|
|
35
|
+
e.isDirectory() ? r(o) : e.isFile() && t.push(d(this.rootDir, o));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
r(this.rootDir);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
return t.sort();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
D as NodeFileSystem
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=node-fs.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CompositorOptions } from '@sequio/engine';
|
|
2
|
+
import { Composer } from './composer';
|
|
3
|
+
import { Composition, CompositionEnv, defineComposition, isComposition } from './composition';
|
|
4
|
+
import { Externals } from './module-runtime';
|
|
5
|
+
import { FileSystem } from './vfs';
|
|
6
|
+
import { AssetLoader } from './assets';
|
|
7
|
+
/**
|
|
8
|
+
* The module object exposed to sandboxed code as `@sequio/runtime`.
|
|
9
|
+
*
|
|
10
|
+
* `loadAsset` here is the *default* (no host loader → throws {@link NO_ASSET_LOADER});
|
|
11
|
+
* a {@link Runtime} instance overrides it with the host's {@link AssetLoader} so a
|
|
12
|
+
* composition's `loadAsset('./clip.mp4')` reaches real bytes.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RUNTIME_MODULE_API: {
|
|
15
|
+
defineComposition: typeof defineComposition;
|
|
16
|
+
isComposition: typeof isComposition;
|
|
17
|
+
COMPOSITION_TAG: "@sequio/runtime:composition";
|
|
18
|
+
loadAsset: AssetLoader;
|
|
19
|
+
};
|
|
20
|
+
/** Bare specifier under which the authoring API is injected. */
|
|
21
|
+
export declare const RUNTIME_MODULE_ID = "@sequio/runtime";
|
|
22
|
+
/** Bare specifier under which the engine namespace is injected. */
|
|
23
|
+
export declare const ENGINE_MODULE_ID = "@sequio/engine";
|
|
24
|
+
export interface RuntimeOptions {
|
|
25
|
+
/** Source files. Either a ready {@link FileSystem} or a path→content map. */
|
|
26
|
+
files?: FileSystem | Record<string, string>;
|
|
27
|
+
/** Entry module path. @default '/index.ts' (falls back to `/index.js`, `/main.ts`). */
|
|
28
|
+
entry?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Extra bare modules user code may `import`. Merged over the built-in
|
|
31
|
+
* `@sequio/engine` and `@sequio/runtime` modules
|
|
32
|
+
* (host entries win on key collision), so a host can expose more libraries.
|
|
33
|
+
*/
|
|
34
|
+
externals?: Externals;
|
|
35
|
+
/**
|
|
36
|
+
* Resolver for **local media assets** a composition pulls in with
|
|
37
|
+
* `loadAsset('./clip.mp4')` (imported from `@sequio/runtime`). The host maps a
|
|
38
|
+
* project-relative path to its bytes as a `Blob`; the runtime normalizes the
|
|
39
|
+
* path ({@link resolveAssetPath}) before calling it. Left unset, `loadAsset`
|
|
40
|
+
* throws a clear error — assets are opt-in and never bundled. See
|
|
41
|
+
* [`assets.ts`](./assets.ts).
|
|
42
|
+
*/
|
|
43
|
+
loadAsset?: AssetLoader;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A copy of the engine namespace whose `Compositor` folds `overrides` into every
|
|
47
|
+
* construction. This is how `env.compositorOptions` is injected **implicitly**:
|
|
48
|
+
* user code writes a plain `new Compositor({ width, height, timebase })` — exactly
|
|
49
|
+
* like a demo — and a host (a Node WebGPU renderer, an output scale) still reaches
|
|
50
|
+
* it. Overrides win on conflict so a server's `createRenderer` takes precedence.
|
|
51
|
+
* With no overrides (the browser default) the real namespace is returned as-is.
|
|
52
|
+
*/
|
|
53
|
+
export declare function engineForEnv(overrides: Partial<CompositorOptions>): Externals[string];
|
|
54
|
+
export declare class Runtime {
|
|
55
|
+
private readonly fs;
|
|
56
|
+
private readonly entry;
|
|
57
|
+
private readonly hostExternals;
|
|
58
|
+
/** Transpiled output cached across re-links (linking runs once per build). */
|
|
59
|
+
private readonly compileCache;
|
|
60
|
+
constructor(options?: RuntimeOptions);
|
|
61
|
+
/** The filesystem backing this runtime (add/edit files then re-`run`). */
|
|
62
|
+
get fileSystem(): FileSystem;
|
|
63
|
+
private resolveEntry;
|
|
64
|
+
/** Snapshot the current files into a portable {@link RuntimeBundle}. */
|
|
65
|
+
private toBundle;
|
|
66
|
+
private compile;
|
|
67
|
+
/**
|
|
68
|
+
* Compile + link + run the entry for a given environment and return the
|
|
69
|
+
* {@link Composition} it produced. Called once per build so the injected engine
|
|
70
|
+
* `Compositor` carries that environment's options (implicit `compositorOptions`)
|
|
71
|
+
* and each build gets independent module state. Transpilation is cached, so
|
|
72
|
+
* re-linking only re-executes the (cheap) module bodies.
|
|
73
|
+
*/
|
|
74
|
+
link(env: CompositionEnv): Composition;
|
|
75
|
+
/**
|
|
76
|
+
* Compile + run the program and return the {@link Composition} its entry
|
|
77
|
+
* produced (the tagged builder), without wrapping it in a `Composer`. Uses the
|
|
78
|
+
* plain browser environment (no injected renderer).
|
|
79
|
+
*/
|
|
80
|
+
runToComposition(): Composition;
|
|
81
|
+
/** Compile + run the program and wrap its result in a {@link Composer}. */
|
|
82
|
+
run(): Promise<Composer>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One-shot convenience: build a {@link Runtime} from `files`/options and run it.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const composer = await runComposition({ '/index.ts': "export default defineComposition(async () => {…})" });
|
|
89
|
+
* await composer.preview(document.body);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function runComposition(files: RuntimeOptions['files'], options?: Omit<RuntimeOptions, 'files'>): Promise<Composer>;
|
|
93
|
+
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/vfs.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal **virtual filesystem** the module runtime reads source files from.
|
|
3
|
+
*
|
|
4
|
+
* The runtime never touches `node:fs` directly — it resolves and reads modules
|
|
5
|
+
* through this interface. That keeps the core browser-safe (the studio "code
|
|
6
|
+
* mode" runs entirely in the tab against an {@link InMemoryFileSystem}) while
|
|
7
|
+
* letting a host inject a **real** filesystem instead: any object satisfying
|
|
8
|
+
* {@link FileSystem} works, so a Node caller can wrap `node:fs` (see
|
|
9
|
+
* `src/node-fs.ts`, kept out of the browser barrel).
|
|
10
|
+
*
|
|
11
|
+
* Paths are POSIX-style, absolute from a virtual root `/`. The runtime
|
|
12
|
+
* normalizes relative imports to absolute paths before calling {@link readFile}.
|
|
13
|
+
*/
|
|
14
|
+
export interface FileSystem {
|
|
15
|
+
/** Return the file's text, or `null` if it doesn't exist. */
|
|
16
|
+
readFile(path: string): string | null;
|
|
17
|
+
/** Whether a regular file exists at `path`. */
|
|
18
|
+
exists(path: string): boolean;
|
|
19
|
+
/** Every file path this filesystem can serve (used for diagnostics). */
|
|
20
|
+
listFiles(): string[];
|
|
21
|
+
}
|
|
22
|
+
/** Collapse `.`/`..` segments and force a single leading `/`. */
|
|
23
|
+
export declare function normalizePath(path: string): string;
|
|
24
|
+
/** Directory portion of a normalized path (`/a/b/c.ts` → `/a/b`). */
|
|
25
|
+
export declare function dirname(path: string): string;
|
|
26
|
+
/** Join a base directory with a (possibly relative) specifier, normalized. */
|
|
27
|
+
export declare function joinPath(base: string, specifier: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* An in-memory {@link FileSystem} backed by a plain path→content map. This is
|
|
30
|
+
* the default for the browser (the code-mode editor keeps its files here) and
|
|
31
|
+
* makes the module runtime unit-testable with no disk.
|
|
32
|
+
*/
|
|
33
|
+
export declare class InMemoryFileSystem implements FileSystem {
|
|
34
|
+
private readonly files;
|
|
35
|
+
constructor(files?: Record<string, string>);
|
|
36
|
+
/** Add/replace a file. Keys are normalized to absolute paths. */
|
|
37
|
+
writeFile(path: string, content: string): void;
|
|
38
|
+
/** Remove a file; returns whether it existed. */
|
|
39
|
+
deleteFile(path: string): boolean;
|
|
40
|
+
readFile(path: string): string | null;
|
|
41
|
+
exists(path: string): boolean;
|
|
42
|
+
listFiles(): string[];
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=vfs.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sequio/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compile and run multi-file TS/JS (virtual or injected real filesystem) into a Composer that renders/exports in the client or feeds server-side rendering.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "slightc",
|
|
8
|
+
"homepage": "https://sequio-demo.vercel.app/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/slightc/sequio.git",
|
|
12
|
+
"directory": "packages/runtime"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/slightc/sequio/issues"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./node-fs": {
|
|
27
|
+
"types": "./dist/node-fs.d.ts",
|
|
28
|
+
"import": "./dist/node-fs.js",
|
|
29
|
+
"require": "./dist/node-fs.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"!dist/**/*.map"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"typescript",
|
|
45
|
+
"compile",
|
|
46
|
+
"virtual-filesystem",
|
|
47
|
+
"video-editor",
|
|
48
|
+
"composer"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"@sequio/engine": "^0.1.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"dev": "vite",
|
|
56
|
+
"build": "vite build",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:watch": "vitest",
|
|
60
|
+
"verify:runtime": "node scripts/verify-page.cjs example/runtime-test.html __RUNTIME_TEST__ \"runtime compile+run\""
|
|
61
|
+
}
|
|
62
|
+
}
|