跳到正文
FunCoding

tinyjs

Build and modify tinyjs desktop apps — tiny macOS (and beta Windows/Linux) apps with a txiki.js JavaScript backend and a native webview window. Use when working in a project with a tinyjs.json, when the user mentions tinyjs, tiny.api, or tinyjs dev/build, or when porting an Electron app to tinyjs.

Star
975
许可证
MIT
最近提交
2026年10月13日

skill/SKILL.md

在 GitHub 上查看

Building tinyjs apps

tinyjs (https://tinyjs.app, repo tarwin/tinyjsapp) makes ~6 MB desktop apps: a txiki.js backend (full system access: files, sockets, processes, FFI)

  • a native webview window — WKWebView on macOS, WebView2 on Windows, WebKitGTK 4.1 on Linux (both beta). They talk over a private socket — no HTTP server, no ports. The page has no direct system access: everything privileged crosses tiny.api (which the tinyjs.json "api" gate can narrow), which is why anything interpolated into innerHTML must be escaped. Two launcher-side paths skip that crossing, both limited to the app's own pages and the origins "api" trusts: tiny.proxyURL's tiny-media:// proxy (cross-origin http(s) reads, macOS + Linux; media.proxy) and mic/camera (media.microphone / media.camera).

Current release: 0.50.1. App floors: macOS 15+ — a default build opens only on the build Mac's CPU (build --arch arm64|x86_64 or --universal for the other); Windows 10/11 (WebView2); Linux glibc 2.35+ (Ubuntu 22.04 / Debian 12 / Mint 21 and up).

Read the reference for the job at hand

doingread
any API beyond the basics below (windows, menus, tray, clipboard, audio, permissions, system…)references/api.md
per-OS support, Windows/Linux quirks, capability gatingreferences/platforms.md
starting a tray app, desktop pet/overlay, media app, document app, site wrapper, agent appreferences/recipes.md
shipping: build/sign/notarize, per-OS packaging, auto-update manifestsreferences/release.md
porting from Electron (API map, what won't work)references/electron-migration.md
deciding where code runs, wire/binary payloads, throttling, memoryreferences/performance.md

Commands

tinyjs new <dir>    # scaffold (zero dependencies)
tinyjs new <dir> --template react-ts|vue-ts|svelte-ts|solid-ts|preact-ts|lit-ts|alpine-ts|vanilla-ts|…
                    #   create-vite + tinyjs overlay: HMR dev server in the
                    #   native window, esbuild-bundled TS backend (npm pkgs ok).
                    #   No name = asks framework + language. --pm npm|pnpm|
                    #   yarn|bun|vp[:pnpm] (else asks; npm off a terminal),
                    #   --no-install. Agents: pass both flags — no prompts.
                    #   vp needs Vite+ ≥ 1.0. alpine = vanilla + Alpine.js.
tinyjs wrap <url>   # site wrapper: the site IS the app, origin-gated API
                    #   (--origins subdomains, --menubar [--panel], --top,
                    #   --external a.com, --ua, --force re-wraps in place)
tinyjs dev          # run with hot reload (frontend edits swap in place;
                    #   backend edits restart the process)
tinyjs build        # dist/<name> binary + dist/<Name>.app (codesigned)
                    #   --dmg installer image; --cli [name] terminal shim
                    #   --arch arm64|x86_64: macOS .app for that CPU (any Mac;
                    #   0.42+); --universal: one .app for both (needs the
                    #   Command Line Tools' lipo). Bare dist/<name> = host CPU
tinyjs publish      # build + dist/publish/<name>-<ver>.zip|tarball + manifest
tinyjs notarize     # macOS: notarytool submit + staple (--dmg re-makes dmg)
                    # publish/notarize take --arch too — repeat the build's
tinyjs update       # update tinyjs itself (--check); also: uninstall, version
TINYJS_DEBUG=1 tinyjs dev   # trace every bridge message

Project layout

tinyjs.json          { name, title, size, id, version, icon?,
                       minTinyjsVersion?,     // refuse older tinyjs with a
                                              // real message; `tinyjs new`
                                              // stamps its own version — raise
                                              // it for newer APIs you adopt
                       update?: { url: "https://…/manifest.json", auto? },
                       urlScheme?: "myapp", fileExtensions?: ["md"],
                       openFolders?: true, readAccess?: true | "/path",
                       userAgent?: "…", activation?: "accessory",
                       mainWindow?: false,  // no main window/WebView (tray agents)
                       offscreenRescue?: false, windowPlacement?: true,
                       contextMenu?: false, audioTap?: "app" | "system",
                       audioTapReason?: "why", about?: "menu",
                       attribution?: "…",  // About panel credit line (macOS);
                                           // a project-root Credits.html wins
                       debug?: true | "open", browserAccelerators?: true,
                       permissions?: { microphone?: "why", camera?: "why",
                                       speechRecognition?: "why" },
                       chrome?: { frame, windowControls, windowControlsPos,
                                  transparent, vibrancy, squareCorners,
                                  acceptsFirstMouse, menu },
                       signIdentity?, notarize?: { profile },
                       backend?: "backend/main.ts",   // .ts → esbuild bundle;
                                      // default src/main.{js,ts} or backend/main.{js,ts}
                       frontend?: { dir?, build, dist, dev, devUrl },
                                      // dir: plain page folder (default src/frontend);
                                      // build/dev need the packages installed first
                       // wrapping a hosted site (recipes.md) — "url" replaces
                       // the local frontend, "api" gates what that origin may
                       // call. NEVER wrap a site you don't control without it.
                       url?: "https://app.example.com",
                       inject?: "src/shim.js",        // document-start, every page
                       downloads?: "auto" | "ask" | "deny",
                       popups?: "external" | "window" | "deny",
                       api?: "wrapper" | { disable?, enable?, origins? },
                       macos?/windows?/linux?: { …merged on top per OS } }
icon.png             1024×1024 app icon
src/main.js          backend
src/frontend/        index.html + assets — served as real files (file://),
                     so relative paths just work

Backend (src/main.js)

export const api = {
  // page calls tiny.api.call('readNotes', { dir }) — return resolves the
  // page's promise, throwing rejects it; meta.window = calling window id
  readNotes: async ({ dir }, app, meta) => { ... },
};
export function init(app) {
  app.push('event-name', data);            // page: tiny.api.on('event-name')
  // app mostly mirrors the page: app.window(id).*, app.openWindow,
  // app.tray.*, app.audio.sampler.*, app.clipboard.*, app.store.*,
  // app.paths (plain object), notify, quit, … — but some names differ
  // (app.setMenu, app.setChrome, …): name map in references/api.md
}
// other exports the scaffold wires, each (info, app): onMenu, onTray (id
// null = bare icon click), onContextMenu, onHotkey, onWindowState,
// onWindowClosed, onOpenUrl, onOpenFiles, onMediaKey, onNotificationClick,
// onNotificationAction, onUpdateAvailable, onClipboardChange, onLocale,
// onAudioTap, onNavigate, onDownload, onWindowOpen — except
// onSystem(kind, value, app)

Runtime is txiki.js (tjs global): tjs.readFile/writeFile/readDir/stat, tjs.spawn, tjs.watch, fetch, WebSocket, tjs:sqlite, FFI. It is NOT Node (no require, no Node builtins, no native npm modules) and it has no JIT — compute-heavy work belongs in the page (references/performance.md). Streams need getReader() (no for await); tjs.cwd is a property; no Intl (format in the page). Every tjs.* fs call is async — await it — while tjs:sqlite is fully sync (never await; statements have run/all/finalize only, no get(), run() returns void).

Frontend essentials

await tiny.api.call('method', { params });    // -> backend api.<method>
const off = tiny.api.on('event', fn);         // <- app.push; returns unsubscribe
audio.src = tiny.fileURL(path);   // ALWAYS this, never 'file://' + path
                                  // (breaks on Windows drive letters)
const r = await tiny.fetch(url, opts);        // backend-proxied, no CORS/CSP
tiny.win.open('settings', { page: 'settings.html', size: '420x300',
                            chrome: { frame: false } });
// chrome/x/y/minSize in open() (or tinyjs.json "chrome" for main) apply
// BEFORE first paint — a late setChrome flashes the default window.

Everything else — windows/chrome/state events, menus (in every window), dialogs, tray, notifications, clipboard, hotkeys, audio (sampler / filters / audioTap / proxyURL), store/secrets, permissions, deep links, auto-update, tiny.macos.* — is in references/api.md with signatures and gotchas.

Cross-platform rules

  • Gate features, don't fork code. capabilities() lists only the EXCEPTIONS: test caps.x !== false, never if (caps.x) (the truthy form reports "unsupported" on the OS that has it). Query calls resolve null where unsupported, capability calls reject with the reason, fire-and-forget ones no-op — but tiny.macos.* off macOS REJECTS. caps.sampler is a string ('native'|'page') — informational, don't branch.
  • Use app.paths / tiny.app.paths() — never hardcode ~/Library or %APPDATA%; join with '/' (works everywhere).
  • tiny.system.os()/isMacOS()/isWindows()/isLinux() are synchronous; architecture() must be awaited (navigator lies on Apple Silicon).
  • Missing system pieces (codecs, speech, tray…) → tiny.system.promptMissing([ids]) puts the fix in front of the user.
  • Edit menu: never declare { title: 'Edit' } — macOS already has one, so the bar shows two. Use { role: 'edit', items: [...] }; for the SAME menu on all three OSes, place the stock items yourself: items: [{ role: 'standard' }, { separator: true }, ...yours] (details in references/api.md → Menus).

Rules of thumb

  • Backend capabilities go in api methods; keep the frontend thin — but keep COMPUTE in the page and bytes off the wire (pass paths, not ArrayBuffers; references/performance.md).
  • Escape anything interpolated into innerHTML — the page holds an RPC channel to full system access.
  • Never declare a top-level chrome identifier in frontend code: window.chrome is a non-configurable global on WebView2, so a top-level const chrome is a PARSE-time SyntaxError that kills the whole script.
  • Linux audio: never route Web Audio to ctx.destination (it crackles — measured, unfixable page-side). SFX → tiny.audio.sampler; EQ → tiny.audio.filters; details in references/platforms.md.
  • Occluded/hidden windows are throttled (rAF stops) — continuous work lives in a visible window or the backend.
  • Verify changes with a self-driving test page (references/recipes.md): TINYJS_HTML=/abs/page.html tinyjs dev. The bundled test/smoke.html only fits an UNMODIFIED zero-dependency scaffold (it calls the template's sysinfo/listDir and waits for its tick push).
  • No display (SSH, CI, sandbox, headless Linux): the launcher prints a tinyjs: explanation and exits 3 ("launcher exited before connecting") — the environment, not your code; there is no headless mode.
  • dist/<Name>.app is the distributable; bare dist/<name> is local-only.

相似的 Skill

官方

internal-comms

ANanthropics/skills181k近 7 天新增 1,119 个 star

canary

GAgarrytan/gstack136k近 7 天新增 883 个 star

observability-and-instrumentation

ADaddyosmani/agent-skills106k近 7 天新增 3,628 个 star

精选

shipping-and-launch

上线前逐项检查,规划监控、分阶段发布和回滚方案。

ADaddyosmani/agent-skills106k近 7 天新增 3,628 个 star

paperclip-board

PApaperclipai/paperclip101k近 7 天新增 2,948 个 star

ruview-configure

RUruvnet/RuView98k近 7 天新增 968 个 star