@gtkx/codegen 1.1.0 → 1.2.0

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 (149) hide show
  1. package/dist/analysis/descriptor-render.d.ts.map +1 -1
  2. package/dist/analysis/descriptor-render.js +14 -10
  3. package/dist/analysis/descriptor-render.js.map +1 -1
  4. package/dist/analysis/descriptor.d.ts +11 -5
  5. package/dist/analysis/descriptor.d.ts.map +1 -1
  6. package/dist/analysis/descriptor.js +25 -9
  7. package/dist/analysis/descriptor.js.map +1 -1
  8. package/dist/analysis/interface-conflicts.d.ts +3 -1
  9. package/dist/analysis/interface-conflicts.d.ts.map +1 -1
  10. package/dist/analysis/interface-conflicts.js +3 -8
  11. package/dist/analysis/interface-conflicts.js.map +1 -1
  12. package/dist/analysis/ts-type.d.ts +3 -2
  13. package/dist/analysis/ts-type.d.ts.map +1 -1
  14. package/dist/analysis/ts-type.js +16 -9
  15. package/dist/analysis/ts-type.js.map +1 -1
  16. package/dist/analysis/type-shape.d.ts +3 -2
  17. package/dist/analysis/type-shape.d.ts.map +1 -1
  18. package/dist/analysis/type-shape.js +2 -1
  19. package/dist/analysis/type-shape.js.map +1 -1
  20. package/dist/compile.d.ts +4 -5
  21. package/dist/compile.d.ts.map +1 -1
  22. package/dist/compile.js +56 -4
  23. package/dist/compile.js.map +1 -1
  24. package/dist/docs/api-reference.d.ts +2 -0
  25. package/dist/docs/api-reference.d.ts.map +1 -1
  26. package/dist/docs/api-reference.js +1 -1
  27. package/dist/docs/api-reference.js.map +1 -1
  28. package/dist/docs/pipeline.d.ts +1 -0
  29. package/dist/docs/pipeline.d.ts.map +1 -1
  30. package/dist/docs/pipeline.js +10 -3
  31. package/dist/docs/pipeline.js.map +1 -1
  32. package/dist/docs/symbol-page.d.ts.map +1 -1
  33. package/dist/docs/symbol-page.js +1 -3
  34. package/dist/docs/symbol-page.js.map +1 -1
  35. package/dist/fingerprint.d.ts +11 -5
  36. package/dist/fingerprint.d.ts.map +1 -1
  37. package/dist/fingerprint.js +22 -19
  38. package/dist/fingerprint.js.map +1 -1
  39. package/dist/gi.d.ts +1 -0
  40. package/dist/gi.d.ts.map +1 -1
  41. package/dist/gi.js +7 -2
  42. package/dist/gi.js.map +1 -1
  43. package/dist/gir/gir-path.d.ts +1 -1
  44. package/dist/gir/gir-path.js +1 -1
  45. package/dist/gir/gir-path.js.map +1 -1
  46. package/dist/gir/library.d.ts +5 -1
  47. package/dist/gir/library.d.ts.map +1 -1
  48. package/dist/gir/library.js +8 -1
  49. package/dist/gir/library.js.map +1 -1
  50. package/dist/gir/parse.d.ts.map +1 -1
  51. package/dist/gir/parse.js +4 -1
  52. package/dist/gir/parse.js.map +1 -1
  53. package/dist/gir/primitives.js +1 -1
  54. package/dist/gir/primitives.js.map +1 -1
  55. package/dist/khronos/args.d.ts.map +1 -1
  56. package/dist/khronos/args.js +8 -6
  57. package/dist/khronos/args.js.map +1 -1
  58. package/dist/khronos/render.d.ts.map +1 -1
  59. package/dist/khronos/render.js +8 -4
  60. package/dist/khronos/render.js.map +1 -1
  61. package/dist/runner.d.ts +2 -0
  62. package/dist/runner.d.ts.map +1 -1
  63. package/dist/runner.js +5 -3
  64. package/dist/runner.js.map +1 -1
  65. package/dist/staging.d.ts.map +1 -1
  66. package/dist/staging.js +1 -1
  67. package/dist/staging.js.map +1 -1
  68. package/dist/store/compile-store.d.ts +0 -1
  69. package/dist/store/compile-store.d.ts.map +1 -1
  70. package/dist/store/compile-store.js +7 -34
  71. package/dist/store/compile-store.js.map +1 -1
  72. package/dist/store/gi/callables.d.ts +1 -3
  73. package/dist/store/gi/callables.d.ts.map +1 -1
  74. package/dist/store/gi/callables.js +19 -4
  75. package/dist/store/gi/callables.js.map +1 -1
  76. package/dist/store/gi/class.d.ts.map +1 -1
  77. package/dist/store/gi/class.js +48 -9
  78. package/dist/store/gi/class.js.map +1 -1
  79. package/dist/store/gi/constant.js +1 -1
  80. package/dist/store/gi/constant.js.map +1 -1
  81. package/dist/store/gi/enum.d.ts.map +1 -1
  82. package/dist/store/gi/enum.js +2 -5
  83. package/dist/store/gi/enum.js.map +1 -1
  84. package/dist/store/gi/interface.js +2 -2
  85. package/dist/store/gi/interface.js.map +1 -1
  86. package/dist/store/gi/method.d.ts.map +1 -1
  87. package/dist/store/gi/method.js +4 -7
  88. package/dist/store/gi/method.js.map +1 -1
  89. package/dist/store/gi/property-accessor.d.ts +0 -3
  90. package/dist/store/gi/property-accessor.d.ts.map +1 -1
  91. package/dist/store/gi/property-accessor.js +15 -108
  92. package/dist/store/gi/property-accessor.js.map +1 -1
  93. package/dist/store/gi/record-constructor.js +3 -3
  94. package/dist/store/gi/record-constructor.js.map +1 -1
  95. package/dist/store/gi/record-field-accessor.d.ts.map +1 -1
  96. package/dist/store/gi/record-field-accessor.js +15 -3
  97. package/dist/store/gi/record-field-accessor.js.map +1 -1
  98. package/dist/store/gi/return-wrap.js +1 -1
  99. package/dist/store/gi/return-wrap.js.map +1 -1
  100. package/dist/store/gi/signal.d.ts.map +1 -1
  101. package/dist/store/gi/signal.js +25 -4
  102. package/dist/store/gi/signal.js.map +1 -1
  103. package/dist/store/jsx-store.d.ts.map +1 -1
  104. package/dist/store/jsx-store.js +0 -1
  105. package/dist/store/jsx-store.js.map +1 -1
  106. package/dist/store/store-fs.d.ts +0 -1
  107. package/dist/store/store-fs.d.ts.map +1 -1
  108. package/dist/store/store-fs.js +2 -7
  109. package/dist/store/store-fs.js.map +1 -1
  110. package/dist/xml.d.ts +1 -0
  111. package/dist/xml.d.ts.map +1 -1
  112. package/dist/xml.js +28 -1
  113. package/dist/xml.js.map +1 -1
  114. package/overrides/gobject/object.ts.ejs +2 -0
  115. package/package.json +3 -3
  116. package/src/analysis/descriptor-render.ts +19 -10
  117. package/src/analysis/descriptor.ts +43 -9
  118. package/src/analysis/interface-conflicts.ts +6 -8
  119. package/src/analysis/ts-type.ts +30 -11
  120. package/src/analysis/type-shape.ts +5 -2
  121. package/src/compile.ts +87 -6
  122. package/src/docs/api-reference.ts +3 -1
  123. package/src/docs/pipeline.ts +13 -7
  124. package/src/docs/symbol-page.ts +0 -4
  125. package/src/fingerprint.ts +31 -38
  126. package/src/gi.ts +9 -2
  127. package/src/gir/gir-path.ts +1 -1
  128. package/src/gir/library.ts +9 -1
  129. package/src/gir/parse.ts +6 -1
  130. package/src/gir/primitives.ts +1 -1
  131. package/src/khronos/args.ts +9 -6
  132. package/src/khronos/render.ts +8 -6
  133. package/src/runner.ts +7 -3
  134. package/src/staging.ts +1 -3
  135. package/src/store/compile-store.ts +7 -40
  136. package/src/store/gi/callables.ts +26 -7
  137. package/src/store/gi/class.ts +63 -7
  138. package/src/store/gi/constant.ts +1 -1
  139. package/src/store/gi/enum.ts +2 -6
  140. package/src/store/gi/interface.ts +2 -2
  141. package/src/store/gi/method.ts +4 -9
  142. package/src/store/gi/property-accessor.ts +22 -193
  143. package/src/store/gi/record-constructor.ts +3 -3
  144. package/src/store/gi/record-field-accessor.ts +18 -3
  145. package/src/store/gi/return-wrap.ts +1 -1
  146. package/src/store/gi/signal.ts +30 -4
  147. package/src/store/jsx-store.ts +0 -1
  148. package/src/store/store-fs.ts +3 -9
  149. package/src/xml.ts +43 -1
package/src/compile.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { errorMessage, normalizeError } from "@gtkx/utils";
1
+ import { errorCode, errorMessage, normalizeError } from "@gtkx/utils";
2
2
  import { existsSync, mkdirSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join, relative } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -14,10 +14,19 @@ type SourceModule = ProjectFile & {
14
14
  source: string;
15
15
  };
16
16
 
17
- type CompileProjectParams = {
17
+ type ProjectContext = {
18
18
  projectDir: string;
19
19
  files: ProjectFile[];
20
+ label: string;
21
+ };
22
+
23
+ type CompileProjectParams = ProjectContext & {
20
24
  compilerOptions: Record<string, unknown>;
25
+ };
26
+
27
+ type EmitModulesParams = {
28
+ projectDir: string;
29
+ files: SourceModule[];
21
30
  label: string;
22
31
  };
23
32
 
@@ -60,9 +69,33 @@ const CHECK_OPTIONS = {
60
69
  noEmit: true,
61
70
  };
62
71
 
72
+ const DECLARATION_EMIT = {
73
+ declaration: true,
74
+ isolatedDeclarations: true,
75
+ removeComments: false,
76
+ };
77
+
78
+ const MODULE_EMIT = {
79
+ removeComments: false,
80
+ };
81
+
82
+ const DECLARATION_OPTIONS = ts.convertCompilerOptionsFromJson(
83
+ { ...BASE_COMPILER_OPTIONS, ...DECLARATION_EMIT },
84
+ ".",
85
+ ).options;
86
+
87
+ const MODULE_OPTIONS = ts.convertCompilerOptionsFromJson(
88
+ { ...BASE_COMPILER_OPTIONS, ...MODULE_EMIT },
89
+ ".",
90
+ ).options;
91
+
92
+ const TS_EXTENSION_PATTERN = /\.tsx?$/;
93
+ const DECLARATION_EXTENSION = ".d.ts";
94
+ const JAVASCRIPT_EXTENSION = ".js";
63
95
  const CHECK_DIR = ".gtkx-check";
64
96
  const FAILED_CHECK_DIR = `${CHECK_DIR}.failed`;
65
97
  const LEGACY_CHECK_PREFIX = `${CHECK_DIR}-`;
98
+ const ESM_SCOPE_MANIFEST = `${JSON.stringify({ type: "module" }, null, 4)}\n`;
66
99
 
67
100
  const FORMAT_HOST: ts.FormatDiagnosticsHost = {
68
101
  getCanonicalFileName: (fileName) => fileName,
@@ -90,6 +123,16 @@ const linkToolingModules = (projectDir: string): (() => void) => {
90
123
  };
91
124
  };
92
125
 
126
+ const ensureModuleScope = (projectDir: string): void => {
127
+ try {
128
+ writeFileSync(join(projectDir, "package.json"), ESM_SCOPE_MANIFEST, { flag: "wx" });
129
+ } catch (error) {
130
+ if (errorCode(error) !== "EEXIST") {
131
+ throw error;
132
+ }
133
+ }
134
+ };
135
+
93
136
  const isProjectFile = (rel: string): boolean =>
94
137
  !rel.startsWith("..") && !isAbsolute(rel) && !rel.split(/[/\\]/).includes("node_modules");
95
138
 
@@ -160,17 +203,17 @@ const originLines = (diagnosed: DiagnosedFile[], files: ProjectFile[]): string[]
160
203
  ([origin, generated]) => `Generated from ${origin}: ${[...generated].join(", ")}`,
161
204
  );
162
205
 
163
- const diagnosticError = (params: CompileProjectParams, diagnostics: ts.Diagnostic[]): Error => {
206
+ const diagnosticError = (params: ProjectContext, diagnostics: ts.Diagnostic[]): Error => {
164
207
  const diagnosed = projectDiagnosticFiles(diagnostics, params.projectDir);
165
208
  const { label } = params;
166
209
 
167
210
  if (diagnosed.length === 0) {
168
- return new Error(`Type checking ${label} failed:\n${ts.formatDiagnostics(diagnostics, FORMAT_HOST).trim()}`);
211
+ return new Error(`Compiling ${label} failed:\n${ts.formatDiagnostics(diagnostics, FORMAT_HOST).trim()}`);
169
212
  }
170
213
 
171
214
  const lines = [...diagnosed.map((entry) => entry.text), ...originLines(diagnosed, params.files)];
172
215
 
173
- return new Error(`Type checking ${label} found ${String(diagnosed.length)} error(s):\n${lines.join("\n")}`);
216
+ return new Error(`Compiling ${label} found ${String(diagnosed.length)} error(s):\n${lines.join("\n")}`);
174
217
  };
175
218
 
176
219
  const keepFailedProject = (input: FailedProjectInput): Error => {
@@ -214,7 +257,45 @@ const runProgram = (params: CompileProjectParams): ts.Diagnostic[] => {
214
257
  return [...program.emit().diagnostics];
215
258
  };
216
259
 
260
+ const replaceExtension = (fileName: string, extension: string): string =>
261
+ `${fileName.replace(TS_EXTENSION_PATTERN, "")}${extension}`;
262
+
263
+ const transpileOptions = (compilerOptions: ts.CompilerOptions, fileName: string): ts.TranspileOptions => ({
264
+ compilerOptions,
265
+ fileName,
266
+ reportDiagnostics: true,
267
+ });
268
+
269
+ const emitModule = (module: SourceModule, projectDir: string): ts.Diagnostic[] => {
270
+ const fileName = join(projectDir, module.fileName);
271
+ const declaration = ts.transpileDeclaration(module.source, transpileOptions(DECLARATION_OPTIONS, fileName));
272
+ const javascript = ts.transpileModule(module.source, transpileOptions(MODULE_OPTIONS, fileName));
273
+ const diagnostics = [...declaration.diagnostics ?? [], ...javascript.diagnostics ?? []];
274
+
275
+ if (diagnostics.length > 0) {
276
+ return diagnostics;
277
+ }
278
+
279
+ writeFileSync(replaceExtension(fileName, DECLARATION_EXTENSION), declaration.outputText);
280
+ writeFileSync(replaceExtension(fileName, JAVASCRIPT_EXTENSION), javascript.outputText);
281
+
282
+ return [];
283
+ };
284
+
285
+ const emitModules = (params: EmitModulesParams): void => {
286
+ const diagnostics: ts.Diagnostic[] = [];
287
+
288
+ for (const module of params.files) {
289
+ diagnostics.push(...emitModule(module, params.projectDir));
290
+ }
291
+
292
+ if (diagnostics.length > 0) {
293
+ throw diagnosticError(params, diagnostics);
294
+ }
295
+ };
296
+
217
297
  const compileProject = (params: CompileProjectParams): void => {
298
+ ensureModuleScope(params.projectDir);
218
299
  const unlinkToolingModules = linkToolingModules(params.projectDir);
219
300
 
220
301
  try {
@@ -261,4 +342,4 @@ const checkModules = (params: { modules: SourceModule[]; resolveFrom: string; la
261
342
  rmSync(keepAt, { recursive: true, force: true });
262
343
  };
263
344
 
264
- export { compileProject, checkModules, keepFailedProject, type ProjectFile, type SourceModule };
345
+ export { checkModules, emitModules, keepFailedProject, type SourceModule };
@@ -24,6 +24,8 @@ type ApiReferenceOptions = {
24
24
  props?: ElementProps;
25
25
  /** GObject properties the project omits from generated props; without it pages show props that do not exist. */
26
26
  omittedProps?: OmittedProps;
27
+ /** Describes GIR byte sequences as `Uint8Array` rather than `number[]`; defaults to false. */
28
+ isByteArrayTyped?: boolean;
27
29
  };
28
30
 
29
31
  /** Narrows an `ApiReference.symbols` enumeration. */
@@ -371,7 +373,7 @@ class ApiReference {
371
373
  this.libraries = options.libraries;
372
374
  this.props = options.props ?? {};
373
375
  this.omittedProps = options.omittedProps ?? {};
374
- this.library = Library.load(options.libraries, options.girPath);
376
+ this.library = Library.load(options.libraries, options.girPath, options.isByteArrayTyped === true);
375
377
  this.elementContext = { library: this.library, linkFor: (): string | undefined => undefined };
376
378
  this.buildIndex();
377
379
  }
@@ -5,6 +5,7 @@ import {
5
5
  computeDocsFingerprint,
6
6
  type DocsFingerprintInput,
7
7
  FINGERPRINT_FILENAME,
8
+ type GiInputs,
8
9
  isDocsOutputFresh,
9
10
  } from "../fingerprint.js";
10
11
  import { Library } from "../gir/library.js";
@@ -37,6 +38,7 @@ type DocsOptions = {
37
38
  props?: ElementProps;
38
39
  omittedProps?: OmittedProps;
39
40
  isForced?: boolean;
41
+ isByteArrayTyped?: boolean;
40
42
  };
41
43
 
42
44
  type DocsManifest = {
@@ -253,6 +255,13 @@ const outDirRefusal = (outDir: string, reason: string): Error =>
253
255
  "empty directory or at one gtkx generated, or remove it yourself first.",
254
256
  );
255
257
 
258
+ const giInputs = (options: DocsOptions, girFiles: string[]): GiInputs => ({
259
+ girFiles,
260
+ libraries: options.libraries,
261
+ girPath: options.girPath,
262
+ isByteArrayTyped: options.isByteArrayTyped === true,
263
+ });
264
+
256
265
  const assertOwnedOutDir = (options: DocsOptions, manifest: DocsManifest | undefined): void => {
257
266
  const stats = statSync(options.outDir, { throwIfNoEntry: false });
258
267
 
@@ -301,7 +310,7 @@ const cachedDocsResult = (
301
310
  return undefined;
302
311
  }
303
312
 
304
- if (!isDocsOutputFresh(options.outDir, options.libraries, options.girPath, input)) {
313
+ if (!isDocsOutputFresh(options.outDir, giInputs(options, []), input)) {
305
314
  return undefined;
306
315
  }
307
316
 
@@ -335,18 +344,15 @@ const writeDocs = (options: DocsOptions): DocsResult => {
335
344
  }
336
345
 
337
346
  assertOwnedOutDir(options, previous);
338
- const library = Library.load(options.libraries, options.girPath);
347
+ const library = Library.load(options.libraries, options.girPath, options.isByteArrayTyped === true);
339
348
  const { pages, namespaces } = generatePages(options, input.basePath, library);
340
349
  clearOutDir(options, previous);
341
350
  mkdirSync(options.outDir, { recursive: true });
342
351
  const manifest: DocsManifest = { generator: MANIFEST_GENERATOR, namespaces };
343
352
  writeFileSync(manifestPath, JSON.stringify(manifest));
344
353
  writePages(options.outDir, pages);
345
-
346
- writeFileSync(
347
- join(options.outDir, FINGERPRINT_FILENAME),
348
- JSON.stringify(computeDocsFingerprint(library.girFiles, options.libraries, options.girPath, input)),
349
- );
354
+ const fingerprint = computeDocsFingerprint(giInputs(options, library.girFiles), input);
355
+ writeFileSync(join(options.outDir, FINGERPRINT_FILENAME), JSON.stringify(fingerprint));
350
356
 
351
357
  return { isRegenerated: true, namespaces };
352
358
  };
@@ -16,7 +16,6 @@ import { PRIMITIVE_TS_TYPE, primitiveCategory } from "../gir/primitives.js";
16
16
  import { callableSpec } from "../store/gi/callable-doc.js";
17
17
  import {
18
18
  dedupeCallables,
19
- indexMethodsByName,
20
19
  instanceScope,
21
20
  matchStaticFinishFunction,
22
21
  renderInstanceMethodSignature,
@@ -146,7 +145,6 @@ type MemberOwner = {
146
145
  type PropertyAccessorSetup = {
147
146
  context: ModuleContext;
148
147
  claimedNames: Set<string>;
149
- methodByName: Map<string, GirFunction>;
150
148
  };
151
149
 
152
150
  type ResolvedRecordField = NonNullable<ReturnType<typeof resolveRecordFieldEntry>>;
@@ -391,7 +389,6 @@ const propertyAccessorSetup = (
391
389
  return {
392
390
  context: docsSignatureContext(owner.namespace, library),
393
391
  claimedNames,
394
- methodByName: indexMethodsByName(dedupeCallables(owner.klass.methods)),
395
392
  };
396
393
  };
397
394
 
@@ -418,7 +415,6 @@ const ownerPropertyEntries = (owner: MemberOwner, setup: PropertyAccessorSetup,
418
415
  context: setup.context,
419
416
  property,
420
417
  claimedNames: setup.claimedNames,
421
- methodByName: setup.methodByName,
422
418
  });
423
419
 
424
420
  if (accessor === undefined || seen.has(accessor.jsName)) {
@@ -7,6 +7,13 @@ import packageManifest from "../package.json" with { type: "json" };
7
7
  import { arrayGuard, hasFields, isNumber, isString, optionalGuard } from "./guards.js";
8
8
  import { readJsonFile } from "./json.js";
9
9
 
10
+ type GiInputs = {
11
+ girFiles: string[];
12
+ libraries: string[];
13
+ girPath: string[];
14
+ isByteArrayTyped: boolean;
15
+ };
16
+
10
17
  type GiFingerprint = {
11
18
  value: string;
12
19
  girFiles: string[];
@@ -56,15 +63,17 @@ const overrideFiles = (): string[] => {
56
63
  .map((entry: Dirent) => join(entry.parentPath, entry.name));
57
64
  };
58
65
 
59
- const hashGi = (girFiles: string[], libraries: string[], girPath: string[]): string => {
66
+ const hashGi = (inputs: GiInputs): string => {
60
67
  const hash = createHash("sha256");
61
68
  hash.update(CODEGEN_VERSION);
62
69
  hash.update("\n");
63
- hash.update(sortAlpha(libraries));
70
+ hash.update(sortAlpha(inputs.libraries));
71
+ hash.update("\n");
72
+ hash.update(sortAlpha(inputs.girPath));
64
73
  hash.update("\n");
65
- hash.update(sortAlpha(girPath));
74
+ hash.update(String(inputs.isByteArrayTyped));
66
75
  hash.update("\n");
67
- const hashedFiles = sortStrings([...girFiles, ...overrideFiles()]);
76
+ const hashedFiles = sortStrings([...inputs.girFiles, ...overrideFiles()]);
68
77
 
69
78
  for (const file of hashedFiles) {
70
79
  hash.update("\n");
@@ -76,15 +85,11 @@ const hashGi = (girFiles: string[], libraries: string[], girPath: string[]): str
76
85
  return hash.digest("hex");
77
86
  };
78
87
 
79
- const computeGiFingerprint = (
80
- girFiles: string[],
81
- libraries: string[],
82
- girPath: string[],
83
- ): GiFingerprint => ({
84
- value: hashGi(girFiles, libraries, girPath),
85
- girFiles,
86
- libraries,
87
- girPath,
88
+ const computeGiFingerprint = (inputs: GiInputs): GiFingerprint => ({
89
+ value: hashGi(inputs),
90
+ girFiles: inputs.girFiles,
91
+ libraries: inputs.libraries,
92
+ girPath: inputs.girPath,
88
93
  });
89
94
 
90
95
  const readSentinel = (storeDir: string): unknown => readJsonFile(join(storeDir, FINGERPRINT_FILENAME));
@@ -103,30 +108,27 @@ const isDocsFingerprint = (value: unknown): value is DocsFingerprint =>
103
108
  const isJsxFingerprint = (value: unknown): value is JsxFingerprint =>
104
109
  hasFields<JsxFingerprint>(value, { value: isString, intrinsicElementCount: isNumber });
105
110
 
106
- const recordedGiValue = (sentinel: GiFingerprint, libraries: string[], girPath: string[]): string | undefined => {
107
- if (!hasMatchingRecordedInputs(sentinel, libraries, girPath)) {
111
+ const recordedGiValue = (sentinel: GiFingerprint, inputs: GiInputs): string | undefined => {
112
+ if (!hasMatchingRecordedInputs(sentinel, inputs)) {
108
113
  return undefined;
109
114
  }
110
115
 
111
116
  try {
112
- return hashGi(sentinel.girFiles, sentinel.libraries, girPath);
117
+ return hashGi({ ...inputs, girFiles: sentinel.girFiles, libraries: sentinel.libraries });
113
118
  } catch {
114
119
  return undefined;
115
120
  }
116
121
  };
117
122
 
118
- const isGiStoreFresh = (
119
- giStoreDir: string,
120
- libraries: string[],
121
- girPath: string[],
122
- ): boolean => {
123
+ const isGiStoreFresh = (giStoreDir: string, inputs: GiInputs): boolean => {
123
124
  const sentinel = readSentinel(giStoreDir);
124
125
 
125
- return isGiFingerprint(sentinel) && recordedGiValue(sentinel, libraries, girPath) === sentinel.value;
126
+ return isGiFingerprint(sentinel) && recordedGiValue(sentinel, inputs) === sentinel.value;
126
127
  };
127
128
 
128
- const hasMatchingRecordedInputs = (sentinel: GiFingerprint, libraries: string[], girPath: string[]): boolean =>
129
- sortAlpha(sentinel.libraries) === sortAlpha(libraries) && sortAlpha(sentinel.girPath ?? []) === sortAlpha(girPath);
129
+ const hasMatchingRecordedInputs = (sentinel: GiFingerprint, inputs: GiInputs): boolean =>
130
+ sortAlpha(sentinel.libraries) === sortAlpha(inputs.libraries) &&
131
+ sortAlpha(sentinel.girPath ?? []) === sortAlpha(inputs.girPath);
130
132
 
131
133
  const hashDocs = (giValue: string, input: DocsFingerprintInput): string =>
132
134
  createHash("sha256")
@@ -140,30 +142,20 @@ const hashDocs = (giValue: string, input: DocsFingerprintInput): string =>
140
142
  )
141
143
  .digest("hex");
142
144
 
143
- const computeDocsFingerprint = (
144
- girFiles: string[],
145
- libraries: string[],
146
- girPath: string[],
147
- input: DocsFingerprintInput,
148
- ): DocsFingerprint => {
149
- const gi = computeGiFingerprint(girFiles, libraries, girPath);
145
+ const computeDocsFingerprint = (inputs: GiInputs, input: DocsFingerprintInput): DocsFingerprint => {
146
+ const gi = computeGiFingerprint(inputs);
150
147
 
151
148
  return { value: hashDocs(gi.value, input), gi };
152
149
  };
153
150
 
154
- const isDocsOutputFresh = (
155
- outDir: string,
156
- libraries: string[],
157
- girPath: string[],
158
- input: DocsFingerprintInput,
159
- ): boolean => {
151
+ const isDocsOutputFresh = (outDir: string, inputs: GiInputs, input: DocsFingerprintInput): boolean => {
160
152
  const sentinel = readSentinel(outDir);
161
153
 
162
154
  if (!isDocsFingerprint(sentinel)) {
163
155
  return false;
164
156
  }
165
157
 
166
- const giValue = recordedGiValue(sentinel.gi, libraries, girPath);
158
+ const giValue = recordedGiValue(sentinel.gi, inputs);
167
159
 
168
160
  return giValue !== undefined && hashDocs(giValue, input) === sentinel.value;
169
161
  };
@@ -217,6 +209,7 @@ export {
217
209
  computeJsxFingerprint,
218
210
  jsxStoreFreshness,
219
211
  type DocsFingerprintInput,
212
+ type GiInputs,
220
213
  type GiFingerprint,
221
214
  type JsxFingerprintInput,
222
215
  };
package/src/gi.ts CHANGED
@@ -9,10 +9,11 @@ type GiCodegenOptions = {
9
9
  gi: StoreOptions;
10
10
  libraries: string[];
11
11
  girPath: string[];
12
+ isByteArrayTyped: boolean;
12
13
  };
13
14
 
14
15
  const runGiCodegen = (library: Library, options: GiCodegenOptions): number => {
15
- const { gi, libraries, girPath } = options;
16
+ const { gi, libraries, girPath, isByteArrayTyped } = options;
16
17
 
17
18
  const namespaces: GiNamespaceInput[] = Array.from(library.namespaces.values(), (namespace) => ({
18
19
  directory: namespaceDirectory(namespace),
@@ -20,7 +21,13 @@ const runGiCodegen = (library: Library, options: GiCodegenOptions): number => {
20
21
  girFile: namespace.girFile,
21
22
  }));
22
23
 
23
- const fingerprint = computeGiFingerprint(library.girFiles, [...libraries], [...girPath]);
24
+ const fingerprint = computeGiFingerprint({
25
+ girFiles: library.girFiles,
26
+ libraries: [...libraries],
27
+ girPath: [...girPath],
28
+ isByteArrayTyped,
29
+ });
30
+
24
31
  writeGiStore(gi, namespaces, fingerprint);
25
32
 
26
33
  return library.namespaces.size;
@@ -7,7 +7,7 @@ const SYSTEM_GIR_PATH = "/usr/share/gir-1.0";
7
7
  /**
8
8
  * The directories to search for `.gir` files, in precedence order: the config's own `girPath`, then
9
9
  * `GTKX_GIR_PATH`, then `/usr/share/gir-1.0`, then the girdir pkg-config reports for
10
- * gobject-introspection-1.0. Duplicates are dropped, and the last two are included only when they exist.
10
+ * gobject-introspection-1.0. Duplicates are dropped, and the system directories are included only when they exist.
11
11
  *
12
12
  * @throws If pkg-config is installed but fails while being queried.
13
13
  */
@@ -84,10 +84,12 @@ class Library {
84
84
  *
85
85
  * @param libraries GIR identifiers to load, such as `Gtk-4.0`.
86
86
  * @param girPath Directories searched, in order, for each `.gir` file.
87
+ * @param isByteArrayTyped Whether byte sequences render as `Uint8Array` rather than `number[]`.
87
88
  * @returns A library holding every namespace that was parsed.
88
89
  */
89
- static load(libraries: string[], girPath: string[]): Library {
90
+ static load(libraries: string[], girPath: string[], isByteArrayTyped = false): Library {
90
91
  const library = new Library();
92
+ library.isByteArrayTypedValue = isByteArrayTyped;
91
93
  this.drive(library, libraries, girPath);
92
94
 
93
95
  return library;
@@ -99,6 +101,7 @@ class Library {
99
101
  private nsIdByName: Map<string, number> = new Map();
100
102
  private typeTables: TypeTable[] = [];
101
103
  private girFilesValue: string[] = [];
104
+ private isByteArrayTypedValue = false;
102
105
 
103
106
  /** Creates a library with no namespaces loaded; {@link Library.load} builds a populated one. */
104
107
  constructor() {
@@ -328,6 +331,11 @@ class Library {
328
331
  return this.namespacesByName;
329
332
  }
330
333
 
334
+ /** Whether byte sequences render as `Uint8Array` rather than `number[]`. */
335
+ public get isByteArrayTyped(): boolean {
336
+ return this.isByteArrayTypedValue;
337
+ }
338
+
331
339
  /** Paths of the `.gir` files that were read, in the order they were discovered. */
332
340
  public get girFiles(): string[] {
333
341
  return this.girFilesValue;
package/src/gir/parse.ts CHANGED
@@ -5,6 +5,11 @@ type RawNode = Record<string, unknown>;
5
5
 
6
6
  const GIR_LABEL = "GIR file";
7
7
 
8
+ const GIR_MALFORMED_REMEDY =
9
+ "No XML parser can read this file, so report it to whoever ships it. To get past it now, name the " +
10
+ "libraries you need explicitly in `libraries` and leave this one out, or put a corrected copy of the " +
11
+ "file in a directory listed in `girPath`, which is searched ahead of the system location.";
12
+
8
13
  const MULTI_TAGS: Set<string> = new Set([
9
14
  "include",
10
15
  "class",
@@ -44,7 +49,7 @@ function renameReservedTag(tag: string): string {
44
49
  }
45
50
 
46
51
  const parseGirFile = (path: string): RawNode =>
47
- parseXmlFile({ parser: PARSER, label: GIR_LABEL, path }) as RawNode;
52
+ parseXmlFile({ parser: PARSER, label: GIR_LABEL, path, malformedRemedy: GIR_MALFORMED_REMEDY }) as RawNode;
48
53
 
49
54
  const attr = (node: RawNode | undefined, name: string): string | undefined => {
50
55
  if (node === undefined) {
@@ -110,7 +110,7 @@ const PRIMITIVE_TS_TYPE: Record<PrimitiveCategory, string> = {
110
110
  float64: "number",
111
111
  string: "string",
112
112
  unichar: "string",
113
- pointer: "number",
113
+ pointer: "bigint",
114
114
  };
115
115
 
116
116
  const primitiveCategory = (name: string): PrimitiveCategory | undefined => PRIMITIVE_BY_NAME.get(name);
@@ -2,6 +2,7 @@ import { toCamelIdentifier } from "@gtkx/utils";
2
2
  import type { GlCommand, GlParam } from "./model.js";
3
3
  import type { CommandPlan, GlScalar, ParamPlan, ReturnPlan } from "./plan.js";
4
4
  import {
5
+ type ArrayLayout,
5
6
  tArray,
6
7
  tBoolean,
7
8
  tBuffer,
@@ -50,6 +51,7 @@ type BuildArgOptions = {
50
51
  };
51
52
 
52
53
  const OUT_PLAN_KINDS: Set<string> = new Set(["ref-out", "ref-array-out", "ref-fixed-out", "string-out"]);
54
+ const NUMERIC_LAYOUT: ArrayLayout = { isBytes: false };
53
55
 
54
56
  const scalarAliasOrGroup = (scalar: GlScalar, group: string | undefined): string =>
55
57
  group !== undefined && scalar.isGroupBearing === true ? group : scalar.tsAlias;
@@ -122,12 +124,13 @@ const buildInArg = (options: BuildArgOptions, name: string, track: (alias: strin
122
124
  return inArg(name, "string", tString("borrowed"));
123
125
  }
124
126
  case "string-array-in": {
125
- return inArg(name, "string[]", tArray(tString("borrowed")));
127
+ return inArg(name, "string[]", tArray(tString("borrowed"), undefined, NUMERIC_LAYOUT));
126
128
  }
127
129
  case "array-in": {
128
130
  track(scalarAliasOrGroup(plan.scalar, param.group));
131
+ const descriptor = tArray(plan.scalar.descriptor, undefined, NUMERIC_LAYOUT);
129
132
 
130
- return inArg(name, arrayInTsType(plan.scalar, param.group), tArray(plan.scalar.descriptor));
133
+ return inArg(name, arrayInTsType(plan.scalar, param.group), descriptor);
131
134
  }
132
135
  case "buffer": {
133
136
  return inArg(name, `ArrayBufferView | ${track("GLintptr")} | null`, tBuffer);
@@ -136,7 +139,7 @@ const buildInArg = (options: BuildArgOptions, name: string, track: (alias: strin
136
139
  return inArg(name, track("GLintptr"), tUint64);
137
140
  }
138
141
  case "byte-offset-array": {
139
- return inArg(name, `${track("GLintptr")}[]`, tArray(tUint64));
142
+ return inArg(name, `${track("GLintptr")}[]`, tArray(tUint64, undefined, NUMERIC_LAYOUT));
140
143
  }
141
144
  case "ref-array-out":
142
145
  case "ref-fixed-out":
@@ -165,14 +168,14 @@ const outArgFields = (options: OutArgFieldsOptions): OutArgFields => {
165
168
  return {
166
169
  seed: `const ${cellName} = { value: new Array<number>(${lenIdentifier}).fill(0) };`,
167
170
  tsType: `${track(scalarAliasOrGroup(plan.scalar, param.group))}[]`,
168
- descriptor: tRef(tSizedArray(plan.scalar.descriptor, sizeIndex)),
171
+ descriptor: tRef(tSizedArray(plan.scalar.descriptor, sizeIndex, undefined, NUMERIC_LAYOUT)),
169
172
  };
170
173
  }
171
174
  case "ref-fixed-out": {
172
175
  return {
173
176
  seed: `const ${cellName} = { value: new Array<number>(${String(plan.length)}).fill(0) };`,
174
177
  tsType: `${track(plan.scalar.tsAlias)}[]`,
175
- descriptor: tRef(tFixedArray(plan.scalar.descriptor, plan.length)),
178
+ descriptor: tRef(tFixedArray(plan.scalar.descriptor, plan.length, undefined, NUMERIC_LAYOUT)),
176
179
  };
177
180
  }
178
181
  case "string-out": {
@@ -204,7 +207,7 @@ const buildOutArg = (options: BuildArgOptions, track: (alias: string) => string)
204
207
  throw new Error(`Parameter index ${String(index)} out of range on ${command.name}`);
205
208
  }
206
209
 
207
- const cellName = `out${String(outIndex)}`;
210
+ const cellName = `__out${String(outIndex)}`;
208
211
  const cell: OutArgCell = { isOut: true, cellName, paramIndex: index };
209
212
 
210
213
  return { ...cell, ...outArgFields({ command, plan, param, cellName, track }) };
@@ -114,7 +114,7 @@ const returnStatements = (call: string, returned: EmittedReturn, outs: OutArg[])
114
114
  return [`${call};`, tail];
115
115
  }
116
116
 
117
- return [`const result = ${call};`, `return [${returned.expr("result")}, ${outValues.join(", ")}];`];
117
+ return [`const __result = ${call};`, `return [${returned.expr("__result")}, ${outValues.join(", ")}];`];
118
118
  };
119
119
 
120
120
  const renderCommand = (
@@ -135,7 +135,7 @@ const renderCommand = (
135
135
  const seeds = outs.map((out) => out.seed);
136
136
  const bindExpression = glBind(command.name, descriptors, returned.descriptor);
137
137
  const isInline = plan.params.some((paramPlan) => paramPlan.kind === "string-out");
138
- const bindingName = isInline ? "binding" : toCamelIdentifier(command.name);
138
+ const bindingName = isInline ? "__binding" : toCamelIdentifier(command.name);
139
139
 
140
140
  const body = [
141
141
  ...seeds,
@@ -252,12 +252,14 @@ const deriveGenSingular = (
252
252
  const descriptors = [...prefix.map((arg) => arg.descriptor), countScalar.descriptor, tRef(outScalar.descriptor)];
253
253
  usedTypes.add(outScalar.tsAlias);
254
254
  const signature = prefix.map((arg) => `${arg.name}: ${arg.tsType}`).join(", ");
255
- const callArgs = [...prefix.map((arg) => arg.name), "1", "out"].join(", ");
255
+ const callArgs = [...prefix.map((arg) => arg.name), "1", "__out"].join(", ");
256
256
  const jsDoc = genSingularJsDoc({ plan, provenance, docs }, prefix, shape);
257
257
 
258
- const body = [" const out = { value: 0 };", ` ${bindingName}(${callArgs});`, " return out.value;"].join(
259
- "\n",
260
- );
258
+ const body = [
259
+ " const __out = { value: 0 };",
260
+ ` ${bindingName}(${callArgs});`,
261
+ " return __out.value;",
262
+ ].join("\n");
261
263
 
262
264
  const binding = glBind(plan.command.name, renderDescriptorList(descriptors), tVoid);
263
265
 
package/src/runner.ts CHANGED
@@ -39,6 +39,8 @@ type CodegenRunnerOptions = {
39
39
  userOmittedProps?: OmittedProps;
40
40
  /** Regenerates both stores even when their fingerprints are fresh. */
41
41
  isForced?: boolean;
42
+ /** Emits GIR byte sequences as `Uint8Array` rather than `number[]`; defaults to false. */
43
+ isByteArrayTyped?: boolean;
42
44
  };
43
45
 
44
46
  /** What a `runCodegen` run produced. */
@@ -157,12 +159,14 @@ const emitStoresWithConfig = async (config: {
157
159
  girPath: string[];
158
160
  }): Promise<StoreResult> => {
159
161
  const { options, gi, jsx, libraries, girPath } = config;
162
+ const isByteArrayTyped = options.isByteArrayTyped === true;
160
163
  let library: Library | undefined;
161
- const loadLibrary = (): Library => (library ??= Library.load(libraries, girPath));
162
- const isGiRegenerated = options.isForced === true || !isGiStoreFresh(gi.storeDir, libraries, girPath);
164
+ const loadLibrary = (): Library => (library ??= Library.load(libraries, girPath, isByteArrayTyped));
165
+ const giInputs = { girFiles: [] as string[], libraries, girPath, isByteArrayTyped };
166
+ const isGiRegenerated = options.isForced === true || !isGiStoreFresh(gi.storeDir, giInputs);
163
167
 
164
168
  const namespaces = isGiRegenerated
165
- ? runGiCodegen(loadLibrary(), { gi, libraries, girPath })
169
+ ? runGiCodegen(loadLibrary(), { gi, libraries, girPath, isByteArrayTyped })
166
170
  : 0;
167
171
 
168
172
  if (jsx === undefined) {
package/src/staging.ts CHANGED
@@ -1,12 +1,10 @@
1
+ import { errorCode } from "@gtkx/utils";
1
2
  import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2
3
  import { basename, dirname, join } from "node:path";
3
4
 
4
5
  const STAGING_SUFFIX = ".tmp-";
5
6
  const OWNER_PATTERN = /^(?<pid>\d+)-/;
6
7
 
7
- const errorCode = (error: unknown): string | undefined =>
8
- error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
9
-
10
8
  const isOwnerRunning = (pid: number): boolean => {
11
9
  if (!Number.isSafeInteger(pid) || pid <= 0) {
12
10
  return false;