Huakun Shen e4d1441d73
Feature: support npm extension publish (#62)
* feat: npm package registry API

* refactor: move package registry files

* refactor: move jsr and npm api to a new package

* ci: add verify-package-export

* test: implement tests for npm package validation as kunkun extension

* chore: add missing dep for package-registry pkg

* feat: make provenance an optional input for npm validation function

* ci: add verify-package-export as dev dep to 2 packages that uses it

* feat: add rekor log API, and return commit from jsr & npm package in validation function

* feat: return github repo info from validation function of jsr and npm

* feat: extend ExtPublishMetadata to include optional GitHub repository details

* fix: eslint for ui package

* refactor: format desktop

* fix: eslint errors in desktop

* format: all code

* ci: add lint to CI

* feat: add more info to validation function returned from package-registry npm jsr

* pnpm lock

* feat: add 2 more variables to supabase extension metadata model

* format

* feat: add provenance card

* feat: add workflow path to ExtPublishMetadata and jsr/npm validation

* update provenance

* feat: make store extension and provenance more responsive

* chore: add globals to ui package

* fix: remove unnecessary any to fix eslint

* fix: svg sanitize

* chore: add @typescript-eslint/eslint-plugin to ui package to fix eslint

* fix: update eslint dep to fix error

* fix: try fixing eslint

* fix: update eslint configuration for improved compatibility

* chore: add globals package and update README for Discord invite

* fix: update eslint rules and upgrade typescript-eslint dependency

- Disabled additional eslint rules to resolve errors:
  - @typescript-eslint/no-unused-expressions
  - svelte/no-inner-declarations
- Upgraded typescript-eslint from version 8.19.1 to 8.20.0 for improved compatibility.

* update pnpm lock

---------

Co-authored-by: Huakun Shen <huaukun.shen@huakunshen.com>
2025-01-16 06:00:07 -05:00

215 lines
5.8 KiB
Svelte

<script lang="ts">
import DanceTransition from "@/components/dance/dance-transition.svelte"
import { appConfig, winExtMap } from "@/stores"
import { goBackOnEscape } from "@/utils/key"
import { goHome } from "@/utils/route"
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
import { isInMainWindow } from "@/utils/window"
import { db } from "@kksh/api/commands"
import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models"
import {
constructJarvisServerAPIWithPermissions,
// exposeApiToWindow,
type IApp,
type IUiIframe
} from "@kksh/api/ui"
import { toast, type IUiIframeServer1, type IUiIframeServer2 } from "@kksh/api/ui/iframe"
import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/ui/utils"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { goto } from "$app/navigation"
import { IframeParentIO, RPCChannel } from "kkrpc/browser"
import { ArrowLeftIcon, MoveIcon, RefreshCcwIcon, XIcon } from "lucide-svelte"
import { onDestroy, onMount } from "svelte"
import type { PageData } from "./$types"
let { data }: { data: PageData } = $props()
const { loadedExt, url, extPath, extInfoInDB } = data
const appWin = getCurrentWindow()
let iframeRef: HTMLIFrameElement
let uiControl = $state<{
iframeLoaded: boolean
showBackBtn: boolean
showMoveBtn: boolean
showRefreshBtn: boolean
backBtnPosition: Position
moveBtnPosition: Position
refreshBtnPosition: Position
transparentBg: boolean
}>({
iframeLoaded: false,
showBackBtn: true, // if open in new window, hide back button
showMoveBtn: true,
showRefreshBtn: true,
backBtnPosition: "top-left",
moveBtnPosition: "bottom-left",
refreshBtnPosition: "top-right",
transparentBg: false
})
const iframeUiAPI: IUiIframeServer2 = {
goBack: async () => {
if (isInMainWindow()) {
goto("/app/")
} else {
appWin.close()
}
},
hideBackButton: async () => {
uiControl.showBackBtn = false
},
hideMoveButton: async () => {
uiControl.showMoveBtn = false
},
hideRefreshButton: async () => {
console.log("hideRefreshButton")
uiControl.showRefreshBtn = false
},
showBackButton: async (position?: Position) => {
console.log("showBackBtn", position)
uiControl.showBackBtn = true
uiControl.backBtnPosition = position ?? "top-left"
},
showMoveButton: async (position?: Position) => {
uiControl.showMoveBtn = true
uiControl.moveBtnPosition = position ?? "bottom-left"
},
showRefreshButton: async (position?: Position) => {
uiControl.showRefreshBtn = true
uiControl.refreshBtnPosition = position ?? "top-right"
},
getTheme: () => {
const theme = $appConfig.theme
return Promise.resolve({
theme: theme.theme as ThemeColor,
radius: theme.radius,
lightMode: theme.lightMode
})
},
async reloadPage() {
location.reload()
},
async setTransparentWindowBackground(transparent: boolean) {
if (isInMainWindow()) {
throw new Error("Cannot set background in main window")
}
if (transparent) {
document.body.style.backgroundColor = "transparent"
} else {
document.body.style.backgroundColor = ""
}
}
}
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions,
loadedExt.extPath
)
const serverAPI2 = {
...serverAPI,
iframeUi: {
...serverAPI.iframeUi,
...iframeUiAPI
} satisfies IUiIframeServer1 & IUiIframeServer2,
db: new db.JarvisExtDB(extInfoInDB.extId),
kv: new db.KV(extInfoInDB.extId),
app: {
language: () => Promise.resolve("en") // TODO: get locale
} satisfies IApp
}
function onBackBtnClicked() {
if (isInMainWindow()) {
goHome()
} else {
appWin.close()
}
}
function onIframeLoaded() {
setTimeout(() => {
iframeRef.focus()
uiControl.iframeLoaded = true
}, 300)
}
onMount(() => {
setTimeout(() => {
appWin.show()
}, 200)
if (iframeRef?.contentWindow) {
const io = new IframeParentIO(iframeRef.contentWindow)
const rpc = new RPCChannel(io, { expose: serverAPI2 })
} else {
toast.warning("iframeRef.contentWindow not available")
}
setTimeout(() => {
if (!uiControl.iframeLoaded) {
toast.error("Extension failed to load")
}
}, 3_000)
})
onDestroy(() => {
winExtMap.unregisterExtensionFromWindow(appWin.label)
})
</script>
<svelte:window on:keydown={goBackOnEscape} />
{#if uiControl.backBtnPosition && uiControl.showBackBtn}
<Button
class={cn("absolute", positionToTailwindClasses(uiControl.backBtnPosition))}
size="icon"
variant="outline"
onclick={onBackBtnClicked}
style={`${positionToCssStyleString(uiControl.backBtnPosition)}`}
>
{#if appWin.label === "main"}
<ArrowLeftIcon class="w-4" />
{:else}
<XIcon class="w-4" />
{/if}
</Button>
{/if}
{#if uiControl.moveBtnPosition && uiControl.showMoveBtn}
<Button
class={cn("absolute", positionToTailwindClasses(uiControl.moveBtnPosition))}
style={`${positionToCssStyleString(uiControl.moveBtnPosition)}`}
size="icon"
variant="outline"
data-tauri-drag-region
>
<MoveIcon data-tauri-drag-region class="w-4" />
</Button>
{/if}
{#if uiControl.refreshBtnPosition && uiControl.showRefreshBtn}
<Button
class={cn("absolute", positionToTailwindClasses(uiControl.refreshBtnPosition))}
style={`${positionToCssStyleString(uiControl.refreshBtnPosition)}`}
size="icon"
variant="outline"
onclick={iframeUiAPI.reloadPage}
>
<RefreshCcwIcon class="w-4" />
</Button>
{/if}
<main class="h-screen">
<DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} />
<iframe
bind:this={iframeRef}
class={cn("h-full", {
hidden: !uiControl.iframeLoaded
})}
onload={onIframeLoaded}
width="100%"
height="100%"
frameborder="0"
src={data.url}
title={data.extPath}
></iframe>
</main>