@flare-desktop/cli 0.1.3 → 0.1.5

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 CHANGED
@@ -40,7 +40,7 @@ environment and local layers. The executable does not read JSON from disk. Rebui
40
40
  build system tracks them. `flare dev` embeds the development layer and `flare build` embeds production. The `--config`
41
41
  and `--environment` CLI flags select the input at build time. On Windows the generated CMake project enables
42
42
  `FLARE_NATIVE_WEBVIEW` before fetching the C++ core; the CLI verifies that the native backend was enabled.
43
- The generated project requires a CLI release containing `src/lib/embed-config-cli.js` (0.1.2 or newer). The new scaffold targets C++ core `v0.1.1`; publish that tag and its SDK release before using prebuilt mode.
43
+ Generated projects require CLI 0.1.5 or newer. They target C++ core v0.1.3; prebuilt mode requires the matching GitHub SDK Release. Only TypeScript templates install the types-only npm core v0.1.0; JavaScript runtime is injected by C++.
44
44
 
45
45
  `flare build` passes `build.frontendDist` to CMake as `FLARE_FRONTEND_DIR`. The scaffolded `CMakeLists.txt` uses
46
46
  that variable for `FlareEmbedFrontend` and for the development directory. Change the config key, not the CMake
@@ -87,16 +87,19 @@ The configuration is validated against
87
87
 
88
88
  ## Frontend templates, prebuilt SDK and codegen
89
89
 
90
- `npm create flare-desktop@latest` offers JavaScript (the existing plain frontend) and TypeScript + Vite.
91
- TypeScript projects copy version-matched `core.js` and `core.d.ts` into `frontend/`; `tsconfig.json` maps
92
- `/core.js` to the declaration, while Vite bundles the JavaScript frontend into `dist/` before CMake embeds it.
90
+ `npm create flare-desktop@latest` asks separately for framework (Vanilla, React or Vue),
91
+ variant (JavaScript or TypeScript), and C++ core source or prebuilt SDK. The CLI accepts
92
+ `--template=javascript|typescript|react-js|react-ts|vue-js|vue-ts` for scripted creation.
93
+ Vanilla JavaScript has no bundler; every other template uses Vite. The C++ library injects `globalThis.flare` into the webview before navigation. Frontend code calls `flare.invoke(...)` without importing any runtime module; neither Vite nor the app serves a core JS URL. TypeScript variants install the types-only `@flare-desktop/core@0.1.0` and include it through `tsconfig.json` `types`. JavaScript variants do not require the npm core package. The first npm publication of core must be performed manually before installing TypeScript templates.
93
94
 
94
- Choose `--sdk=source` (FetchContent of core tag `v0.1.1`) or `--sdk=prebuilt` (download the matching
95
+ The generated CMakeLists delegates embedded configuration generation, frontend directory selection and embedding to the library's `FlareConfigureApplication(target)` helper (provided both by FetchContent and the prebuilt SDK).
96
+
97
+ Choose `--sdk=source` (FetchContent of core tag `v0.1.3`) or `--sdk=prebuilt` (download the matching
95
98
  GitHub Release SDK archives into `vendor/flare`). The first binary SDK matrix covers Windows x64 MSVC
96
99
  Debug/Release with MD/MT, Linux x64 GCC Debug/Release and macOS x64/arm64 Clang Debug/Release.
97
- Unsupported architectures/ABIs must use source mode. The current C++ core's native webview runtime is
98
- implemented on Windows; macOS and Linux builds remain headless. Release SDK archives do not exist until
99
- the core v0.1.1 tag workflow finishes.
100
+ Unsupported architectures/ABIs must use source mode. The native webview runs on Windows, macOS and Linux;
101
+ headless mode is an explicit testing option. Prebuilt mode requires the published core v0.1.3 SDK Release,
102
+ created by the core SDK workflow after successful platform builds.
100
103
 
101
104
  The former C++ `codegen` executable is replaced by `npx flare codegen --language=header|cpp|json` with
102
105
  `--config`, `--out`, `--namespace`, `--embed-assets` and `--frontend`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flare-desktop/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Command line interface for the flare desktop toolkit",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,12 +2,12 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
2
2
  import path from "node:path";
3
3
 
4
4
  import { FlareError } from "../lib/errors.js";
5
- import { placeholderIcon } from "../lib/icons.js";
5
+ import { flareIcon } from "../lib/icons.js";
6
6
  import { isValidAppName, toKebabCase } from "../lib/project.js";
7
7
  import { installSdk } from "../lib/sdk.js";
8
8
  import { writePng } from "../lib/png.js";
9
9
  import { runShell } from "../lib/process.js";
10
- import { CORE_VERSION, MIN_CLI_VERSION } from "../release-versions.js";
10
+ import { CORE_VERSION, CORE_NPM_VERSION, MIN_CLI_VERSION } from "../release-versions.js";
11
11
  import {
12
12
  cmakeTemplate,
13
13
  configTemplate,
@@ -17,6 +17,9 @@ import {
17
17
  readmeTemplate,
18
18
  scriptTemplate,
19
19
  typescriptTemplate,
20
+ reactTemplate,
21
+ vueEntryTemplate,
22
+ vueComponentTemplate,
20
23
  tsconfigTemplate,
21
24
  viteConfigTemplate,
22
25
  sourceTemplate,
@@ -41,7 +44,10 @@ export async function initProject({
41
44
  force = false,
42
45
  logger
43
46
  }) {
44
- if (!["javascript", "typescript"].includes(template)) throw new FlareError("template must be javascript or typescript");
47
+ const templates = ["javascript", "typescript", "react-js", "react-ts", "vue-js", "vue-ts"];
48
+ if (!templates.includes(template)) throw new FlareError(`template must be one of: ${templates.join(", ")}`);
49
+ const vite = template !== "javascript";
50
+ const typed = template === "typescript" || template.endsWith("-ts");
45
51
  if (!["source", "prebuilt"].includes(sdkMode)) throw new FlareError("SDK mode must be source or prebuilt");
46
52
  const projectName = toKebabCase(name);
47
53
  if (!isValidAppName(projectName)) {
@@ -77,7 +83,7 @@ export async function initProject({
77
83
  {
78
84
  file: "package.json",
79
85
  content: `${JSON.stringify(
80
- packageJsonTemplate({ name: projectName, version: appVersion, cliVersion: MIN_CLI_VERSION, template }),
86
+ packageJsonTemplate({ name: projectName, version: appVersion, cliVersion: MIN_CLI_VERSION, coreNpmVersion: CORE_NPM_VERSION, template }),
81
87
  null,
82
88
  2
83
89
  )}\n`
@@ -92,16 +98,19 @@ export async function initProject({
92
98
  },
93
99
  { file: "frontend/index.html", content: indexHtmlTemplate(projectName, template) },
94
100
  { file: "frontend/style.css", content: styleTemplate() },
95
- ...(template === "typescript" ? [
96
- { file: "frontend/main.ts", content: typescriptTemplate() },
97
- { file: "frontend/core.js", content: readFileSync(new URL("../templates/core.js", import.meta.url), "utf8") },
98
- { file: "frontend/core.d.ts", content: readFileSync(new URL("../templates/core.d.ts", import.meta.url), "utf8") },
99
- { file: "tsconfig.json", content: JSON.stringify(tsconfigTemplate(), null, 2) + "\n" },
100
- { file: "vite.config.mjs", content: viteConfigTemplate() }
101
- ] : [{ file: "frontend/main.js", content: scriptTemplate() }]),
101
+ ...(template === "javascript" ? [{ file: "frontend/main.js", content: scriptTemplate() }] : []),
102
+ ...(template === "typescript" ? [{ file: "frontend/main.ts", content: typescriptTemplate() }] : []),
103
+ ...(template === "react-js" ? [{ file: "frontend/main.jsx", content: reactTemplate(projectName) }] : []),
104
+ ...(template === "react-ts" ? [{ file: "frontend/main.tsx", content: reactTemplate(projectName, true) }] : []),
105
+ ...(template.startsWith("vue-") ? [
106
+ { file: `frontend/main.${typed ? "ts" : "js"}`, content: vueEntryTemplate() },
107
+ { file: "frontend/App.vue", content: vueComponentTemplate(projectName, typed) }
108
+ ] : []),
109
+ ...(typed ? [{ file: "tsconfig.json", content: JSON.stringify(tsconfigTemplate(template), null, 2) + "\n" }] : []),
110
+ ...(vite ? [{ file: "vite.config.mjs", content: viteConfigTemplate(template) }] : []),
102
111
  { file: ".gitignore", content: gitignoreTemplate() },
103
112
  { file: ".clang-format", content: readFileSync(new URL("../templates/clang-format.txt", import.meta.url), "utf8") },
104
- { file: "README.md", content: readmeTemplate(projectName) }
113
+ { file: "README.md", content: readmeTemplate(projectName, template) }
105
114
  ];
106
115
 
107
116
  for (const entry of files) {
@@ -118,8 +127,8 @@ export async function initProject({
118
127
  }
119
128
  }
120
129
 
121
- writeFileSync(path.join(targetDirectory, "icons", "icon.png"), writePng(placeholderIcon()));
122
- writeFileSync(path.join(targetDirectory, "icons", "32x32.png"), writePng(placeholderIcon(32)));
130
+ writeFileSync(path.join(targetDirectory, "icons", "icon.png"), writePng(flareIcon()));
131
+ writeFileSync(path.join(targetDirectory, "icons", "32x32.png"), writePng(flareIcon(32)));
123
132
 
124
133
  const written = [...files.map((entry) => entry.file), "icons/icon.png", "icons/32x32.png"];
125
134
 
package/src/help.js CHANGED
@@ -20,7 +20,7 @@ export function helpText() {
20
20
  " --project=<path> project directory (default: .)",
21
21
  " --config=<path> configuration file (default: flare.config.json)",
22
22
  " --environment=<name> add the environment configuration layer",
23
- " --template=<javascript|typescript> frontend template (init)",
23
+ " --template=<javascript|typescript|react-js|react-ts|vue-js|vue-ts> frontend template (init)",
24
24
  " --sdk=<source|prebuilt> C++ dependency mode (init)",
25
25
  " --language=<header|cpp|json> codegen format",
26
26
  " --namespace=<name> codegen C++ namespace",
@@ -152,7 +152,7 @@ export function generateEmbeddedConfig(config, raw = config, namespace = "FlareG
152
152
  lines.push(`window.position = Flare::WindowPosition{${x}, ${y}};`);
153
153
  }
154
154
  if (typeof value.theme === "string") {
155
- const theme = { light: "Light", dark: "Dark", automatic: "Automatic", system: "Automatic", auto: "Automatic" }[value.theme.toLowerCase()];
155
+ const theme = { light: "LIGHT", dark: "DARK", automatic: "AUTOMATIC", system: "AUTOMATIC", auto: "AUTOMATIC" }[value.theme.toLowerCase()];
156
156
  if (theme) lines.push(`window.theme = Flare::WindowTheme::${theme};`);
157
157
  }
158
158
  lines.push("config.windows.push_back(window); }");
package/src/lib/icons.js CHANGED
@@ -5,7 +5,7 @@ import { readPng, resizePng, writePng } from "./png.js";
5
5
 
6
6
  let defaultMark;
7
7
 
8
- export function placeholderIcon(size = 512) {
8
+ export function flareIcon(size = 512) {
9
9
  defaultMark ??= readPng(readFileSync(new URL("../assets/flare-mark.png", import.meta.url)));
10
10
  return resizePng(defaultMark, size, size);
11
11
  }
@@ -1,2 +1,3 @@
1
- export const CORE_VERSION = "0.1.1";
2
- export const MIN_CLI_VERSION = "0.1.2";
1
+ export const CORE_VERSION = "0.1.3";
2
+ export const CORE_NPM_VERSION = "0.1.0";
3
+ export const MIN_CLI_VERSION = "0.1.5";
@@ -8,6 +8,7 @@ export function embeddedIdentifier(name) {
8
8
  }
9
9
 
10
10
  export function configTemplate({ name, identifier, version, template = "javascript" }) {
11
+ const vite = template !== "javascript";
11
12
  return {
12
13
  $schema: SCHEMA_URL,
13
14
  package: { productName: name, version },
@@ -18,11 +19,11 @@ export function configTemplate({ name, identifier, version, template = "javascri
18
19
  version
19
20
  },
20
21
  build: {
21
- frontendDist: template === "typescript" ? "dist" : "frontend",
22
- devUrl: template === "typescript" ? "http://127.0.0.1:5173" : "",
23
- beforeDevCommand: template === "typescript" ? "npm run frontend:dev" : "",
22
+ frontendDist: vite ? "dist" : "frontend",
23
+ devUrl: vite ? "http://127.0.0.1:5173" : "",
24
+ beforeDevCommand: vite ? "npm run frontend:dev" : "",
24
25
  afterDevCommand: "",
25
- beforeBuildCommand: template === "typescript" ? "npm run frontend:build" : "",
26
+ beforeBuildCommand: vite ? "npm run frontend:build" : "",
26
27
  afterBuildCommand: ""
27
28
  },
28
29
  bundle: {
@@ -51,7 +52,11 @@ export function configTemplate({ name, identifier, version, template = "javascri
51
52
  };
52
53
  }
53
54
 
54
- export function packageJsonTemplate({ name, version, cliVersion, template = "javascript" }) {
55
+ export function packageJsonTemplate({ name, version, cliVersion, coreNpmVersion, template = "javascript" }) {
56
+ const vite = template !== "javascript";
57
+ const typed = template === "typescript" || template.endsWith("-ts");
58
+ const react = template.startsWith("react-");
59
+ const vue = template.startsWith("vue-");
55
60
  return {
56
61
  name,
57
62
  version,
@@ -62,11 +67,20 @@ export function packageJsonTemplate({ name, version, cliVersion, template = "jav
62
67
  build: "flare build",
63
68
  bundle: "flare bundle",
64
69
  icon: "flare icon icons/icon.png",
65
- ...(template === "typescript" ? { "frontend:dev": "vite --host 127.0.0.1", "frontend:build": "tsc --noEmit && vite build" } : {})
70
+ ...(vite ? {
71
+ "frontend:dev": "vite --host 127.0.0.1",
72
+ "frontend:build": typed ? `${vue ? "vue-tsc" : "tsc"} --noEmit && vite build` : "vite build"
73
+ } : {})
66
74
  },
67
75
  devDependencies: {
68
76
  "@flare-desktop/cli": `^${cliVersion}`,
69
- ...(template === "typescript" ? { typescript: "^5.9.3", vite: "^6.4.5" } : {})
77
+ ...(vite ? { vite: "^6.4.5" } : {}),
78
+ ...(typed ? { "@flare-desktop/core": coreNpmVersion } : {}),
79
+ ...(typed ? { typescript: "^5.9.3" } : {}),
80
+ ...(react ? { react: "^19.2.0", "react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.7.0" } : {}),
81
+ ...(react && typed ? { "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0" } : {}),
82
+ ...(vue ? { vue: "^3.5.21", "@vitejs/plugin-vue": "^5.2.4" } : {}),
83
+ ...(vue && typed ? { "vue-tsc": "^3.1.3" } : {})
70
84
  }
71
85
  };
72
86
  }
@@ -95,37 +109,10 @@ FetchContent_MakeAvailable(flare)`}
95
109
  add_executable(${name} source/main.cpp)
96
110
  target_link_libraries(${name} PRIVATE flare::core flare::assets)
97
111
  if(MSVC)
98
- target_compile_options(${name} PRIVATE /utf-8)
112
+ target_compile_options(${name} PRIVATE /utf-8 /EHsc)
99
113
  endif()
100
114
 
101
- find_program(FLARE_NODE_EXECUTABLE node REQUIRED)
102
- set(FLARE_CONFIG_FILE "flare.config.json" CACHE STRING "Source application configuration")
103
- set(FLARE_CONFIG_ENVIRONMENT "production" CACHE STRING "Build configuration layer")
104
- get_filename_component(FLARE_CONFIG_ABSOLUTE "\${FLARE_CONFIG_FILE}" ABSOLUTE BASE_DIR "\${CMAKE_CURRENT_SOURCE_DIR}")
105
- get_filename_component(FLARE_CONFIG_DIRECTORY "\${FLARE_CONFIG_ABSOLUTE}" DIRECTORY)
106
- get_filename_component(FLARE_CONFIG_NAME "\${FLARE_CONFIG_ABSOLUTE}" NAME_WE)
107
- file(GLOB FLARE_CONFIG_LAYERS CONFIGURE_DEPENDS "\${FLARE_CONFIG_DIRECTORY}/\${FLARE_CONFIG_NAME}*.json")
108
- set(FLARE_CONFIG_GENERATOR "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/embed-config-cli.js")
109
- set(FLARE_CONFIG_IMPLEMENTATION "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/embedded-config.js")
110
- set(FLARE_CONFIG_LOADER "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/project.js")
111
- set(FLARE_GENERATED_CONFIG "\${CMAKE_CURRENT_BINARY_DIR}/flare-embedded-config.h")
112
- file(GENERATE OUTPUT "\${CMAKE_CURRENT_BINARY_DIR}/flare-config-selection.txt"
113
- CONTENT "\${FLARE_CONFIG_FILE}\n\${FLARE_CONFIG_ENVIRONMENT}\n")
114
- add_custom_command(
115
- OUTPUT "\${FLARE_GENERATED_CONFIG}"
116
- COMMAND "\${FLARE_NODE_EXECUTABLE}" "\${FLARE_CONFIG_GENERATOR}"
117
- "\${CMAKE_CURRENT_SOURCE_DIR}" "\${FLARE_CONFIG_FILE}" "\${FLARE_CONFIG_ENVIRONMENT}"
118
- "\${FLARE_GENERATED_CONFIG}"
119
- DEPENDS "\${FLARE_CONFIG_GENERATOR}" "\${FLARE_CONFIG_IMPLEMENTATION}" "\${FLARE_CONFIG_LOADER}"
120
- "\${CMAKE_CURRENT_BINARY_DIR}/flare-config-selection.txt" \${FLARE_CONFIG_LAYERS}
121
- VERBATIM)
122
- target_sources(${name} PRIVATE "\${FLARE_GENERATED_CONFIG}")
123
- target_include_directories(${name} PRIVATE "\${CMAKE_CURRENT_BINARY_DIR}")
124
-
125
- set(FLARE_FRONTEND_DIR "frontend" CACHE PATH "Frontend directory")
126
- file(TO_CMAKE_PATH "\${FLARE_FRONTEND_DIR}" FLARE_FRONTEND_DIR)
127
- target_compile_definitions(${name} PRIVATE FLARE_FRONTEND_DIR="\${FLARE_FRONTEND_DIR}")
128
- FlareEmbedFrontend(${name} "\${FLARE_FRONTEND_DIR}")
115
+ FlareConfigureApplication(${name})
129
116
  `;
130
117
  }
131
118
 
@@ -174,6 +161,10 @@ int main() {
174
161
  }
175
162
 
176
163
  export function indexHtmlTemplate(name, template = "javascript") {
164
+ const react = template.startsWith("react-");
165
+ const vue = template.startsWith("vue-");
166
+ const typed = template === "typescript" || template.endsWith("-ts");
167
+ const entry = react ? `main.${typed ? "tsx" : "jsx"}` : `main.${typed ? "ts" : "js"}`;
177
168
  return `<!doctype html>
178
169
  <html lang="en">
179
170
  <head>
@@ -183,13 +174,13 @@ export function indexHtmlTemplate(name, template = "javascript") {
183
174
  <link rel="stylesheet" href="style.css">
184
175
  </head>
185
176
  <body>
186
- <main>
177
+ ${react || vue ? `<div id="${react ? "root" : "app"}"></div>` : `<main>
187
178
  <h1>${name}</h1>
188
179
  <p>the frontend runs in the system webview, the backend in C++.</p>
189
180
  <button id="greet">call the backend</button>
190
181
  <pre id="output"></pre>
191
- </main>
192
- <script type="module" src="${template === "typescript" ? "main.ts" : "main.js"}"></script>
182
+ </main>`}
183
+ <script type="module" src="${entry}"></script>
193
184
  </body>
194
185
  </html>
195
186
  `;
@@ -257,63 +248,116 @@ pre {
257
248
  }
258
249
 
259
250
  export function scriptTemplate() {
260
- return `import { app, invoke } from "/core.js";
261
-
262
- const output = document.querySelector("#output");
251
+ return `const output = document.querySelector("#output");
263
252
 
264
253
  document.querySelector("#greet").addEventListener("click", async () => {
265
- const message = await invoke("greet", { name: "flare" });
266
- const version = await app.version();
254
+ const message = await flare.invoke("greet", { name: "flare" });
255
+ const version = await flare.app.version();
267
256
  output.textContent = \`\${message} (\${version})\`;
268
257
  });
269
258
  `;
270
259
  }
271
260
 
272
261
  export function typescriptTemplate() {
273
- return `import { app, invoke } from "/core.js";
274
-
275
- const output = document.querySelector<HTMLPreElement>("#output");
262
+ return `const output = document.querySelector<HTMLPreElement>("#output");
276
263
  const button = document.querySelector<HTMLButtonElement>("#greet");
277
264
 
278
265
  button?.addEventListener("click", async () => {
279
- const message = await invoke<string>("greet", { name: "flare" });
280
- const version = await app.version();
266
+ const message = await flare.invoke<string>("greet", { name: "flare" });
267
+ const version = await flare.app.version();
281
268
  if (output) output.textContent = \`\${message} (\${version})\`;
282
269
  });
283
270
  `;
284
271
  }
285
272
 
286
- export function tsconfigTemplate() {
273
+ export function reactTemplate(name, typed = false) {
274
+ return `import { useState } from "react";
275
+ import { createRoot } from "react-dom/client";
276
+ function App() {
277
+ const [output, setOutput] = useState${typed ? "<string>" : ""}("");
278
+ async function greet() {
279
+ const message = await flare.invoke${typed ? "<string>" : ""}("greet", { name: "flare" });
280
+ setOutput(\`\${message} (\${await flare.app.version()})\`);
281
+ }
282
+ return <main>
283
+ <h1>${name}</h1>
284
+ <p>React in the system webview. C++ on the backend.</p>
285
+ <button onClick={greet}>call the backend</button>
286
+ <pre>{output}</pre>
287
+ </main>;
288
+ }
289
+
290
+ createRoot(document.getElementById("root")${typed ? "!" : ""}).render(<App />);
291
+ `;
292
+ }
293
+
294
+ export function vueEntryTemplate() {
295
+ return `import { createApp } from "vue";
296
+ import App from "./App.vue";
297
+
298
+ createApp(App).mount("#app");
299
+ `;
300
+ }
301
+
302
+ export function vueComponentTemplate(name, typed = false) {
303
+ return `<script setup${typed ? ' lang="ts"' : ""}>
304
+ import { ref } from "vue";
305
+ const output = ref${typed ? "<string>" : ""}("");
306
+ async function greet() {
307
+ const message = await flare.invoke${typed ? "<string>" : ""}("greet", { name: "flare" });
308
+ output.value = \`\${message} (\${await flare.app.version()})\`;
309
+ }
310
+ </script>
311
+
312
+ <template>
313
+ <main>
314
+ <h1>${name}</h1>
315
+ <p>Vue in the system webview. C++ on the backend.</p>
316
+ <button @click="greet">call the backend</button>
317
+ <pre>{{ output }}</pre>
318
+ </main>
319
+ </template>
320
+ `;
321
+ }
322
+
323
+ export function tsconfigTemplate(template = "typescript") {
287
324
  return {
288
325
  compilerOptions: {
289
326
  target: "ES2022", lib: ["ES2022", "DOM"], module: "ESNext", moduleResolution: "Bundler",
290
- strict: true, noEmit: true, skipLibCheck: true, types: ["vite/client"],
291
- paths: { "/core.js": ["./frontend/core.d.ts"] }
327
+ strict: true, noEmit: true, skipLibCheck: true, types: ["vite/client", "@flare-desktop/core"],
328
+ ...(template === "react-ts" ? { jsx: "react-jsx" } : {})
292
329
  },
293
- include: ["frontend/main.ts"]
330
+ include: ["frontend/**/*.ts", "frontend/**/*.tsx", "frontend/**/*.vue"]
294
331
  };
295
332
  }
296
333
 
297
- export function viteConfigTemplate() {
334
+ export function viteConfigTemplate(template = "typescript") {
335
+ const plugin = template.startsWith("react-") ? "@vitejs/plugin-react" :
336
+ template.startsWith("vue-") ? "@vitejs/plugin-vue" : null;
298
337
  return `import { defineConfig } from "vite";
338
+ ${plugin ? `import frameworkPlugin from "${plugin}";\n` : ""}
299
339
  export default defineConfig({
300
340
  root: "frontend",
301
341
  base: "./",
342
+ plugins: [${plugin ? "frameworkPlugin()" : ""}],
302
343
  build: { outDir: "../dist", emptyOutDir: true },
303
344
  server: { host: "127.0.0.1", port: 5173, strictPort: true }
304
345
  });
305
346
  `;
347
+
306
348
  }
307
349
 
308
350
  export function gitignoreTemplate() {
309
351
  return "build/\ntarget/\nnode_modules/\ndist/\n.DS_Store\n";
310
352
  }
311
353
 
312
- export function readmeTemplate(name) {
354
+ export function readmeTemplate(name, template = "javascript") {
313
355
  return `# ${name}
314
356
 
315
357
  A desktop application built with [flare](https://github.com/flare-desktop/flare).
316
358
 
359
+ Frontend template: ${template}.
360
+
317
361
  ## Development
318
362
 
319
363
  \`\`\`bash
@@ -329,7 +373,7 @@ npm run bundle
329
373
  \`\`\`
330
374
 
331
375
  \`npm run dev\` builds the C++ core and serves \`frontend/\` in the system webview.
332
- \`npm run build\` compiles the frontend and the effective typed configuration into the executable. No JSON file is needed at runtime. On Windows, the native webview is enabled automatically. A configuration change requires rebuilding.
376
+ \`npm run build\` compiles the frontend and the effective typed configuration into the executable. No JSON file is needed at runtime. The native webview is selected automatically on Windows, macOS and Linux. A configuration change requires rebuilding.
333
377
  \`npm run bundle\` assembles the configured bundle targets under \`target/bundle\`.
334
378
 
335
379
  ## Icons
@@ -1,135 +0,0 @@
1
- export type Payload = Record<string, unknown> | unknown[] | string | number | boolean | null;
2
-
3
- export interface InvokeOptions {
4
- id?: string;
5
- window?: string;
6
- webview?: string;
7
- }
8
-
9
- export interface ListenOptions {
10
- window?: string;
11
- }
12
-
13
- export interface IpcErrorShape {
14
- code: string;
15
- message: string;
16
- details?: Payload;
17
- }
18
-
19
- export interface IpcFailure extends Error {
20
- code: string;
21
- details?: Payload;
22
- }
23
-
24
- export interface ChannelMessage {
25
- sequence: number;
26
- payload: Payload;
27
- state: 'open' | 'closed';
28
- }
29
-
30
- export interface Channel {
31
- identifier: number;
32
- name: string;
33
- send(payload?: Payload): Promise<void>;
34
- deliver(message: ChannelMessage): void;
35
- close(): void;
36
- }
37
-
38
- export interface Metadata {
39
- readonly protocol: string;
40
- readonly host: string;
41
- readonly version: string;
42
- }
43
-
44
- export function isFlare(): boolean;
45
- export function metadata(): Metadata;
46
- export function invoke<T = unknown>(command: string, payload?: Payload, options?: InvokeOptions): Promise<T>;
47
- export function listen(event: string, handler: (payload: Payload) => void, options?: ListenOptions): Promise<() => Promise<void>>;
48
- export function once(event: string, handler: (payload: Payload) => void, options?: ListenOptions): Promise<() => Promise<void>>;
49
- export function unlisten(event: string, id: number): Promise<void>;
50
- export function emit(event: string, payload?: Payload): Promise<void>;
51
- export function dispatch(event: string, payload?: Payload): number;
52
- export function createChannel(onMessage: (message: ChannelMessage) => void): Channel;
53
- export function currentWindow(): {
54
- label: string;
55
- close(): Promise<unknown>;
56
- setTitle(title: string): Promise<unknown>;
57
- show(): Promise<unknown>;
58
- hide(): Promise<unknown>;
59
- };
60
-
61
- export const app: {
62
- version(): Promise<string>;
63
- describe(): Promise<Payload>;
64
- config(): Promise<Payload>;
65
- platform(): Promise<Payload>;
66
- exit(): Promise<unknown>;
67
- };
68
-
69
- export const window: {
70
- list(): Promise<string[]>;
71
- emit(event: string, payload?: Payload): Promise<unknown>;
72
- };
73
-
74
- export const plugins: { list(): Promise<Payload> };
75
- export interface MenuItem {
76
- id?: string;
77
- kind?: 'text' | 'check' | 'radio' | 'separator' | 'submenu' | 'predefined';
78
- text?: string;
79
- accelerator?: string;
80
- enabled?: boolean;
81
- checked?: boolean;
82
- icon?: string;
83
- items?: MenuItem[];
84
- }
85
-
86
- export const menu: {
87
- describe(): Promise<{ id: string; items: MenuItem[] }>;
88
- set(items: MenuItem[]): Promise<boolean>;
89
- dispatch(id: string): Promise<boolean>;
90
- onCommand(handler: (id: string, payload: { id: string }) => void): () => Promise<boolean>;
91
- };
92
- export const commands: {
93
- register(name: string, handler: (payload: Payload) => Payload): void;
94
- unregister(name: string): boolean;
95
- has(name: string): boolean;
96
- names(): string[];
97
- };
98
- export const log: {
99
- trace(message: string): Promise<void>;
100
- debug(message: string): Promise<void>;
101
- info(message: string): Promise<void>;
102
- warn(message: string): Promise<void>;
103
- error(message: string): Promise<void>;
104
- };
105
-
106
- export type DirectoryName =
107
- | 'appData'
108
- | 'appLocalData'
109
- | 'appConfig'
110
- | 'appCache'
111
- | 'appLog'
112
- | 'home'
113
- | 'documents'
114
- | 'downloads'
115
- | 'desktop'
116
- | 'pictures'
117
- | 'temp'
118
- | 'executable'
119
- | 'resource'
120
- | 'current';
121
-
122
- export const path: {
123
- resolve(name: DirectoryName | string): Promise<{ name: string; path: string }>;
124
- all(): Promise<Record<DirectoryName, string>>;
125
- };
126
-
127
- export const tray: {
128
- describe(): Promise<Payload & { menu: { id: string; items: MenuItem[] } }>;
129
- setIcon(icon: string): Promise<Payload>;
130
- setTooltip(tooltip: string): Promise<Payload>;
131
- setTitle(title: string): Promise<Payload>;
132
- setVisible(visible: boolean): Promise<Payload>;
133
- setMenu(items: MenuItem[]): Promise<Payload>;
134
- dispatch(id: string): Promise<boolean>;
135
- };
@@ -1,278 +0,0 @@
1
- const protocol = 'flare';
2
- const defaultHost = 'localhost';
3
-
4
- const coreCommands = new Map();
5
- const eventListeners = new Map();
6
- const channels = new Map();
7
-
8
- let listenerSequence = 0;
9
- let channelSequence = 0;
10
-
11
- function resolveBridge() {
12
- const bridge = globalThis.__FLARE_BRIDGE__;
13
- if (!bridge) {
14
- throw new Error('flare: the ipc bridge is not available in this context');
15
- }
16
- return bridge;
17
- }
18
-
19
- function normalizeCallback(callback) {
20
- if (callback === undefined || callback === null) {
21
- return undefined;
22
- }
23
- if (typeof callback === 'function') {
24
- return callback;
25
- }
26
- throw new TypeError('flare: callback must be a function');
27
- }
28
-
29
- export function isFlare() {
30
- return typeof globalThis.__FLARE_BRIDGE__ !== 'undefined';
31
- }
32
-
33
- export function metadata() {
34
- const bridge = globalThis.__FLARE_BRIDGE__;
35
- return Object.freeze({
36
- protocol,
37
- host: (bridge && bridge.host) || defaultHost,
38
- version: (bridge && bridge.version) || '0.0.0'
39
- });
40
- }
41
-
42
- export async function invoke(command, payload = {}, options = {}) {
43
- if (typeof command !== 'string' || command.length === 0) {
44
- throw new TypeError('flare: command must be a non empty string');
45
- }
46
- const bridge = resolveBridge();
47
- const request = {
48
- id: options.id || `${Date.now().toString(36)}-${(listenerSequence += 1).toString(36)}`,
49
- command,
50
- payload: payload === undefined ? {} : payload,
51
- window: options.window || '',
52
- webview: options.webview || ''
53
- };
54
- const response = await bridge.invoke(request);
55
- if (response === undefined || response === null) {
56
- throw new Error(`flare: command '${command}' returned no response`);
57
- }
58
- if (response.successful === false) {
59
- const error = response.error || {};
60
- const failure = new Error(error.message || `flare: command '${command}' failed`);
61
- failure.code = error.code || 'command-failed';
62
- failure.details = error.details;
63
- throw failure;
64
- }
65
- return response.result;
66
- }
67
-
68
- export async function listen(event, handler, options = {}) {
69
- if (typeof event !== 'string' || event.length === 0) {
70
- throw new TypeError('flare: event name must be a non empty string');
71
- }
72
- const callback = normalizeCallback(handler);
73
- listenerSequence += 1;
74
- const id = listenerSequence;
75
- const window = options.window || '';
76
- if (!eventListeners.has(event)) {
77
- eventListeners.set(event, new Map());
78
- }
79
- eventListeners.get(event).set(id, { callback, window, once: false });
80
- const bridge = resolveBridge();
81
- if (typeof bridge.attachListener === 'function') {
82
- bridge.attachListener(event, id, callback);
83
- }
84
- try {
85
- await bridge.listen({ event, id, window });
86
- } catch (error) {
87
- eventListeners.get(event)?.delete(id);
88
- await bridge.unlisten?.({ event, id });
89
- throw error;
90
- }
91
- return () => unlisten(event, id);
92
- }
93
-
94
- export async function once(event, handler, options = {}) {
95
- const remove = await listen(
96
- event,
97
- (payload) => {
98
- remove();
99
- if (typeof handler === 'function') {
100
- handler(payload);
101
- }
102
- },
103
- options
104
- );
105
- return remove;
106
- }
107
-
108
- export async function unlisten(event, id) {
109
- const entries = eventListeners.get(event);
110
- if (entries) {
111
- entries.delete(id);
112
- if (entries.size === 0) {
113
- eventListeners.delete(event);
114
- }
115
- }
116
- const bridge = globalThis.__FLARE_BRIDGE__;
117
- if (bridge && typeof bridge.unlisten === 'function') {
118
- await bridge.unlisten({ event, id });
119
- }
120
- }
121
-
122
- export async function emit(event, payload = {}) {
123
- const bridge = resolveBridge();
124
- await bridge.emit({ event, payload });
125
- }
126
-
127
- export function dispatch(event, payload = {}) {
128
- const entries = eventListeners.get(event);
129
- if (!entries) {
130
- return 0;
131
- }
132
- const pending = [];
133
- for (const entry of entries.values()) {
134
- pending.push(entry);
135
- }
136
- for (const entry of pending) {
137
- if (entry.once) {
138
- entries.delete(entry);
139
- }
140
- try {
141
- entry.callback(payload);
142
- } catch (error) {
143
- console.error(`flare: listener for '${event}' failed`, error);
144
- }
145
- }
146
- return pending.length;
147
- }
148
-
149
- export function createChannel(onMessage) {
150
- channelSequence += 1;
151
- const id = channelSequence;
152
- const name = `channel-${id}`;
153
- channels.set(id, { onMessage, closed: false });
154
- return {
155
- identifier: id,
156
- name,
157
- async send(payload = {}) {
158
- const bridge = resolveBridge();
159
- await bridge.channel({ id, payload });
160
- },
161
- deliver(message) {
162
- if (onMessage) {
163
- onMessage(message);
164
- }
165
- if (message && message.state === 'closed') {
166
- channels.delete(id);
167
- }
168
- },
169
- close() {
170
- channels.delete(id);
171
- }
172
- };
173
- }
174
-
175
- export function currentWindow() {
176
- const label = (globalThis.__FLARE_BRIDGE__ && globalThis.__FLARE_BRIDGE__.window) || 'main';
177
- return {
178
- label,
179
- async close() {
180
- return invoke('flare:window:close', { label });
181
- },
182
- async setTitle(title) {
183
- return invoke('flare:window:set-title', { label, title });
184
- },
185
- async show() {
186
- return invoke('flare:window:show', { label });
187
- },
188
- async hide() {
189
- return invoke('flare:window:hide', { label });
190
- }
191
- };
192
- }
193
-
194
- export const app = Object.freeze({
195
- version: () => invoke('flare:app:version'),
196
- describe: () => invoke('flare:app:describe'),
197
- config: () => invoke('flare:app:config'),
198
- platform: () => invoke('flare:app:platform'),
199
- exit: () => invoke('flare:app:exit')
200
- });
201
-
202
- export const window = Object.freeze({
203
- list: () => invoke('flare:window:list'),
204
- emit: (event, payload = {}) => invoke('flare:window:emit', { event, payload })
205
- });
206
-
207
- export const plugins = Object.freeze({
208
- list: () => invoke('flare:plugin:list')
209
- });
210
-
211
- export const menu = Object.freeze({
212
- describe: () => invoke('flare:menu:describe'),
213
- set: (items) => invoke('flare:menu:set', { items }),
214
- dispatch: (id) => invoke('flare:menu:dispatch', { id }),
215
- onCommand: (handler) => listen('flare:menu:command', (payload) => handler(payload.id, payload))
216
- });
217
-
218
- export const commands = Object.freeze({
219
- register(name, handler) {
220
- if (typeof name !== 'string' || typeof handler !== 'function') {
221
- throw new TypeError('flare: register expects a name and a handler');
222
- }
223
- coreCommands.set(name, handler);
224
- },
225
- unregister(name) {
226
- return coreCommands.delete(name);
227
- },
228
- has(name) {
229
- return coreCommands.has(name);
230
- },
231
- names() {
232
- return Array.from(coreCommands.keys());
233
- }
234
- });
235
-
236
- export const log = Object.freeze({
237
- trace: (message) => emit('flare:log:trace', { message }),
238
- debug: (message) => emit('flare:log:debug', { message }),
239
- info: (message) => emit('flare:log:info', { message }),
240
- warn: (message) => emit('flare:log:warn', { message }),
241
- error: (message) => emit('flare:log:error', { message })
242
- });
243
-
244
- export const path = Object.freeze({
245
- resolve: (name) => invoke('flare:path:resolve', { name }),
246
- all: () => invoke('flare:path:all')
247
- });
248
-
249
- export const tray = Object.freeze({
250
- describe: () => invoke('flare:tray:describe'),
251
- setIcon: (icon) => invoke('flare:tray:set-icon', { icon }),
252
- setTooltip: (tooltip) => invoke('flare:tray:set-tooltip', { tooltip }),
253
- setTitle: (title) => invoke('flare:tray:set-title', { title }),
254
- setVisible: (visible) => invoke('flare:tray:set-visible', { visible }),
255
- setMenu: (items) => invoke('flare:tray:set-menu', { items }),
256
- dispatch: (id) => invoke('flare:tray:dispatch', { id })
257
- });
258
-
259
- export default {
260
- app,
261
- commands,
262
- createChannel,
263
- currentWindow,
264
- dispatch,
265
- emit,
266
- invoke,
267
- isFlare,
268
- listen,
269
- log,
270
- menu,
271
- metadata,
272
- once,
273
- path,
274
- plugins,
275
- tray,
276
- unlisten,
277
- window
278
- };