velxio/frontend/src/utils/importProject.ts

82 lines
3.2 KiB
TypeScript

/**
* Unified project import dispatcher.
*
* Velxio accepts two on-disk project formats:
*
* - `.vlx` — Velxio's native single-file JSON (boards + components + wires +
* file groups, optionally multi-board). Handled by importVlxFile,
* which writes directly into the simulator + editor stores.
*
* - `.zip` — Wokwi-compatible bundle (diagram.json + sketch.ino + headers +
* libraries.txt). Handled by importFromWokwiZip, which returns a
* structured payload that callers apply to the stores themselves
* (so the toolbar can also pop the "install missing libraries"
* modal afterwards).
*
* Both UI entry points (the toolbar "Import" button and the file-explorer
* "Open project" button) route through this dispatcher so the user sees the
* same behaviour regardless of which button they click — and so we have ONE
* place to extend if a third format ever shows up.
*/
import type { Wire } from '../types/wire';
import { importVlxFile, VlxParseError } from './vlxFile';
import { importFromWokwiZip, type VelxioComponent } from './wokwiZip';
/**
* Common result shape: `kind` tells callers which path ran, so they can
* react appropriately (e.g. only the zip path needs the library-install
* modal — the .vlx format inlines library state).
*/
export type ProjectImportResult =
| { kind: 'vlx' }
| {
kind: 'zip';
boardType: 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
boardPosition: { x: number; y: number };
components: VelxioComponent[];
wires: Wire[];
files: Array<{ name: string; content: string }>;
libraries: string[];
};
/**
* Detect format from the filename + MIME type and dispatch to the right
* loader. Returns the loader's result so callers can chain UI behaviour
* (e.g. show the install-libraries modal for zips). Throws when the
* format is unrecognised or the loader fails — callers should catch and
* surface to the user.
*
* Note: `.vlx` loads itself directly into the stores; `.zip` returns a
* payload that the caller must apply (we keep that asymmetry so the
* toolbar can still trigger the libraries modal at the end).
*/
export async function importProjectFile(file: File): Promise<ProjectImportResult> {
const lower = file.name.toLowerCase();
if (lower.endsWith('.vlx') || (file.type === 'application/json' && !lower.endsWith('.zip'))) {
try {
await importVlxFile(file);
return { kind: 'vlx' };
} catch (err) {
// Re-raise with a friendlier prefix. VlxParseError already has a
// good message; native errors get a clear context label.
const msg = err instanceof VlxParseError ? err.message : (err as Error).message;
throw new Error(`Could not load .vlx file:\n\n${msg}`);
}
}
if (lower.endsWith('.zip')) {
const result = await importFromWokwiZip(file);
return { kind: 'zip', ...result };
}
throw new Error(
`Unsupported project file: ${file.name}.\n` +
`Velxio accepts .vlx (Velxio projects) and .zip (Wokwi bundles).`,
);
}
/** File-input `accept` attribute that pairs with importProjectFile. */
export const PROJECT_FILE_ACCEPT = '.vlx,.zip,application/json,application/zip';