chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Packaging — assemble the emitted text project + collected asset bytes +
|
||||
* vendored GSAP into the self-contained export ZIP.
|
||||
*
|
||||
* The emitter ({@link emitHyperframes}) produced every text file (index.html,
|
||||
* manifest, subtitles, README) and the collection layer ({@link collectVideoAssets})
|
||||
* produced the binary bytes keyed by the compiler's asset-plan paths. This layer
|
||||
* lays them out under `assets/`, adds the vendored GSAP (determinism: shipped in
|
||||
* the zip, never a CDN), and streams the archive to a Blob. JSZip is imported
|
||||
* lazily so it stays out of the main app bundle.
|
||||
*
|
||||
* App-side / impure: does IO (fetch for GSAP) and dynamic-imports JSZip.
|
||||
*/
|
||||
import { assetUrl, type EmittedProject } from '@/lib/video-export';
|
||||
|
||||
/** Default location the app serves the vendored GSAP from (committed at public/vendor). */
|
||||
const GSAP_PUBLIC_URL = '/vendor/gsap.min.js';
|
||||
|
||||
export interface PackageOptions {
|
||||
/**
|
||||
* The vendored GSAP source text. Defaults to fetching {@link GSAP_PUBLIC_URL}
|
||||
* (served from `public/vendor/gsap.min.js`). Passing it explicitly avoids the
|
||||
* fetch (e.g. in tests).
|
||||
*/
|
||||
gsapSource?: string;
|
||||
/** Load one app-local vendored binary. Injectable for deterministic unit tests. */
|
||||
loadVendorAsset?: (sourceUrl: string) => Promise<Blob>;
|
||||
onProgress?: (message: string) => void;
|
||||
}
|
||||
|
||||
/** Fetch the app's committed, vendored GSAP so the export bundles it (no CDN at render). */
|
||||
async function loadGsapSource(): Promise<string> {
|
||||
const res = await fetch(GSAP_PUBLIC_URL);
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to load vendored GSAP from ${GSAP_PUBLIC_URL} (${res.status})`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function loadVendorAsset(sourceUrl: string): Promise<Blob> {
|
||||
const res = await fetch(sourceUrl);
|
||||
if (!res.ok) throw new Error(`Failed to load vendored export asset ${sourceUrl} (${res.status})`);
|
||||
return res.blob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the export ZIP blob from an emitted project and its collected assets.
|
||||
* Text files land at the project root; asset blobs land under `assets/<planPath>`
|
||||
* (matching the URLs the emitted HTML references); GSAP is vendored at the
|
||||
* project's `gsapVendorPath`.
|
||||
*/
|
||||
export async function packageVideoZip(
|
||||
project: EmittedProject,
|
||||
assetBlobs: Map<string, Blob>,
|
||||
options: PackageOptions = {},
|
||||
): Promise<Blob> {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
// Emitted project-relative text files (including nested license files).
|
||||
for (const file of project.files) {
|
||||
zip.file(file.path, file.content);
|
||||
}
|
||||
|
||||
// Collected binary assets under assets/<planPath>.
|
||||
for (const [planPath, blob] of assetBlobs) {
|
||||
// JSZip's Node build cannot reliably consume a web Blob directly; an
|
||||
// ArrayBuffer works in both browser and Node and preserves the exact bytes.
|
||||
zip.file(assetUrl(planPath), await blob.arrayBuffer());
|
||||
}
|
||||
|
||||
// Quiz fonts are project-declared: cover-only/no-Quiz exports never fetch or
|
||||
// parse their bytes, while Quiz projects keep every font local to the ZIP.
|
||||
const vendorAssetLoader = options.loadVendorAsset ?? loadVendorAsset;
|
||||
const vendorAssets = await Promise.all(
|
||||
project.vendorAssets.map(async (asset) => ({
|
||||
path: asset.path,
|
||||
bytes: await (await vendorAssetLoader(asset.sourceUrl)).arrayBuffer(),
|
||||
})),
|
||||
);
|
||||
for (const asset of vendorAssets) {
|
||||
zip.file(asset.path, asset.bytes);
|
||||
}
|
||||
|
||||
// Vendored GSAP at the path the HTML loads it from.
|
||||
options.onProgress?.('Bundling GSAP runtime');
|
||||
const gsapSource = options.gsapSource ?? (await loadGsapSource());
|
||||
zip.file(project.gsapVendorPath, gsapSource);
|
||||
|
||||
options.onProgress?.('Compressing archive');
|
||||
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
|
||||
}
|
||||
Reference in new issue
Block a user