Add extension templates, create-kunkun and cli package (#26)

* chore: add extension templates

* feat: add create-kunkun and cli package

* fix: cli and create-kunkun package location

* fix: cli package test

* ci: run test for CI pipeline only on Linux

The most important E2E test is run with docker, Linux anyways, no need to run on Mac and Windows
This commit is contained in:
Huakun Shen 2024-11-15 18:15:17 -05:00 committed by GitHub
parent e9609cf8ee
commit 5ead38bf71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
159 changed files with 6214 additions and 9 deletions

View File

@ -36,4 +36,5 @@ jobs:
- name: Build
run: pnpm build
- name: Test
if: matrix.os == 'ubuntu-24.04'
run: pnpm test

175
apps/cli/.gitignore vendored Normal file
View File

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

81
apps/cli/CHANGELOG.md Normal file
View File

@ -0,0 +1,81 @@
# kksh
## 0.0.22
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.23
## 0.0.21
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.22
## 0.0.20
### Patch Changes
- Re-enable server-based dev extension refresh. Deep Link forces switch focus to kunkun
- Updated dependencies
- @kksh/api@0.0.21
## 0.0.19
### Patch Changes
- Fix Some Windows incompatibilities
- Updated dependencies
- @kksh/api@0.0.20
## 0.0.12
### Patch Changes
- Updated dependencies [9fa3d8e]
- Updated dependencies [0a1ab7c]
- Updated dependencies [b48d53b]
- @kksh/api@0.0.20
## 0.0.9
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.10
## 0.0.8
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.9
## 0.0.7
### Patch Changes
- Add publish mode to verify command, will exit with 1 when invalid.
## 0.0.6
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.6
## 0.0.5
### Patch Changes
- Updated dependencies [ec47e1e]
- @kksh/api@0.0.5
## 0.0.4
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4

16
apps/cli/README.md Normal file
View File

@ -0,0 +1,16 @@
# kksh
This is a CLI tool for developers to develop Kunkun extensions.
## Usage
```bash
# Create an extension template first
npm init kunkun@latest
# You can verify the extension manifest, this expect `npm run build` is done already and all generated artifacts listed in manifest is present.
npx kksh verify <path to extension>
# Build extension with docker, simulate how Kunkun's CI builds the extension
npx kksh build <path to extension>
```

View File

@ -0,0 +1,80 @@
import os from "os"
import path from "path"
import { getRootDir } from "@/constants"
import type { BuildResult } from "@/types"
import { buildWithDockerAndValidate } from "@/utils"
import { $ } from "bun"
import { afterAll, expect, test } from "bun:test"
import fs from "fs-extra"
import { verifyCmd } from "../src/commands/verify"
const rootDir = getRootDir()
const createKKDir = path.join(rootDir, "../create-kunkun")
const createKKDistDir = path.join(createKKDir, "dist")
const createKKIndexjsPath = path.join(createKKDistDir, "index.mjs")
const testDir = path.join(os.tmpdir(), "kunkun-cli-test")
console.log("Test Dir: ", testDir)
const templateNames = ["react", "vue", "nuxt", "svelte", "sveltekit", "next", "template"]
fs.rmdirSync(testDir, { recursive: true })
fs.mkdirpSync(testDir)
const testTemplateDirs: string[] = []
await Promise.all(
templateNames.map(async (templateName) => {
const folderName = `${templateName}-ext`
await $`node ${createKKIndexjsPath} --outdir ${testDir} --name ${folderName} --template ${templateName}`
const templateDir = path.join(testDir, folderName)
console.log("templateDir", templateDir)
await $`pnpm install`.cwd(templateDir).quiet()
await $`pnpm build`.cwd(templateDir).quiet()
testTemplateDirs.push(templateDir)
})
)
test("Build And Verify", async () => {
for (const templateDir of testTemplateDirs) {
expect(verifyCmd(templateDir, false)).toBeTrue()
}
})
const testDirDocker = path.join(os.tmpdir(), "kunkun-cli-test-docker")
fs.rmdirSync(testDirDocker, { recursive: true })
fs.mkdirpSync(testDirDocker)
const templateData: Record<string, { dir: string; buildResult: BuildResult }> = {}
await Promise.all(
templateNames.map(async (templateName) => {
const folderName = `${templateName}-ext`
await $`node ${createKKIndexjsPath} --outdir ${testDirDocker} --name ${folderName} --template ${templateName}`
const templateDir = path.join(testDirDocker, folderName)
console.log("templateDir:", templateDir)
const buildResult = await buildWithDockerAndValidate(templateDir)
templateData[templateName] = {
dir: templateDir,
buildResult
}
})
)
console.log(templateData)
test("Template Exist", () => {
Object.entries(templateData).forEach(async ([templateName, { dir }]) => {
console.log("Expect dir exist: ", dir)
expect(fs.existsSync(dir)).toBeTrue()
})
})
test("Build Result Tarball Exist", () => {
Object.entries(templateData).forEach(async ([templateName, { buildResult, dir }]) => {
const expectedTarballPath = path.join(dir, buildResult.tarballFilename)
expect(fs.existsSync(expectedTarballPath)).toBeTrue()
})
})
afterAll(() => {
fs.rmdirSync(testDir, { recursive: true })
fs.rmdirSync(testDirDocker, { recursive: true })
})

View File

@ -0,0 +1,30 @@
import { expect, test } from "bun:test"
import path from "path"
import { getRootDir } from "@/constants"
import fs from "fs-extra"
import { verifyCmd } from "../src/commands/verify"
const rootDir = getRootDir()
const extensionsDir = path.join(rootDir, "../../packages/extensions")
const templatesDir = path.join(rootDir, "../../templates")
const extsPaths = fs
.readdirSync(extensionsDir)
.map((extensionName) => path.join(extensionsDir, extensionName))
.filter((extPath) => fs.statSync(extPath).isDirectory())
const templatesPaths = fs
.readdirSync(templatesDir)
.map((templateName) => path.join(templatesDir, templateName))
.filter((extPath) => fs.statSync(extPath).isDirectory())
test("Verify Extensions", () => {
for (const extPath of extsPaths) {
expect(verifyCmd(extPath, false)).toBeTrue()
}
})
// test("Verify Templates", () => {
// for (const templatePath of templatesPaths) {
// expect(verifyCmd(templatePath, false)).toBeTrue()
// }
// })

24
apps/cli/build.ts Normal file
View File

@ -0,0 +1,24 @@
import { $ } from "bun"
import fs from "fs-extra"
process.env.NODE_ENV = "production"
if (Bun.env.NODE_ENV !== "production") {
console.error("This script should be run in production mode. Set NODE_ENV=production.")
process.exit(1)
}
await $`rm -rf dist`
// building with bun doesn't work with debug
fs.mkdirSync("./dist")
process.env.NODE_ENV = "production"
await Bun.build({
entrypoints: ["./cli.ts"],
outdir: "./dist",
target: "node",
// minify: true,
format: "esm"
})
// await $`bun build --target node cli.ts > dist/cli.js`
fs.cpSync("./src/docker", "./dist/docker", { recursive: true })

BIN
apps/cli/bun.lockb Executable file

Binary file not shown.

50
apps/cli/cli.ts Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env node
import fs from "fs"
import path from "path"
import { buildCmd, verifyCmd } from "@/commands"
import { getDockerFolder, NODE_ENV } from "@/constants"
import logger from "@/logger"
import { program } from "commander"
import { version } from "./package.json"
const cwd = process.cwd()
console.log("Environment:", NODE_ENV)
program.name("Kunkun CLI").description("CLI for Kunkun Extension Development").version(version)
function computeProjectDir(projectPath: string | undefined) {
if (!projectPath) {
projectPath = cwd
} else if (fs.existsSync(projectPath)) {
projectPath = path.resolve(projectPath)
} else if (fs.existsSync(path.join(cwd, projectPath))) {
projectPath = path.join(cwd, projectPath)
} else {
logger.error("Invalid project path")
process.exit(1)
}
return projectPath
}
program
.command("verify [project_path]")
.description("Verify the validity of a Kunkun extension")
.option("-b, --batch", "Batch mode", false)
.option("-p, --publish", "Publish Mode. Will exit with 1 if invalid", false)
.action((projectPath: string | undefined, opts: { batch: boolean; publish: boolean }) => {
logger.info("cwd:", cwd)
const valid = verifyCmd(computeProjectDir(projectPath), opts.batch)
if (opts.publish && !valid) {
process.exit(1)
}
})
program
.command("build [project_path]")
.description("Build extension with docker and validate (You must have docker installed)")
.action((projectPath: string | undefined) => {
logger.info("cwd:", cwd)
buildCmd(computeProjectDir(projectPath))
})
program.parse()

9
apps/cli/mod.ts Normal file
View File

@ -0,0 +1,9 @@
export { buildWithDocker, buildWithDockerAndValidate } from "@/utils"
export type { BuildResult } from "@/types"
export {
verifyCustomUiCommand,
verifyTemplateUiCommand,
verifySingleProject,
verifyCmd
} from "@/commands/verify"
export { buildCmd } from "@/commands/build"

44
apps/cli/package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "kksh",
"module": "dist/cli.js",
"version": "0.0.23",
"type": "module",
"bin": {
"kksh": "./dist/cli.js",
"docker-entrypoint.sh": "./dist/docker/entrypoint.sh"
},
"author": "Huakun",
"scripts": {
"build": "bun build.ts",
"test": "cross-env NODE_ENV=test bun test --coverage"
},
"devDependencies": {
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@types/bun": "latest",
"@types/debug": "^4.1.12",
"@types/fs-extra": "^11.0.4",
"cross-env": "^7.0.3",
"rollup": "^4.24.0",
"rollup-plugin-visualizer": "^5.12.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@inquirer/prompts": "^5.2.1",
"@kksh/api": "workspace:*",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"console-table-printer": "^2.12.1",
"debug": "^4.3.6",
"fs-extra": "^11.2.0",
"inquirer": "^10.1.2",
"valibot": "^0.40.0"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,12 @@
import { getRootDir } from "@/constants"
import { buildWithDockerAndValidate } from "@/utils"
export async function buildCmd(projectPath: string) {
const rootDir = getRootDir()
console.log("rootDir: ", rootDir)
const buildResult = await buildWithDockerAndValidate(projectPath)
console.log(buildResult)
}
export default buildCmd

View File

@ -0,0 +1,2 @@
export { default as verifyCmd } from "./verify"
export { default as buildCmd } from "./build"

View File

@ -0,0 +1,106 @@
import path from "path"
import { NODE_ENV } from "@/constants"
import logger from "@/logger"
import { CustomUiCmd, ExtPackageJson, TemplateUiCmd } from "@kksh/api/models"
import { printTable } from "console-table-printer"
import fs from "fs-extra"
import * as v from "valibot"
export function verifyCustomUiCommand(projectRoot: string, cmd: CustomUiCmd): boolean {
if (!cmd.main.startsWith("http")) {
const mainPath = path.join(projectRoot, cmd.dist, cmd.main)
if (
!(
fs.existsSync(mainPath) ||
fs.existsSync(mainPath + ".html") ||
fs.existsSync(path.join(mainPath, "index.html"))
)
) {
logger.error(`main file not found at '${mainPath}' for command ${cmd.name}`)
return false
}
}
return true
}
export function verifyTemplateUiCommand(projectRoot: string, cmd: TemplateUiCmd): boolean {
const mainPath = path.join(projectRoot, cmd.main)
if (!fs.existsSync(mainPath)) {
logger.error(`main file not found at ${mainPath} for command ${cmd.name}`)
return false
}
return true
}
export function verifySingleProject(projectPath: string): boolean {
logger.info(`Verifying project at ${projectPath}`)
const pkgJsonPath = path.join(projectPath, "package.json")
if (!fs.existsSync(pkgJsonPath)) {
logger.error(`package.json not found at [${pkgJsonPath}]`)
return false
}
const pkgJson = fs.readJSONSync(pkgJsonPath)
const result = v.safeParse(ExtPackageJson, pkgJson)
if (!result.success) {
logger.error("package.json is invalid, see issues below:")
console.error(v.flatten<typeof ExtPackageJson>(result.issues))
return false
}
const pkg = result.output
logger.info(`package.json is valid`)
logger.info(`name`, pkg.name)
logger.info(`version`, pkg.version)
logger.info(`identifier`, pkg.kunkun.identifier)
if (pkg.files.length === 0) {
logger.warn(
`"files" field is empty, it is recommended to include only the necessary files, e.g. dist`
)
}
// check if kunkun extension name is the same as the folder name
const folderName = path.basename(projectPath)
if (NODE_ENV === "test") {
// if (pkg.kunkun.identifier === "{{projectName}}") {
console.log("Patching project name from {{projectName}} to", folderName)
pkg.kunkun.identifier = folderName
// }
}
if (pkg.kunkun.identifier !== folderName) {
logger.error(
`Extension package name at [pkg.kunkun.identifier](${pkg.kunkun.identifier}) is not the same as the folder name [${folderName}], please fix it`
)
return false
}
for (const cmd of pkg.kunkun.customUiCmds) {
if (!verifyCustomUiCommand(projectPath, cmd)) {
return false
}
}
for (const cmd of pkg.kunkun.templateUiCmds) {
if (!verifyTemplateUiCommand(projectPath, cmd)) {
return false
}
}
return true
}
export function verifyCmd(projectPath: string, batch: boolean): boolean {
let success = true
if (!batch) {
success = verifySingleProject(projectPath)
} else {
const records: { valid: boolean; path: string }[] = []
fs.readdirSync(projectPath).forEach((dir) => {
const dirPath = path.join(projectPath, dir)
if (fs.existsSync(path.join(dirPath, "package.json"))) {
records.push({ path: dirPath, valid: verifySingleProject(dirPath) })
logger.printDivider("=")
}
})
printTable(records)
success = records.every((record) => record.valid)
}
return success
}
export default verifyCmd

31
apps/cli/src/constants.ts Normal file
View File

@ -0,0 +1,31 @@
import path from "path"
import { fileURLToPath } from "url"
const filepath = fileURLToPath(import.meta.url)
const filename = path.basename(filepath)
const __dirname = path.dirname(filepath)
const isInJs = filename.endsWith(".js")
function inferNodeEnv() {
if (isInJs) {
return "production"
}
if (process.env.NODE_ENV) {
return process.env.NODE_ENV
}
return "development"
}
export const NODE_ENV = inferNodeEnv()
export function getRootDir() {
return isInJs ? __dirname : path.dirname(__dirname)
}
export function getDockerFolder() {
return isInJs ? path.join(getRootDir(), "docker") : path.join(getRootDir(), "src/docker")
}
export function getDockerEntrypoint() {
return path.join(getDockerFolder(), "entrypoint.sh")
}

View File

@ -0,0 +1,17 @@
. ~/.bashrc
cd /workspace
rm *.tgz
rm -rf node_modules
cp -r /workspace /workspace-copy
cd /workspace-copy
pnpm i
pnpm run build
npm pack
# check number of *.tgz file in current directory
# if more than 1, then exit with error
if [ $(ls -1 *.tgz 2>/dev/null | wc -l) -gt 1 ]; then
echo "More than one tgz file found"
exit 1
fi
cp *.tgz /workspace

17
apps/cli/src/logger.ts Normal file
View File

@ -0,0 +1,17 @@
import chalk from "chalk"
import debug from "debug"
debug.enable("*")
export function printDivider(char: string = "=") {
const divider = chalk.blue(char.repeat(process.stdout.columns))
console.log(divider)
}
export default {
debug: debug("debug"),
info: debug("info"),
warn: debug("warn"),
error: debug("error"),
printDivider
}

9
apps/cli/src/types.ts Normal file
View File

@ -0,0 +1,9 @@
import type { ExtPackageJson } from "@kksh/api/models"
export type BuildResult = {
shasum: string
tarballFilename: string
tarballPath: string
extPath: string
pkg: ExtPackageJson
}

179
apps/cli/src/utils.ts Normal file
View File

@ -0,0 +1,179 @@
import { exec, spawn } from "child_process"
import crypto from "crypto"
import path from "path"
import { ExtPackageJson } from "@kksh/api/models"
import fs from "fs-extra"
import * as v from "valibot"
import { getDockerEntrypoint } from "./constants"
import type { BuildResult } from "./types"
/**
* Package Name can be scoped or not
* Use regex to extract package name
* @param packageName
* @param version
*/
export function computeTarballName(packageName: string, version: string): string {
const scoped = packageName.startsWith("@")
if (scoped) {
const [scope, name] = packageName.split("/")
return `${scope.substring(1)}-${name}-${version}.tgz`
} else {
return `${packageName}-${version}.tgz`
}
}
export function computeFileHash(filePath: string, algorithm: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(algorithm)
const stream = fs.createReadStream(filePath)
stream.on("data", (data) => {
// @ts-ignore
hash.update(data)
})
stream.on("end", () => {
const shasum = hash.digest("hex")
resolve(shasum)
})
stream.on("error", (err) => {
reject(err)
})
})
}
export function computeFileSha1(filePath: string): Promise<string> {
return computeFileHash(filePath, "sha1")
}
export function computeFileSha512(filePath: string): Promise<string> {
return computeFileHash(filePath, "sha512")
}
export function computeHash(buffer: Buffer, algorithm: "sha1" | "sha256" | "sha512") {
const hash = crypto.createHash(algorithm)
// @ts-ignore
hash.update(buffer)
return hash.digest("hex")
}
/**
* Docker is used to build each individual extension for safety
* Packages could potentially modify other extensions if they share environment.
* There is also a possibility of leaking environment variables.
* docker run -v $(pwd)/scripts/docker/entrypoint.sh:/entrypoint.sh \
* -v $(pwd)/extensions/$ext:/workspace \
* -w /workspace --rm \
* --platform=linux/amd64 \
* node:20 /entrypoint.sh
* @param extPath
* @returns shasum of the tarball parsed from stderr output
*/
export function buildWithDocker(extPath: string): Promise<{
stderrShasum: string
stderrTarballFilename: string
pkg: ExtPackageJson
}> {
console.log(`Building ${extPath}`)
return new Promise((resolve, reject) => {
const pkg = v.parse(ExtPackageJson, fs.readJsonSync(path.join(extPath, "package.json")))
const dockerEntrypoint = getDockerEntrypoint()
console.log("Docker Entrypoint", dockerEntrypoint)
const dockerCmd = `
run -v ${dockerEntrypoint}:/entrypoint.sh -v ${extPath}:/workspace -w /workspace --rm huakunshen/kunkun-ext-builder:latest /entrypoint.sh`
console.log("dockerCmd", dockerCmd)
const args = dockerCmd
.split(" ")
.filter((arg) => arg.length > 0)
.filter((arg) => arg !== "\n")
const subprocess = spawn("docker", args)
let stderrShasum = ""
let stderrTarballFilename = ""
subprocess.stdout.on("data", (data) => {
console.log(`stdout: ${data}`)
})
subprocess.stderr.on("data", (data) => {
const dataStr = data.toString()
console.error(`stderr: ${dataStr}`)
// if (data instanceof String) {
if (dataStr.includes("npm notice shasum")) {
console.log("shasum found")
const shasumMatch = dataStr.match(/npm notice shasum:\s+([a-f0-9]+)/)
if (shasumMatch) {
stderrShasum = shasumMatch[1]
console.log("Parsed shasum:", stderrShasum)
}
}
if (dataStr.includes("npm notice filename:")) {
const tarballFilename = dataStr.match(/npm notice filename:\s+([^\s]+)/)
if (tarballFilename) {
stderrTarballFilename = tarballFilename[1]
console.log("Parsed tarball:", stderrTarballFilename)
}
} else if (dataStr.includes("filename:")) {
const tarballFilename = dataStr.match(/filename:\s+([^\s]+)/)
if (tarballFilename) {
stderrTarballFilename = tarballFilename[1]
console.log("Parsed tarball:", stderrTarballFilename)
}
}
// } else {
// console.error("data is not string");
// }
})
subprocess.on("close", (code) => {
console.log(`child process exited with code ${code}`)
if (stderrShasum.trim().length === 0 || stderrTarballFilename.trim().length === 0) {
return reject("shasum or tarball filename not found")
}
if (code !== 0) {
return reject(`child process exited with code ${code}`)
} else {
return resolve({ stderrShasum, stderrTarballFilename, pkg })
}
})
})
}
/**
* Use this function to build an extension with docker and validate the tarball
* If this passes, the tarball is ready to be inserted into the database
* @param extPath Extension Path
* @returns
*/
export function buildWithDockerAndValidate(extPath: string): Promise<BuildResult> {
return buildWithDocker(extPath)
.then((res) => {
const parsedTarballPath = path.join(extPath, res.stderrTarballFilename)
if (!fs.existsSync(parsedTarballPath)) {
console.error(`Tarball not found: ${parsedTarballPath}`)
process.exit(1)
}
return computeFileSha1(parsedTarballPath).then((computedShasum) => {
if (computedShasum !== res.stderrShasum) {
console.error(
`Shasum mismatch: Computed(${computedShasum}) !== Output from docker(${res.stderrShasum})`
)
process.exit(1)
} else {
console.log("Shasum matches")
}
return {
shasum: computedShasum,
tarballFilename: res.stderrTarballFilename,
tarballPath: parsedTarballPath,
extPath: extPath,
pkg: res.pkg
}
})
})
.catch((err) => {
console.error(err)
process.exit(1)
})
}

32
apps/cli/tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/*"]
},
"outDir": "dist",
"declaration": true,
"declarationMap": true
}
}

175
apps/create-kunkun/.gitignore vendored Normal file
View File

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@ -0,0 +1,116 @@
# create-kunkun
## 0.1.30
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.23
## 0.1.29
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.22
## 0.1.28
### Patch Changes
- Re-enable server-based dev extension refresh. Deep Link forces switch focus to kunkun
- Updated dependencies
- @kksh/api@0.0.21
## 0.1.27
### Patch Changes
- Fix Some Windows incompatibilities
- Updated dependencies
- @kksh/api@0.0.20
## 0.1.26
### Patch Changes
- Fix windows compatibility errors
## 0.1.23
### Patch Changes
- Store templates as .tgz in dist, to avoid missing .gitignore problem
## 0.1.18
### Patch Changes
- Update templates README with detailed instructions
## 0.1.17
### Patch Changes
- Updated dependencies [9fa3d8e]
- Updated dependencies [0a1ab7c]
- Updated dependencies [b48d53b]
- @kksh/api@0.0.20
## 0.1.16
### Patch Changes
- Updated dependencies [9fa3d8e]
- Updated dependencies [0a1ab7c]
- Updated dependencies [b48d53b]
- @kksh/api@0.0.20
## 0.1.13
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.11
## 0.1.12
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.10
## 0.1.11
### Patch Changes
- Update templates, custom ui templates now requires base url. Due to a Tauri API problem on Windows.
## 0.1.10
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.9
## 0.1.7
### Patch Changes
- Add some helper functions for template worker extension building. Replace rollup with bun.
- Updated dependencies
- @kksh/api@0.0.6
## 0.1.6
### Patch Changes
- Updated dependencies [ec47e1e]
- @kksh/api@0.0.5
## 0.1.5
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4

View File

@ -0,0 +1,16 @@
# Kunkun Extension Initializer
## Usage
```bash
npm init kunkun@latest
npx create-kunkun@latest
```
## Develop
```bash
bun index.ts --help
bun index.ts
```

View File

@ -0,0 +1,42 @@
/**
* This is a E2E test, create every template from production build and run `npm install` and `npm run build`
* When running `npm install` with bun shell, it fails in bun test environment, so I simply run everything as regular ts without test()
*/
import { $ } from "bun"
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
import os from "os"
import path from "path"
import fs from "fs-extra"
import { getRootDir } from "../src/constants"
const testDir = path.join(os.tmpdir(), "kunkun-create-kunkun-test")
console.log("Test Dir: ", testDir)
const distDir = path.join(getRootDir(), "dist")
const indexjsPath = path.join(distDir, "index.mjs")
const templateNames = ["template", "react", "vue", "nuxt", "svelte", "sveltekit"]
fs.rmdirSync(testDir, { recursive: true })
fs.mkdirpSync(testDir)
await Promise.all(
templateNames.map(async (templateName) => {
const folderName = `${templateName}-ext`
await $`node ${indexjsPath} --outdir ${testDir} --name ${folderName} --template ${templateName}`
const templateDir = path.join(testDir, folderName)
await $`rm -rf node_modules`.cwd(templateDir).text() // this doesn't work within bun test
await $`pnpm install`.cwd(templateDir).text() // this doesn't work within bun test
await $`pnpm run build`.cwd(templateDir).text()
})
)
test("Build Artifact Existence", () => {
templateNames.forEach(async (templateName) => {
const expectedOutDir = templateName === "sveltekit" ? "build" : "dist"
const folderName = `${templateName}-ext`
const templateDir = path.join(testDir, folderName)
expect(fs.existsSync(path.join(templateDir, expectedOutDir))).toBeTrue()
})
})
afterAll(() => {
fs.rmdirSync(testDir, { recursive: true })
})

View File

@ -0,0 +1,79 @@
import os from "os"
import path from "path"
import { $ } from "bun"
import chalk from "chalk"
import fs from "fs-extra"
import getFolderSize from "get-folder-size"
import { getRootDir } from "./src/constants"
import { cleanExtension, patchManifestJsonSchema, patchPkgJsonDep } from "./src/patch"
import { tarCompress } from "./src/utils"
await $`rm -rf dist`
await $`bun build index.ts --outfile=dist/index.mjs --target node`.env({
NODE_ENV: "production"
})
// await $`pnpm rolldown -c`
/* -------------------------------------------------------------------------- */
/* Post Build */
/* -------------------------------------------------------------------------- */
const distPath = path.join(getRootDir(), "dist")
const distTemplatesPath = path.join(distPath, "templates")
const tmpDistTemplatesPath = path.join(distPath, "tmp-templates")
// clear distTemplatesPath
fs.emptyDirSync(distTemplatesPath)
fs.emptyDirSync(tmpDistTemplatesPath)
/* -------------------------------------------------------------------------- */
/* copy ../../templates to dist/templates */
/* -------------------------------------------------------------------------- */
console.log(getRootDir())
const templatesPath = path.join(getRootDir(), "../..", "templates")
fs.copySync(templatesPath, tmpDistTemplatesPath, { dereference: os.platform() === "win32" })
/* -------------------------------------------------------------------------- */
/* Clean Dist Folder */
/* -------------------------------------------------------------------------- */
for (const p of fs.readdirSync(tmpDistTemplatesPath)) {
console.log("Clean Extension", path.join(tmpDistTemplatesPath, p))
cleanExtension(path.join(tmpDistTemplatesPath, p))
}
/* -------------------------------------------------------------------------- */
/* Patch Templates */
/* -------------------------------------------------------------------------- */
for (const p of fs.readdirSync(tmpDistTemplatesPath)) {
const pkgJsonPath = path.join(tmpDistTemplatesPath, p, "package.json")
if (fs.existsSync(pkgJsonPath)) {
/* ----------------------- Patch Package Dependencies ----------------------- */
// Replace local dependencies (workspace:*) with real dependencies
await patchPkgJsonDep(pkgJsonPath)
/* ----------------------- Patch Manifest JSON Schema ----------------------- */
// Replace local template with remote schema
patchManifestJsonSchema(pkgJsonPath)
// remove node_modules
fs.rmdirSync(path.join(distPath, "templates", p, "node_modules"), { recursive: true })
}
}
/* -------------------------------------------------------------------------- */
/* Zip Templates */
/* -------------------------------------------------------------------------- */
for (const p of fs.readdirSync(tmpDistTemplatesPath)) {
const src = path.join(tmpDistTemplatesPath, p)
// skip if src is not a directory
if (!fs.statSync(src).isDirectory()) {
continue
}
const dest = path.join(distTemplatesPath, `${p}.tgz`)
console.log(`${chalk.green("Zipping")} ${chalk.blue(src)} to ${chalk.blue(dest)}`)
await tarCompress(src, dest)
}
fs.rmSync(tmpDistTemplatesPath, { recursive: true })
// get total folder size of distTemplatesPath
const size = await getFolderSize.loose(distTemplatesPath)
console.log(`dist size ${(size / 1000 / 1000).toFixed(2)} MB`)

BIN
apps/create-kunkun/bun.lockb Executable file

Binary file not shown.

193
apps/create-kunkun/index.ts Normal file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env node
import path from "path"
import { input, select } from "@inquirer/prompts"
import { version as kkApiVersion } from "@kksh/api/package.json"
import chalk from "chalk"
import { Command, Option } from "commander"
import fs from "fs-extra"
import pkgJson from "./package.json"
import { createKunkunVersion, getTemplateRoot, isProduction } from "./src/constants"
import { cleanExtension, patchHBS, patchManifestJsonSchema, patchPkgJsonDep } from "./src/patch"
import { getLatestNpmPkgVersion, tarExtract } from "./src/utils"
console.log(`${chalk.blue("create-kunkun version:")} ${createKunkunVersion}`)
const latestCreateKunkunVersion = await getLatestNpmPkgVersion("create-kunkun")
console.log(`${chalk.blue("Latest create-kunkun version:")} latestCreateKunkunVersion`)
if (latestCreateKunkunVersion !== createKunkunVersion) {
const msg = `You are using create-kunkun version ${createKunkunVersion}, but the latest version is ${latestCreateKunkunVersion}. It may not work with the latest Kunkun app.`
console.warn(chalk.red(msg))
}
const cwd = process.cwd()
const templateRoot = getTemplateRoot()
console.info(`${chalk.blue("Current Working Directory")}: ${cwd}`)
console.info(`${chalk.blue("Template Root:")}`, templateRoot)
if (!fs.existsSync(templateRoot)) {
console.error(`Template directory not found; Expected at ${templateRoot}`)
process.exit(1)
}
const program = new Command()
program
.version(pkgJson.version)
.addOption(
new Option("-t, --template <template>", "Extension Template").choices([
"template",
"react",
"vue",
"svelte",
"nuxt",
"sveltekit",
"next"
])
)
.addOption(new Option("-n, --name <name>", "Extension Name"))
.addOption(new Option("-f, --force", "Overwrite existing files").default(false))
.addOption(new Option("-o, --outdir <outdir>", "Output directory").default(cwd))
.parse(process.argv)
type Template = "react" | "template" | "vue" | "svelte" | "nuxt" | "sveltekit" | "next"
const options = program.opts<{
template?: Template
outdir: string
force: boolean
name?: string
}>()
let template: Template | undefined = options.template
let name = options.name
console.log("Options:", options)
const outdir = path.resolve(options.outdir)
console.info(`${chalk.blue("Outdir: ")}${outdir}`)
if (!fs.existsSync(outdir)) {
fs.mkdirSync(outdir, { recursive: true })
}
async function copyTemplate(templateTgz: string, targetFolderName: string): Promise<string> {
const destDir = path.join(outdir, targetFolderName)
if (!fs.existsSync(templateTgz)) {
console.error(`Worker Extension Template not found at ${templateTgz}`)
process.exit(1)
}
console.info(`${chalk.blue("Template Source Path:")} ${templateTgz}`)
if (fs.existsSync(destDir)) {
if (!options.force) {
console.error(`Destination directory already exists: ${destDir}`)
process.exit(1)
} else {
fs.removeSync(destDir)
}
}
await tarExtract(templateTgz, destDir)
// fs.mkdirSync(destDir, { recursive: true })
console.info(
`Template copied from \n\t${chalk.blue(templateTgz)} \nto \n\t${chalk.blue(destDir)}`
)
// fs.copySync(templateTgz, destDir)
return destDir
}
;(async function () {
if (!template) {
template = await select({
message: "Select an Extension Template",
choices: [
{
name: "Preset Template (Web Worker)",
value: "template",
description:
"Write regular logic in TypeScript in OOP manner to render extension UI based on predefined template."
},
{
name: "React Custom UI",
value: "react",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use React to build complex UI."
},
{
name: "Vue Custom UI",
value: "vue",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use React to build complex UI."
},
{
name: "Svelte Custom UI",
value: "svelte",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use React to build complex UI."
},
{
name: "Nuxt Custom UI",
value: "nuxt",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use Nuxt to build complex UI."
},
{
name: "Next.js Custom UI",
value: "next",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use Next.js to build complex UI."
},
{
name: "Sveltekit Custom UI",
value: "sveltekit",
description:
"Extension will be rendered within iframe as a regular web app. The UI can be arbitrarily complex. Choose this if you want to use Sveltekit to build complex UI."
}
]
})
}
if (!name) {
name = await input({
message: "Enter Extension Name",
default: `kunkun-extension-${template}`
})
}
let destDir = ""
if (template === "template") {
destDir = await copyTemplate(path.join(templateRoot, "template-ext-worker.tgz"), name)
cleanExtension(destDir)
} else if (["react", "vue", "svelte", "nuxt", "sveltekit", "next"].includes(template)) {
destDir = await copyTemplate(path.join(templateRoot, `template-ext-${template}.tgz`), name)
cleanExtension(destDir)
} else {
console.error("Invalid template")
process.exit(1)
}
console.log("Destination Dir:", destDir)
if (!isProduction) {
const pkgJsonPath = path.join(destDir, "package.json")
patchManifestJsonSchema(pkgJsonPath)
patchPkgJsonDep(pkgJsonPath)
}
/* -------------------------------------------------------------------------- */
/* Patch HBS Templates */
/* -------------------------------------------------------------------------- */
console.log(`Start Patching ${name}`)
await new Promise((resolve) => setTimeout(resolve, 1000)) // add some delay after files are created, otherwsie files can't be overwritten
patchHBS(path.join(destDir, "package.json"), { projectName: name })
switch (template) {
case "nuxt":
patchHBS(path.join(destDir, "nuxt.config.ts"), { projectName: name })
break
case "react":
patchHBS(path.join(destDir, "vite.config.ts"), { projectName: name })
break
case "vue":
patchHBS(path.join(destDir, "vite.config.ts"), { projectName: name })
break
case "svelte":
patchHBS(path.join(destDir, "vite.config.ts"), { projectName: name })
break
case "sveltekit":
patchHBS(path.join(destDir, "svelte.config.js"), { projectName: name })
break
case "next":
patchHBS(path.join(destDir, "next.config.mjs"), { projectName: name })
break
default:
break
}
})()

View File

@ -0,0 +1,43 @@
{
"name": "create-kunkun",
"type": "module",
"version": "0.1.33",
"bin": {
"create-kunkun": "dist/index.mjs"
},
"scripts": {
"prepublishOnly": "bun build.ts",
"build": "bun build.ts",
"test": "bun test --coverage"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@types/bun": "latest",
"@types/fs-extra": "^11.0.4",
"get-folder-size": "^5.0.0",
"rollup": "^4.24.0",
"rollup-plugin-visualizer": "^5.12.0",
"tar": "^7.4.3",
"vitest": "^2.0.0"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@inquirer/prompts": "^5.2.1",
"@kksh/api": "workspace:*",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8",
"valibot": "^0.40.0"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,32 @@
import commonjs from "@rollup/plugin-commonjs"
import json from "@rollup/plugin-json"
import resolve from "@rollup/plugin-node-resolve"
import replace from "@rollup/plugin-replace"
import terser from "@rollup/plugin-terser"
import typescript from "@rollup/plugin-typescript"
import { visualizer } from "rollup-plugin-visualizer"
/** @type {import('rollup').RollupOptions} */
const config = {
input: "index.ts", // Path to your worker file
output: {
file: "dist/index.cjs",
format: "cjs"
},
plugins: [
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production")
}),
json(),
typescript(),
resolve({
preferBuiltins: true
}),
commonjs(),
terser(),
visualizer()
]
}
export default config

View File

@ -0,0 +1,39 @@
import path from "path"
import fs from "fs-extra"
import { getRootDir } from "../src/constants"
import { cleanExtension, patchManifestJsonSchema, patchPkgJsonDep } from "../src/patch"
const distPath = path.join(getRootDir(), "dist")
const distTemplatesPath = path.join(distPath, "templates")
// clear distTemplatesPath
fs.emptyDirSync(distTemplatesPath)
/* -------------------------------------------------------------------------- */
/* copy ../../templates to dist/templates */
/* -------------------------------------------------------------------------- */
const templatesPath = path.join(getRootDir(), "../..", "templates")
await fs.copy(templatesPath, distTemplatesPath)
/* -------------------------------------------------------------------------- */
/* Clean Dist Folder */
/* -------------------------------------------------------------------------- */
for (const p of fs.readdirSync(distTemplatesPath)) {
cleanExtension(path.join(distPath, "templates", p))
}
/* -------------------------------------------------------------------------- */
/* Patch Templates */
/* -------------------------------------------------------------------------- */
for (const p of fs.readdirSync(distTemplatesPath)) {
const pkgJsonPath = path.join(distPath, "templates", p, "package.json")
if (fs.existsSync(pkgJsonPath)) {
/* ----------------------- Patch Package Dependencies ----------------------- */
// Replace local dependencies (workspace:*) with real dependencies
await patchPkgJsonDep(pkgJsonPath)
/* ----------------------- Patch Manifest JSON Schema ----------------------- */
// Replace local template with remote schema
patchManifestJsonSchema(pkgJsonPath)
// remove node_modules
fs.rmdirSync(path.join(distPath, "templates", p, "node_modules"), { recursive: true })
}
}

View File

@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test"
import { getLatestNpmPkgInfo, getLatestNpmPkgVersion } from "../utils"
test("getLatestNpmPkgInfo", async () => {
const pkg = await getLatestNpmPkgInfo("@kksh/vue")
expect(pkg.name).toBe("@kksh/vue")
expect(pkg.version).toBeDefined()
})
test("getLatestNpmPkgVersion", async () => {
const version = await getLatestNpmPkgVersion("@kksh/vue")
expect(version).toBeDefined()
})

View File

@ -0,0 +1,18 @@
import path from "path"
import { fileURLToPath } from "url"
import { version } from "../package.json"
export const NODE_ENV = process.env.NODE_ENV ?? "development"
export const isProduction = NODE_ENV === "production"
export function getRootDir() {
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
return isProduction ? __dirname : path.dirname(__dirname)
}
export function getTemplateRoot() {
return isProduction
? path.join(getRootDir(), "templates")
: path.join(getRootDir(), "../../templates")
}
export const createKunkunVersion = version

View File

@ -0,0 +1,101 @@
import { execSync } from "child_process"
import path from "path"
import { ExtPackageJson } from "@kksh/api/models"
import { $ } from "bun"
import fs from "fs-extra"
import Handlebars from "handlebars"
import { flatten, safeParse } from "valibot"
import { isProduction } from "./constants"
import { findPkgVersions } from "./utils"
/* -------------------------------------------------------------------------- */
/* Worker Extension */
/* -------------------------------------------------------------------------- */
export function cleanExtension(dir: string) {
// Read .gitignore if it exists
const gitignorePath = path.join(dir, ".gitignore")
let ignorePatterns: string[] = []
if (fs.existsSync(gitignorePath)) {
ignorePatterns = fs
.readFileSync(gitignorePath, "utf-8")
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
}
// Always include some common build/dependency directories
const defaultIgnores = ["node_modules", "dist", ".turbo", "build"]
ignorePatterns.push(...defaultIgnores)
// Find and remove all ignored files/directories
for (const pattern of ignorePatterns) {
const itemPath = path.join(dir, pattern)
if (fs.existsSync(itemPath)) {
fs.removeSync(itemPath)
}
}
}
export function patchManifestJsonSchema(pkgJsonPath: string) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"))
pkgJson["$schema"] = "https://schema.kunkun.sh"
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
}
/**
* Remove workspace:* dependencies and add dependencies with proper versions
* This should be run only in development mode
* @param pkgJsonPath path to created template's package.json
* @param kkApiVersion @kksh/api version with the current create-kunkun version
*/
export async function patchPkgJsonDep(pkgJsonPath: string) {
if (isProduction) {
throw new Error("This function is only available in development mode")
}
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"))
pkgJson.name = `{{projectName}}`
pkgJson.kunkun.identifier = `{{projectName}}`
const monorepoPkgVersions = await findPkgVersions()
for (const [dep, v] of Object.entries(pkgJson.dependencies)) {
if ((v as string).startsWith("workspace:")) {
if (!monorepoPkgVersions[dep]) {
console.error(`Package ${dep} not found in monorepo`)
process.exit(1)
}
pkgJson.dependencies[dep] = monorepoPkgVersions[dep]
}
}
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
}
export function validatePackageJson(pkgJsonPath: string) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"))
const parseRes = safeParse(ExtPackageJson, pkgJson)
if (!parseRes.success) {
console.error(
`Unexpected Error: Invalid package.json: ${flatten<typeof ExtPackageJson>(parseRes.issues)}`
)
process.exit(1)
}
}
export function patchInstallAPI(dir: string) {
// cd into the directory and run `npm install`, and run `npm install @kksh/react`
console.info(`Running: npm install`)
execSync("npm install", { cwd: dir, stdio: "inherit" })
// TODO: Uncomment the following line after @kksh/react is published
// console.info(`Running: npm install @kksh/react`)
// execSync("npm install @kksh/react", { cwd: dir, stdio: "inherit" })
}
export function patchHBS(filePath: string, data: Record<string, any>) {
if (!fs.existsSync(filePath)) {
console.error(`Patch HBS: File ${filePath} not found`)
process.exit(1)
}
const template = Handlebars.compile(fs.readFileSync(filePath, "utf-8"))
const result = template(data)
fs.writeFileSync(filePath, result)
}

View File

@ -0,0 +1,108 @@
import os from "os"
import path from "path"
import chalk from "chalk"
import fs from "fs-extra"
import { create as createTar, extract as extractTar } from "tar"
import * as v from "valibot"
import { getRootDir, isProduction } from "../src/constants"
export function getLatestNpmPkgInfo(pkgName: string): Promise<Record<string, any>> {
return fetch(`https://registry.npmjs.org/${pkgName}/latest`).then((res) => res.json())
}
export function getLatestNpmPkgVersion(pkgName: string): Promise<string> {
return getLatestNpmPkgInfo(pkgName).then(
(data) => v.parse(v.object({ version: v.string() }), data).version
)
}
/**
* Obtain the current package verisons of all packages in the monorepo
* This function is used only in development mode
* @returns
*/
export async function findPkgVersions() {
if (isProduction) {
throw new Error("This function is only available in development mode")
}
const pkgVersions: Record<string, string> = {}
const root = getRootDir()
const repoRoot = path.join(root, "../../")
const searchFolders = [path.join(repoRoot, "apps"), path.join(repoRoot, "packages")]
for (const folder of searchFolders) {
const packages = fs.readdirSync(folder)
// console.log("Packages: ", packages);
for (const pkg of packages) {
const pkgJsonPath = path.join(folder, pkg, "package.json")
if (fs.existsSync(pkgJsonPath)) {
const pkgJson = fs.readJsonSync(pkgJsonPath)
pkgVersions[pkgJson.name] = pkgJson.version
}
}
}
for (const pkgName of ["@kksh/vue", "@kksh/react", "@kksh/svelte"]) {
const version = await getLatestNpmPkgVersion(pkgName)
pkgVersions[pkgName] = version
}
return pkgVersions
}
export function tarCompress(src: string, dest: string) {
// get src parent dir
const srcParentDir = path.dirname(src)
const srcFileName = path.basename(src)
return createTar(
{
file: dest,
gzip: true,
cwd: srcParentDir,
filter: (path) => {
const ignoreList = ["node_modules", "dist", ".turbo", "extensions_support"]
return !ignoreList.some((ignore) => path.includes(ignore))
}
},
[srcFileName]
)
}
export function tarExtract(src: string, dest: string) {
const srcFileName = path.basename(src)
const destDir = path.dirname(dest)
const destFolderName = path.basename(dest)
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
if (fs.existsSync(dest)) {
const msg = `${dest} already exists`
console.log(chalk.red(msg))
process.exit(1)
}
// get a tmp dir
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-kunkun-extract"))
if (fs.existsSync(tmpDir)) {
// remove tmp dir
fs.rmSync(tmpDir, { recursive: true })
}
fs.mkdirSync(tmpDir, { recursive: true })
return extractTar({
file: src,
C: tmpDir
}).then(() => {
const srcFileNameExt = path.extname(src)
const srcFileNameWithoutExt = path.basename(src, srcFileNameExt)
const intermediateDest = path.join(tmpDir, srcFileNameWithoutExt)
if (!fs.existsSync(intermediateDest)) {
const msg = `Intermediate destination ${intermediateDest} not found, extraction failed`
console.log(chalk.red(msg))
process.exit(1)
}
// move intermediateDest to dest
fs.copySync(intermediateDest, dest)
// remove tmp dir
fs.rmSync(tmpDir, { recursive: true })
})
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

820
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
templates/index.html Normal file
View File

@ -0,0 +1 @@
<h1>Dev Extensions</h1>

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
templates/template-ext-next/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
extensions_support/

View File

@ -0,0 +1,8 @@
# template-ext-next
## 0.1.1
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4

View File

@ -0,0 +1,69 @@
# Kunkun Custom UI Extension Template (Next.js)
[Custom UI Extension Documentation](https://docs.kunkun.sh/extensions/custom-ui-ext/)
This is a template for a custom UI extension.
This type of extension is basically a static website. You can use any frontend framework you like, this template uses [Next.js](https://nextjs.org/).
It is assumed that you have some knowledge of frontend development with Next.js.
## Development
Development is the same as developing a normal website.
```bash
pnpm install
pnpm dev
pnpm build
```
- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path.
In `package.json`, `"devMain"` is the url for development server, and `"main"` is the path to static `.html` file for production.
To load the extension in development mode, you have to enable it with `Toggle Dev Extension Live Load Mode` command in Kunkun. A `Live` badge will be shown on the commands. This indicates that dev extensions will be loaded from `devMain` instead of `main`.
## Advanced
### Rendering Mode
This is a Meta-Framework template, and already configured with SSG rendering mode.
Please do not enable SSR unless you know what you are doing.
There will not be a JS runtime in production, and Kunkun always load the extension as static files.
The main benefit of using a meta-framework is that it comes with routing, and will output multiple `.html` files, which makes multi-command support much easier.
## Verify Build and Publish
```bash
pnpm build # make sure the build npm script works
npx kksh@latest verify # Verify some basic settings before publishing
```
It is recommended to build the extension with the same environment our CI uses.
The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`.
You can use the following command to build the extension with the same environment our CI uses.
This requires you to have docker installed, and the shell you are using has access to it via `docker` command.
```bash
npx kksh@latest build # Build the extension with
```
`pnpm` is used to install dependencies and build the extension.
The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed.
If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`.
After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension.
The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files.
This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun.
After verifying the tarball, it's ready to be published.
Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR.
Once CI passed and PR merged, you can use your extension in Kunkun.

View File

@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
transpilePackages: ["@kksh/api", "comlink-stdio"]
}
export default nextConfig

View File

@ -0,0 +1,62 @@
{
"$schema": "./node_modules/@kksh/api/dist/schema.json",
"name": "template-ext-next",
"version": "0.1.1",
"private": true,
"kunkun": {
"name": "TODO: Change Display Name",
"shortDescription": "A Custom UI template for next",
"longDescription": "A Custom UI template for next",
"identifier": "template-ext-next",
"icon": {
"type": "iconify",
"value": "logos:nextjs-icon"
},
"demoImages": [],
"permissions": [],
"customUiCmds": [
{
"main": "/",
"dist": "out",
"devMain": "http://localhost:5173/",
"name": "Next Template Home Page",
"cmds": []
},
{
"main": "/about.html",
"dist": "out",
"devMain": "http://localhost:5173/about",
"name": "Next Template About Page",
"cmds": []
}
],
"templateUiCmds": []
},
"scripts": {
"dev": "next dev -p 5173",
"build": "next build",
"start": "next start -p 5173",
"lint": "next lint"
},
"dependencies": {
"@kksh/api": "workspace:*",
"@kksh/react": "0.1.1",
"@radix-ui/react-icons": "^1.3.0",
"next": "14.2.15",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.7",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
"files": [
"out"
]
}

View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,10 @@
"use client"
import dynamic from "next/dynamic"
const About = dynamic(() => import("@/components/about"), {
ssr: false
})
export default function AboutPage() {
return <About />
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,6 @@
@import url("@kksh/react/css");
@import url("@kksh/react/themes");
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@ -0,0 +1,11 @@
// dev url: http://localhost:5173/dev-extensions/template-ext-next/out
"use client"
import dynamic from "next/dynamic"
const Main = dynamic(() => import("@/components/main"), {
ssr: false
})
export default function Home() {
return <Main />
}

View File

@ -0,0 +1,16 @@
"use client"
import { ui } from "@kksh/api/ui/iframe"
import { useEffect } from "react"
export default function About() {
useEffect(() => {
ui.showBackButton("bottom-right")
}, [])
return (
<div>
<h1 className="text-xl font-bold">About Page</h1>
<a href="./">Go To Home Page</a>
</div>
)
}

View File

@ -0,0 +1,17 @@
"use client"
import { ui } from "@kksh/api/ui/iframe"
import { Button } from "@kksh/react"
import { useEffect } from "react"
export default function Home() {
useEffect(() => {
ui.showBackButton("bottom-right")
}, [])
return (
<div>
<h1 className="text-xl font-bold">Home Page</h1>
<Button>Button</Button>
</div>
)
}

View File

@ -0,0 +1,20 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"allowImportingTsExtensions": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

25
templates/template-ext-nuxt/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
extensions_support/

View File

@ -0,0 +1,15 @@
# template-ext-nuxt
## 0.0.3
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4
## 0.0.2
### Patch Changes
- Updated dependencies [fba6a49]
- @kksh/vue@0.0.1

View File

@ -0,0 +1,69 @@
# Kunkun Custom UI Extension Template (Nuxt)
[Custom UI Extension Documentation](https://docs.kunkun.sh/extensions/custom-ui-ext/)
This is a template for a custom UI extension.
This type of extension is basically a static website. You can use any frontend framework you like, this template uses [Nuxt](https://nuxt.com/).
It is assumed that you have some knowledge of frontend development with Nuxt.
## Development
Development is the same as developing a normal website.
```bash
pnpm install
pnpm dev
pnpm build
```
- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path.
In `package.json`, `"devMain"` is the url for development server, and `"main"` is the path to static `.html` file for production.
To load the extension in development mode, you have to enable it with `Toggle Dev Extension Live Load Mode` command in Kunkun. A `Live` badge will be shown on the commands. This indicates that dev extensions will be loaded from `devMain` instead of `main`.
## Advanced
### Rendering Mode
This is a Meta-Framework template, and already configured with SSG rendering mode.
Please do not enable SSR unless you know what you are doing.
There will not be a JS runtime in production, and Kunkun always load the extension as static files.
The main benefit of using a meta-framework is that it comes with routing, and will output multiple `.html` files, which makes multi-command support much easier.
## Verify Build and Publish
```bash
pnpm build # make sure the build npm script works
npx kksh@latest verify # Verify some basic settings before publishing
```
It is recommended to build the extension with the same environment our CI uses.
The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`.
You can use the following command to build the extension with the same environment our CI uses.
This requires you to have docker installed, and the shell you are using has access to it via `docker` command.
```bash
npx kksh@latest build # Build the extension with
```
`pnpm` is used to install dependencies and build the extension.
The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed.
If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`.
After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension.
The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files.
This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun.
After verifying the tarball, it's ready to be published.
Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR.
Once CI passed and PR merged, you can use your extension in Kunkun.

View File

@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>

View File

@ -0,0 +1,16 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
css: ["@kksh/vue/css", "@kksh/vue/themes"],
devtools: { enabled: true },
ssr: false,
devServer: {
port: 5173 // avoid conflict Desktop App in Dev Mode
},
nitro: {
output: {
publicDir: "dist"
}
},
modules: ["@nuxtjs/tailwindcss"]
})

View File

@ -0,0 +1,59 @@
{
"$schema": "./node_modules/@kksh/api/dist/schema.json",
"name": "template-ext-nuxt",
"version": "0.0.3",
"private": true,
"type": "module",
"kunkun": {
"name": "TODO: Change Display Name",
"shortDescription": "A Custom UI template for nuxt",
"longDescription": "A Custom UI template for nuxt",
"identifier": "template-ext-nuxt",
"icon": {
"type": "iconify",
"value": "logos:nuxt-icon"
},
"demoImages": [],
"permissions": [],
"customUiCmds": [
{
"main": "/",
"dist": "dist",
"devMain": "http://localhost:5173",
"name": "Nuxt Template Home Page",
"cmds": []
},
{
"main": "/about",
"dist": "dist",
"devMain": "http://localhost:5173/about",
"name": "Nuxt Template About Page",
"cmds": []
}
],
"templateUiCmds": []
},
"scripts": {
"build": "nuxt generate",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@kksh/api": "workspace:*",
"@kksh/vue": "0.1.3",
"@nuxtjs/tailwindcss": "^6.12.1",
"nuxt": "^3.12.4",
"tailwindcss": "^3.4.7",
"vite": "^5.4.9",
"vue": "latest"
},
"devDependencies": {
"typescript": "^5.5.4"
},
"files": [
"dist",
".gitignore"
]
}

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { Button } from "@kksh/vue"
</script>
<template>
<main class="p-4">
<h1 class="text-3xl font-bold">About</h1>
<NuxtLink to="/">
<Button>Home</Button>
</NuxtLink>
</main>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { ui } from "@kksh/api/ui/iframe"
import {
Button,
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
updateTheme
} from "@kksh/vue"
onMounted(() => {
ui.getTheme().then((theme) => {
updateTheme(theme)
})
})
</script>
<template>
<main class="h-screen">
<Command>
<CommandInput placeholder="Type a command or search..." />
<div class="grow">
<CommandList class="h-full">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem value="calendar"> Calendar </CommandItem>
<CommandItem value="search-emoji"> Search Emoji </CommandItem>
<CommandItem value="calculator"> Calculator </CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem value="profile"> Profile </CommandItem>
<CommandItem value="billing"> Billing </CommandItem>
<CommandItem value="settings"> Settings </CommandItem>
</CommandGroup>
</CommandList>
</div>
<div class="h-10 border">
<NuxtLink to="/about">
<Button class="float-right">About Page</Button>
</NuxtLink>
</div>
</Command>
</main>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

26
templates/template-ext-react/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
extensions_support/

View File

@ -0,0 +1,15 @@
# template-ext-react
## 0.0.2
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4
## 0.0.1
### Patch Changes
- Updated dependencies [fba6a49]
- @kksh/react@0.0.2

View File

@ -0,0 +1,67 @@
# Kunkun Custom UI Extension Template (React)
[Custom UI Extension Documentation](https://docs.kunkun.sh/extensions/custom-ui-ext/)
This is a template for a custom UI extension.
This type of extension is basically a static website. You can use any frontend framework you like, this template uses [React](https://react.dev/).
It is assumed that you have some knowledge of frontend development with React.
## Development
Development is the same as developing a normal website.
```bash
pnpm install
pnpm dev
pnpm build
```
- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path.
In `package.json`, `"devMain"` is the url for development server, and `"main"` is the path to static `.html` file for production.
To load the extension in development mode, you have to enable it with `Toggle Dev Extension Live Load Mode` command in Kunkun. A `Live` badge will be shown on the commands. This indicates that dev extensions will be loaded from `devMain` instead of `main`.
## Advanced
### Multi-Command
To support multiple commands, you will need multiple `.html` files as entrypoints, and register each command in `package.json`.
It is recommended to use a meta-framework and build with SSG rendering mode, which comes with routing and will output multiple `.html` files.
Kunkun provides meta-framework templates for Nuxt, Next, SvelteKit.
## Verify Build and Publish
```bash
pnpm build # make sure the build npm script works
npx kksh@latest verify # Verify some basic settings before publishing
```
It is recommended to build the extension with the same environment our CI uses.
The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`.
You can use the following command to build the extension with the same environment our CI uses.
This requires you to have docker installed, and the shell you are using has access to it via `docker` command.
```bash
npx kksh@latest build # Build the extension with
```
`pnpm` is used to install dependencies and build the extension.
The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed.
If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`.
After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension.
The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files.
This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun.
After verifying the tarball, it's ready to be published.
Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR.
Once CI passed and PR merged, you can use your extension in Kunkun.

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,63 @@
{
"$schema": "./node_modules/@kksh/api/dist/schema.json",
"name": "template-ext-react",
"private": true,
"version": "0.0.2",
"type": "module",
"kunkun": {
"name": "TODO: Change Display Name",
"shortDescription": "A Custom UI template for react",
"longDescription": "A Custom UI template for react",
"identifier": "template-ext-react",
"icon": {
"type": "iconify",
"value": "logos:react"
},
"demoImages": [],
"permissions": [],
"customUiCmds": [
{
"main": "/",
"dist": "dist",
"devMain": "http://localhost:5173",
"name": "React Extension Template",
"cmds": []
}
],
"templateUiCmds": []
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@kksh/api": "workspace:*",
"@kksh/react": "0.1.1",
"@radix-ui/react-icons": "^1.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.7.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.6",
"typescript": "^5.2.2",
"vite": "^5.4.9",
"vue": "^3.4.35"
},
"files": [
"dist",
".gitignore"
]
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,156 @@
import { ui } from "@kksh/api/ui/iframe"
import {
ActionPanel,
Button,
Command,
CommandDemo,
CommandEmpty,
CommandFooter,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
ThemeCustomizer,
ThemeProvider,
ThemeWrapper,
TooltipProvider,
VertifcalSeparator
} from "@kksh/react"
import {
CalendarIcon,
EnvelopeClosedIcon,
FaceIcon,
GearIcon,
InstagramLogoIcon,
LinkedInLogoIcon,
PersonIcon,
RocketIcon,
TwitterLogoIcon
} from "@radix-ui/react-icons"
import { useEffect, useRef, useState } from "react"
function App() {
const [value, setValue] = useState("linear")
const actionInputRef = useRef<HTMLInputElement | null>(null)
const [input, setInput] = useState("")
const listRef = useRef(null)
const seachInputEle = useRef<HTMLInputElement | null>(null)
useEffect(() => {
ui.registerDragRegion()
ui.showMoveButton({
// top: 10,
// right: 20
bottom: 0.2,
left: 0.2
})
}, [])
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Escape") {
if (input.length === 0) {
ui.goBack()
} else {
setInput("")
}
}
}
return (
<ThemeProvider>
<main className="h-screen">
<Command
onValueChange={(v) => {
setValue(v)
}}
>
<CommandInput
autoFocus
ref={seachInputEle}
placeholder="Type a command or search..."
className="h-12"
onInput={(e) => {
setInput((e.target as HTMLInputElement).value)
}}
value={input}
onKeyDown={onKeyDown}
>
<div className="h-8 w-8"></div>
</CommandInput>
<CommandList className="h-full">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>
<TwitterLogoIcon className="mr-2 h-4 w-4" />
<span>Twitter</span>
</CommandItem>
<CommandItem>
<InstagramLogoIcon className="mr-2 h-4 w-4" />
<span>Instagram</span>
</CommandItem>
<CommandItem>
<LinkedInLogoIcon className="mr-2 h-4 w-4" />
<span>LinkedIn</span>
</CommandItem>
<CommandItem>
<CalendarIcon className="mr-2 h-4 w-4" />
<span>Calendar</span>
</CommandItem>
<CommandItem>
<FaceIcon className="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</CommandItem>
<CommandItem>
<RocketIcon className="mr-2 h-4 w-4" />
<span>Launch</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>
<PersonIcon className="mr-2 h-4 w-4" />
<span>Profile</span>
<CommandShortcut>P</CommandShortcut>
</CommandItem>
<CommandItem>
<EnvelopeClosedIcon className="mr-2 h-4 w-4" />
<span>Mail</span>
<CommandShortcut>B</CommandShortcut>
</CommandItem>
<CommandItem>
<GearIcon className="mr-2 h-4 w-4" />
<span>Settings</span>
<CommandShortcut>S</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
<CommandFooter className="kunkun-drag-region">
<GearIcon className="ml-2 h-4 w-4" />
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm">
Open Application
<kbd className="ml-1"></kbd>
</Button>
<VertifcalSeparator />
<ActionPanel
listRef={listRef}
selectedValue={value}
inputRef={actionInputRef}
actionItems={[
{ label: "Open Application", value: "open" },
{ label: "Show in Finder", value: "finder" },
{ label: "Show Info in Finder", value: "info" },
{ label: "Add to Favorites", value: "favorites" }
]}
></ActionPanel>
</div>
</CommandFooter>
</Command>
</main>
</ThemeProvider>
)
}
export default App

View File

@ -0,0 +1,6 @@
@import url("@kksh/react/css");
@import url("@kksh/react/themes");
/* @tailwind base; */
/* This adds white border to command components under dark mode */
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}"
// "./node_modules/@kksh/react/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {}
},
plugins: []
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})

View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
extensions_support/

View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@ -0,0 +1,15 @@
# template-ext-svelte
## 0.0.2
### Patch Changes
- Updated dependencies
- @kksh/api@0.0.4
## 0.0.1
### Patch Changes
- Updated dependencies [fba6a49]
- @kksh/svelte@0.0.2

View File

@ -0,0 +1,67 @@
# Kunkun Custom UI Extension Template (Svelte)
[Custom UI Extension Documentation](https://docs.kunkun.sh/extensions/custom-ui-ext/)
This is a template for a custom UI extension.
This type of extension is basically a static website. You can use any frontend framework you like, this template uses [Svelte](https://svelte.dev/).
It is assumed that you have some knowledge of frontend development with Svelte.
## Development
Development is the same as developing a normal website.
```bash
pnpm install
pnpm dev
pnpm build
```
- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path.
In `package.json`, `"devMain"` is the url for development server, and `"main"` is the path to static `.html` file for production.
To load the extension in development mode, you have to enable it with `Toggle Dev Extension Live Load Mode` command in Kunkun. A `Live` badge will be shown on the commands. This indicates that dev extensions will be loaded from `devMain` instead of `main`.
## Advanced
### Multi-Command
To support multiple commands, you will need multiple `.html` files as entrypoints, and register each command in `package.json`.
It is recommended to use a meta-framework and build with SSG rendering mode, which comes with routing and will output multiple `.html` files.
Kunkun provides meta-framework templates for Nuxt, Next, SvelteKit.
## Verify Build and Publish
```bash
pnpm build # make sure the build npm script works
npx kksh@latest verify # Verify some basic settings before publishing
```
It is recommended to build the extension with the same environment our CI uses.
The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`.
You can use the following command to build the extension with the same environment our CI uses.
This requires you to have docker installed, and the shell you are using has access to it via `docker` command.
```bash
npx kksh@latest build # Build the extension with
```
`pnpm` is used to install dependencies and build the extension.
The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed.
If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`.
After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension.
The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files.
This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun.
After verifying the tarball, it's ready to be published.
Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR.
Once CI passed and PR merged, you can use your extension in Kunkun.

View File

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,65 @@
{
"$schema": "./node_modules/@kksh/api/dist/schema.json",
"name": "template-ext-svelte",
"private": true,
"version": "0.0.2",
"type": "module",
"kunkun": {
"name": "TODO: Change Display Name",
"shortDescription": "A Custom UI template for svelte",
"longDescription": "A Custom UI template for svelte",
"identifier": "template-ext-svelte",
"icon": {
"type": "iconify",
"value": "logos:svelte-icon"
},
"demoImages": [],
"permissions": [
"clipboard:read-text",
"notification:all"
],
"customUiCmds": [
{
"main": "/",
"dist": "dist",
"devMain": "http://localhost:5173",
"name": "Svelte Template",
"cmds": [],
"window": {
"title": "Svelte Template"
}
}
],
"templateUiCmds": []
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
},
"dependencies": {
"@kksh/api": "workspace:*",
"@kksh/svelte": "0.1.4",
"clsx": "^2.1.1",
"lucide-svelte": "^0.416.0",
"tailwind-merge": "^2.4.0",
"tailwind-variants": "^0.2.1"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tsconfig/svelte": "^5.0.4",
"svelte": "^5.0.3",
"svelte-check": "^4.0.5",
"tslib": "^2.8.0",
"typescript": "~5.6.2",
"vite": "^5.4.9",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4"
},
"files": [
"dist",
".gitignore"
]
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,98 @@
<script lang="ts">
import { notification, ui } from "@kksh/api/ui/iframe"
import {
Button,
Command,
CommandFooter,
ModeWatcher,
Separator,
ThemeWrapper,
updateTheme
} from "@kksh/svelte"
import ThemeCustomizer from "$lib/components/ThemeCustomizer.svelte"
import {
Calculator,
Calendar,
CreditCard,
Settings,
SettingsIcon,
Smile,
User
} from "lucide-svelte"
import { onMount } from "svelte"
onMount(() => {
ui.registerDragRegion()
ui.getTheme().then((theme) => {
updateTheme(theme)
})
notification.sendNotification("Hello from template-ext-svelte")
})
let highlighted = ""
$: {
console.log("highlighted:", highlighted)
}
let searchTerm = ""
$: {
console.log("search term:", searchTerm)
}
</script>
<ModeWatcher />
<ThemeWrapper>
<Command.Root class="h-screen rounded-lg border shadow-md" bind:value={highlighted}>
<Command.Input placeholder="Type a command or search..." autofocus bind:value={searchTerm} />
<div class="grow">
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item onSelect={(v) => console.log("selected:", v)}>
<Calendar class="mr-2 h-4 w-4" />
<span>Calendar</span>
</Command.Item>
<Command.Item onSelect={(v) => console.log("selected:", v)}>
<Smile class="mr-2 h-4 w-4" />
<span>Search Emoji</span>
</Command.Item>
<Command.Item onSelect={(v) => console.log("selected:", v)}>
<Calculator class="mr-2 h-4 w-4" />
<span>Calculator</span>
</Command.Item>
</Command.Group>
<Command.Separator />
<Command.Group heading="Settings">
<Command.Item onSelect={(v) => console.log("selected:", v)}>
<User class="mr-2 h-4 w-4" />
<span>Profile</span>
<Command.Shortcut>⌘P</Command.Shortcut>
</Command.Item>
<Command.Item value="billllling">
<CreditCard class="mr-2 h-4 w-4" />
<span>Billing</span>
<Command.Shortcut>⌘B</Command.Shortcut>
</Command.Item>
<Command.Item onSelect={(v) => console.log("selected:", v)}>
<Settings class="mr-2 h-4 w-4" />
<span>Settings</span>
<Command.Shortcut>⌘S</Command.Shortcut>
</Command.Item>
</Command.Group>
</Command.List>
</div>
<CommandFooter>
<SettingsIcon class="ml-2 h-4 w-4" />
<div class="flex items-center space-x-2">
<Button variant="ghost" size="sm">
Open Application
<kbd class="ml-1"></kbd>
</Button>
<Separator orientation="vertical" />
<ThemeCustomizer />
</div>
</CommandFooter>
</Command.Root>
</ThemeWrapper>

View File

@ -0,0 +1,80 @@
@import url("@kksh/svelte/themes");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,10 @@
<script lang="ts">
let count: number = 0
const increment = () => {
count += 1
}
</script>
<button on:click={increment}>
count is {count}
</button>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import { ThemeCustomizerButton, type ThemeConfig, updateTheme } from '@kksh/svelte';
import { ui } from '@kksh/api/ui/iframe';
import { onMount } from 'svelte';
let config: ThemeConfig = {
radius: 0.5,
theme: 'zinc',
lightMode: 'auto'
};
onMount(() => {
ui.getTheme().then((theme) => {
config = theme;
});
});
$: updateTheme(config);
</script>
<ThemeCustomizerButton bind:config />

View File

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View File

@ -0,0 +1,9 @@
import { mount } from "svelte"
import "./app.css"
import App from "./App.svelte"
const app = mount(App, {
target: document.getElementById("app")!
})
export default app

View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

Some files were not shown because too many files have changed in this diff Show More