@flare-desktop/cli 0.1.0 → 0.1.2
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 +25 -0
- package/package.json +5 -2
- package/src/cli.js +4 -0
- package/src/commands/build.js +2 -0
- package/src/commands/bundle.js +2 -0
- package/src/commands/codegen.js +53 -0
- package/src/commands/dev.js +2 -0
- package/src/commands/init.js +36 -10
- package/src/help.js +7 -1
- package/src/lib/args.js +5 -0
- package/src/lib/build.js +4 -2
- package/src/lib/cmake.js +8 -3
- package/src/lib/commands.js +1 -0
- package/src/lib/embed-config-cli.js +14 -0
- package/src/lib/embedded-config.js +181 -0
- package/src/lib/project.js +4 -3
- package/src/lib/sdk.js +54 -0
- package/src/release-versions.js +2 -0
- package/src/templates/clang-format.txt +32 -0
- package/src/templates/core.d.ts +135 -0
- package/src/templates/core.js +278 -0
- package/src/templates/project.js +84 -28
package/README.md
CHANGED
|
@@ -33,6 +33,15 @@ Global options: `--project=<path>`, `--config=<path>`, `--environment=<name>`, `
|
|
|
33
33
|
|
|
34
34
|
## Frontend directory
|
|
35
35
|
|
|
36
|
+
## Self-contained application configuration
|
|
37
|
+
|
|
38
|
+
The scaffold generates a typed C++ `Flare::Config` header at build time from `flare.config.json` and its platform,
|
|
39
|
+
environment and local layers. The executable does not read JSON from disk. Rebuild after changing any layer; the
|
|
40
|
+
build system tracks them. `flare dev` embeds the development layer and `flare build` embeds production. The `--config`
|
|
41
|
+
and `--environment` CLI flags select the input at build time. On Windows the generated CMake project enables
|
|
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.
|
|
44
|
+
|
|
36
45
|
`flare build` passes `build.frontendDist` to CMake as `FLARE_FRONTEND_DIR`. The scaffolded `CMakeLists.txt` uses
|
|
37
46
|
that variable for `FlareEmbedFrontend` and for the development directory. Change the config key, not the CMake
|
|
38
47
|
file:
|
|
@@ -75,3 +84,19 @@ binaries are staged next to the application binary.
|
|
|
75
84
|
|
|
76
85
|
The configuration is validated against
|
|
77
86
|
[`schemas/flare.config.schema.json`](https://raw.githubusercontent.com/flare-desktop/flare/main/schemas/flare.config.schema.json).
|
|
87
|
+
|
|
88
|
+
## Frontend templates, prebuilt SDK and codegen
|
|
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.
|
|
93
|
+
|
|
94
|
+
Choose `--sdk=source` (FetchContent of core tag `v0.1.1`) or `--sdk=prebuilt` (download the matching
|
|
95
|
+
GitHub Release SDK archives into `vendor/flare`). The first binary SDK matrix covers Windows x64 MSVC
|
|
96
|
+
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
|
+
|
|
101
|
+
The former C++ `codegen` executable is replaced by `npx flare codegen --language=header|cpp|json` with
|
|
102
|
+
`--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
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Command line interface for the flare desktop toolkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,9 +36,12 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"test": "node --test
|
|
39
|
+
"test": "node --test"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"ajv": "^8.20.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"tar": "^7.5.22"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { buildCommand } from "./commands/build.js";
|
|
4
|
+
import { codegenCommand } from "./commands/codegen.js";
|
|
4
5
|
import { bundleCommand } from "./commands/bundle.js";
|
|
5
6
|
import { completionsCommand } from "./commands/completions.js";
|
|
6
7
|
import { devCommand } from "./commands/dev.js";
|
|
@@ -24,6 +25,8 @@ async function initCommand(context) {
|
|
|
24
25
|
identifier: context.flags.identifier,
|
|
25
26
|
appVersion: context.flags.appVersion ?? "0.1.0",
|
|
26
27
|
force: Boolean(context.flags.force),
|
|
28
|
+
template: context.flags.template || "javascript",
|
|
29
|
+
sdkMode: context.flags.sdk || "source",
|
|
27
30
|
logger: context.logger
|
|
28
31
|
});
|
|
29
32
|
return 0;
|
|
@@ -35,6 +38,7 @@ const handlers = {
|
|
|
35
38
|
build: buildCommand,
|
|
36
39
|
bundle: bundleCommand,
|
|
37
40
|
icon: iconCommand,
|
|
41
|
+
codegen: codegenCommand,
|
|
38
42
|
info: infoCommand,
|
|
39
43
|
inspect: inspectCommand,
|
|
40
44
|
doctor: doctorCommand,
|
package/src/commands/build.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function buildCommand(context) {
|
|
|
21
21
|
const { binary, directory } = await compileApplication({
|
|
22
22
|
projectDirectory: context.projectDirectory,
|
|
23
23
|
config,
|
|
24
|
+
configFile: context.flags.config || "flare.config.json",
|
|
25
|
+
environment: context.flags.environment,
|
|
24
26
|
mode,
|
|
25
27
|
logger: context.logger
|
|
26
28
|
});
|
package/src/commands/bundle.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function bundleCommand(context) {
|
|
|
21
21
|
const { binary } = await compileApplication({
|
|
22
22
|
projectDirectory: context.projectDirectory,
|
|
23
23
|
config,
|
|
24
|
+
configFile: context.flags.config || "flare.config.json",
|
|
25
|
+
environment: context.flags.environment,
|
|
24
26
|
mode: "Release",
|
|
25
27
|
logger: context.logger
|
|
26
28
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { generateEmbeddedConfig } from "../lib/embedded-config.js";
|
|
5
|
+
import { FlareError } from "../lib/errors.js";
|
|
6
|
+
import { loadProject, mergeConfig, parseConfig, toKebabCase } from "../lib/project.js";
|
|
7
|
+
|
|
8
|
+
function filesIn(directory) {
|
|
9
|
+
const result = [];
|
|
10
|
+
const walk = (current) => {
|
|
11
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
12
|
+
const absolute = path.join(current, entry.name);
|
|
13
|
+
if (entry.isDirectory()) walk(absolute);
|
|
14
|
+
else if (entry.isFile()) result.push({ key: path.relative(directory, absolute).replaceAll(path.sep, "/"), size: statSync(absolute).size });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
walk(directory);
|
|
18
|
+
return result.sort((a, b) => a.key.localeCompare(b.key));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png" };
|
|
22
|
+
|
|
23
|
+
export async function codegenCommand(context) {
|
|
24
|
+
const { config, files } = loadProject({
|
|
25
|
+
projectDirectory: context.projectDirectory,
|
|
26
|
+
configFile: context.flags.config,
|
|
27
|
+
environment: context.flags.environment
|
|
28
|
+
});
|
|
29
|
+
const output = path.resolve(context.projectDirectory, context.flags.out || "generated");
|
|
30
|
+
const language = context.flags.language || "header";
|
|
31
|
+
if (!["header", "hpp", "cpp", "source", "json"].includes(language)) throw new FlareError(`unsupported codegen language '${language}'`);
|
|
32
|
+
const namespace = context.flags.namespace || "FlareGenerated";
|
|
33
|
+
if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(namespace)) throw new FlareError("invalid C++ namespace");
|
|
34
|
+
mkdirSync(output, { recursive: true });
|
|
35
|
+
let raw = {};
|
|
36
|
+
for (const file of files) raw = mergeConfig(raw, parseConfig(readFileSync(file, "utf8"), file));
|
|
37
|
+
if (language === "json") {
|
|
38
|
+
const name = config.app.identifier.replace(/[^A-Za-z0-9]/g, "_");
|
|
39
|
+
writeFileSync(path.join(output, `${name}-config.json`), JSON.stringify(config, null, 2) + "\n");
|
|
40
|
+
} else {
|
|
41
|
+
const name = `${toKebabCase(config.app.productName)}-config.h`;
|
|
42
|
+
writeFileSync(path.join(output, name), generateEmbeddedConfig(config, raw, namespace));
|
|
43
|
+
const commands = (config.build?.removeUnusedCommands || []).map((item) => ` application.Commands().Unregister(${JSON.stringify(item)});`).join("\n");
|
|
44
|
+
writeFileSync(path.join(output, "commands.cpp"), `#include <flare/core/application.h>\n\nnamespace ${namespace} {\ninline void RegisterCommands(Flare::Application& application) {\n${commands}\n (void)application;\n}\n}\n`);
|
|
45
|
+
}
|
|
46
|
+
if (context.flags.embedAssets) {
|
|
47
|
+
const directory = path.resolve(context.projectDirectory, context.flags.frontend || config.build?.frontendDist || "frontend");
|
|
48
|
+
const assets = filesIn(directory).map(({ key, size }) => ({ key, size, mimeType: MIME[path.extname(key).toLowerCase()] || "application/octet-stream" }));
|
|
49
|
+
writeFileSync(path.join(output, "asset-manifest.json"), JSON.stringify({ count: assets.length, totalBytes: assets.reduce((a, b) => a + b.size, 0), assets }, null, 2) + "\n");
|
|
50
|
+
}
|
|
51
|
+
context.logger.info(`generated configuration in '${output}'`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
package/src/commands/dev.js
CHANGED
|
@@ -61,6 +61,8 @@ export async function devCommand(context) {
|
|
|
61
61
|
const { binary } = await compileApplication({
|
|
62
62
|
projectDirectory: context.projectDirectory,
|
|
63
63
|
config,
|
|
64
|
+
configFile: context.flags.config || "flare.config.json",
|
|
65
|
+
environment: context.flags.environment,
|
|
64
66
|
mode: "Debug",
|
|
65
67
|
logger: context.logger
|
|
66
68
|
});
|
package/src/commands/init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { FlareError } from "../lib/errors.js";
|
|
5
5
|
import { placeholderIcon } from "../lib/icons.js";
|
|
6
6
|
import { isValidAppName, toKebabCase } from "../lib/project.js";
|
|
7
|
+
import { installSdk } from "../lib/sdk.js";
|
|
7
8
|
import { writePng } from "../lib/png.js";
|
|
8
|
-
import {
|
|
9
|
+
import { CORE_VERSION, MIN_CLI_VERSION } from "../release-versions.js";
|
|
9
10
|
import {
|
|
10
11
|
cmakeTemplate,
|
|
11
12
|
configTemplate,
|
|
@@ -14,6 +15,9 @@ import {
|
|
|
14
15
|
packageJsonTemplate,
|
|
15
16
|
readmeTemplate,
|
|
16
17
|
scriptTemplate,
|
|
18
|
+
typescriptTemplate,
|
|
19
|
+
tsconfigTemplate,
|
|
20
|
+
viteConfigTemplate,
|
|
17
21
|
sourceTemplate,
|
|
18
22
|
styleTemplate
|
|
19
23
|
} from "../templates/project.js";
|
|
@@ -28,10 +32,15 @@ export async function initProject({
|
|
|
28
32
|
baseDirectory = ".",
|
|
29
33
|
identifier,
|
|
30
34
|
appVersion = "0.1.0",
|
|
31
|
-
coreVersion =
|
|
35
|
+
coreVersion = CORE_VERSION,
|
|
36
|
+
template = "javascript",
|
|
37
|
+
sdkMode = "source",
|
|
38
|
+
sdkFetch,
|
|
32
39
|
force = false,
|
|
33
40
|
logger
|
|
34
41
|
}) {
|
|
42
|
+
if (!["javascript", "typescript"].includes(template)) throw new FlareError("template must be javascript or typescript");
|
|
43
|
+
if (!["source", "prebuilt"].includes(sdkMode)) throw new FlareError("SDK mode must be source or prebuilt");
|
|
35
44
|
const projectName = toKebabCase(name);
|
|
36
45
|
if (!isValidAppName(projectName)) {
|
|
37
46
|
throw new FlareError(
|
|
@@ -58,7 +67,7 @@ export async function initProject({
|
|
|
58
67
|
{
|
|
59
68
|
file: "flare.config.json",
|
|
60
69
|
content: `${JSON.stringify(
|
|
61
|
-
configTemplate({ name: projectName, identifier: appIdentifier, version: appVersion }),
|
|
70
|
+
configTemplate({ name: projectName, identifier: appIdentifier, version: appVersion, template }),
|
|
62
71
|
null,
|
|
63
72
|
2
|
|
64
73
|
)}\n`
|
|
@@ -66,23 +75,30 @@ export async function initProject({
|
|
|
66
75
|
{
|
|
67
76
|
file: "package.json",
|
|
68
77
|
content: `${JSON.stringify(
|
|
69
|
-
packageJsonTemplate({ name: projectName, version: appVersion, cliVersion:
|
|
78
|
+
packageJsonTemplate({ name: projectName, version: appVersion, cliVersion: MIN_CLI_VERSION, template }),
|
|
70
79
|
null,
|
|
71
80
|
2
|
|
72
81
|
)}\n`
|
|
73
82
|
},
|
|
74
83
|
{
|
|
75
84
|
file: "CMakeLists.txt",
|
|
76
|
-
content: cmakeTemplate({ name: projectName, version: appVersion, coreVersion })
|
|
85
|
+
content: cmakeTemplate({ name: projectName, version: appVersion, coreVersion, sdkMode })
|
|
77
86
|
},
|
|
78
87
|
{
|
|
79
88
|
file: "source/main.cpp",
|
|
80
|
-
content: sourceTemplate({ name: projectName
|
|
89
|
+
content: sourceTemplate({ name: projectName })
|
|
81
90
|
},
|
|
82
|
-
{ file: "frontend/index.html", content: indexHtmlTemplate(projectName) },
|
|
91
|
+
{ file: "frontend/index.html", content: indexHtmlTemplate(projectName, template) },
|
|
83
92
|
{ file: "frontend/style.css", content: styleTemplate() },
|
|
84
|
-
|
|
93
|
+
...(template === "typescript" ? [
|
|
94
|
+
{ file: "frontend/main.ts", content: typescriptTemplate() },
|
|
95
|
+
{ file: "frontend/core.js", content: readFileSync(new URL("../templates/core.js", import.meta.url), "utf8") },
|
|
96
|
+
{ file: "frontend/core.d.ts", content: readFileSync(new URL("../templates/core.d.ts", import.meta.url), "utf8") },
|
|
97
|
+
{ file: "tsconfig.json", content: JSON.stringify(tsconfigTemplate(), null, 2) + "\n" },
|
|
98
|
+
{ file: "vite.config.mjs", content: viteConfigTemplate() }
|
|
99
|
+
] : [{ file: "frontend/main.js", content: scriptTemplate() }]),
|
|
85
100
|
{ file: ".gitignore", content: gitignoreTemplate() },
|
|
101
|
+
{ file: ".clang-format", content: readFileSync(new URL("../templates/clang-format.txt", import.meta.url), "utf8") },
|
|
86
102
|
{ file: "README.md", content: readmeTemplate(projectName) }
|
|
87
103
|
];
|
|
88
104
|
|
|
@@ -90,6 +106,16 @@ export async function initProject({
|
|
|
90
106
|
write(path.join(targetDirectory, entry.file), entry.content);
|
|
91
107
|
}
|
|
92
108
|
|
|
109
|
+
if (sdkMode === "prebuilt") {
|
|
110
|
+
if (logger) logger.info(`downloading flare SDK v${coreVersion} for ${process.platform}/${process.arch}`);
|
|
111
|
+
try {
|
|
112
|
+
await installSdk({ destination: path.join(targetDirectory, "vendor", "flare"), version: coreVersion, ...(sdkFetch ? { fetchImpl: sdkFetch } : {}) });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!force) rmSync(targetDirectory, { recursive: true, force: true });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
writeFileSync(path.join(targetDirectory, "icons", "icon.png"), writePng(placeholderIcon()));
|
|
94
120
|
writeFileSync(path.join(targetDirectory, "icons", "32x32.png"), writePng(placeholderIcon(32)));
|
|
95
121
|
|
|
@@ -104,5 +130,5 @@ export async function initProject({
|
|
|
104
130
|
logger.step("npm run dev");
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
return { directory: targetDirectory, name: projectName, identifier: appIdentifier, files: written };
|
|
133
|
+
return { directory: targetDirectory, name: projectName, identifier: appIdentifier, template, sdkMode, files: written };
|
|
108
134
|
}
|
package/src/help.js
CHANGED
|
@@ -11,7 +11,7 @@ export function helpText() {
|
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
for (const command of COMMANDS) {
|
|
14
|
-
lines.push(` ${command.usage.padEnd(
|
|
14
|
+
lines.push(` ${command.usage.padEnd(39)}${command.description}`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
lines.push(
|
|
@@ -20,6 +20,12 @@ 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)",
|
|
24
|
+
" --sdk=<source|prebuilt> C++ dependency mode (init)",
|
|
25
|
+
" --language=<header|cpp|json> codegen format",
|
|
26
|
+
" --namespace=<name> codegen C++ namespace",
|
|
27
|
+
" --frontend=<path> codegen asset directory",
|
|
28
|
+
" --embed-assets codegen asset manifest",
|
|
23
29
|
" --bundles=<list> bundle targets, for example nsis,msi,deb",
|
|
24
30
|
" --out=<path> bundle output directory (default: target/bundle)",
|
|
25
31
|
" --json machine readable output",
|
package/src/lib/args.js
CHANGED
package/src/lib/build.js
CHANGED
|
@@ -10,14 +10,16 @@ export async function runHook(command, { cwd, env, logger, label }) {
|
|
|
10
10
|
if (result.code !== 0) throw new FlareError(`${label} failed with code ${result.code}`);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function compileApplication({ projectDirectory, config, mode, env, logger }) {
|
|
13
|
+
export async function compileApplication({ projectDirectory, config, mode, env, logger, configFile, environment }) {
|
|
14
14
|
const directory = buildDirectory(projectDirectory, mode);
|
|
15
15
|
await configureProject(projectDirectory, {
|
|
16
16
|
directory,
|
|
17
17
|
mode,
|
|
18
18
|
env,
|
|
19
19
|
logger,
|
|
20
|
-
frontendDirectory: frontendDirectory(projectDirectory, config)
|
|
20
|
+
frontendDirectory: frontendDirectory(projectDirectory, config),
|
|
21
|
+
configFile,
|
|
22
|
+
environment
|
|
21
23
|
});
|
|
22
24
|
await buildProject(projectDirectory, { directory, mode, env });
|
|
23
25
|
const binary = findExecutable(directory, binaryName(config));
|
package/src/lib/cmake.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { FlareError } from "./errors.js";
|
|
@@ -35,7 +35,7 @@ export function buildDirectory(projectDirectory, mode) {
|
|
|
35
35
|
return path.join(projectDirectory, "build", mode === "Debug" ? "debug" : "release");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export async function configureProject(projectDirectory, { directory, mode, env, logger, frontendDirectory }) {
|
|
38
|
+
export async function configureProject(projectDirectory, { directory, mode, env, logger, frontendDirectory, configFile = "flare.config.json", environment = "production" }) {
|
|
39
39
|
if (!cmakeVersion()) throw new FlareError("cmake was not found in PATH");
|
|
40
40
|
const outputDirectory = path.join(directory, "bin");
|
|
41
41
|
const configuration = mode.toUpperCase();
|
|
@@ -50,7 +50,6 @@ export async function configureProject(projectDirectory, { directory, mode, env,
|
|
|
50
50
|
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_${configuration}=${outputDirectory}`,
|
|
51
51
|
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
|
52
52
|
`-DFLARE_FRONTEND_DIR=${frontendDirectory ?? path.join(projectDirectory, "frontend")}`,
|
|
53
|
-
"-DFLARE_BUILD_TOOLS=OFF",
|
|
54
53
|
"-DFLARE_BUILD_EXAMPLES=OFF",
|
|
55
54
|
"-DFLARE_BUILD_TESTS=OFF",
|
|
56
55
|
"-DFLARE_INSTALL=OFF"
|
|
@@ -58,6 +57,12 @@ export async function configureProject(projectDirectory, { directory, mode, env,
|
|
|
58
57
|
logger.debug(`cmake ${args.join(" ")}`);
|
|
59
58
|
const result = await run("cmake", args, { cwd: projectDirectory, env });
|
|
60
59
|
if (result.code !== 0) throw new FlareError("cmake configuration failed");
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
const cache = readFileSync(path.join(directory, "CMakeCache.txt"), "utf8");
|
|
62
|
+
if (!/^FLARE_NATIVE_WEBVIEW:BOOL=ON$/m.test(cache)) {
|
|
63
|
+
throw new FlareError("Windows build has no native webview backend; configure with -DFLARE_NATIVE_WEBVIEW=ON");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export async function buildProject(projectDirectory, { directory, mode, target, env }) {
|
package/src/lib/commands.js
CHANGED
|
@@ -3,6 +3,7 @@ export const COMMANDS = [
|
|
|
3
3
|
{ name: "dev", usage: "dev", description: "build and run the application in development mode" },
|
|
4
4
|
{ name: "build", usage: "build [--debug]", description: "build the application" },
|
|
5
5
|
{ name: "bundle", usage: "bundle", description: "build and prepare the platform bundle targets" },
|
|
6
|
+
{ name: "codegen", usage: "codegen [--language=header|json]", description: "generate a typed C++ config, JSON or an asset manifest" },
|
|
6
7
|
{ name: "icon", usage: "icon <source.png>", description: "generate the icon set from a square png" },
|
|
7
8
|
{ name: "info", usage: "info", description: "print project and platform information" },
|
|
8
9
|
{ name: "inspect", usage: "inspect", description: "print the resolved configuration layers" },
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { writeEmbeddedConfig } from "./embedded-config.js";
|
|
2
|
+
|
|
3
|
+
const [projectDirectory, configFile, environment, output] = process.argv.slice(2);
|
|
4
|
+
if (!projectDirectory || !configFile || !environment || !output) {
|
|
5
|
+
process.stderr.write("usage: embed-config-cli.js <project> <config> <environment> <output>\n");
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
} else {
|
|
8
|
+
try {
|
|
9
|
+
writeEmbeddedConfig({ projectDirectory, configFile, environment, output });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
process.stderr.write(`failed to embed configuration: ${error.message}\n`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import { loadProject, mergeConfig, parseConfig } from "./project.js";
|
|
5
|
+
|
|
6
|
+
function cppString(value) {
|
|
7
|
+
const bytes = Buffer.from(value, "utf8");
|
|
8
|
+
let output = '"';
|
|
9
|
+
for (const byte of bytes) {
|
|
10
|
+
if (byte === 34) output += '\\"';
|
|
11
|
+
else if (byte === 92) output += "\\\\";
|
|
12
|
+
else if (byte >= 32 && byte <= 126) output += String.fromCharCode(byte);
|
|
13
|
+
else output += `\\${byte.toString(8).padStart(3, "0")}`;
|
|
14
|
+
}
|
|
15
|
+
return `${output}"`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cppJson(value) {
|
|
19
|
+
if (value === null) return "Flare::JSON(nullptr)";
|
|
20
|
+
if (Array.isArray(value)) return `Flare::JSON::array({${value.map(cppJson).join(", ")}})`;
|
|
21
|
+
if (typeof value === "object") {
|
|
22
|
+
const entries = Object.entries(value).map(([key, item]) => `{${cppString(key)}, ${cppJson(item)}}`);
|
|
23
|
+
return `Flare::JSON::object({${entries.join(", ")}})`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "string") return `Flare::JSON(${cppString(value)})`;
|
|
26
|
+
if (typeof value === "boolean") return `Flare::JSON(${value})`;
|
|
27
|
+
if (typeof value === "number" && Number.isFinite(value)) return `Flare::JSON(${value})`;
|
|
28
|
+
throw new TypeError("unsupported JSON value in configuration");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function scalar(value, kind) {
|
|
32
|
+
if (kind === "string" && typeof value === "string") return cppString(value);
|
|
33
|
+
if (kind === "bool" && typeof value === "boolean") return `${value}`;
|
|
34
|
+
if (kind === "number" && typeof value === "number" && Number.isFinite(value)) return `${value}`;
|
|
35
|
+
if (kind === "int" && Number.isSafeInteger(value)) return `${value}`;
|
|
36
|
+
if (kind === "strings" && Array.isArray(value)) {
|
|
37
|
+
return `{${value.filter((item) => typeof item === "string").map(cppString).join(", ")}}`;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const FIELDS = {
|
|
43
|
+
package: { productName: "string", version: "string" },
|
|
44
|
+
app: { productName: "string", mainBinaryName: "string", version: "string", identifier: "string" },
|
|
45
|
+
build: {
|
|
46
|
+
beforeDevCommand: "string", afterDevCommand: "string", beforeBuildCommand: "string",
|
|
47
|
+
afterBuildCommand: "string", frontendDist: "string", devUrl: "string", removeUnusedCommands: "strings"
|
|
48
|
+
},
|
|
49
|
+
"build.features": { customProtocol: "bool", devtools: "bool", isolation: "bool", systemTray: "bool" },
|
|
50
|
+
bundle: {
|
|
51
|
+
active: "bool", identifier: "string", icon: "string", version: "string", publisher: "string",
|
|
52
|
+
homepage: "string", longDescription: "string", copyright: "string", category: "string",
|
|
53
|
+
shortDescription: "string", licenseFile: "string", targets: "strings", resources: "strings",
|
|
54
|
+
externalBin: "strings"
|
|
55
|
+
},
|
|
56
|
+
"bundle.linuxBundle": { depends: "string", desktopTemplate: "string", section: "string", priority: "string", files: "strings" },
|
|
57
|
+
"bundle.windows": { language: "string", installerIcon: "string", installMode: "int", webviewInstallModeOffline: "bool" },
|
|
58
|
+
"bundle.macos": { minimumSystemVersion: "string", frameworks: "bool", signingIdentity: "string" },
|
|
59
|
+
security: {
|
|
60
|
+
freezePrototype: "bool", dangerousDisableAssetCspModification: "bool", dangerousUseHttpScheme: "bool",
|
|
61
|
+
dangerousAcceptInvalidCertificates: "bool", capabilities: "strings", dangerousRemoteDomainIpcAccess: "strings"
|
|
62
|
+
},
|
|
63
|
+
window: {
|
|
64
|
+
label: "string", title: "string", resizable: "bool", maximized: "bool", fullscreen: "bool",
|
|
65
|
+
alwaysOnTop: "bool", decorations: "bool", transparent: "bool", visible: "bool", center: "bool",
|
|
66
|
+
devtools: "bool", url: "string"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function emitFields(lines, target, input, fields) {
|
|
71
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return;
|
|
72
|
+
for (const [key, kind] of Object.entries(fields)) {
|
|
73
|
+
const result = scalar(input[key], kind);
|
|
74
|
+
if (result !== null) lines.push(`${target}.${key} = ${result};`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emitSize(lines, target, input) {
|
|
79
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return;
|
|
80
|
+
for (const key of ["width", "height"]) {
|
|
81
|
+
const result = scalar(input[key], "number");
|
|
82
|
+
if (result !== null) lines.push(`${target}.${key} = ${result};`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function emitCsp(lines, input) {
|
|
87
|
+
if (typeof input === "string") {
|
|
88
|
+
const directives = input.split(";").map((statement) => statement.trim()).filter(Boolean);
|
|
89
|
+
lines.push(`config.security.csp.enabled = ${input.trim() ? "true" : "false"};`);
|
|
90
|
+
for (const statement of directives) {
|
|
91
|
+
const [name, ...sources] = statement.split(/\s+/);
|
|
92
|
+
lines.push(`config.security.csp.directives.push_back(Flare::CspDirective{${cppString(name)}, ${scalar(sources, "strings")}});`);
|
|
93
|
+
}
|
|
94
|
+
} else if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
95
|
+
lines.push("config.security.csp.enabled = true;");
|
|
96
|
+
for (const [name, sources] of Object.entries(input).sort(([a], [b]) => a.localeCompare(b))) {
|
|
97
|
+
const values = typeof sources === "string" ? sources.trim().split(/\s+/).filter(Boolean) : sources;
|
|
98
|
+
lines.push(`config.security.csp.directives.push_back(Flare::CspDirective{${cppString(name)}, ${scalar(values, "strings") ?? "{}"}});`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function generateEmbeddedConfig(config, raw = config, namespace = "FlareGenerated") {
|
|
104
|
+
if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(namespace)) throw new Error("invalid C++ namespace");
|
|
105
|
+
config = structuredClone(config);
|
|
106
|
+
const lines = [
|
|
107
|
+
"#pragma once", "", "#include <flare/config/config.h>", "", `namespace ${namespace} {`,
|
|
108
|
+
"inline Flare::Config EmbeddedConfig() {", "Flare::Config config;"
|
|
109
|
+
];
|
|
110
|
+
if (raw.package && typeof raw.package === "object" && !Array.isArray(raw.package)) {
|
|
111
|
+
config.package.version ??= "0.1.0";
|
|
112
|
+
}
|
|
113
|
+
if (raw.build && typeof raw.build === "object" && !Array.isArray(raw.build)) {
|
|
114
|
+
config.build.frontendDist ??= "../dist";
|
|
115
|
+
}
|
|
116
|
+
if (raw.bundle && typeof raw.bundle === "object" && !Array.isArray(raw.bundle)) {
|
|
117
|
+
config.bundle.active ??= true;
|
|
118
|
+
config.bundle.icon ??= "icons/icon.png";
|
|
119
|
+
config.bundle.category ??= "DeveloperTool";
|
|
120
|
+
}
|
|
121
|
+
if (raw.bundle?.linux && typeof raw.bundle.linux === "object") {
|
|
122
|
+
config.bundle.linux.section ??= "utils";
|
|
123
|
+
config.bundle.linux.priority ??= "optional";
|
|
124
|
+
}
|
|
125
|
+
if (raw.bundle?.windows && typeof raw.bundle.windows === "object") {
|
|
126
|
+
config.bundle.windows.language ??= "en-US";
|
|
127
|
+
}
|
|
128
|
+
emitFields(lines, "config.package", config.package, FIELDS.package);
|
|
129
|
+
emitFields(lines, "config.app", config.app, FIELDS.app);
|
|
130
|
+
emitFields(lines, "config.build", config.build, FIELDS.build);
|
|
131
|
+
emitFields(lines, "config.build.features", config.build?.features, FIELDS["build.features"]);
|
|
132
|
+
emitFields(lines, "config.bundle", config.bundle, FIELDS.bundle);
|
|
133
|
+
emitFields(lines, "config.bundle.linuxBundle", config.bundle?.linux, FIELDS["bundle.linuxBundle"]);
|
|
134
|
+
emitFields(lines, "config.bundle.windows", config.bundle?.windows, FIELDS["bundle.windows"]);
|
|
135
|
+
emitFields(lines, "config.bundle.macos", config.bundle?.macos, FIELDS["bundle.macos"]);
|
|
136
|
+
emitFields(lines, "config.security", config.security, FIELDS.security);
|
|
137
|
+
emitCsp(lines, config.security?.csp);
|
|
138
|
+
if (typeof config.$schema === "string") lines.push(`config.schema = ${cppString(config.$schema)};`);
|
|
139
|
+
const windows = Array.isArray(config.windows) && config.windows.length ? config.windows : [{}];
|
|
140
|
+
for (const value of windows) {
|
|
141
|
+
lines.push("{ Flare::WindowConfig window;");
|
|
142
|
+
emitFields(lines, "window", value, FIELDS.window);
|
|
143
|
+
emitSize(lines, "window.size", value);
|
|
144
|
+
emitSize(lines, "window.size", value.size);
|
|
145
|
+
lines.push("window.minimumSize = Flare::WindowSize{0.0, 0.0};");
|
|
146
|
+
lines.push("window.maximumSize = Flare::WindowSize{0.0, 0.0};");
|
|
147
|
+
emitSize(lines, "window.minimumSize", value.minimumSize);
|
|
148
|
+
emitSize(lines, "window.maximumSize", value.maximumSize);
|
|
149
|
+
if (value.position && typeof value.position === "object" && !Array.isArray(value.position)) {
|
|
150
|
+
const x = scalar(value.position.x, "number") ?? "0.0";
|
|
151
|
+
const y = scalar(value.position.y, "number") ?? "0.0";
|
|
152
|
+
lines.push(`window.position = Flare::WindowPosition{${x}, ${y}};`);
|
|
153
|
+
}
|
|
154
|
+
if (typeof value.theme === "string") {
|
|
155
|
+
const theme = { light: "Light", dark: "Dark", automatic: "Automatic", system: "Automatic", auto: "Automatic" }[value.theme.toLowerCase()];
|
|
156
|
+
if (theme) lines.push(`window.theme = Flare::WindowTheme::${theme};`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("config.windows.push_back(window); }");
|
|
159
|
+
}
|
|
160
|
+
if (config.plugins && typeof config.plugins === "object" && !Array.isArray(config.plugins)) {
|
|
161
|
+
for (const [name, value] of Object.entries(config.plugins).sort(([a], [b]) => a.localeCompare(b))) {
|
|
162
|
+
lines.push(`config.plugins.push_back(Flare::PluginConfig{${cppString(name)}, ${cppJson(value)}});`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
lines.push(`config.raw = ${cppJson(raw)};`);
|
|
166
|
+
lines.push("return config;", "}", "}", "");
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function writeEmbeddedConfig({ projectDirectory, configFile = "flare.config.json", environment = "production", output }) {
|
|
171
|
+
const { config, files } = loadProject({ projectDirectory, configFile, environment });
|
|
172
|
+
let raw = {};
|
|
173
|
+
for (const file of files) raw = mergeConfig(raw, parseConfig(readFileSync(file, "utf8"), file));
|
|
174
|
+
writeFileSync(output, generateEmbeddedConfig(config, raw), "utf8");
|
|
175
|
+
const result = spawnSync("clang-format", ["-style=file", "-i", output], { cwd: projectDirectory });
|
|
176
|
+
if (result.error && result.error.code !== "ENOENT") throw result.error;
|
|
177
|
+
if (!result.error && result.status !== 0) {
|
|
178
|
+
throw new Error(`clang-format failed: ${result.stderr.toString("utf8")}`);
|
|
179
|
+
}
|
|
180
|
+
return files;
|
|
181
|
+
}
|
package/src/lib/project.js
CHANGED
|
@@ -175,8 +175,8 @@ export function mergeConfig(base, overlay) {
|
|
|
175
175
|
function normalizeWindow(window, fallbackTitle) {
|
|
176
176
|
const value = isObject(window) ? { ...window } : {};
|
|
177
177
|
const title = value.title ?? fallbackTitle;
|
|
178
|
-
const width = value.width ?? value.
|
|
179
|
-
const height = value.height ?? value.
|
|
178
|
+
const width = value.size?.width ?? value.width ?? 800;
|
|
179
|
+
const height = value.size?.height ?? value.height ?? 600;
|
|
180
180
|
return {
|
|
181
181
|
...value,
|
|
182
182
|
label: value.label ?? "main",
|
|
@@ -212,7 +212,8 @@ export function normalizeConfig(raw) {
|
|
|
212
212
|
if (!config.app.mainBinaryName) config.app.mainBinaryName = config.app.productName;
|
|
213
213
|
|
|
214
214
|
const fallbackTitle = config.app.productName || "flare-app";
|
|
215
|
-
const
|
|
215
|
+
const sourceWindows = Array.isArray(config.windows) ? config.windows : config.app.windows;
|
|
216
|
+
const windows = Array.isArray(sourceWindows) && sourceWindows.length > 0 ? sourceWindows : [{}];
|
|
216
217
|
config.windows = windows.map((window) => normalizeWindow(window, fallbackTitle));
|
|
217
218
|
|
|
218
219
|
return config;
|
package/src/lib/sdk.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { extract } from "tar";
|
|
5
|
+
|
|
6
|
+
import { FlareError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
export function sdkTarget(platform = process.platform, arch = process.arch) {
|
|
9
|
+
if (!(["x64", "arm64"].includes(arch))) throw new FlareError(`prebuilt SDK is not available for ${arch}; choose source mode`);
|
|
10
|
+
if (platform === "win32") {
|
|
11
|
+
if (arch !== "x64") throw new FlareError("prebuilt Windows SDK supports x64 MSVC only; choose source mode");
|
|
12
|
+
return { platform: "windows-x64", runtimes: ["md", "mt"] };
|
|
13
|
+
}
|
|
14
|
+
if (platform === "darwin") return { platform: `macos-${arch}`, runtimes: ["default"] };
|
|
15
|
+
if (platform === "linux" && arch === "x64") return { platform: "linux-x64", runtimes: ["default"] };
|
|
16
|
+
throw new FlareError(`prebuilt SDK is not available for ${platform}-${arch}; choose source mode`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function installSdk({ destination, version, platform = process.platform, arch = process.arch, fetchImpl = fetch }) {
|
|
20
|
+
const target = sdkTarget(platform, arch);
|
|
21
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
22
|
+
const temporary = mkdtempSync(path.join(path.dirname(destination), ".flare-sdk-"));
|
|
23
|
+
const sdk = path.join(temporary, "flare");
|
|
24
|
+
mkdirSync(sdk);
|
|
25
|
+
try {
|
|
26
|
+
const checksumsUrl = `https://github.com/flare-desktop/flare/releases/download/v${version}/SHA256SUMS.txt`;
|
|
27
|
+
const checksumsResponse = await fetchImpl(checksumsUrl);
|
|
28
|
+
if (!checksumsResponse.ok) throw new FlareError(`SDK checksums are unavailable (HTTP ${checksumsResponse.status}); publish core v${version} first`);
|
|
29
|
+
const sums = new Map((await checksumsResponse.text()).split("\n").map((line) => /^(\w{64})\s+\*?(\S+)$/.exec(line)).filter(Boolean).map((match) => [match[2], match[1].toLowerCase()]));
|
|
30
|
+
for (const runtime of target.runtimes) {
|
|
31
|
+
const file = `flare-sdk-v${version}-${target.platform}-${runtime}.tar.gz`;
|
|
32
|
+
const url = `https://github.com/flare-desktop/flare/releases/download/v${version}/${file}`;
|
|
33
|
+
let response;
|
|
34
|
+
try { response = await fetchImpl(url); } catch (error) { throw new FlareError(`failed to download ${file}: ${error.message}`); }
|
|
35
|
+
if (!response.ok) throw new FlareError(`SDK archive ${file} is unavailable (HTTP ${response.status}); publish core v${version} SDK release first, or choose source mode`);
|
|
36
|
+
const archive = path.join(temporary, file);
|
|
37
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
38
|
+
const digest = createHash("sha256").update(data).digest("hex");
|
|
39
|
+
if (!sums.has(file) || sums.get(file) !== digest) throw new FlareError(`SDK checksum mismatch for ${file}`);
|
|
40
|
+
writeFileSync(archive, data);
|
|
41
|
+
await extract({ file: archive, cwd: temporary, strict: true, filter: (entry) => entry === "flare" || entry.startsWith("flare/") });
|
|
42
|
+
const manifest = JSON.parse(readFileSync(path.join(sdk, "sdk-manifest.json"), "utf8"));
|
|
43
|
+
if (manifest.version !== version || manifest.target !== target.platform || manifest.crt !== runtime) {
|
|
44
|
+
throw new FlareError(`SDK archive ${file} has incorrect platform or version metadata`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
rmSync(destination, { recursive: true, force: true });
|
|
48
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
49
|
+
renameSync(sdk, destination);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
return target;
|
|
54
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
BasedOnStyle: Google
|
|
2
|
+
Standard: c++20
|
|
3
|
+
ColumnLimit: 120
|
|
4
|
+
IndentWidth: 4
|
|
5
|
+
TabWidth: 4
|
|
6
|
+
ContinuationIndentWidth: 4
|
|
7
|
+
ConstructorInitializerIndentWidth: 4
|
|
8
|
+
AccessModifierOffset: -4
|
|
9
|
+
NamespaceIndentation: All
|
|
10
|
+
UseTab: Never
|
|
11
|
+
PointerAlignment: Left
|
|
12
|
+
DerivePointerAlignment: false
|
|
13
|
+
AllowShortFunctionsOnASingleLine: Inline
|
|
14
|
+
AllowShortIfStatementsOnASingleLine: WithoutElse
|
|
15
|
+
AllowShortLoopsOnASingleLine: false
|
|
16
|
+
RemoveBracesLLVM: true
|
|
17
|
+
SortIncludes: Never
|
|
18
|
+
FixNamespaceComments: false
|
|
19
|
+
AlignAfterOpenBracket: BlockIndent
|
|
20
|
+
AlignOperands: DontAlign
|
|
21
|
+
AlignTrailingComments: false
|
|
22
|
+
AllowAllArgumentsOnNextLine: false
|
|
23
|
+
AllowAllParametersOfDeclarationOnNextLine: false
|
|
24
|
+
BinPackArguments: false
|
|
25
|
+
BinPackParameters: false
|
|
26
|
+
BreakBeforeBinaryOperators: NonAssignment
|
|
27
|
+
BreakConstructorInitializers: BeforeColon
|
|
28
|
+
PackConstructorInitializers: Never
|
|
29
|
+
LambdaBodyIndentation: OuterScope
|
|
30
|
+
PenaltyBreakBeforeFirstCallParameter: 10
|
|
31
|
+
AlwaysBreakAfterReturnType: None
|
|
32
|
+
PenaltyReturnTypeOnItsOwnLine: 1000
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
};
|
package/src/templates/project.js
CHANGED
|
@@ -7,7 +7,7 @@ export function embeddedIdentifier(name) {
|
|
|
7
7
|
.join("");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function configTemplate({ name, identifier, version }) {
|
|
10
|
+
export function configTemplate({ name, identifier, version, template = "javascript" }) {
|
|
11
11
|
return {
|
|
12
12
|
$schema: SCHEMA_URL,
|
|
13
13
|
package: { productName: name, version },
|
|
@@ -18,11 +18,11 @@ export function configTemplate({ name, identifier, version }) {
|
|
|
18
18
|
version
|
|
19
19
|
},
|
|
20
20
|
build: {
|
|
21
|
-
frontendDist: "frontend",
|
|
22
|
-
devUrl: "",
|
|
23
|
-
beforeDevCommand: "",
|
|
21
|
+
frontendDist: template === "typescript" ? "dist" : "frontend",
|
|
22
|
+
devUrl: template === "typescript" ? "http://127.0.0.1:5173" : "",
|
|
23
|
+
beforeDevCommand: template === "typescript" ? "npm run frontend:dev" : "",
|
|
24
24
|
afterDevCommand: "",
|
|
25
|
-
beforeBuildCommand: "",
|
|
25
|
+
beforeBuildCommand: template === "typescript" ? "npm run frontend:build" : "",
|
|
26
26
|
afterBuildCommand: ""
|
|
27
27
|
},
|
|
28
28
|
bundle: {
|
|
@@ -51,7 +51,7 @@ export function configTemplate({ name, identifier, version }) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export function packageJsonTemplate({ name, version, cliVersion }) {
|
|
54
|
+
export function packageJsonTemplate({ name, version, cliVersion, template = "javascript" }) {
|
|
55
55
|
return {
|
|
56
56
|
name,
|
|
57
57
|
version,
|
|
@@ -61,15 +61,17 @@ export function packageJsonTemplate({ name, version, cliVersion }) {
|
|
|
61
61
|
dev: "flare dev",
|
|
62
62
|
build: "flare build",
|
|
63
63
|
bundle: "flare bundle",
|
|
64
|
-
icon: "flare icon icons/icon.png"
|
|
64
|
+
icon: "flare icon icons/icon.png",
|
|
65
|
+
...(template === "typescript" ? { "frontend:dev": "vite --host 127.0.0.1", "frontend:build": "tsc --noEmit && vite build" } : {})
|
|
65
66
|
},
|
|
66
67
|
devDependencies: {
|
|
67
|
-
"@flare-desktop/cli": `^${cliVersion}
|
|
68
|
+
"@flare-desktop/cli": `^${cliVersion}`,
|
|
69
|
+
...(template === "typescript" ? { typescript: "^5.9.3", vite: "^6.4.5" } : {})
|
|
68
70
|
}
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
export function cmakeTemplate({ name, version, coreVersion }) {
|
|
74
|
+
export function cmakeTemplate({ name, version, coreVersion, sdkMode = "source" }) {
|
|
73
75
|
return `cmake_minimum_required(VERSION 3.24)
|
|
74
76
|
|
|
75
77
|
project(${name} VERSION ${version} LANGUAGES CXX)
|
|
@@ -77,18 +79,46 @@ project(${name} VERSION ${version} LANGUAGES CXX)
|
|
|
77
79
|
set(CMAKE_CXX_STANDARD 20)
|
|
78
80
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
if(WIN32 AND NOT DEFINED FLARE_NATIVE_WEBVIEW)
|
|
83
|
+
set(FLARE_NATIVE_WEBVIEW ON CACHE BOOL "Use the Windows system webview")
|
|
84
|
+
endif()
|
|
85
|
+
|
|
86
|
+
${sdkMode === "prebuilt" ? `add_subdirectory("\${CMAKE_CURRENT_SOURCE_DIR}/vendor/flare" "\${CMAKE_CURRENT_BINARY_DIR}/flare-sdk")` : `include(FetchContent)
|
|
81
87
|
|
|
82
88
|
FetchContent_Declare(
|
|
83
89
|
flare
|
|
84
90
|
GIT_REPOSITORY https://github.com/flare-desktop/flare.git
|
|
85
91
|
GIT_TAG v${coreVersion}
|
|
86
92
|
GIT_SHALLOW TRUE)
|
|
87
|
-
FetchContent_MakeAvailable(flare)
|
|
93
|
+
FetchContent_MakeAvailable(flare)`}
|
|
88
94
|
|
|
89
95
|
add_executable(${name} source/main.cpp)
|
|
90
96
|
target_link_libraries(${name} PRIVATE flare::core flare::assets)
|
|
91
97
|
|
|
98
|
+
find_program(FLARE_NODE_EXECUTABLE node REQUIRED)
|
|
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
|
+
|
|
92
122
|
set(FLARE_FRONTEND_DIR "frontend" CACHE PATH "Frontend directory")
|
|
93
123
|
file(TO_CMAKE_PATH "\${FLARE_FRONTEND_DIR}" FLARE_FRONTEND_DIR)
|
|
94
124
|
target_compile_definitions(${name} PRIVATE FLARE_FRONTEND_DIR="\${FLARE_FRONTEND_DIR}")
|
|
@@ -96,13 +126,14 @@ FlareEmbedFrontend(${name} "\${FLARE_FRONTEND_DIR}")
|
|
|
96
126
|
`;
|
|
97
127
|
}
|
|
98
128
|
|
|
99
|
-
export function sourceTemplate({ name
|
|
129
|
+
export function sourceTemplate({ name }) {
|
|
100
130
|
const embedded = embeddedIdentifier(name);
|
|
101
131
|
return `#include <flare/config/config.h>
|
|
102
132
|
#include <flare/core/application.h>
|
|
103
133
|
#include <flare/log/log.h>
|
|
104
134
|
|
|
105
135
|
#include <${name}-frontend.h>
|
|
136
|
+
#include <flare-embedded-config.h>
|
|
106
137
|
|
|
107
138
|
#include <cstdlib>
|
|
108
139
|
#include <string>
|
|
@@ -115,20 +146,9 @@ int main() {
|
|
|
115
146
|
const bool development = std::getenv("FLARE_DEV") != nullptr;
|
|
116
147
|
|
|
117
148
|
Flare::ApplicationOptions options;
|
|
118
|
-
options.config =
|
|
119
|
-
options.config.app.productName = "${name}";
|
|
120
|
-
options.config.windows.front().title = "${name}";
|
|
149
|
+
options.config = FlareGenerated::EmbeddedConfig();
|
|
121
150
|
options.development = development;
|
|
122
151
|
|
|
123
|
-
std::string error;
|
|
124
|
-
Flare::ConfigLoader loader(
|
|
125
|
-
Flare::ConfigMergeOptions{"flare.config.json", development ? "development" : "production", "", {}});
|
|
126
|
-
if (const auto loaded = loader.Load(error); loaded.has_value()) {
|
|
127
|
-
options.config = *loaded;
|
|
128
|
-
} else {
|
|
129
|
-
Flare::LogWarn("using the built-in configuration: {}", error);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
152
|
const char* devUrl = std::getenv("FLARE_DEV_URL");
|
|
133
153
|
if (devUrl != nullptr) options.devUrl = devUrl;
|
|
134
154
|
if (development && options.devUrl.empty()) options.frontendDirectory = FLARE_FRONTEND_DIR;
|
|
@@ -150,7 +170,7 @@ int main() {
|
|
|
150
170
|
`;
|
|
151
171
|
}
|
|
152
172
|
|
|
153
|
-
export function indexHtmlTemplate(name) {
|
|
173
|
+
export function indexHtmlTemplate(name, template = "javascript") {
|
|
154
174
|
return `<!doctype html>
|
|
155
175
|
<html lang="en">
|
|
156
176
|
<head>
|
|
@@ -166,7 +186,7 @@ export function indexHtmlTemplate(name) {
|
|
|
166
186
|
<button id="greet">call the backend</button>
|
|
167
187
|
<pre id="output"></pre>
|
|
168
188
|
</main>
|
|
169
|
-
<script type="module" src="main.js"></script>
|
|
189
|
+
<script type="module" src="${template === "typescript" ? "main.ts" : "main.js"}"></script>
|
|
170
190
|
</body>
|
|
171
191
|
</html>
|
|
172
192
|
`;
|
|
@@ -246,8 +266,44 @@ document.querySelector("#greet").addEventListener("click", async () => {
|
|
|
246
266
|
`;
|
|
247
267
|
}
|
|
248
268
|
|
|
269
|
+
export function typescriptTemplate() {
|
|
270
|
+
return `import { app, invoke } from "/core.js";
|
|
271
|
+
|
|
272
|
+
const output = document.querySelector<HTMLPreElement>("#output");
|
|
273
|
+
const button = document.querySelector<HTMLButtonElement>("#greet");
|
|
274
|
+
|
|
275
|
+
button?.addEventListener("click", async () => {
|
|
276
|
+
const message = await invoke<string>("greet", { name: "flare" });
|
|
277
|
+
const version = await app.version();
|
|
278
|
+
if (output) output.textContent = \`\${message} (\${version})\`;
|
|
279
|
+
});
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function tsconfigTemplate() {
|
|
284
|
+
return {
|
|
285
|
+
compilerOptions: {
|
|
286
|
+
target: "ES2022", lib: ["ES2022", "DOM"], module: "ESNext", moduleResolution: "Bundler",
|
|
287
|
+
strict: true, noEmit: true, skipLibCheck: true, types: ["vite/client"],
|
|
288
|
+
paths: { "/core.js": ["./frontend/core.d.ts"] }
|
|
289
|
+
},
|
|
290
|
+
include: ["frontend/main.ts"]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function viteConfigTemplate() {
|
|
295
|
+
return `import { defineConfig } from "vite";
|
|
296
|
+
export default defineConfig({
|
|
297
|
+
root: "frontend",
|
|
298
|
+
base: "./",
|
|
299
|
+
build: { outDir: "../dist", emptyOutDir: true },
|
|
300
|
+
server: { host: "127.0.0.1", port: 5173, strictPort: true }
|
|
301
|
+
});
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
|
|
249
305
|
export function gitignoreTemplate() {
|
|
250
|
-
return "build/\ntarget/\nnode_modules/\n.DS_Store\n";
|
|
306
|
+
return "build/\ntarget/\nnode_modules/\ndist/\n.DS_Store\n";
|
|
251
307
|
}
|
|
252
308
|
|
|
253
309
|
export function readmeTemplate(name) {
|
|
@@ -270,7 +326,7 @@ npm run bundle
|
|
|
270
326
|
\`\`\`
|
|
271
327
|
|
|
272
328
|
\`npm run dev\` builds the C++ core and serves \`frontend/\` in the system webview.
|
|
273
|
-
\`npm run build\` compiles the frontend into the executable
|
|
329
|
+
\`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.
|
|
274
330
|
\`npm run bundle\` assembles the configured bundle targets under \`target/bundle\`.
|
|
275
331
|
|
|
276
332
|
## Icons
|