@dennisrongo/skills 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/package.json +1 -1
- package/skills/tauri-2-app/SKILL.md +381 -0
- package/skills/tauri-2-app/references/anti-patterns.md +434 -0
- package/skills/tauri-2-app/references/folder-layout.md +161 -0
- package/skills/tauri-2-app/references/good-patterns.md +477 -0
- package/skills/tauri-2-app/references/templates/build-rs.md +51 -0
- package/skills/tauri-2-app/references/templates/capabilities.md +68 -0
- package/skills/tauri-2-app/references/templates/cargo-toml.md +118 -0
- package/skills/tauri-2-app/references/templates/ci-workflow.md +99 -0
- package/skills/tauri-2-app/references/templates/encryption-mod.md +228 -0
- package/skills/tauri-2-app/references/templates/error-mod.md +126 -0
- package/skills/tauri-2-app/references/templates/lib-rs.md +98 -0
- package/skills/tauri-2-app/references/templates/macos-plist.md +89 -0
- package/skills/tauri-2-app/references/templates/main-rs.md +21 -0
- package/skills/tauri-2-app/references/templates/platform-traits.md +217 -0
- package/skills/tauri-2-app/references/templates/storage-mod.md +172 -0
- package/skills/tauri-2-app/references/templates/tauri-conf.md +136 -0
- package/skills/tauri-2-app/references/templates/use-tauri-command.md +194 -0
- package/skills/tauri-2-app/references/templates/vite-and-tsconfig.md +189 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# `src/hooks/useTauriCommand.ts`
|
|
2
|
+
|
|
3
|
+
Generic hook that wraps `invoke()` with `data` / `isLoading` / `error` / `execute`. Every per-domain hook (`useSettings`, `useRecording`, …) composes this. **Components never call `invoke()` directly.**
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
7
|
+
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { isTauriReady } from "../tauriReady";
|
|
9
|
+
|
|
10
|
+
export interface UseCommandOptions<T> {
|
|
11
|
+
command: string;
|
|
12
|
+
args?: Record<string, unknown>;
|
|
13
|
+
onSuccess?: (data: T) => void;
|
|
14
|
+
onError?: (error: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CommandState<T> {
|
|
18
|
+
data: T | null;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
execute: (args?: Record<string, unknown>) => Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useTauriCommand<T = unknown>(
|
|
25
|
+
options: UseCommandOptions<T>,
|
|
26
|
+
): CommandState<T> {
|
|
27
|
+
const { command, args: defaultArgs, onSuccess, onError } = options;
|
|
28
|
+
const [data, setData] = useState<T | null>(null);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
30
|
+
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
const execute = useCallback(
|
|
33
|
+
async (args?: Record<string, unknown>) => {
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const result = await invoke<T>(command, args || defaultArgs);
|
|
38
|
+
setData(result);
|
|
39
|
+
onSuccess?.(result);
|
|
40
|
+
return result;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const errorMsg = String(err);
|
|
43
|
+
setError(errorMsg);
|
|
44
|
+
onError?.(errorMsg);
|
|
45
|
+
throw err;
|
|
46
|
+
} finally {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[command, defaultArgs, onSuccess, onError],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { data, isLoading, error, execute };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UseLoadOptions<T> extends UseCommandOptions<T> {
|
|
57
|
+
loadOnMount?: boolean;
|
|
58
|
+
defaultValue?: T;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LoadState<T> {
|
|
62
|
+
data: T;
|
|
63
|
+
isLoading: boolean;
|
|
64
|
+
error: string | null;
|
|
65
|
+
execute: (args?: Record<string, unknown>) => Promise<T>;
|
|
66
|
+
reload: () => Promise<T>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function useTauriLoad<T = unknown>(
|
|
70
|
+
options: UseLoadOptions<T>,
|
|
71
|
+
): LoadState<T> {
|
|
72
|
+
const { loadOnMount = true, defaultValue, ...commandOptions } = options;
|
|
73
|
+
const { data, isLoading, error, execute } = useTauriCommand<T>(commandOptions);
|
|
74
|
+
|
|
75
|
+
const reload = useCallback(() => execute(), [execute]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!loadOnMount) return;
|
|
79
|
+
if (!isTauriReady()) return;
|
|
80
|
+
execute().catch(() => {
|
|
81
|
+
/* error already captured */
|
|
82
|
+
});
|
|
83
|
+
}, [loadOnMount, execute]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
data: (data ?? defaultValue) as T,
|
|
87
|
+
isLoading,
|
|
88
|
+
error,
|
|
89
|
+
execute,
|
|
90
|
+
reload,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface UseMutationOptions<T> extends UseCommandOptions<T> {
|
|
95
|
+
onMutate?: () => void | Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MutationState<T> {
|
|
99
|
+
isMutating: boolean;
|
|
100
|
+
error: string | null;
|
|
101
|
+
execute: (args?: Record<string, unknown>) => Promise<T>;
|
|
102
|
+
resetError: () => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function useTauriMutation<T = unknown>(
|
|
106
|
+
options: UseMutationOptions<T>,
|
|
107
|
+
): MutationState<T> {
|
|
108
|
+
const { command, args: defaultArgs, onSuccess, onError, onMutate } = options;
|
|
109
|
+
const [isMutating, setIsMutating] = useState(false);
|
|
110
|
+
const [error, setError] = useState<string | null>(null);
|
|
111
|
+
|
|
112
|
+
const execute = useCallback(
|
|
113
|
+
async (args?: Record<string, unknown>) => {
|
|
114
|
+
if (!isTauriReady()) {
|
|
115
|
+
return Promise.reject(new Error("Tauri not ready"));
|
|
116
|
+
}
|
|
117
|
+
setIsMutating(true);
|
|
118
|
+
setError(null);
|
|
119
|
+
try {
|
|
120
|
+
await onMutate?.();
|
|
121
|
+
const result = await invoke<T>(command, args || defaultArgs);
|
|
122
|
+
onSuccess?.(result);
|
|
123
|
+
return result;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const errorMsg = String(err);
|
|
126
|
+
setError(errorMsg);
|
|
127
|
+
onError?.(errorMsg);
|
|
128
|
+
throw err;
|
|
129
|
+
} finally {
|
|
130
|
+
setIsMutating(false);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[command, defaultArgs, onMutate, onSuccess, onError],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const resetError = useCallback(() => setError(null), []);
|
|
137
|
+
|
|
138
|
+
return { isMutating, error, execute, resetError };
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Per-domain hook example
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// src/hooks/useSettings.ts
|
|
146
|
+
import type { Settings } from "../types";
|
|
147
|
+
import { useTauriLoad, useTauriMutation } from "./useTauriCommand";
|
|
148
|
+
|
|
149
|
+
const DEFAULT_SETTINGS: Settings = { /* sensible defaults */ };
|
|
150
|
+
|
|
151
|
+
export function useSettings() {
|
|
152
|
+
return useTauriLoad<Settings>({
|
|
153
|
+
command: "get_settings",
|
|
154
|
+
defaultValue: DEFAULT_SETTINGS,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function useSaveSettings() {
|
|
159
|
+
return useTauriMutation<void>({ command: "save_settings_command" });
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Component usage
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
function SettingsPage() {
|
|
167
|
+
const { data: settings, isLoading, error, reload } = useSettings();
|
|
168
|
+
const { execute: saveSettings, isMutating } = useSaveSettings();
|
|
169
|
+
|
|
170
|
+
if (isLoading) return <Spinner />;
|
|
171
|
+
if (error) return <ErrorBanner error={error} onRetry={reload} />;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Form
|
|
175
|
+
initialValues={settings}
|
|
176
|
+
onSubmit={async (values) => {
|
|
177
|
+
await saveSettings({ settings: values });
|
|
178
|
+
await reload();
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Why this shape
|
|
186
|
+
|
|
187
|
+
- **One place to add cross-cutting concerns** — retry, exponential backoff, telemetry, error toasts, request-id headers — all change one file.
|
|
188
|
+
- **Type safety** — `T` flows through `invoke<T>`. The hook user gets `data: T | null`, not `data: any`.
|
|
189
|
+
- **Three flavors** —
|
|
190
|
+
- `useTauriCommand`: imperative; component triggers it.
|
|
191
|
+
- `useTauriLoad`: auto-fires on mount; for "get me this data".
|
|
192
|
+
- `useTauriMutation`: imperative; explicitly skips auto-fire; for write paths.
|
|
193
|
+
- **`isTauriReady()` guard** — In Tauri 2 the IPC bridge is ready before React mounts, but the guard makes the code safe to preview via plain `vite dev` for static UI iteration.
|
|
194
|
+
- **No `invoke()` in components** — Refactoring command names, adding logging, fixing race conditions all happen in one file. Without this discipline, every component reinvents loading/error state slightly differently.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Frontend configs: `vite.config.ts`, `tsconfig.json`, `package.json`, `index.html`
|
|
2
|
+
|
|
3
|
+
## `vite.config.ts`
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineConfig } from "vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
|
|
10
|
+
// Read tauri.conf.json so the frontend has a single source of truth for version.
|
|
11
|
+
const tauriConfig = JSON.parse(
|
|
12
|
+
readFileSync("./src-tauri/tauri.conf.json", "utf-8"),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error process is a Node global
|
|
16
|
+
const host = process.env.TAURI_DEV_HOST;
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
// Relative base so bundled HTML loads from tauri://localhost
|
|
20
|
+
base: "./",
|
|
21
|
+
|
|
22
|
+
plugins: [react()],
|
|
23
|
+
|
|
24
|
+
// Don't hide Rust compiler errors
|
|
25
|
+
clearScreen: false,
|
|
26
|
+
|
|
27
|
+
server: {
|
|
28
|
+
port: 5173,
|
|
29
|
+
strictPort: true, // Tauri's devUrl pins 5173 — fail loudly if taken
|
|
30
|
+
host: host || false,
|
|
31
|
+
hmr: host
|
|
32
|
+
? { protocol: "ws", host, port: 1421 }
|
|
33
|
+
: undefined,
|
|
34
|
+
watch: {
|
|
35
|
+
// Cargo handles src-tauri/ rebuilds; Vite ignoring it prevents HMR loops
|
|
36
|
+
ignored: ["**/src-tauri/**"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
esbuild: { jsx: "automatic" },
|
|
41
|
+
|
|
42
|
+
define: {
|
|
43
|
+
__APP_VERSION__: JSON.stringify(tauriConfig.version),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Vanilla TypeScript (no React)? Drop the `react()` plugin and the `esbuild.jsx` line.
|
|
49
|
+
|
|
50
|
+
## `tsconfig.json`
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"compilerOptions": {
|
|
55
|
+
"target": "ES2020",
|
|
56
|
+
"useDefineForClassFields": true,
|
|
57
|
+
"module": "ESNext",
|
|
58
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
59
|
+
"skipLibCheck": true,
|
|
60
|
+
"jsx": "react-jsx",
|
|
61
|
+
|
|
62
|
+
"moduleResolution": "bundler",
|
|
63
|
+
"allowImportingTsExtensions": true,
|
|
64
|
+
"resolveJsonModule": true,
|
|
65
|
+
"isolatedModules": true,
|
|
66
|
+
"noEmit": true,
|
|
67
|
+
|
|
68
|
+
"strict": true,
|
|
69
|
+
"noUnusedLocals": true,
|
|
70
|
+
"noUnusedParameters": true,
|
|
71
|
+
"noFallthroughCasesInSwitch": true
|
|
72
|
+
},
|
|
73
|
+
"include": ["src"],
|
|
74
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`tsconfig.node.json` is a tiny companion for the `vite.config.ts` file itself:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"compilerOptions": {
|
|
83
|
+
"composite": true,
|
|
84
|
+
"skipLibCheck": true,
|
|
85
|
+
"module": "ESNext",
|
|
86
|
+
"moduleResolution": "bundler",
|
|
87
|
+
"allowSyntheticDefaultImports": true
|
|
88
|
+
},
|
|
89
|
+
"include": ["vite.config.ts"]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## `package.json`
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"name": "{{app-name}}",
|
|
98
|
+
"private": true,
|
|
99
|
+
"version": "0.1.0",
|
|
100
|
+
"type": "module",
|
|
101
|
+
"scripts": {
|
|
102
|
+
"dev": "vite",
|
|
103
|
+
"build": "tsc && vite build",
|
|
104
|
+
"preview": "vite preview",
|
|
105
|
+
"tauri": "tauri",
|
|
106
|
+
"tauri:dev": "tauri dev",
|
|
107
|
+
"tauri:build": "tauri build",
|
|
108
|
+
"clean": "rimraf node_modules/.vite dist",
|
|
109
|
+
"test": "cd src-tauri && cargo test"
|
|
110
|
+
},
|
|
111
|
+
"dependencies": {
|
|
112
|
+
"@tauri-apps/api": "<latest 2.x>",
|
|
113
|
+
"@tauri-apps/plugin-dialog": "<latest 2.x>",
|
|
114
|
+
"@tauri-apps/plugin-fs": "<latest 2.x>",
|
|
115
|
+
"@tauri-apps/plugin-notification": "<latest 2.x>",
|
|
116
|
+
"@tauri-apps/plugin-opener": "<latest 2.x>",
|
|
117
|
+
"react": "<latest>",
|
|
118
|
+
"react-dom": "<latest>"
|
|
119
|
+
},
|
|
120
|
+
"devDependencies": {
|
|
121
|
+
"@tauri-apps/cli": "<latest 2.x>",
|
|
122
|
+
"@types/react": "<latest>",
|
|
123
|
+
"@types/react-dom": "<latest>",
|
|
124
|
+
"@vitejs/plugin-react": "<latest>",
|
|
125
|
+
"rimraf": "<latest>",
|
|
126
|
+
"typescript": "<latest>",
|
|
127
|
+
"vite": "<latest>"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Resolve `<latest>` markers at scaffold time — never paste a literal version into a generated file.
|
|
133
|
+
|
|
134
|
+
## `index.html`
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<!DOCTYPE html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="UTF-8" />
|
|
141
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
142
|
+
<title>{{ProductName}}</title>
|
|
143
|
+
|
|
144
|
+
<!-- Optional: anti-flash theme script. Runs BEFORE React mounts so the
|
|
145
|
+
initial paint matches the user's preference. -->
|
|
146
|
+
<script>
|
|
147
|
+
(function() {
|
|
148
|
+
const stored = localStorage.getItem("{{app-name}}-theme");
|
|
149
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
150
|
+
const isDark = stored === "dark" || ((!stored || stored === "system") && prefersDark);
|
|
151
|
+
if (isDark) document.documentElement.classList.add("dark");
|
|
152
|
+
})();
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
#app { opacity: 0; transition: opacity 0.15s ease-in; }
|
|
157
|
+
#app.loaded { opacity: 1; }
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<div id="app"></div>
|
|
162
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
// src/main.tsx
|
|
169
|
+
import { createRoot } from "react-dom/client";
|
|
170
|
+
import App from "./App";
|
|
171
|
+
import "./styles.css";
|
|
172
|
+
|
|
173
|
+
const rootElement = document.getElementById("app");
|
|
174
|
+
if (rootElement) {
|
|
175
|
+
createRoot(rootElement).render(<App />);
|
|
176
|
+
requestAnimationFrame(() => rootElement.classList.add("loaded"));
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Why these choices
|
|
181
|
+
|
|
182
|
+
- **`base: './'`** — Tauri serves the bundled frontend from `tauri://localhost/`. Absolute paths (`base: '/'`) break asset URLs like `/assets/index.css` because they resolve against the protocol root.
|
|
183
|
+
- **`strictPort: true`** — Tauri's `devUrl` is `http://localhost:5173`. If Vite silently falls back to 5174, Tauri opens a blank window with no error.
|
|
184
|
+
- **`server.watch.ignored: ['**/src-tauri/**']`** — Cargo writes to `src-tauri/target/` constantly during dev. Without this, Vite's HMR triggers on every Cargo file write and reloads the frontend in a tight loop.
|
|
185
|
+
- **`clearScreen: false`** — Vite's default behavior clears the terminal on startup, hiding Rust compile errors. Tauri devs need both visible.
|
|
186
|
+
- **`isolatedModules: true`** — Required for Vite (which transpiles file-by-file, not whole-program).
|
|
187
|
+
- **`strict: true` + `noUnusedLocals` + `noUnusedParameters`** — Catches the kind of "I forgot to wire this up" mistakes that slip past code review.
|
|
188
|
+
- **Anti-flash script** — Without it, a dark-mode user sees a white flash before React's theme provider mounts. The script reads `localStorage` synchronously and applies the `dark` class before first paint.
|
|
189
|
+
- **Single `createRoot` call** — React 18+ doesn't tolerate multiple roots on the same node. Use one entry point.
|