@flare-desktop/cli 0.0.0-stage → 0.1.1

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/src/lib/png.js ADDED
@@ -0,0 +1,288 @@
1
+ import { deflateSync, inflateSync } from "node:zlib";
2
+
3
+ import { FlareError } from "./errors.js";
4
+
5
+ const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
6
+ const COLOR_CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
7
+
8
+ const crcTable = (() => {
9
+ const table = new Uint32Array(256);
10
+ for (let index = 0; index < 256; index += 1) {
11
+ let value = index;
12
+ for (let bit = 0; bit < 8; bit += 1) {
13
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
14
+ }
15
+ table[index] = value >>> 0;
16
+ }
17
+ return table;
18
+ })();
19
+
20
+ function crc32(buffer) {
21
+ let crc = 0xffffffff;
22
+ for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
23
+ return (crc ^ 0xffffffff) >>> 0;
24
+ }
25
+
26
+ function chunk(type, data) {
27
+ const header = Buffer.alloc(8);
28
+ header.writeUInt32BE(data.length, 0);
29
+ header.write(type, 4, 4, "ascii");
30
+ const checksum = Buffer.alloc(4);
31
+ checksum.writeUInt32BE(crc32(Buffer.concat([header.subarray(4), data])), 0);
32
+ return Buffer.concat([header, data, checksum]);
33
+ }
34
+
35
+ function paeth(left, up, upperLeft) {
36
+ const estimate = left + up - upperLeft;
37
+ const leftDistance = Math.abs(estimate - left);
38
+ const upDistance = Math.abs(estimate - up);
39
+ const upperLeftDistance = Math.abs(estimate - upperLeft);
40
+ if (leftDistance <= upDistance && leftDistance <= upperLeftDistance) return left;
41
+ if (upDistance <= upperLeftDistance) return up;
42
+ return upperLeft;
43
+ }
44
+
45
+ function unfilter(raw, width, height, bytesPerPixel) {
46
+ const stride = width * bytesPerPixel;
47
+ const pixels = Buffer.alloc(stride * height);
48
+ let offset = 0;
49
+
50
+ for (let y = 0; y < height; y += 1) {
51
+ const filter = raw[offset];
52
+ offset += 1;
53
+ const row = pixels.subarray(y * stride, (y + 1) * stride);
54
+ const previous = y > 0 ? pixels.subarray((y - 1) * stride, y * stride) : null;
55
+
56
+ for (let x = 0; x < stride; x += 1) {
57
+ const value = raw[offset + x];
58
+ const left = x >= bytesPerPixel ? row[x - bytesPerPixel] : 0;
59
+ const up = previous ? previous[x] : 0;
60
+ const upperLeft = previous && x >= bytesPerPixel ? previous[x - bytesPerPixel] : 0;
61
+
62
+ if (filter === 0) row[x] = value;
63
+ else if (filter === 1) row[x] = (value + left) & 0xff;
64
+ else if (filter === 2) row[x] = (value + up) & 0xff;
65
+ else if (filter === 3) row[x] = (value + ((left + up) >> 1)) & 0xff;
66
+ else if (filter === 4) row[x] = (value + paeth(left, up, upperLeft)) & 0xff;
67
+ else throw new FlareError(`unsupported png filter ${filter}`);
68
+ }
69
+
70
+ offset += stride;
71
+ }
72
+
73
+ return pixels;
74
+ }
75
+
76
+ function toRgba(pixels, header, palette, transparency) {
77
+ const { width, height, colorType } = header;
78
+ const data = Buffer.alloc(width * height * 4);
79
+
80
+ for (let index = 0; index < width * height; index += 1) {
81
+ const target = index * 4;
82
+ if (colorType === 0) {
83
+ const value = pixels[index];
84
+ data[target] = value;
85
+ data[target + 1] = value;
86
+ data[target + 2] = value;
87
+ data[target + 3] = 255;
88
+ continue;
89
+ }
90
+ if (colorType === 2) {
91
+ data[target] = pixels[index * 3];
92
+ data[target + 1] = pixels[index * 3 + 1];
93
+ data[target + 2] = pixels[index * 3 + 2];
94
+ data[target + 3] = 255;
95
+ continue;
96
+ }
97
+ if (colorType === 3) {
98
+ if (!palette) throw new FlareError("png palette is missing");
99
+ const entry = pixels[index];
100
+ data[target] = palette[entry * 3] ?? 0;
101
+ data[target + 1] = palette[entry * 3 + 1] ?? 0;
102
+ data[target + 2] = palette[entry * 3 + 2] ?? 0;
103
+ data[target + 3] = transparency?.[entry] ?? 255;
104
+ continue;
105
+ }
106
+ if (colorType === 4) {
107
+ const value = pixels[index * 2];
108
+ data[target] = value;
109
+ data[target + 1] = value;
110
+ data[target + 2] = value;
111
+ data[target + 3] = pixels[index * 2 + 1];
112
+ continue;
113
+ }
114
+ data[target] = pixels[index * 4];
115
+ data[target + 1] = pixels[index * 4 + 1];
116
+ data[target + 2] = pixels[index * 4 + 2];
117
+ data[target + 3] = pixels[index * 4 + 3];
118
+ }
119
+
120
+ return data;
121
+ }
122
+
123
+ export function readPng(buffer) {
124
+ if (!Buffer.isBuffer(buffer) || buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) {
125
+ throw new FlareError("not a png file");
126
+ }
127
+
128
+ let offset = 8;
129
+ let header = null;
130
+ let palette = null;
131
+ let transparency = null;
132
+ const chunks = [];
133
+
134
+ while (offset + 12 <= buffer.length) {
135
+ const length = buffer.readUInt32BE(offset);
136
+ const type = buffer.toString("ascii", offset + 4, offset + 8);
137
+ const data = buffer.subarray(offset + 8, offset + 8 + length);
138
+ offset += length + 12;
139
+
140
+ if (type === "IHDR") {
141
+ header = {
142
+ width: data.readUInt32BE(0),
143
+ height: data.readUInt32BE(4),
144
+ bitDepth: data[8],
145
+ colorType: data[9],
146
+ compression: data[10],
147
+ filter: data[11],
148
+ interlace: data[12]
149
+ };
150
+ } else if (type === "PLTE") {
151
+ palette = Buffer.from(data);
152
+ } else if (type === "tRNS") {
153
+ transparency = Buffer.from(data);
154
+ } else if (type === "IDAT") {
155
+ chunks.push(Buffer.from(data));
156
+ } else if (type === "IEND") {
157
+ break;
158
+ }
159
+ }
160
+
161
+ if (!header) throw new FlareError("png header is missing");
162
+ if (header.bitDepth !== 8) throw new FlareError("only 8 bit png images are supported");
163
+ if (header.compression !== 0) throw new FlareError("unsupported png compression");
164
+ if (header.filter !== 0) throw new FlareError("unsupported png filter method");
165
+ if (header.interlace !== 0) throw new FlareError("interlaced png images are not supported");
166
+ if (!COLOR_CHANNELS[header.colorType]) throw new FlareError(`unsupported png color type ${header.colorType}`);
167
+ if (header.width <= 0 || header.height <= 0) throw new FlareError("png image is empty");
168
+ if (chunks.length === 0) throw new FlareError("png image data is missing");
169
+
170
+ const bytesPerPixel = COLOR_CHANNELS[header.colorType];
171
+ const raw = inflateSync(Buffer.concat(chunks));
172
+ const expected = header.height * (header.width * bytesPerPixel + 1);
173
+ if (raw.length < expected) throw new FlareError("png image data is truncated");
174
+
175
+ const pixels = unfilter(raw, header.width, header.height, bytesPerPixel);
176
+ return {
177
+ width: header.width,
178
+ height: header.height,
179
+ data: toRgba(pixels, header, palette, transparency)
180
+ };
181
+ }
182
+
183
+ export function writePng(image) {
184
+ const { width, height, data } = image;
185
+ if (width <= 0 || height <= 0) throw new FlareError("cannot encode an empty image");
186
+ if (data.length !== width * height * 4) throw new FlareError("pixel buffer does not match the image size");
187
+
188
+ const stride = width * 4;
189
+ const raw = Buffer.alloc((stride + 1) * height);
190
+ for (let y = 0; y < height; y += 1) {
191
+ raw[y * (stride + 1)] = 0;
192
+ data.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
193
+ }
194
+
195
+ const header = Buffer.alloc(13);
196
+ header.writeUInt32BE(width, 0);
197
+ header.writeUInt32BE(height, 4);
198
+ header[8] = 8;
199
+ header[9] = 6;
200
+ header[10] = 0;
201
+ header[11] = 0;
202
+ header[12] = 0;
203
+
204
+ return Buffer.concat([
205
+ SIGNATURE,
206
+ chunk("IHDR", header),
207
+ chunk("IDAT", deflateSync(raw, { level: 9 })),
208
+ chunk("IEND", Buffer.alloc(0))
209
+ ]);
210
+ }
211
+
212
+ function sampleBilinear(image, x, y) {
213
+ const { width, height, data } = image;
214
+ const left = Math.max(0, Math.min(width - 1, Math.floor(x)));
215
+ const top = Math.max(0, Math.min(height - 1, Math.floor(y)));
216
+ const right = Math.max(0, Math.min(width - 1, left + 1));
217
+ const bottom = Math.max(0, Math.min(height - 1, top + 1));
218
+ const xWeight = Math.max(0, Math.min(1, x - left));
219
+ const yWeight = Math.max(0, Math.min(1, y - top));
220
+ const result = [0, 0, 0, 0];
221
+
222
+ for (let channel = 0; channel < 4; channel += 1) {
223
+ const topLeft = data[(top * width + left) * 4 + channel];
224
+ const topRight = data[(top * width + right) * 4 + channel];
225
+ const bottomLeft = data[(bottom * width + left) * 4 + channel];
226
+ const bottomRight = data[(bottom * width + right) * 4 + channel];
227
+ const upper = topLeft + (topRight - topLeft) * xWeight;
228
+ const lower = bottomLeft + (bottomRight - bottomLeft) * xWeight;
229
+ result[channel] = upper + (lower - upper) * yWeight;
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ export function resizePng(image, width, height) {
236
+ if (width <= 0 || height <= 0) throw new FlareError("cannot resize an image to an empty size");
237
+ if (image.width === width && image.height === height) {
238
+ return { width, height, data: Buffer.from(image.data) };
239
+ }
240
+
241
+ const data = Buffer.alloc(width * height * 4);
242
+ const scaleX = image.width / width;
243
+ const scaleY = image.height / height;
244
+
245
+ if (scaleX >= 1 && scaleY >= 1) {
246
+ for (let y = 0; y < height; y += 1) {
247
+ const fromY = Math.floor(y * scaleY);
248
+ const toY = Math.max(fromY + 1, Math.min(image.height, Math.ceil((y + 1) * scaleY)));
249
+ for (let x = 0; x < width; x += 1) {
250
+ const fromX = Math.floor(x * scaleX);
251
+ const toX = Math.max(fromX + 1, Math.min(image.width, Math.ceil((x + 1) * scaleX)));
252
+ const total = [0, 0, 0, 0];
253
+ let count = 0;
254
+ for (let sourceY = fromY; sourceY < toY; sourceY += 1) {
255
+ for (let sourceX = fromX; sourceX < toX; sourceX += 1) {
256
+ const source = (sourceY * image.width + sourceX) * 4;
257
+ total[0] += image.data[source];
258
+ total[1] += image.data[source + 1];
259
+ total[2] += image.data[source + 2];
260
+ total[3] += image.data[source + 3];
261
+ count += 1;
262
+ }
263
+ }
264
+ const target = (y * width + x) * 4;
265
+ data[target] = Math.round(total[0] / count);
266
+ data[target + 1] = Math.round(total[1] / count);
267
+ data[target + 2] = Math.round(total[2] / count);
268
+ data[target + 3] = Math.round(total[3] / count);
269
+ }
270
+ }
271
+ return { width, height, data };
272
+ }
273
+
274
+ for (let y = 0; y < height; y += 1) {
275
+ const sourceY = (y + 0.5) * scaleY - 0.5;
276
+ for (let x = 0; x < width; x += 1) {
277
+ const sourceX = (x + 0.5) * scaleX - 0.5;
278
+ const pixel = sampleBilinear(image, sourceX, sourceY);
279
+ const target = (y * width + x) * 4;
280
+ data[target] = Math.round(pixel[0]);
281
+ data[target + 1] = Math.round(pixel[1]);
282
+ data[target + 2] = Math.round(pixel[2]);
283
+ data[target + 3] = Math.round(pixel[3]);
284
+ }
285
+ }
286
+
287
+ return { width, height, data };
288
+ }
@@ -0,0 +1,57 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+
3
+ import { FlareError } from "./errors.js";
4
+
5
+ export function run(command, args = [], { cwd, env, stdio = "inherit", shell = false } = {}) {
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn(command, args, {
8
+ cwd,
9
+ env: env ? { ...process.env, ...env } : process.env,
10
+ stdio,
11
+ shell
12
+ });
13
+ child.on("error", reject);
14
+ child.on("close", (code, signal) => resolve({ code: code ?? 1, signal }));
15
+ });
16
+ }
17
+
18
+ export function startShell(command, { cwd, env } = {}) {
19
+ return spawn(command, {
20
+ cwd,
21
+ env: env ? { ...process.env, ...env } : process.env,
22
+ shell: true,
23
+ stdio: "inherit",
24
+ detached: process.platform !== "win32"
25
+ });
26
+ }
27
+
28
+ export async function runShell(command, { cwd, env, stdio = "inherit" } = {}) {
29
+ return run(command, [], { cwd, env, stdio, shell: true });
30
+ }
31
+
32
+ export async function runChecked(command, args, options = {}) {
33
+ const result = await run(command, args, options);
34
+ if (result.code !== 0) throw new FlareError(`'${command}' exited with code ${result.code}`);
35
+ return result;
36
+ }
37
+
38
+ export function capture(command, args = [], options = {}) {
39
+ return spawnSync(command, args, { encoding: "utf8", ...options });
40
+ }
41
+
42
+ export function firstLine(text) {
43
+ return String(text ?? "").split(/\r?\n/)[0].trim();
44
+ }
45
+
46
+ export function terminate(child) {
47
+ if (!child || child.exitCode !== null) return;
48
+ if (process.platform === "win32") {
49
+ spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
50
+ return;
51
+ }
52
+ try {
53
+ process.kill(-child.pid, "SIGTERM");
54
+ } catch {
55
+ child.kill("SIGTERM");
56
+ }
57
+ }
@@ -0,0 +1,285 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { FlareError } from "./errors.js";
5
+ import { SCHEMA_URL } from "../schema.js";
6
+
7
+ export const DEFAULT_CONFIG_FILE = "flare.config.json";
8
+ export const DEFAULT_SCHEMA_URL = SCHEMA_URL;
9
+ export { SCHEMA_URL };
10
+
11
+ const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
12
+ const APP_NAME = /^[a-z][a-z0-9-_]*$/;
13
+
14
+ export function isObject(value) {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ export function platformName(platform = process.platform) {
19
+ if (platform === "win32") return "windows";
20
+ if (platform === "darwin") return "macos";
21
+ if (platform === "linux") return "linux";
22
+ return platform;
23
+ }
24
+
25
+ export function platformDisplayName(platform = process.platform) {
26
+ if (platform === "win32") return "Windows";
27
+ if (platform === "darwin") return "macOS";
28
+ if (platform === "linux") return "Linux";
29
+ return platform;
30
+ }
31
+
32
+ export function architectureName(architecture = process.arch) {
33
+ if (architecture === "x64") return "x86_64";
34
+ if (architecture === "arm64") return "aarch64";
35
+ if (architecture === "ia32") return "x86";
36
+ return architecture;
37
+ }
38
+
39
+ export function formatsForPlatform(platform = process.platform) {
40
+ if (platform === "linux") return ["deb", "rpm", "appimage"];
41
+ if (platform === "darwin") return ["app", "dmg"];
42
+ if (platform === "win32") return ["msi", "nsis"];
43
+ return [];
44
+ }
45
+
46
+ export function toKebabCase(value) {
47
+ const normalized = String(value)
48
+ .trim()
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, "-")
51
+ .replace(/^-+|-+$/g, "")
52
+ .replace(/-{2,}/g, "-");
53
+ return normalized || "flare-app";
54
+ }
55
+
56
+ export function isValidAppName(name) {
57
+ return APP_NAME.test(String(name));
58
+ }
59
+
60
+ export function defaults(identifier, version) {
61
+ return {
62
+ $schema: SCHEMA_URL,
63
+ package: { productName: "flare-app", version },
64
+ app: {
65
+ identifier,
66
+ productName: "flare-app",
67
+ mainBinaryName: "flare-app",
68
+ version
69
+ },
70
+ build: {
71
+ beforeDevCommand: "",
72
+ afterDevCommand: "",
73
+ beforeBuildCommand: "",
74
+ afterBuildCommand: "",
75
+ frontendDist: "frontend",
76
+ devUrl: "",
77
+ removeUnusedCommands: [],
78
+ features: {
79
+ customProtocol: true,
80
+ devtools: false,
81
+ isolation: false,
82
+ systemTray: false
83
+ }
84
+ },
85
+ bundle: {
86
+ active: true,
87
+ identifier,
88
+ icon: "icons/icon.png",
89
+ version,
90
+ publisher: "",
91
+ homepage: "",
92
+ longDescription: "",
93
+ shortDescription: "",
94
+ copyright: "",
95
+ category: "DeveloperTool",
96
+ licenseFile: "",
97
+ targets: [],
98
+ resources: [],
99
+ externalBin: [],
100
+ linux: { depends: "", desktopTemplate: "", section: "utils", priority: "optional", files: [] },
101
+ windows: { language: "en-US", installerIcon: "", installMode: 0, webviewInstallModeOffline: false },
102
+ macos: { minimumSystemVersion: "10.15", frameworks: false, signingIdentity: "" }
103
+ },
104
+ security: {
105
+ freezePrototype: true,
106
+ dangerousDisableAssetCspModification: false,
107
+ dangerousUseHttpScheme: false,
108
+ dangerousAcceptInvalidCertificates: false,
109
+ capabilities: [],
110
+ dangerousRemoteDomainIpcAccess: []
111
+ },
112
+ windows: [
113
+ {
114
+ label: "main",
115
+ title: "flare-app",
116
+ width: 800,
117
+ height: 600,
118
+ minimumSize: { width: 0, height: 0 },
119
+ maximumSize: { width: 0, height: 0 },
120
+ resizable: true,
121
+ maximized: false,
122
+ fullscreen: false,
123
+ alwaysOnTop: false,
124
+ decorations: true,
125
+ transparent: false,
126
+ visible: true,
127
+ center: true,
128
+ devtools: false,
129
+ theme: "automatic"
130
+ }
131
+ ],
132
+ plugins: {}
133
+ };
134
+ }
135
+
136
+ export function layerFileNames(configFile, environment) {
137
+ const extension = path.extname(configFile) || ".json";
138
+ const stem = path.basename(configFile, extension);
139
+ return [
140
+ `${stem}.${platformName()}${extension}`,
141
+ ...(environment ? [`${stem}.${environment}${extension}`] : []),
142
+ `${stem}.local${extension}`
143
+ ];
144
+ }
145
+
146
+ export function resolveConfigFiles(projectDirectory, { configFile = DEFAULT_CONFIG_FILE, environment = "" } = {}) {
147
+ const base = path.isAbsolute(configFile) ? configFile : path.join(projectDirectory, configFile);
148
+ if (!existsSync(base)) return [];
149
+ const directory = path.dirname(base);
150
+ const files = [base];
151
+ for (const name of layerFileNames(path.basename(base), environment)) {
152
+ const candidate = path.join(directory, name);
153
+ if (existsSync(candidate) && !files.includes(candidate)) files.push(candidate);
154
+ }
155
+ return files;
156
+ }
157
+
158
+ export function mergeConfig(base, overlay) {
159
+ if (!isObject(base) || !isObject(overlay)) return overlay;
160
+ const result = { ...base };
161
+ for (const [key, value] of Object.entries(overlay)) {
162
+ if (value === null) {
163
+ delete result[key];
164
+ continue;
165
+ }
166
+ if (isObject(result[key]) && isObject(value)) {
167
+ result[key] = mergeConfig(result[key], value);
168
+ continue;
169
+ }
170
+ result[key] = value;
171
+ }
172
+ return result;
173
+ }
174
+
175
+ function normalizeWindow(window, fallbackTitle) {
176
+ const value = isObject(window) ? { ...window } : {};
177
+ const title = value.title ?? fallbackTitle;
178
+ const width = value.width ?? value.size?.width ?? 800;
179
+ const height = value.height ?? value.size?.height ?? 600;
180
+ return {
181
+ ...value,
182
+ label: value.label ?? "main",
183
+ title,
184
+ width,
185
+ height,
186
+ minimumSize: value.minimumSize ?? { width: 0, height: 0 },
187
+ maximumSize: value.maximumSize ?? { width: 0, height: 0 },
188
+ resizable: value.resizable ?? true,
189
+ maximized: value.maximized ?? false,
190
+ fullscreen: value.fullscreen ?? false,
191
+ alwaysOnTop: value.alwaysOnTop ?? false,
192
+ decorations: value.decorations ?? true,
193
+ transparent: value.transparent ?? false,
194
+ visible: value.visible ?? true,
195
+ center: value.center ?? true,
196
+ devtools: value.devtools ?? false,
197
+ theme: value.theme ?? "automatic"
198
+ };
199
+ }
200
+
201
+ export function normalizeConfig(raw) {
202
+ const config = isObject(raw) ? structuredClone(raw) : {};
203
+ config.package = isObject(config.package) ? config.package : {};
204
+ config.app = isObject(config.app) ? config.app : {};
205
+ config.build = isObject(config.build) ? config.build : {};
206
+ config.bundle = isObject(config.bundle) ? config.bundle : {};
207
+ config.security = isObject(config.security) ? config.security : {};
208
+ config.plugins = isObject(config.plugins) ? config.plugins : {};
209
+
210
+ if (!config.app.productName) config.app.productName = config.package.productName ?? "";
211
+ if (!config.app.version) config.app.version = config.package.version ?? "";
212
+ if (!config.app.mainBinaryName) config.app.mainBinaryName = config.app.productName;
213
+
214
+ const fallbackTitle = config.app.productName || "flare-app";
215
+ const windows = Array.isArray(config.windows) && config.windows.length > 0 ? config.windows : [{}];
216
+ config.windows = windows.map((window) => normalizeWindow(window, fallbackTitle));
217
+
218
+ return config;
219
+ }
220
+
221
+ export function validateConfig(config) {
222
+ if (!isObject(config)) return "configuration root must be an object";
223
+ const identifier = config.app?.identifier;
224
+ if (!identifier || typeof identifier !== "string") return "app.identifier must not be empty";
225
+ if (!identifier.includes(".")) return "app.identifier must contain at least one dot";
226
+ const version = config.app?.version;
227
+ if (!version || !SEMVER.test(version)) return `app.version '${version}' is not a valid semver version`;
228
+ if (!Array.isArray(config.windows) || config.windows.length === 0) return "at least one window must be configured";
229
+
230
+ const labels = [];
231
+ for (const window of config.windows) {
232
+ if (!window.label) return "window label must not be empty";
233
+ if (!(window.width > 0) || !(window.height > 0)) return `window '${window.label}' has an invalid size`;
234
+ if (labels.includes(window.label)) return `duplicate window label '${window.label}'`;
235
+ labels.push(window.label);
236
+ }
237
+
238
+ return "";
239
+ }
240
+
241
+ export function parseConfig(text, source = DEFAULT_CONFIG_FILE) {
242
+ let document;
243
+ try {
244
+ document = JSON.parse(text);
245
+ } catch (error) {
246
+ throw new FlareError(`failed to parse '${source}': ${error.message}`);
247
+ }
248
+ if (!isObject(document)) throw new FlareError(`configuration root must be an object in '${source}'`);
249
+ return document;
250
+ }
251
+
252
+ export function loadProject({ projectDirectory = ".", configFile = DEFAULT_CONFIG_FILE, environment = "" } = {}) {
253
+ const directory = path.resolve(projectDirectory);
254
+ const files = resolveConfigFiles(directory, { configFile, environment });
255
+ if (files.length === 0) {
256
+ throw new FlareError(`no configuration file found for '${path.resolve(directory, configFile)}'`);
257
+ }
258
+
259
+ let merged = {};
260
+ for (const file of files) {
261
+ merged = mergeConfig(merged, parseConfig(readFileSync(file, "utf8"), file));
262
+ }
263
+
264
+ const config = normalizeConfig(merged);
265
+ const validation = validateConfig(config);
266
+ if (validation) throw new FlareError(`${validation} (${files[0]})`);
267
+
268
+ return { config, files, directory, configPath: files[0] };
269
+ }
270
+
271
+ export function mainWindow(config) {
272
+ return config.windows?.[0] ?? normalizeWindow({}, config.app?.productName);
273
+ }
274
+
275
+ export function productName(config) {
276
+ return config.app?.productName || config.package?.productName || "flare-app";
277
+ }
278
+
279
+ export function binaryName(config) {
280
+ return config.app?.mainBinaryName || productName(config);
281
+ }
282
+
283
+ export function frontendDirectory(projectDirectory, config) {
284
+ return path.resolve(projectDirectory, config.build?.frontendDist || "frontend");
285
+ }
package/src/schema.js ADDED
@@ -0,0 +1,2 @@
1
+ export const SCHEMA_URL =
2
+ "https://raw.githubusercontent.com/flare-desktop/flare/main/schemas/flare.config.schema.json";