xbintsc 0.3.18 → 0.3.35

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.
Files changed (194) hide show
  1. package/README.md +33 -0
  2. package/dist/src/cli/main.js +124 -30
  3. package/dist/src/cli/main.js.map +1 -1
  4. package/dist/src/codegen/generator/calls/access.js +8 -1
  5. package/dist/src/codegen/generator/calls/access.js.map +1 -1
  6. package/dist/src/codegen/generator/state.js +20 -0
  7. package/dist/src/codegen/generator/state.js.map +1 -1
  8. package/dist/src/codegen/generator/tables.d.ts +6 -0
  9. package/dist/src/codegen/generator/tables.js +38 -0
  10. package/dist/src/codegen/generator/tables.js.map +1 -1
  11. package/dist/src/driver/bundler/commonjs.d.ts +52 -0
  12. package/dist/src/driver/bundler/commonjs.js +493 -0
  13. package/dist/src/driver/bundler/commonjs.js.map +1 -0
  14. package/dist/src/driver/bundler/graph.d.ts +2 -1
  15. package/dist/src/driver/bundler/graph.js +32 -3
  16. package/dist/src/driver/bundler/graph.js.map +1 -1
  17. package/dist/src/driver/bundler/merge.d.ts +2 -1
  18. package/dist/src/driver/bundler/merge.js +16 -2
  19. package/dist/src/driver/bundler/merge.js.map +1 -1
  20. package/dist/src/driver/bundler/types.d.ts +8 -0
  21. package/dist/src/driver/compiler.d.ts +8 -1
  22. package/dist/src/driver/compiler.js +75 -7
  23. package/dist/src/driver/compiler.js.map +1 -1
  24. package/dist/src/driver/config.d.ts +53 -0
  25. package/dist/src/driver/config.js +141 -0
  26. package/dist/src/driver/config.js.map +1 -0
  27. package/dist/src/driver/icon.d.ts +62 -0
  28. package/dist/src/driver/icon.js +151 -0
  29. package/dist/src/driver/icon.js.map +1 -0
  30. package/dist/src/driver/index.d.ts +4 -0
  31. package/dist/src/driver/index.js +4 -0
  32. package/dist/src/driver/index.js.map +1 -1
  33. package/dist/src/driver/mac-bundle.d.ts +41 -0
  34. package/dist/src/driver/mac-bundle.js +123 -0
  35. package/dist/src/driver/mac-bundle.js.map +1 -0
  36. package/dist/src/driver/win-icon.d.ts +27 -0
  37. package/dist/src/driver/win-icon.js +80 -0
  38. package/dist/src/driver/win-icon.js.map +1 -0
  39. package/dist/src/extensions/gui/html.d.ts +25 -0
  40. package/dist/src/extensions/gui/html.js +203 -0
  41. package/dist/src/extensions/gui/html.js.map +1 -0
  42. package/dist/src/extensions/gui/index.js +11 -0
  43. package/dist/src/extensions/gui/index.js.map +1 -1
  44. package/dist/src/extensions/registry.d.ts +29 -0
  45. package/dist/src/extensions/registry.js +14 -0
  46. package/dist/src/extensions/registry.js.map +1 -1
  47. package/dist/src/index.d.ts +1 -1
  48. package/dist/src/index.js.map +1 -1
  49. package/dist/src/lexer/scanner/identifiers.js +4 -12
  50. package/dist/src/lexer/scanner/identifiers.js.map +1 -1
  51. package/dist/src/parser/statements.js +17 -4
  52. package/dist/src/parser/statements.js.map +1 -1
  53. package/dist/tests/cli/main.test.js +46 -1
  54. package/dist/tests/cli/main.test.js.map +1 -1
  55. package/dist/tests/driver/bundler-resolve.test.d.ts +1 -0
  56. package/dist/tests/driver/bundler-resolve.test.js +230 -0
  57. package/dist/tests/driver/bundler-resolve.test.js.map +1 -0
  58. package/dist/tests/driver/config.test.d.ts +1 -0
  59. package/dist/tests/driver/config.test.js +124 -0
  60. package/dist/tests/driver/config.test.js.map +1 -0
  61. package/dist/tests/driver/icon.test.d.ts +1 -0
  62. package/dist/tests/driver/icon.test.js +421 -0
  63. package/dist/tests/driver/icon.test.js.map +1 -0
  64. package/dist/tests/driver/modules.test.js +177 -0
  65. package/dist/tests/driver/modules.test.js.map +1 -1
  66. package/dist/tests/e2e/fetch.test.d.ts +12 -0
  67. package/dist/tests/e2e/fetch.test.js +272 -0
  68. package/dist/tests/e2e/fetch.test.js.map +1 -0
  69. package/dist/tests/e2e/gui-dom.test.d.ts +5 -0
  70. package/dist/tests/e2e/gui-dom.test.js +102 -0
  71. package/dist/tests/e2e/gui-dom.test.js.map +1 -0
  72. package/dist/tests/e2e/gui-events.test.d.ts +5 -0
  73. package/dist/tests/e2e/gui-events.test.js +60 -0
  74. package/dist/tests/e2e/gui-events.test.js.map +1 -0
  75. package/dist/tests/e2e/gui-helpers.d.ts +31 -0
  76. package/dist/tests/e2e/gui-helpers.js +74 -0
  77. package/dist/tests/e2e/gui-helpers.js.map +1 -0
  78. package/dist/tests/e2e/gui-layout.test.d.ts +4 -0
  79. package/dist/tests/e2e/gui-layout.test.js +88 -0
  80. package/dist/tests/e2e/gui-layout.test.js.map +1 -0
  81. package/dist/tests/e2e/gui-lifecycle.test.d.ts +5 -0
  82. package/dist/tests/e2e/gui-lifecycle.test.js +58 -0
  83. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -0
  84. package/dist/tests/e2e/gui-paint.test.d.ts +5 -0
  85. package/dist/tests/e2e/gui-paint.test.js +111 -0
  86. package/dist/tests/e2e/gui-paint.test.js.map +1 -0
  87. package/dist/tests/e2e/gui-script.test.d.ts +5 -0
  88. package/dist/tests/e2e/gui-script.test.js +78 -0
  89. package/dist/tests/e2e/gui-script.test.js.map +1 -0
  90. package/dist/tests/e2e/gui-style.test.d.ts +5 -0
  91. package/dist/tests/e2e/gui-style.test.js +175 -0
  92. package/dist/tests/e2e/gui-style.test.js.map +1 -0
  93. package/dist/tests/e2e/icon.test.d.ts +7 -0
  94. package/dist/tests/e2e/icon.test.js +80 -0
  95. package/dist/tests/e2e/icon.test.js.map +1 -0
  96. package/dist/tests/e2e/npm-modules.test.d.ts +4 -1
  97. package/dist/tests/e2e/npm-modules.test.js +47 -1
  98. package/dist/tests/e2e/npm-modules.test.js.map +1 -1
  99. package/dist/tests/e2e/runtime-coverage-typed-array.test.d.ts +9 -0
  100. package/dist/tests/e2e/runtime-coverage-typed-array.test.js +140 -0
  101. package/dist/tests/e2e/runtime-coverage-typed-array.test.js.map +1 -0
  102. package/dist/tests/extensions/gui-index.test.d.ts +1 -0
  103. package/dist/tests/extensions/gui-index.test.js +49 -0
  104. package/dist/tests/extensions/gui-index.test.js.map +1 -0
  105. package/dist/tests/extensions/gui.test.d.ts +1 -0
  106. package/dist/tests/extensions/gui.test.js +177 -0
  107. package/dist/tests/extensions/gui.test.js.map +1 -0
  108. package/dist/tests/extensions/registry.test.js +36 -0
  109. package/dist/tests/extensions/registry.test.js.map +1 -1
  110. package/dist/tests/parser/statements.test.js +11 -0
  111. package/dist/tests/parser/statements.test.js.map +1 -1
  112. package/dist/tests/public-api.test.d.ts +1 -0
  113. package/dist/tests/public-api.test.js +31 -0
  114. package/dist/tests/public-api.test.js.map +1 -0
  115. package/package.json +1 -1
  116. package/runtime/ext_gui/css.cpp +4 -517
  117. package/runtime/ext_gui/css_internal.h +27 -0
  118. package/runtime/ext_gui/css_match.cpp +195 -0
  119. package/runtime/ext_gui/css_selector.cpp +207 -0
  120. package/runtime/ext_gui/css_sheet.cpp +129 -0
  121. package/runtime/ext_gui/document.cpp +90 -0
  122. package/runtime/ext_gui/document.h +23 -0
  123. package/runtime/ext_gui/dom.cpp +43 -403
  124. package/runtime/ext_gui/dom.h +7 -0
  125. package/runtime/ext_gui/dom_api.cpp +164 -0
  126. package/runtime/ext_gui/dom_api.h +12 -0
  127. package/runtime/ext_gui/dom_api_document.cpp +96 -0
  128. package/runtime/ext_gui/dom_api_events.cpp +123 -0
  129. package/runtime/ext_gui/dom_api_internal.h +135 -0
  130. package/runtime/ext_gui/dom_api_node.cpp +329 -0
  131. package/runtime/ext_gui/dom_api_node_read.cpp +294 -0
  132. package/runtime/ext_gui/dom_api_proto.cpp +130 -0
  133. package/runtime/ext_gui/dom_api_style.cpp +192 -0
  134. package/runtime/ext_gui/dom_internal.h +39 -0
  135. package/runtime/ext_gui/dom_parse.cpp +348 -0
  136. package/runtime/ext_gui/dom_serialize.cpp +54 -0
  137. package/runtime/ext_gui/gui.cpp +61 -245
  138. package/runtime/ext_gui/gui_engine.h +54 -0
  139. package/runtime/ext_gui/gui_events.cpp +294 -0
  140. package/runtime/ext_gui/gui_internal.h +29 -0
  141. package/runtime/ext_gui/layout.cpp +6 -597
  142. package/runtime/ext_gui/layout_flex.cpp +200 -0
  143. package/runtime/ext_gui/layout_flow.cpp +191 -0
  144. package/runtime/ext_gui/layout_flow_internal.h +149 -0
  145. package/runtime/ext_gui/layout_inline.cpp +181 -0
  146. package/runtime/ext_gui/layout_internal.h +20 -0
  147. package/runtime/ext_gui/renderer.cpp +15 -679
  148. package/runtime/ext_gui/renderer_image.cpp +233 -0
  149. package/runtime/ext_gui/renderer_internal.h +112 -0
  150. package/runtime/ext_gui/renderer_shaders.h +108 -0
  151. package/runtime/ext_gui/renderer_text.cpp +342 -0
  152. package/runtime/ext_gui/script.cpp +63 -0
  153. package/runtime/ext_gui/style.cpp +12 -660
  154. package/runtime/ext_gui/style_internal.h +33 -0
  155. package/runtime/ext_gui/style_print.cpp +113 -0
  156. package/runtime/ext_gui/style_ua.cpp +98 -0
  157. package/runtime/ext_gui/style_values.cpp +468 -0
  158. package/runtime/ext_gui/window.cpp +60 -0
  159. package/runtime/rt.h +6 -0
  160. package/runtime/rt_internal.h +4 -0
  161. package/runtime/xt_builtins.c +1 -0
  162. package/runtime/xt_containers/access.inc +32 -1
  163. package/runtime/xt_fetch/buffer.inc +64 -0
  164. package/runtime/xt_fetch/headers.inc +312 -0
  165. package/runtime/xt_fetch/request.inc +386 -0
  166. package/runtime/xt_fetch/response.inc +107 -0
  167. package/runtime/xt_fetch/socket.inc +72 -0
  168. package/runtime/xt_fetch/url.inc +108 -0
  169. package/runtime/xt_fetch.c +39 -0
  170. package/runtime/xt_stdlib2/set.inc +1 -1
  171. package/runtime/xt_typed_array.c +614 -0
  172. package/src/cli/main.ts +137 -29
  173. package/src/codegen/generator/calls/access.ts +9 -1
  174. package/src/codegen/generator/state.ts +20 -0
  175. package/src/codegen/generator/tables.ts +40 -0
  176. package/src/driver/bundler/commonjs.ts +553 -0
  177. package/src/driver/bundler/graph.ts +35 -2
  178. package/src/driver/bundler/merge.ts +18 -1
  179. package/src/driver/bundler/types.ts +8 -0
  180. package/src/driver/compiler.ts +85 -7
  181. package/src/driver/config.ts +173 -0
  182. package/src/driver/icon.ts +184 -0
  183. package/src/driver/index.ts +30 -0
  184. package/src/driver/mac-bundle.ts +156 -0
  185. package/src/driver/win-icon.ts +96 -0
  186. package/src/extensions/gui/html.ts +236 -0
  187. package/src/extensions/gui/index.ts +11 -0
  188. package/src/extensions/registry.ts +39 -0
  189. package/src/index.ts +1 -1
  190. package/src/lexer/scanner/identifiers.ts +4 -11
  191. package/src/parser/statements.ts +17 -4
  192. package/dist/tests/e2e/gui.test.d.ts +0 -12
  193. package/dist/tests/e2e/gui.test.js +0 -493
  194. package/dist/tests/e2e/gui.test.js.map +0 -1
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
11
11
  import { basename, extname, join, resolve } from "node:path";
12
- import { DiagnosticBag, type Diagnostic } from "../diagnostics/diagnostic.js";
12
+ import { DiagnosticBag, DiagnosticCode, type Diagnostic } from "../diagnostics/diagnostic.js";
13
13
  import { SourceFile } from "../diagnostics/source.js";
14
14
  import { Parser } from "../parser/parser.js";
15
15
  import { generate } from "../codegen/llvm.js";
@@ -20,6 +20,10 @@ import { BuildCache, hashParts } from "./cache.js";
20
20
  import { findRuntimeDir } from "./paths.js";
21
21
  import { findRuntimeLibrary } from "./runtime-lib.js";
22
22
  import { resolveToolchain } from "./toolchain-provider.js";
23
+ import { ensureIconObject, readIcon, EMPTY_ICON, IconError, type IconInfo } from "./icon.js";
24
+ import { ensureWindowsIconResource, resolveResourceCompiler } from "./win-icon.js";
25
+ import { bundlePathFor, packageMacApp } from "./mac-bundle.js";
26
+ import type { AppConfig } from "./config.js";
23
27
  import {
24
28
  compileC,
25
29
  compileIr,
@@ -47,6 +51,10 @@ export interface BuildOptions {
47
51
  /** Prefer a prebuilt `runtime/lib/<os>-<arch>/*.a` archive when present (default true). */
48
52
  readonly preferPrebuilt?: boolean;
49
53
  readonly write?: boolean;
54
+ /** Application icon embedded into the binary (shorthand for `app.icon`). */
55
+ readonly icon?: string;
56
+ /** Application metadata: name, icon and macOS bundle settings. */
57
+ readonly app?: AppConfig;
50
58
  }
51
59
 
52
60
  export interface BuildResult {
@@ -55,6 +63,8 @@ export interface BuildResult {
55
63
  readonly cached: boolean;
56
64
  readonly diagnostics: readonly Diagnostic[];
57
65
  readonly ir?: string;
66
+ /** macOS `.app` bundle produced by `app.bundle` (darwin only). */
67
+ readonly bundlePath?: string;
58
68
  }
59
69
 
60
70
  export interface CompileStringResult {
@@ -109,7 +119,7 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
109
119
  statement.kind === SyntaxKind.ExportAssignment,
110
120
  );
111
121
  if (isModule) {
112
- const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
122
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry), registry.assetLoaders());
113
123
  if (bundled) sourceFile = bundled.sourceFile;
114
124
  }
115
125
  const { ir } = generate(sourceFile, diagnostics, {
@@ -185,13 +195,28 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
185
195
  );
186
196
  let cacheText = sourceText;
187
197
  if (isModule) {
188
- const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
198
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry), registry.assetLoaders());
189
199
  if (bundled) {
190
200
  sourceFile = bundled.sourceFile;
191
201
  cacheText = bundled.text;
192
202
  }
193
203
  }
194
204
 
205
+ const app = options.app ?? {};
206
+ const iconPath = options.icon ?? app.icon;
207
+ let icon: IconInfo | undefined;
208
+ if (iconPath) {
209
+ try {
210
+ icon = readIcon(iconPath);
211
+ } catch (error) {
212
+ if (error instanceof IconError) {
213
+ diagnostics.error(DiagnosticCode.IOError, error.message);
214
+ return { outputPath, cached: false, diagnostics: diagnostics.diagnostics };
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+
195
220
  const runtimeDir = findRuntimeDir();
196
221
  const cacheKey = hashParts([
197
222
  COMPILER_VERSION,
@@ -201,12 +226,22 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
201
226
  process.platform,
202
227
  registryFingerprint(registry),
203
228
  runtimeFingerprint(runtimeDir),
229
+ icon ? hashParts(["icon", icon.format, Buffer.from(icon.bytes).toString("base64")]) : "",
230
+ app.name ?? "",
231
+ String(app.bundle ?? false),
232
+ app.bundleId ?? "",
204
233
  ]);
205
234
 
235
+ // A `.app` bundle is deterministic from the output path and name, so it can be
236
+ // part of the cache key before the bundle actually exists.
237
+ const bundle =
238
+ process.platform === "darwin" && emit === "exe" && app.bundle === true
239
+ ? bundlePathFor(outputPath, app.name ?? baseName)
240
+ : undefined;
206
241
  const cache = new BuildCache(cacheDir);
207
- const outputs = [outputPath];
242
+ const outputs = bundle ? [outputPath, bundle] : [outputPath];
208
243
  if (!options.force && !diagnostics.hasErrors && cache.isFresh(cacheKey, outputs)) {
209
- return { outputPath, cached: true, diagnostics: [] };
244
+ return { outputPath, cached: true, diagnostics: [], ...(bundle ? { bundlePath: bundle } : {}) };
210
245
  }
211
246
 
212
247
  const { ir } = generate(sourceFile, diagnostics, {
@@ -259,9 +294,32 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
259
294
  toolchain.env,
260
295
  );
261
296
 
297
+ const iconObjects: string[] = [];
298
+ // The GUI extension references the icon symbols unconditionally, so a GUI
299
+ // program gets an (empty) icon object even with no icon configured.
300
+ if (icon !== undefined || registry.has("gui")) {
301
+ try {
302
+ iconObjects.push(ensureIconObject(runner, clang, cacheDir, icon ?? EMPTY_ICON, toolchain.env));
303
+ if (icon && process.platform === "win32") {
304
+ const resourceCompiler = resolveResourceCompiler(runner, clang);
305
+ if (resourceCompiler) {
306
+ iconObjects.push(ensureWindowsIconResource(runner, cacheDir, icon, resourceCompiler, toolchain.env));
307
+ } else if (options.verbose) {
308
+ process.stderr.write("xbintsc: no llvm-rc/windres found; skipping the embedded .exe icon\n");
309
+ }
310
+ }
311
+ } catch (error) {
312
+ if (error instanceof IconError) {
313
+ diagnostics.error(DiagnosticCode.ToolchainError, error.message);
314
+ return { outputPath, irPath, cached: false, diagnostics: diagnostics.diagnostics, ir };
315
+ }
316
+ throw error;
317
+ }
318
+ }
319
+
262
320
  link(runner, {
263
321
  clang,
264
- objectPaths: [objectPath, ...runtimeObjects, ...extensionObjects, ...nativeObjects],
322
+ objectPaths: [objectPath, ...runtimeObjects, ...extensionObjects, ...nativeObjects, ...iconObjects],
265
323
  outputPath,
266
324
  linkerFlags: [
267
325
  ...toolchain.linkerArgs,
@@ -272,9 +330,27 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
272
330
  env: toolchain.env,
273
331
  });
274
332
 
333
+ const bundlePath =
334
+ bundle !== undefined
335
+ ? packageMacApp({
336
+ executablePath: outputPath,
337
+ name: app.name ?? baseName,
338
+ bundleId: app.bundleId ?? defaultBundleId(baseName),
339
+ ...(icon ? { icon } : {}),
340
+ runner,
341
+ env: toolchain.env,
342
+ })
343
+ : undefined;
344
+
275
345
  cache.record(cacheKey, outputs);
276
346
  cache.save();
277
- return { outputPath, irPath, cached: false, diagnostics: [], ir };
347
+ return { outputPath, irPath, cached: false, diagnostics: [], ir, ...(bundlePath ? { bundlePath } : {}) };
348
+ }
349
+
350
+ /** Default `CFBundleIdentifier` for a bundle when none is configured. */
351
+ function defaultBundleId(name: string): string {
352
+ const sanitized = name.replace(/[^A-Za-z0-9.-]/g, "");
353
+ return `com.xbintsc.${sanitized || "app"}`;
278
354
  }
279
355
 
280
356
  interface RuntimeObjects {
@@ -290,11 +366,13 @@ export const RUNTIME_SOURCES = [
290
366
  "xt_values.c",
291
367
  "xt_bigint.c",
292
368
  "xt_containers.c",
369
+ "xt_typed_array.c",
293
370
  "xt_stdlib.c",
294
371
  "xt_stdlib2.c",
295
372
  "xt_promise.c",
296
373
  "xt_loop.c",
297
374
  "xt_generator.c",
375
+ "xt_fetch.c",
298
376
  "xt_symbol.c",
299
377
  "xt_builtins.c",
300
378
  "xt_io.c",
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Project build configuration (`xbintsc.config.json`).
3
+ *
4
+ * A single checked-in file makes a build reproducible and shareable:
5
+ * `xbintsc build` with no arguments reads the config, so options like the entry
6
+ * file, output directory, extensions and the application icon are not repeated
7
+ * on every command line. CLI flags always override the config.
8
+ *
9
+ * The schema is intentionally small and strict JSON (no comments) so it needs
10
+ * no parser beyond `JSON.parse`. Relative paths resolve against the config
11
+ * file's directory, not the current working directory.
12
+ */
13
+
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { dirname, isAbsolute, join, resolve } from "node:path";
16
+
17
+ /** Well-known file name searched for by {@link findProjectConfig}. */
18
+ export const CONFIG_FILE_NAME = "xbintsc.config.json";
19
+
20
+ /** Application-level metadata, shared by the icon and (future) packaging. */
21
+ export interface AppConfig {
22
+ readonly name?: string;
23
+ readonly icon?: string;
24
+ /** macOS only: produce a `<name>.app` bundle. */
25
+ readonly bundle?: boolean;
26
+ readonly bundleId?: string;
27
+ }
28
+
29
+ /** The on-disk schema of `xbintsc.config.json`. Every field is optional. */
30
+ export interface ProjectConfig {
31
+ readonly entry?: string;
32
+ readonly outDir?: string;
33
+ readonly output?: string;
34
+ readonly optimize?: "0" | "1" | "2" | "3";
35
+ readonly extensions?: readonly string[];
36
+ readonly extNative?: readonly string[];
37
+ readonly force?: boolean;
38
+ readonly app?: AppConfig;
39
+ }
40
+
41
+ export class ProjectConfigError extends Error {
42
+ constructor(message: string) {
43
+ super(message);
44
+ this.name = "ProjectConfigError";
45
+ }
46
+ }
47
+
48
+ export interface LoadedProjectConfig {
49
+ readonly config: ProjectConfig;
50
+ readonly path: string;
51
+ /** Directory the config lives in; the base for every relative path. */
52
+ readonly directory: string;
53
+ }
54
+
55
+ function expectObject(value: unknown, what: string): Record<string, unknown> {
56
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
57
+ throw new ProjectConfigError(`${what} must be a JSON object`);
58
+ }
59
+ return value as Record<string, unknown>;
60
+ }
61
+
62
+ function readString(root: Record<string, unknown>, key: string, where: string): string | undefined {
63
+ const value = root[key];
64
+ if (value === undefined) return undefined;
65
+ if (typeof value !== "string") throw new ProjectConfigError(`${where}: '${key}' must be a string`);
66
+ return value;
67
+ }
68
+
69
+ function readBoolean(root: Record<string, unknown>, key: string, where: string): boolean | undefined {
70
+ const value = root[key];
71
+ if (value === undefined) return undefined;
72
+ if (typeof value !== "boolean") throw new ProjectConfigError(`${where}: '${key}' must be a boolean`);
73
+ return value;
74
+ }
75
+
76
+ function readStringArray(root: Record<string, unknown>, key: string, where: string): string[] | undefined {
77
+ const value = root[key];
78
+ if (value === undefined) return undefined;
79
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
80
+ throw new ProjectConfigError(`${where}: '${key}' must be an array of strings`);
81
+ }
82
+ return value as string[];
83
+ }
84
+
85
+ /** Parse and validate config text. Throws {@link ProjectConfigError}. */
86
+ export function parseProjectConfig(text: string, where: string = CONFIG_FILE_NAME): ProjectConfig {
87
+ let raw: unknown;
88
+ try {
89
+ raw = JSON.parse(text);
90
+ } catch (error) {
91
+ throw new ProjectConfigError(`${where}: invalid JSON (${(error as Error).message})`);
92
+ }
93
+ const root = expectObject(raw, where);
94
+
95
+ const optimize = readString(root, "optimize", where);
96
+ if (optimize !== undefined && !["0", "1", "2", "3"].includes(optimize)) {
97
+ throw new ProjectConfigError(`${where}: 'optimize' must be one of "0", "1", "2", "3"`);
98
+ }
99
+
100
+ let app: AppConfig | undefined;
101
+ if (root.app !== undefined) {
102
+ const appRoot = expectObject(root.app, `${where}: 'app'`);
103
+ app = {
104
+ name: readString(appRoot, "name", `${where}: 'app'`),
105
+ icon: readString(appRoot, "icon", `${where}: 'app'`),
106
+ bundle: readBoolean(appRoot, "bundle", `${where}: 'app'`),
107
+ bundleId: readString(appRoot, "bundleId", `${where}: 'app'`),
108
+ };
109
+ for (const key of Object.keys(app) as (keyof AppConfig)[]) {
110
+ if (app[key] === undefined) delete app[key];
111
+ }
112
+ }
113
+
114
+ const config: ProjectConfig = {
115
+ entry: readString(root, "entry", where),
116
+ outDir: readString(root, "outDir", where),
117
+ output: readString(root, "output", where),
118
+ optimize: optimize as ProjectConfig["optimize"],
119
+ extensions: readStringArray(root, "extensions", where),
120
+ extNative: readStringArray(root, "extNative", where),
121
+ force: readBoolean(root, "force", where),
122
+ app,
123
+ };
124
+ for (const key of Object.keys(config) as (keyof ProjectConfig)[]) {
125
+ if (config[key] === undefined) delete config[key];
126
+ }
127
+ return config;
128
+ }
129
+
130
+ /** Read and parse a config file. Throws {@link ProjectConfigError}. */
131
+ export function loadProjectConfig(path: string): LoadedProjectConfig {
132
+ const absolute = resolve(path);
133
+ if (!existsSync(absolute)) {
134
+ throw new ProjectConfigError(`Project config not found: ${absolute}`);
135
+ }
136
+ const config = parseProjectConfig(readFileSync(absolute, "utf8"), absolute);
137
+ return { config, path: absolute, directory: dirname(absolute) };
138
+ }
139
+
140
+ /** Walk up from `startDir` looking for {@link CONFIG_FILE_NAME}. */
141
+ export function findProjectConfig(startDir: string): string | undefined {
142
+ let dir = resolve(startDir);
143
+ for (;;) {
144
+ const candidate = join(dir, CONFIG_FILE_NAME);
145
+ if (existsSync(candidate)) return candidate;
146
+ const parent = dirname(dir);
147
+ if (parent === dir) return undefined;
148
+ dir = parent;
149
+ }
150
+ }
151
+
152
+ function resolvePath(base: string, value: string): string {
153
+ return isAbsolute(value) ? value : resolve(base, value);
154
+ }
155
+
156
+ /**
157
+ * Resolve every path in a loaded config against the config file's directory, so
158
+ * downstream code can treat them as absolute regardless of the cwd.
159
+ */
160
+ export function resolveConfigPaths(loaded: LoadedProjectConfig): ProjectConfig {
161
+ const { config, directory } = loaded;
162
+ const app: AppConfig | undefined = config.app
163
+ ? { ...config.app, ...(config.app.icon ? { icon: resolvePath(directory, config.app.icon) } : {}) }
164
+ : undefined;
165
+ return {
166
+ ...config,
167
+ ...(config.entry ? { entry: resolvePath(directory, config.entry) } : {}),
168
+ ...(config.outDir ? { outDir: resolvePath(directory, config.outDir) } : {}),
169
+ ...(config.output ? { output: resolvePath(directory, config.output) } : {}),
170
+ ...(config.extNative ? { extNative: config.extNative.map((path) => resolvePath(directory, path)) } : {}),
171
+ ...(app ? { app } : {}),
172
+ };
173
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Compile-time application icons.
3
+ *
4
+ * `--icon <file>` (or `app.icon` in `xbintsc.config.json`) reads an icon,
5
+ * validates it and embeds the raw bytes into the produced binary as a small
6
+ * linkable object. The `gui` extension picks the symbols up (weakly) to set the
7
+ * window / Dock icon; the Windows driver additionally turns the icon into a PE
8
+ * resource so Explorer and the taskbar show it. The core compiler never sees
9
+ * any of this — it is a driver concern, exactly like the per-platform linker
10
+ * flags.
11
+ *
12
+ * Supported inputs: `.png` (canonical), `.ico` and `.icns` (best effort on their
13
+ * home platform).
14
+ */
15
+
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { join, resolve } from "node:path";
18
+ import { hashParts } from "./cache.js";
19
+ import { compileC, type Runner } from "./toolchain.js";
20
+
21
+ export type IconFormat = "png" | "ico" | "icns";
22
+
23
+ export interface IconInfo {
24
+ /** Absolute path the icon was read from. */
25
+ readonly path: string;
26
+ readonly format: IconFormat;
27
+ readonly bytes: Uint8Array;
28
+ /** Intrinsic size where it is cheap to sniff (0 when unknown, e.g. `.icns`). */
29
+ readonly width: number;
30
+ readonly height: number;
31
+ }
32
+
33
+ export class IconError extends Error {
34
+ constructor(message: string) {
35
+ super(message);
36
+ this.name = "IconError";
37
+ }
38
+ }
39
+
40
+ const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
41
+
42
+ function startsWith(bytes: Uint8Array, magic: readonly number[]): boolean {
43
+ if (bytes.length < magic.length) return false;
44
+ return magic.every((value, index) => bytes[index] === value);
45
+ }
46
+
47
+ function readUInt32BE(bytes: Uint8Array, offset: number): number {
48
+ return (
49
+ ((bytes[offset]! << 24) | (bytes[offset + 1]! << 16) | (bytes[offset + 2]! << 8) | bytes[offset + 3]!) >>> 0
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Sniff the format from the magic bytes and validate the file. Throws
55
+ * {@link IconError} with an actionable message for missing, empty or
56
+ * unsupported files.
57
+ */
58
+ export function readIcon(path: string): IconInfo {
59
+ const absolute = resolve(path);
60
+ if (!existsSync(absolute)) {
61
+ throw new IconError(`Icon file not found: ${absolute}`);
62
+ }
63
+ const bytes = readFileSync(absolute);
64
+ if (bytes.length === 0) {
65
+ throw new IconError(`Icon file is empty: ${absolute}`);
66
+ }
67
+
68
+ if (startsWith(bytes, PNG_MAGIC)) {
69
+ // IHDR: width/height are big-endian at offsets 16 and 20.
70
+ const width = bytes.length >= 24 ? readUInt32BE(bytes, 16) : 0;
71
+ const height = bytes.length >= 24 ? readUInt32BE(bytes, 20) : 0;
72
+ return { path: absolute, format: "png", bytes, width, height };
73
+ }
74
+
75
+ // ICO / CUR: reserved(2) type(2) count(2), then ICONDIRENTRY[].
76
+ if (bytes.length >= 6 && bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0) {
77
+ const width = bytes.length >= 7 ? bytes[6]! || 256 : 0;
78
+ const height = bytes.length >= 8 ? bytes[7]! || 256 : 0;
79
+ return { path: absolute, format: "ico", bytes, width, height };
80
+ }
81
+
82
+ if (startsWith(bytes, [0x69, 0x63, 0x6e, 0x73])) {
83
+ return { path: absolute, format: "icns", bytes, width: 0, height: 0 };
84
+ }
85
+
86
+ throw new IconError(
87
+ `Unsupported icon format: ${absolute} (expected a PNG, ICO or ICNS file)`,
88
+ );
89
+ }
90
+
91
+ /** The C symbols the embedded icon exposes to the runtime (and native code). */
92
+ export const ICON_SYMBOLS = {
93
+ data: "xt_app_icon_data",
94
+ size: "xt_app_icon_size",
95
+ format: "xt_app_icon_format",
96
+ width: "xt_app_icon_width",
97
+ height: "xt_app_icon_height",
98
+ } as const;
99
+
100
+ /**
101
+ * A zero-length icon. The GUI extension references {@link ICON_SYMBOLS}
102
+ * unconditionally, so the driver links this object into GUI programs even when
103
+ * no icon is configured; `size == 0` tells the runtime to skip it.
104
+ */
105
+ export const EMPTY_ICON: IconInfo = {
106
+ path: "",
107
+ format: "png",
108
+ bytes: Buffer.alloc(0),
109
+ width: 0,
110
+ height: 0,
111
+ };
112
+
113
+ /** Generate the C translation unit embedding the icon bytes. */
114
+ export function iconSource(icon: IconInfo): string {
115
+ const lines: string[] = [];
116
+ const bytes = icon.bytes;
117
+ for (let offset = 0; offset < bytes.length; offset += 16) {
118
+ const chunk = Array.from(bytes.subarray(offset, offset + 16), (byte) => `0x${byte.toString(16).padStart(2, "0")}`);
119
+ lines.push(" " + chunk.join(", ") + ",");
120
+ }
121
+ // A zero-length array is not portable C, so an empty icon still emits one
122
+ // placeholder byte while reporting `size == 0`.
123
+ if (lines.length === 0) lines.push(" 0x00,");
124
+ return [
125
+ "/* Generated by xbintsc - embedded application icon. Do not edit. */",
126
+ `const unsigned char ${ICON_SYMBOLS.data}[] = {`,
127
+ ...lines,
128
+ "};",
129
+ `const unsigned long long ${ICON_SYMBOLS.size} = ${bytes.length}ull;`,
130
+ `const char ${ICON_SYMBOLS.format}[] = "${icon.format}";`,
131
+ `const unsigned int ${ICON_SYMBOLS.width} = ${icon.width}u;`,
132
+ `const unsigned int ${ICON_SYMBOLS.height} = ${icon.height}u;`,
133
+ "",
134
+ ].join("\n");
135
+ }
136
+
137
+ /**
138
+ * Compile the icon into an object exposing {@link ICON_SYMBOLS}. Cached by the
139
+ * icon contents, so an unchanged icon is never recompiled.
140
+ */
141
+ export function ensureIconObject(
142
+ runner: Runner,
143
+ clang: string,
144
+ cacheDir: string,
145
+ icon: IconInfo,
146
+ env?: Record<string, string>,
147
+ ): string {
148
+ const key = hashParts(["icon", process.platform, Buffer.from(icon.bytes).toString("base64")]);
149
+ const sourcePath = join(cacheDir, `icon-${key}.c`);
150
+ const objectPath = join(cacheDir, `icon-${key}.o`);
151
+ if (!existsSync(objectPath)) {
152
+ mkdirSync(cacheDir, { recursive: true });
153
+ writeFileSync(sourcePath, iconSource(icon));
154
+ compileC(runner, clang, sourcePath, objectPath, cacheDir, env);
155
+ }
156
+ return objectPath;
157
+ }
158
+
159
+ /**
160
+ * Wrap a PNG in a minimal ICO container. Vista+ accepts a PNG payload directly,
161
+ * so the PE resource path needs no BMP encoder.
162
+ */
163
+ export function pngToIco(png: Uint8Array, width: number, height: number): Uint8Array {
164
+ const header = Buffer.alloc(22);
165
+ header.writeUInt16LE(0, 0); // reserved
166
+ header.writeUInt16LE(1, 2); // type: icon
167
+ header.writeUInt16LE(1, 4); // image count
168
+ header.writeUInt8(width >= 256 || width <= 0 ? 0 : width, 6);
169
+ header.writeUInt8(height >= 256 || height <= 0 ? 0 : height, 7);
170
+ header.writeUInt8(0, 8); // palette size
171
+ header.writeUInt8(0, 9); // reserved
172
+ header.writeUInt16LE(1, 10); // color planes
173
+ header.writeUInt16LE(32, 12); // bits per pixel
174
+ header.writeUInt32LE(png.length, 14);
175
+ header.writeUInt32LE(22, 18); // payload offset
176
+ return Buffer.concat([header, Buffer.from(png)]);
177
+ }
178
+
179
+ /** Normalize any supported icon to ICO bytes (used for the PE resource). */
180
+ export function toIcoBytes(icon: IconInfo): Uint8Array {
181
+ if (icon.format === "ico") return icon.bytes;
182
+ if (icon.format === "png") return pngToIco(icon.bytes, icon.width, icon.height);
183
+ throw new IconError(`Cannot embed a .${icon.format} icon as a Windows PE resource; use a PNG or ICO.`);
184
+ }
@@ -9,3 +9,33 @@ export {
9
9
  type Runner,
10
10
  } from "./toolchain.js";
11
11
  export { resolveToolchain, type ResolvedToolchain, type ToolchainSource } from "./toolchain-provider.js";
12
+ export {
13
+ CONFIG_FILE_NAME,
14
+ findProjectConfig,
15
+ loadProjectConfig,
16
+ parseProjectConfig,
17
+ resolveConfigPaths,
18
+ ProjectConfigError,
19
+ type AppConfig,
20
+ type LoadedProjectConfig,
21
+ type ProjectConfig,
22
+ } from "./config.js";
23
+ export {
24
+ ensureIconObject,
25
+ iconSource,
26
+ readIcon,
27
+ toIcoBytes,
28
+ pngToIco,
29
+ ICON_SYMBOLS,
30
+ EMPTY_ICON,
31
+ IconError,
32
+ type IconFormat,
33
+ type IconInfo,
34
+ } from "./icon.js";
35
+ export {
36
+ ensureWindowsIconResource,
37
+ resolveResourceCompiler,
38
+ type ResourceCompiler,
39
+ type ResourceCompilerKind,
40
+ } from "./win-icon.js";
41
+ export { bundlePathFor, infoPlist, packageMacApp, type MacBundleOptions } from "./mac-bundle.js";