@flare-desktop/cli 0.1.2 → 0.1.4
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 +11 -8
- package/package.json +1 -1
- package/src/assets/flare-mark.png +0 -0
- package/src/commands/dev.js +9 -3
- package/src/commands/init.js +39 -15
- package/src/help.js +1 -1
- package/src/lib/build.js +24 -1
- package/src/lib/cmake.js +17 -4
- package/src/lib/embedded-config.js +1 -1
- package/src/lib/icons.js +9 -19
- package/src/release-versions.js +3 -2
- package/src/templates/project.js +102 -55
- package/src/templates/core.d.ts +0 -135
- package/src/templates/core.js +0 -278
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
|
-
|
|
43
|
+
Generated projects require CLI 0.1.4 or newer. They target C++ core v0.1.2; prebuilt mode requires the matching GitHub SDK Release. Vite templates use npm core v0.1.0 for development and TypeScript declarations.
|
|
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`
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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.2`) 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
|
|
98
|
-
|
|
99
|
-
the core
|
|
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.2 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
|
Binary file
|
package/src/commands/dev.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
|
|
3
|
-
import { compileApplication, runHook } from "../lib/build.js";
|
|
3
|
+
import { compileApplication, ensureFrontendDirectory, runHook } from "../lib/build.js";
|
|
4
4
|
import { FlareError } from "../lib/errors.js";
|
|
5
5
|
import { loadProject, productName } from "../lib/project.js";
|
|
6
6
|
import { run, startShell, terminate } from "../lib/process.js";
|
|
@@ -48,6 +48,12 @@ export async function devCommand(context) {
|
|
|
48
48
|
process.once("SIGTERM", sigtermHandler);
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
+
await ensureFrontendDirectory({
|
|
52
|
+
projectDirectory: context.projectDirectory,
|
|
53
|
+
config,
|
|
54
|
+
logger: context.logger
|
|
55
|
+
});
|
|
56
|
+
|
|
51
57
|
if (config.build.beforeDevCommand) {
|
|
52
58
|
context.logger.info(`before dev command: ${config.build.beforeDevCommand}`);
|
|
53
59
|
server = startShell(config.build.beforeDevCommand, { cwd: context.projectDirectory });
|
|
@@ -61,8 +67,8 @@ export async function devCommand(context) {
|
|
|
61
67
|
const { binary } = await compileApplication({
|
|
62
68
|
projectDirectory: context.projectDirectory,
|
|
63
69
|
config,
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
configFile: context.flags.config || "flare.config.json",
|
|
71
|
+
environment: context.flags.environment,
|
|
66
72
|
mode: "Debug",
|
|
67
73
|
logger: context.logger
|
|
68
74
|
});
|
package/src/commands/init.js
CHANGED
|
@@ -2,11 +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 {
|
|
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
|
-
import {
|
|
9
|
+
import { runShell } from "../lib/process.js";
|
|
10
|
+
import { CORE_VERSION, CORE_NPM_VERSION, MIN_CLI_VERSION } from "../release-versions.js";
|
|
10
11
|
import {
|
|
11
12
|
cmakeTemplate,
|
|
12
13
|
configTemplate,
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
readmeTemplate,
|
|
17
18
|
scriptTemplate,
|
|
18
19
|
typescriptTemplate,
|
|
20
|
+
reactTemplate,
|
|
21
|
+
vueEntryTemplate,
|
|
22
|
+
vueComponentTemplate,
|
|
19
23
|
tsconfigTemplate,
|
|
20
24
|
viteConfigTemplate,
|
|
21
25
|
sourceTemplate,
|
|
@@ -36,10 +40,14 @@ export async function initProject({
|
|
|
36
40
|
template = "javascript",
|
|
37
41
|
sdkMode = "source",
|
|
38
42
|
sdkFetch,
|
|
43
|
+
installDependencies = false,
|
|
39
44
|
force = false,
|
|
40
45
|
logger
|
|
41
46
|
}) {
|
|
42
|
-
|
|
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");
|
|
43
51
|
if (!["source", "prebuilt"].includes(sdkMode)) throw new FlareError("SDK mode must be source or prebuilt");
|
|
44
52
|
const projectName = toKebabCase(name);
|
|
45
53
|
if (!isValidAppName(projectName)) {
|
|
@@ -75,7 +83,7 @@ export async function initProject({
|
|
|
75
83
|
{
|
|
76
84
|
file: "package.json",
|
|
77
85
|
content: `${JSON.stringify(
|
|
78
|
-
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 }),
|
|
79
87
|
null,
|
|
80
88
|
2
|
|
81
89
|
)}\n`
|
|
@@ -90,16 +98,19 @@ export async function initProject({
|
|
|
90
98
|
},
|
|
91
99
|
{ file: "frontend/index.html", content: indexHtmlTemplate(projectName, template) },
|
|
92
100
|
{ file: "frontend/style.css", content: styleTemplate() },
|
|
93
|
-
...(template === "
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{ file: "
|
|
99
|
-
|
|
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) }] : []),
|
|
100
111
|
{ file: ".gitignore", content: gitignoreTemplate() },
|
|
101
112
|
{ file: ".clang-format", content: readFileSync(new URL("../templates/clang-format.txt", import.meta.url), "utf8") },
|
|
102
|
-
{ file: "README.md", content: readmeTemplate(projectName) }
|
|
113
|
+
{ file: "README.md", content: readmeTemplate(projectName, template) }
|
|
103
114
|
];
|
|
104
115
|
|
|
105
116
|
for (const entry of files) {
|
|
@@ -116,17 +127,30 @@ export async function initProject({
|
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
|
-
writeFileSync(path.join(targetDirectory, "icons", "icon.png"), writePng(
|
|
120
|
-
writeFileSync(path.join(targetDirectory, "icons", "32x32.png"), writePng(
|
|
130
|
+
writeFileSync(path.join(targetDirectory, "icons", "icon.png"), writePng(flareIcon()));
|
|
131
|
+
writeFileSync(path.join(targetDirectory, "icons", "32x32.png"), writePng(flareIcon(32)));
|
|
121
132
|
|
|
122
133
|
const written = [...files.map((entry) => entry.file), "icons/icon.png", "icons/32x32.png"];
|
|
123
134
|
|
|
135
|
+
if (installDependencies) {
|
|
136
|
+
if (logger) logger.info("installing project dependencies with npm");
|
|
137
|
+
let result;
|
|
138
|
+
try {
|
|
139
|
+
result = await runShell("npm install --no-audit --no-fund", { cwd: targetDirectory });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new FlareError(`npm install could not start in '${targetDirectory}': ${error.message}; run npm install manually`);
|
|
142
|
+
}
|
|
143
|
+
if (result.code !== 0) {
|
|
144
|
+
throw new FlareError(`npm install failed in '${targetDirectory}' (code ${result.code}); the project was kept, run npm install manually`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
if (logger) {
|
|
125
149
|
logger.info(`created project '${projectName}' in '${targetDirectory}'`);
|
|
126
150
|
logger.info("");
|
|
127
151
|
logger.info("next steps:");
|
|
128
152
|
logger.step(`cd ${path.relative(process.cwd(), targetDirectory) || "."}`);
|
|
129
|
-
logger.step("npm install");
|
|
153
|
+
if (!installDependencies) logger.step("npm install");
|
|
130
154
|
logger.step("npm run dev");
|
|
131
155
|
}
|
|
132
156
|
|
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",
|
package/src/lib/build.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
1
3
|
import { buildDirectory, buildProject, configureProject, findExecutable } from "./cmake.js";
|
|
2
4
|
import { FlareError } from "./errors.js";
|
|
3
5
|
import { binaryName, frontendDirectory } from "./project.js";
|
|
@@ -10,14 +12,35 @@ export async function runHook(command, { cwd, env, logger, label }) {
|
|
|
10
12
|
if (result.code !== 0) throw new FlareError(`${label} failed with code ${result.code}`);
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export async function ensureFrontendDirectory({ projectDirectory, config, logger }) {
|
|
16
|
+
const frontend = frontendDirectory(projectDirectory, config);
|
|
17
|
+
if (existsSync(frontend)) return;
|
|
18
|
+
if (!config.build.beforeBuildCommand) {
|
|
19
|
+
throw new FlareError(`frontend directory '${frontend}' is missing and no beforeBuildCommand is configured`);
|
|
20
|
+
}
|
|
21
|
+
logger.info("preparing frontend assets for CMake embedding");
|
|
22
|
+
await runHook(config.build.beforeBuildCommand, {
|
|
23
|
+
cwd: projectDirectory,
|
|
24
|
+
logger,
|
|
25
|
+
label: "frontend build command"
|
|
26
|
+
});
|
|
27
|
+
if (!existsSync(frontend)) {
|
|
28
|
+
throw new FlareError(`frontend build command did not create '${frontend}'`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
export async function compileApplication({ projectDirectory, config, mode, env, logger, configFile, environment }) {
|
|
33
|
+
const frontend = frontendDirectory(projectDirectory, config);
|
|
34
|
+
if (!existsSync(frontend)) {
|
|
35
|
+
throw new FlareError(`frontend directory '${frontend}' is missing; run the frontend build command before compiling`);
|
|
36
|
+
}
|
|
14
37
|
const directory = buildDirectory(projectDirectory, mode);
|
|
15
38
|
await configureProject(projectDirectory, {
|
|
16
39
|
directory,
|
|
17
40
|
mode,
|
|
18
41
|
env,
|
|
19
42
|
logger,
|
|
20
|
-
frontendDirectory:
|
|
43
|
+
frontendDirectory: frontend,
|
|
21
44
|
configFile,
|
|
22
45
|
environment
|
|
23
46
|
});
|
package/src/lib/cmake.js
CHANGED
|
@@ -35,24 +35,37 @@ export function buildDirectory(projectDirectory, mode) {
|
|
|
35
35
|
return path.join(projectDirectory, "build", mode === "Debug" ? "debug" : "release");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function msvcUtf8Arguments(directory, { platform = process.platform, cxxFlags = process.env.CXXFLAGS ?? "" } = {}) {
|
|
39
|
+
if (platform !== "win32") return [];
|
|
40
|
+
const cacheFile = path.join(directory, "CMakeCache.txt");
|
|
41
|
+
const cache = existsSync(cacheFile) ? readFileSync(cacheFile, "utf8") : "";
|
|
42
|
+
const existing = cache.match(/^CMAKE_CXX_FLAGS:STRING=(.*)$/m)?.[1] ?? cxxFlags;
|
|
43
|
+
if (/(?:^|\s)\/utf-8(?:\s|$)/i.test(existing)) return [];
|
|
44
|
+
return [`-DCMAKE_CXX_FLAGS:STRING=${[existing.trim(), "/utf-8"].filter(Boolean).join(" ")}`];
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
export async function configureProject(projectDirectory, { directory, mode, env, logger, frontendDirectory, configFile = "flare.config.json", environment = "production" }) {
|
|
39
48
|
if (!cmakeVersion()) throw new FlareError("cmake was not found in PATH");
|
|
40
49
|
const outputDirectory = path.join(directory, "bin");
|
|
41
50
|
const configuration = mode.toUpperCase();
|
|
51
|
+
const generator = process.env.CMAKE_GENERATOR ?? "";
|
|
52
|
+
const multiConfiguration = generator
|
|
53
|
+
? /^(Visual Studio|Xcode|Ninja Multi-Config)/.test(generator)
|
|
54
|
+
: process.platform === "win32";
|
|
55
|
+
const prebuiltSdk = existsSync(path.join(projectDirectory, "vendor", "flare", "sdk-manifest.json"));
|
|
42
56
|
const args = [
|
|
43
57
|
"-S",
|
|
44
58
|
projectDirectory,
|
|
45
59
|
"-B",
|
|
46
60
|
directory,
|
|
47
61
|
...generatorArguments(),
|
|
48
|
-
`-DCMAKE_BUILD_TYPE=${mode}
|
|
62
|
+
...(!multiConfiguration ? [`-DCMAKE_BUILD_TYPE=${mode}`] : []),
|
|
49
63
|
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=${outputDirectory}`,
|
|
50
64
|
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_${configuration}=${outputDirectory}`,
|
|
51
65
|
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
|
52
66
|
`-DFLARE_FRONTEND_DIR=${frontendDirectory ?? path.join(projectDirectory, "frontend")}`,
|
|
53
|
-
"-DFLARE_BUILD_EXAMPLES=OFF",
|
|
54
|
-
"
|
|
55
|
-
"-DFLARE_INSTALL=OFF"
|
|
67
|
+
...(!prebuiltSdk ? ["-DFLARE_BUILD_EXAMPLES=OFF", "-DFLARE_BUILD_TESTS=OFF", "-DFLARE_INSTALL=OFF"] : []),
|
|
68
|
+
...msvcUtf8Arguments(directory, { cxxFlags: env?.CXXFLAGS ?? process.env.CXXFLAGS ?? "" })
|
|
56
69
|
];
|
|
57
70
|
logger.debug(`cmake ${args.join(" ")}`);
|
|
58
71
|
const result = await run("cmake", args, { cwd: projectDirectory, env });
|
|
@@ -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: "
|
|
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
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
1
3
|
import { FlareError } from "./errors.js";
|
|
2
|
-
import { resizePng, writePng } from "./png.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
for (let x = 0; x < size; x += 1) {
|
|
10
|
-
const target = (y * size + x) * 4;
|
|
11
|
-
const distance = Math.hypot(x - center, y - center) / radius;
|
|
12
|
-
const glow = Math.max(0, Math.min(1, 1 - Math.abs(distance - 0.55) * 1.6));
|
|
13
|
-
const edge = distance <= 1 ? 1 : 0;
|
|
14
|
-
data[target] = edge ? Math.round(44 + 172 * glow) : 0;
|
|
15
|
-
data[target + 1] = edge ? Math.round(35 + 132 * glow) : 0;
|
|
16
|
-
data[target + 2] = edge ? Math.round(40 + 137 * glow) : 0;
|
|
17
|
-
data[target + 3] = edge ? 255 : 0;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return { width: size, height: size, data };
|
|
4
|
+
import { readPng, resizePng, writePng } from "./png.js";
|
|
5
|
+
|
|
6
|
+
let defaultMark;
|
|
7
|
+
|
|
8
|
+
export function flareIcon(size = 512) {
|
|
9
|
+
defaultMark ??= readPng(readFileSync(new URL("../assets/flare-mark.png", import.meta.url)));
|
|
10
|
+
return resizePng(defaultMark, size, size);
|
|
21
11
|
}
|
|
22
12
|
|
|
23
13
|
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
package/src/release-versions.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export const CORE_VERSION = "0.1.
|
|
2
|
-
export const
|
|
1
|
+
export const CORE_VERSION = "0.1.2";
|
|
2
|
+
export const CORE_NPM_VERSION = "0.1.0";
|
|
3
|
+
export const MIN_CLI_VERSION = "0.1.4";
|
package/src/templates/project.js
CHANGED
|
@@ -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:
|
|
22
|
-
devUrl:
|
|
23
|
-
beforeDevCommand:
|
|
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:
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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
|
}
|
|
@@ -94,35 +108,11 @@ FetchContent_MakeAvailable(flare)`}
|
|
|
94
108
|
|
|
95
109
|
add_executable(${name} source/main.cpp)
|
|
96
110
|
target_link_libraries(${name} PRIVATE flare::core flare::assets)
|
|
111
|
+
if(MSVC)
|
|
112
|
+
target_compile_options(${name} PRIVATE /utf-8)
|
|
113
|
+
endif()
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
set(FLARE_CONFIG_FILE "flare.config.json" CACHE STRING "Source application configuration")
|
|
100
|
-
set(FLARE_CONFIG_ENVIRONMENT "production" CACHE STRING "Build configuration layer")
|
|
101
|
-
get_filename_component(FLARE_CONFIG_ABSOLUTE "\${FLARE_CONFIG_FILE}" ABSOLUTE BASE_DIR "\${CMAKE_CURRENT_SOURCE_DIR}")
|
|
102
|
-
get_filename_component(FLARE_CONFIG_DIRECTORY "\${FLARE_CONFIG_ABSOLUTE}" DIRECTORY)
|
|
103
|
-
get_filename_component(FLARE_CONFIG_NAME "\${FLARE_CONFIG_ABSOLUTE}" NAME_WE)
|
|
104
|
-
file(GLOB FLARE_CONFIG_LAYERS CONFIGURE_DEPENDS "\${FLARE_CONFIG_DIRECTORY}/\${FLARE_CONFIG_NAME}*.json")
|
|
105
|
-
set(FLARE_CONFIG_GENERATOR "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/embed-config-cli.js")
|
|
106
|
-
set(FLARE_CONFIG_IMPLEMENTATION "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/embedded-config.js")
|
|
107
|
-
set(FLARE_CONFIG_LOADER "\${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@flare-desktop/cli/src/lib/project.js")
|
|
108
|
-
set(FLARE_GENERATED_CONFIG "\${CMAKE_CURRENT_BINARY_DIR}/flare-embedded-config.h")
|
|
109
|
-
file(GENERATE OUTPUT "\${CMAKE_CURRENT_BINARY_DIR}/flare-config-selection.txt"
|
|
110
|
-
CONTENT "\${FLARE_CONFIG_FILE}\n\${FLARE_CONFIG_ENVIRONMENT}\n")
|
|
111
|
-
add_custom_command(
|
|
112
|
-
OUTPUT "\${FLARE_GENERATED_CONFIG}"
|
|
113
|
-
COMMAND "\${FLARE_NODE_EXECUTABLE}" "\${FLARE_CONFIG_GENERATOR}"
|
|
114
|
-
"\${CMAKE_CURRENT_SOURCE_DIR}" "\${FLARE_CONFIG_FILE}" "\${FLARE_CONFIG_ENVIRONMENT}"
|
|
115
|
-
"\${FLARE_GENERATED_CONFIG}"
|
|
116
|
-
DEPENDS "\${FLARE_CONFIG_GENERATOR}" "\${FLARE_CONFIG_IMPLEMENTATION}" "\${FLARE_CONFIG_LOADER}"
|
|
117
|
-
"\${CMAKE_CURRENT_BINARY_DIR}/flare-config-selection.txt" \${FLARE_CONFIG_LAYERS}
|
|
118
|
-
VERBATIM)
|
|
119
|
-
target_sources(${name} PRIVATE "\${FLARE_GENERATED_CONFIG}")
|
|
120
|
-
target_include_directories(${name} PRIVATE "\${CMAKE_CURRENT_BINARY_DIR}")
|
|
121
|
-
|
|
122
|
-
set(FLARE_FRONTEND_DIR "frontend" CACHE PATH "Frontend directory")
|
|
123
|
-
file(TO_CMAKE_PATH "\${FLARE_FRONTEND_DIR}" FLARE_FRONTEND_DIR)
|
|
124
|
-
target_compile_definitions(${name} PRIVATE FLARE_FRONTEND_DIR="\${FLARE_FRONTEND_DIR}")
|
|
125
|
-
FlareEmbedFrontend(${name} "\${FLARE_FRONTEND_DIR}")
|
|
115
|
+
FlareConfigureApplication(${name})
|
|
126
116
|
`;
|
|
127
117
|
}
|
|
128
118
|
|
|
@@ -171,6 +161,10 @@ int main() {
|
|
|
171
161
|
}
|
|
172
162
|
|
|
173
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"}`;
|
|
174
168
|
return `<!doctype html>
|
|
175
169
|
<html lang="en">
|
|
176
170
|
<head>
|
|
@@ -180,13 +174,13 @@ export function indexHtmlTemplate(name, template = "javascript") {
|
|
|
180
174
|
<link rel="stylesheet" href="style.css">
|
|
181
175
|
</head>
|
|
182
176
|
<body>
|
|
183
|
-
|
|
177
|
+
${react || vue ? `<div id="${react ? "root" : "app"}"></div>` : `<main>
|
|
184
178
|
<h1>${name}</h1>
|
|
185
179
|
<p>the frontend runs in the system webview, the backend in C++.</p>
|
|
186
180
|
<button id="greet">call the backend</button>
|
|
187
181
|
<pre id="output"></pre>
|
|
188
|
-
</main
|
|
189
|
-
<script type="module" src="${
|
|
182
|
+
</main>`}
|
|
183
|
+
<script type="module" src="${entry}"></script>
|
|
190
184
|
</body>
|
|
191
185
|
</html>
|
|
192
186
|
`;
|
|
@@ -254,63 +248,116 @@ pre {
|
|
|
254
248
|
}
|
|
255
249
|
|
|
256
250
|
export function scriptTemplate() {
|
|
257
|
-
return `
|
|
258
|
-
|
|
259
|
-
const output = document.querySelector("#output");
|
|
251
|
+
return `const output = document.querySelector("#output");
|
|
260
252
|
|
|
261
253
|
document.querySelector("#greet").addEventListener("click", async () => {
|
|
262
|
-
const message = await invoke("greet", { name: "flare" });
|
|
263
|
-
const version = await app.version();
|
|
254
|
+
const message = await flare.invoke("greet", { name: "flare" });
|
|
255
|
+
const version = await flare.app.version();
|
|
264
256
|
output.textContent = \`\${message} (\${version})\`;
|
|
265
257
|
});
|
|
266
258
|
`;
|
|
267
259
|
}
|
|
268
260
|
|
|
269
261
|
export function typescriptTemplate() {
|
|
270
|
-
return `
|
|
271
|
-
|
|
272
|
-
const output = document.querySelector<HTMLPreElement>("#output");
|
|
262
|
+
return `const output = document.querySelector<HTMLPreElement>("#output");
|
|
273
263
|
const button = document.querySelector<HTMLButtonElement>("#greet");
|
|
274
264
|
|
|
275
265
|
button?.addEventListener("click", async () => {
|
|
276
|
-
const message = await invoke<string>("greet", { name: "flare" });
|
|
277
|
-
const version = await app.version();
|
|
266
|
+
const message = await flare.invoke<string>("greet", { name: "flare" });
|
|
267
|
+
const version = await flare.app.version();
|
|
278
268
|
if (output) output.textContent = \`\${message} (\${version})\`;
|
|
279
269
|
});
|
|
280
270
|
`;
|
|
281
271
|
}
|
|
282
272
|
|
|
283
|
-
export function
|
|
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") {
|
|
284
324
|
return {
|
|
285
325
|
compilerOptions: {
|
|
286
326
|
target: "ES2022", lib: ["ES2022", "DOM"], module: "ESNext", moduleResolution: "Bundler",
|
|
287
|
-
strict: true, noEmit: true, skipLibCheck: true, types: ["vite/client"],
|
|
288
|
-
|
|
327
|
+
strict: true, noEmit: true, skipLibCheck: true, types: ["vite/client", "@flare-desktop/core"],
|
|
328
|
+
...(template === "react-ts" ? { jsx: "react-jsx" } : {})
|
|
289
329
|
},
|
|
290
|
-
include: ["frontend
|
|
330
|
+
include: ["frontend/**/*.ts", "frontend/**/*.tsx", "frontend/**/*.vue"]
|
|
291
331
|
};
|
|
292
332
|
}
|
|
293
333
|
|
|
294
|
-
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;
|
|
295
337
|
return `import { defineConfig } from "vite";
|
|
338
|
+
${plugin ? `import frameworkPlugin from "${plugin}";\n` : ""}
|
|
296
339
|
export default defineConfig({
|
|
297
340
|
root: "frontend",
|
|
298
341
|
base: "./",
|
|
342
|
+
plugins: [${plugin ? "frameworkPlugin()" : ""}],
|
|
299
343
|
build: { outDir: "../dist", emptyOutDir: true },
|
|
300
344
|
server: { host: "127.0.0.1", port: 5173, strictPort: true }
|
|
301
345
|
});
|
|
302
346
|
`;
|
|
347
|
+
|
|
303
348
|
}
|
|
304
349
|
|
|
305
350
|
export function gitignoreTemplate() {
|
|
306
351
|
return "build/\ntarget/\nnode_modules/\ndist/\n.DS_Store\n";
|
|
307
352
|
}
|
|
308
353
|
|
|
309
|
-
export function readmeTemplate(name) {
|
|
354
|
+
export function readmeTemplate(name, template = "javascript") {
|
|
310
355
|
return `# ${name}
|
|
311
356
|
|
|
312
357
|
A desktop application built with [flare](https://github.com/flare-desktop/flare).
|
|
313
358
|
|
|
359
|
+
Frontend template: ${template}.
|
|
360
|
+
|
|
314
361
|
## Development
|
|
315
362
|
|
|
316
363
|
\`\`\`bash
|
|
@@ -326,7 +373,7 @@ npm run bundle
|
|
|
326
373
|
\`\`\`
|
|
327
374
|
|
|
328
375
|
\`npm run dev\` builds the C++ core and serves \`frontend/\` in the system webview.
|
|
329
|
-
\`npm run build\` compiles the frontend and the effective typed configuration into the executable. No JSON file is needed at runtime.
|
|
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.
|
|
330
377
|
\`npm run bundle\` assembles the configured bundle targets under \`target/bundle\`.
|
|
331
378
|
|
|
332
379
|
## Icons
|
package/src/templates/core.d.ts
DELETED
|
@@ -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
|
-
};
|
package/src/templates/core.js
DELETED
|
@@ -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
|
-
};
|