sdk: harden goView import (iframe/video/image)

This commit is contained in:
ErSan 2026-01-27 19:54:01 +08:00
parent c574d4e323
commit d00544b0a7
5 changed files with 125 additions and 28 deletions

View File

@ -1,13 +1,16 @@
import type { Screen } from './schema'; import type { Screen } from './schema';
import { convertGoViewProjectToScreen } from './goview/convert';
/** /**
* goView JSON converter (stub). * goView JSON converter.
* *
* The legacy goView format isn't implemented yet; this is a placeholder so we can * We keep this permissive because goView exports / storage snapshots vary across
* start wiring UI + migration flows without committing to the full mapping. * versions and forks. The heavy lifting lives in `goview/convert.ts`.
*/ */
export function convertGoViewJSONToScreen(input: unknown): Screen { export function convertGoViewJSONToScreen(input: unknown): Screen {
// keep reference to avoid unused-vars lint until implemented if (!input || typeof input !== 'object') {
void input; throw new Error('convertGoViewJSONToScreen: expected object');
throw new Error('convertGoViewJSONToScreen: not implemented yet'); }
return convertGoViewProjectToScreen(input as unknown as object);
} }

View File

@ -85,13 +85,40 @@ function isVideo(c: GoViewComponentLike): boolean {
return k.includes('mp4') || k.includes('media') || k.includes('player'); return k.includes('mp4') || k.includes('media') || k.includes('player');
} }
function pick<T>(...values: Array<T | undefined | null>): T | undefined {
for (const v of values) {
if (v !== undefined && v !== null) return v as T;
}
return undefined;
}
export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen { export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
const editCanvasConfig = (input as GoViewStorageLike).editCanvasConfig; // goView exports vary a lot; attempt a few common nesting shapes.
const root = input as unknown as Record<string, unknown>;
const data = (root.data as Record<string, unknown> | undefined) ?? undefined;
const state = (root.state as Record<string, unknown> | undefined) ?? undefined;
const project = (root.project as Record<string, unknown> | undefined) ?? undefined;
const editCanvasConfig = pick<GoViewEditCanvasConfigLike>(
(input as GoViewStorageLike).editCanvasConfig,
data?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
state?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
project?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
);
const width = const width =
editCanvasConfig?.width ?? (input as GoViewProjectLike).canvas?.width ?? (input as GoViewProjectLike).width ?? 1920; editCanvasConfig?.width ??
(input as GoViewProjectLike).canvas?.width ??
(input as GoViewProjectLike).width ??
(data?.width as number | undefined) ??
1920;
const height = const height =
editCanvasConfig?.height ?? (input as GoViewProjectLike).canvas?.height ?? (input as GoViewProjectLike).height ?? 1080; editCanvasConfig?.height ??
(input as GoViewProjectLike).canvas?.height ??
(input as GoViewProjectLike).height ??
(data?.height as number | undefined) ??
1080;
const name = editCanvasConfig?.projectName ?? 'Imported Project'; const name = editCanvasConfig?.projectName ?? 'Imported Project';
const background = editCanvasConfig?.background; const background = editCanvasConfig?.background;
@ -105,7 +132,13 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
nodes: [], nodes: [],
}); });
const componentList = (input as GoViewStorageLike).componentList ?? (input as GoViewProjectLike).componentList ?? []; const componentList =
(input as GoViewStorageLike).componentList ??
(input as GoViewProjectLike).componentList ??
(data?.componentList as GoViewComponentLike[] | undefined) ??
(state?.componentList as GoViewComponentLike[] | undefined) ??
(project?.componentList as GoViewComponentLike[] | undefined) ??
[];
const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = []; const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = [];

View File

@ -5,9 +5,9 @@ import type { IframeWidgetNode } from '../schema';
* Keep it permissive and normalize the common fields. * Keep it permissive and normalize the common fields.
*/ */
export interface GoViewIframeOption { export interface GoViewIframeOption {
dataset?: string; dataset?: unknown;
src?: string; src?: unknown;
url?: string; url?: unknown;
borderRadius?: number; borderRadius?: number;
} }
@ -16,8 +16,23 @@ export interface GoViewIframeOption {
*/ */
export type LegacyIframeOption = GoViewIframeOption; export type LegacyIframeOption = GoViewIframeOption;
function asString(v: unknown): string {
if (typeof v === 'string') return v;
if (!v) return '';
// Some goView widgets store data as { value: '...' } or { url: '...' }.
if (typeof v === 'object') {
const obj = v as Record<string, unknown>;
if (typeof obj.value === 'string') return obj.value;
if (typeof obj.url === 'string') return obj.url;
if (typeof obj.src === 'string') return obj.src;
}
return '';
}
function pickSrc(option: GoViewIframeOption): string { function pickSrc(option: GoViewIframeOption): string {
return option.dataset ?? option.src ?? option.url ?? ''; return asString(option.dataset) || asString(option.src) || asString(option.url);
} }
export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] { export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] {

View File

@ -8,28 +8,51 @@ export interface GoViewImageOption {
/** /**
* Common in existing legacy widgets (same as iframe/video). * Common in existing legacy widgets (same as iframe/video).
*/ */
dataset?: string; dataset?: unknown;
/** /**
* Other variants seen in the wild. * Other variants seen in the wild.
*/ */
src?: string; src?: unknown;
url?: string; url?: unknown;
/** /**
* Styling. * Styling.
*/ */
fit?: ImageWidgetNode['props']['fit']; fit?: unknown;
objectFit?: ImageWidgetNode['props']['fit']; objectFit?: unknown;
borderRadius?: number; borderRadius?: number;
} }
function asString(v: unknown): string {
if (typeof v === 'string') return v;
if (!v) return '';
// Some goView widgets store data as { value: '...' } or { url: '...' }.
if (typeof v === 'object') {
const obj = v as Record<string, unknown>;
if (typeof obj.value === 'string') return obj.value;
if (typeof obj.url === 'string') return obj.url;
if (typeof obj.src === 'string') return obj.src;
}
return '';
}
function pickSrc(option: GoViewImageOption): string { function pickSrc(option: GoViewImageOption): string {
return option.dataset ?? option.src ?? option.url ?? ''; return asString(option.dataset) || asString(option.src) || asString(option.url);
} }
function pickFit(option: GoViewImageOption): ImageWidgetNode['props']['fit'] | undefined { function pickFit(option: GoViewImageOption): ImageWidgetNode['props']['fit'] | undefined {
return option.fit ?? option.objectFit; const raw = asString(option.fit) || asString(option.objectFit);
if (!raw) return undefined;
const v = raw.toLowerCase();
if (v === 'contain' || v === 'cover' || v === 'fill' || v === 'none' || v === 'scale-down') {
return v as ImageWidgetNode['props']['fit'];
}
return undefined;
} }
export function convertGoViewImageOptionToNodeProps(option: GoViewImageOption): ImageWidgetNode['props'] { export function convertGoViewImageOptionToNodeProps(option: GoViewImageOption): ImageWidgetNode['props'] {

View File

@ -5,15 +5,15 @@ import type { VideoWidgetNode } from '../schema';
* Keep it permissive and normalize the common fields. * Keep it permissive and normalize the common fields.
*/ */
export interface GoViewVideoOption { export interface GoViewVideoOption {
dataset?: string; dataset?: unknown;
src?: string; src?: unknown;
url?: string; url?: unknown;
loop?: boolean; loop?: boolean;
muted?: boolean; muted?: boolean;
fit?: VideoWidgetNode['props']['fit']; fit?: unknown;
objectFit?: VideoWidgetNode['props']['fit']; objectFit?: unknown;
borderRadius?: number; borderRadius?: number;
} }
@ -23,12 +23,35 @@ export interface GoViewVideoOption {
*/ */
export type LegacyVideoOption = GoViewVideoOption; export type LegacyVideoOption = GoViewVideoOption;
function asString(v: unknown): string {
if (typeof v === 'string') return v;
if (!v) return '';
if (typeof v === 'object') {
const obj = v as Record<string, unknown>;
if (typeof obj.value === 'string') return obj.value;
if (typeof obj.url === 'string') return obj.url;
if (typeof obj.src === 'string') return obj.src;
}
return '';
}
function pickSrc(option: GoViewVideoOption): string { function pickSrc(option: GoViewVideoOption): string {
return option.dataset ?? option.src ?? option.url ?? ''; return asString(option.dataset) || asString(option.src) || asString(option.url);
} }
function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined { function pickFit(option: GoViewVideoOption): VideoWidgetNode['props']['fit'] | undefined {
return option.fit ?? option.objectFit; const raw = asString(option.fit) || asString(option.objectFit);
if (!raw) return undefined;
// normalize common variants
const v = raw.toLowerCase();
if (v === 'contain' || v === 'cover' || v === 'fill' || v === 'none' || v === 'scale-down') {
return v as VideoWidgetNode['props']['fit'];
}
return undefined;
} }
export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] { export function convertGoViewVideoOptionToNodeProps(option: GoViewVideoOption): VideoWidgetNode['props'] {