From d00544b0a790df20f327d2e6aa51c3b9ba53848b Mon Sep 17 00:00:00 2001 From: ErSan Date: Tue, 27 Jan 2026 19:54:01 +0800 Subject: [PATCH] sdk: harden goView import (iframe/video/image) --- packages/sdk/src/core/goview.ts | 15 +++++---- packages/sdk/src/core/goview/convert.ts | 41 ++++++++++++++++++++++--- packages/sdk/src/core/widgets/iframe.ts | 23 +++++++++++--- packages/sdk/src/core/widgets/image.ts | 37 +++++++++++++++++----- packages/sdk/src/core/widgets/video.ts | 37 +++++++++++++++++----- 5 files changed, 125 insertions(+), 28 deletions(-) diff --git a/packages/sdk/src/core/goview.ts b/packages/sdk/src/core/goview.ts index 7baaeaf..5bb399e 100644 --- a/packages/sdk/src/core/goview.ts +++ b/packages/sdk/src/core/goview.ts @@ -1,13 +1,16 @@ 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 - * start wiring UI + migration flows without committing to the full mapping. + * We keep this permissive because goView exports / storage snapshots vary across + * versions and forks. The heavy lifting lives in `goview/convert.ts`. */ export function convertGoViewJSONToScreen(input: unknown): Screen { - // keep reference to avoid unused-vars lint until implemented - void input; - throw new Error('convertGoViewJSONToScreen: not implemented yet'); + if (!input || typeof input !== 'object') { + throw new Error('convertGoViewJSONToScreen: expected object'); + } + + return convertGoViewProjectToScreen(input as unknown as object); } diff --git a/packages/sdk/src/core/goview/convert.ts b/packages/sdk/src/core/goview/convert.ts index 40a48db..204c944 100644 --- a/packages/sdk/src/core/goview/convert.ts +++ b/packages/sdk/src/core/goview/convert.ts @@ -85,13 +85,40 @@ function isVideo(c: GoViewComponentLike): boolean { return k.includes('mp4') || k.includes('media') || k.includes('player'); } +function pick(...values: Array): 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 { - const editCanvasConfig = (input as GoViewStorageLike).editCanvasConfig; + // goView exports vary a lot; attempt a few common nesting shapes. + const root = input as unknown as Record; + const data = (root.data as Record | undefined) ?? undefined; + const state = (root.state as Record | undefined) ?? undefined; + const project = (root.project as Record | undefined) ?? undefined; + + const editCanvasConfig = pick( + (input as GoViewStorageLike).editCanvasConfig, + data?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined, + state?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined, + project?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined, + ); 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 = - 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 background = editCanvasConfig?.background; @@ -105,7 +132,13 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt 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 = []; diff --git a/packages/sdk/src/core/widgets/iframe.ts b/packages/sdk/src/core/widgets/iframe.ts index 179f6c5..52e95cf 100644 --- a/packages/sdk/src/core/widgets/iframe.ts +++ b/packages/sdk/src/core/widgets/iframe.ts @@ -5,9 +5,9 @@ import type { IframeWidgetNode } from '../schema'; * Keep it permissive and normalize the common fields. */ export interface GoViewIframeOption { - dataset?: string; - src?: string; - url?: string; + dataset?: unknown; + src?: unknown; + url?: unknown; borderRadius?: number; } @@ -16,8 +16,23 @@ export interface 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; + 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 { - return option.dataset ?? option.src ?? option.url ?? ''; + return asString(option.dataset) || asString(option.src) || asString(option.url); } export function convertGoViewIframeOptionToNodeProps(option: GoViewIframeOption): IframeWidgetNode['props'] { diff --git a/packages/sdk/src/core/widgets/image.ts b/packages/sdk/src/core/widgets/image.ts index 9ad1a7b..3bdbcee 100644 --- a/packages/sdk/src/core/widgets/image.ts +++ b/packages/sdk/src/core/widgets/image.ts @@ -8,28 +8,51 @@ export interface GoViewImageOption { /** * Common in existing legacy widgets (same as iframe/video). */ - dataset?: string; + dataset?: unknown; /** * Other variants seen in the wild. */ - src?: string; - url?: string; + src?: unknown; + url?: unknown; /** * Styling. */ - fit?: ImageWidgetNode['props']['fit']; - objectFit?: ImageWidgetNode['props']['fit']; + fit?: unknown; + objectFit?: unknown; 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; + 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 { - 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 { - 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'] { diff --git a/packages/sdk/src/core/widgets/video.ts b/packages/sdk/src/core/widgets/video.ts index 689efe0..2809b11 100644 --- a/packages/sdk/src/core/widgets/video.ts +++ b/packages/sdk/src/core/widgets/video.ts @@ -5,15 +5,15 @@ import type { VideoWidgetNode } from '../schema'; * Keep it permissive and normalize the common fields. */ export interface GoViewVideoOption { - dataset?: string; - src?: string; - url?: string; + dataset?: unknown; + src?: unknown; + url?: unknown; loop?: boolean; muted?: boolean; - fit?: VideoWidgetNode['props']['fit']; - objectFit?: VideoWidgetNode['props']['fit']; + fit?: unknown; + objectFit?: unknown; borderRadius?: number; } @@ -23,12 +23,35 @@ export interface 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; + 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 { - 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 { - 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'] {