@gjsify/vite-plugin-blueprint 0.51.0 → 0.52.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gjsify/vite-plugin-blueprint
2
2
 
3
- Vite/Rollup/Rolldown plugin that compiles GNOME Blueprint (`.blp`) UI files to GTK XML strings via `blueprint-compiler`. Import a `.blp` file and receive its compiled XML as a JavaScript string, ready for `Gtk.Builder`.
3
+ Vite/Rollup/Rolldown plugin that compiles GNOME Blueprint (`.blp`) UI files to GtkBuilder XML strings. Import a `.blp` file and receive its compiled XML as a JavaScript string, ready for `Gtk.Builder`.
4
4
 
5
5
  Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
6
 
@@ -14,10 +14,24 @@ npm install @gjsify/vite-plugin-blueprint
14
14
  yarn add @gjsify/vite-plugin-blueprint
15
15
  ```
16
16
 
17
- Requires `blueprint-compiler` on the system (e.g. `sudo dnf install blueprint-compiler`). It is found on
18
- `PATH`, and on Windows also in an MSYS2 install that is not on `PATH`; set `BLUEPRINT_COMPILER` to point at
19
- one the plugin does not find. When there is none, the build error names the install command for the host it
20
- ran on rather than leaving you to guess — so this README does not repeat one per platform.
17
+ **No `blueprint-compiler` needed, and none is spawned.** The plugin parses and emits in process through
18
+ [`@gjsify/blueprint`](https://github.com/gjsify/gjsify/tree/main/packages/infra/blueprint), which reads the
19
+ introspection it needs out of pinned `@girs` packages rather than an installed typelib — so a build works
20
+ on macOS and Windows, and in a cold bootstrap, where the GNOME tool is not installable in one line. See
21
+ [ADR 0053](https://github.com/gjsify/gjsify/blob/main/docs/adr/0053-blueprint-parsed-in-repo.md).
22
+
23
+ **What it does NOT accept.** The parser holds a documented subset of the language, and anything outside it
24
+ is a build error naming the construct, the file and the line — never a silent pass-through, because output
25
+ that is plausible and wrong is discovered in a shipped program. Today that means an `[internal-child]`
26
+ bracket, a file-level `translation-domain`, an inline `menu` as a property value, a response flag, a
27
+ multi-step lookup chain with no cast (`bind a.b.c`), a closure with no `as <Type>`, and a type from a
28
+ namespace `@girs` publishes no vocabulary for (`Gio`, `Gdk` and `GObject` today) — for that last one the
29
+ extern form is the way through: `Gio.ListStore` is refused and `$GListStore` is not, because it writes the
30
+ GType name out and needs no vocabulary. Every refusal names the construct, the file, the line and one thing
31
+ to do next, and says that `blueprint-compiler` is not involved, so a file it accepts can still stop here.
32
+ The two error classes are `BlueprintSyntaxError` and `BlueprintEmitError` from `@gjsify/blueprint`, and both
33
+ carry `file` and `line` as fields, so a tool wrapping this plugin does not have to read them out of a
34
+ sentence. The package ships `corpus/refused/`, one named fixture per refused construct.
21
35
 
22
36
  ## Usage
23
37
 
package/lib/index.d.ts CHANGED
@@ -3,5 +3,7 @@ export interface BlueprintPluginOptions {
3
3
  minify?: boolean;
4
4
  verbose?: boolean;
5
5
  }
6
- export { BlueprintCompileError, BlueprintCompilerNotFoundError, type BlueprintHost, currentBlueprintHost, formatMissingBlueprintCompiler, type ResolvedBlueprintCompiler, resolveBlueprintCompiler, } from './resolve-compiler.js';
6
+ export { BlueprintProjectionError } from './projection-error.js';
7
+ /** The specifier an import site writes: `./x.blp?shared-tree`. */
8
+ export declare const SHARED_TREE_QUERY = "?shared-tree";
7
9
  export default function blueprintPlugin(options?: BlueprintPluginOptions): Plugin;
package/lib/index.js CHANGED
@@ -1,49 +1,159 @@
1
- import { execa } from 'execa';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { accessibilityElement, accessibilityValue, emitGtkBuilderXml, enumOrFlagsTypeOf, gtypeName, parseBlueprint, projectToSharedNode, resolveIdent, } from '@gjsify/blueprint';
2
3
  import minifyXML from 'minify-xml';
3
- import { BlueprintCompileError, BlueprintCompilerNotFoundError, resolveBlueprintCompiler, } from './resolve-compiler.js';
4
- export { BlueprintCompileError, BlueprintCompilerNotFoundError, currentBlueprintHost, formatMissingBlueprintCompiler, resolveBlueprintCompiler, } from './resolve-compiler.js';
4
+ // Blueprint's OWN two error types are not re-exported here, and that is the shape rather than
5
+ // an omission. What a parse or an emit throws is `@gjsify/blueprint`'s `BlueprintSyntaxError` /
6
+ // `BlueprintEmitError`, raised by the package that DEFINES them and that every consumer of this
7
+ // plugin already has as a declared dependency — `plugin.spec.ts` catches them from there. A
8
+ // re-export would give the same two classes a second import path whose only job is to stay in
9
+ // step with the first.
10
+ //
11
+ // `BlueprintProjectionError` below is the opposite case and belongs here for the same reason
12
+ // those belong there: the refusal it carries is not a fact about the LANGUAGE. The parser reads
13
+ // the file fine and the projection hands back a tree plus the list of what it cost; deciding
14
+ // that such a tree may not reach a renderer is this plugin's decision, taken at the seam where
15
+ // a build target is known. So it is declared where it is thrown.
16
+ export { BlueprintProjectionError } from './projection-error.js';
17
+ import { BlueprintProjectionError } from './projection-error.js';
18
+ /**
19
+ * The five seams `@gjsify/blueprint`'s emitter reaches introspection through, answered by that
20
+ * same package out of the `@girs` vocabulary.
21
+ *
22
+ * ALL FIVE, and that is not tidiness. Every one is optional and every one NARROWS the output:
23
+ * without `resolveIdent` an enum property emits `vertical` where GtkBuilder is given `1`, without
24
+ * `gtypeName` a `Gio.ListStore` emits `GioListStore`, a class GtkBuilder resolves to nothing.
25
+ * `emitGtkBuilderXml` documents a fallback for each, so a dropped seam does not throw — it builds,
26
+ * and the widget is wrong at run time. That is the silent-wrong-output ADR 0053 clause 3 exists to
27
+ * refuse, and no corpus run can catch it: `check-blueprint-corpus.mjs` hands in its own five, so
28
+ * it would stay green over every tracked `.blp` while this file passed four.
29
+ *
30
+ * `Required<EmitOptions>` is what holds it, and it is a TYPE rather than a test because a test
31
+ * would need a `.blp` whose bytes move for each seam and there is no such file. Drop a key and
32
+ * `gjsify run check` fails on this line; change a seam's signature upstream and it fails here too.
33
+ */
34
+ const SEAMS = Object.freeze({
35
+ accessibilityElement,
36
+ accessibilityValue,
37
+ enumOrFlagsTypeOf,
38
+ gtypeName,
39
+ resolveIdent,
40
+ });
41
+ /**
42
+ * The query that asks for ADR 0051's authored-tree node instead of GtkBuilder XML.
43
+ *
44
+ * WHY A SPECIFIER AND NOT A BUILD-TARGET MODE. The obvious wiring is an option — `emit: 'tree'`
45
+ * on the non-GTK app targets — and it is the wrong one, for a reason the whole point of sharing a
46
+ * template rests on: it would make `import Template from './x.blp'` a string on `--app gjs` and
47
+ * an object on `--app browser`. One source, two meanings, chosen by a build flag the file cannot
48
+ * see. A consumer would have no spelling for "give me the XML here" on a target whose default had
49
+ * been flipped, and the ambient `*.blp` declaration — which has exactly one shape per module
50
+ * pattern — could not describe either honestly. The knowledge of WHICH exit a module wants sits
51
+ * at the import site, so that is where it is written.
52
+ *
53
+ * Three properties fall out, and each is worth more than the shorter spelling:
54
+ *
55
+ * · The GTK path cannot change. A bare `.blp` never reaches `projectToSharedNode` at all, so
56
+ * "the XML exit did not move" is a statement about reachable code and not about a default
57
+ * value someone could flip. ADR 0066 § The XML exit does not move made that a property to
58
+ * keep checkable rather than assert; this keeps it checkable one layer out.
59
+ * · The refusal below cannot break a build that did not ask. A `.blp` with losses keeps
60
+ * compiling to XML exactly as it does today; only asking for its TREE fails.
61
+ * · Every app target registers one plugin with no options. There is no target→mode table to
62
+ * keep in step with the list of targets, which is the drift `app/nativescript.ts`'s
63
+ * "NO blueprintPlugin" comment was one half of.
64
+ */
65
+ const SHARED_TREE_PARAM = 'shared-tree';
66
+ /** The specifier an import site writes: `./x.blp?shared-tree`. */
67
+ export const SHARED_TREE_QUERY = `?${SHARED_TREE_PARAM}`;
68
+ /**
69
+ * The `.blp` path a module id names, with the exit it asked for.
70
+ *
71
+ * A PARAMETER, not a suffix, and that distinction is the whole correctness of this plugin
72
+ * under Vite. This package is published as a *vite* plugin, and Vite appends its own segments
73
+ * to a module id on the way through: `?shared-tree&import` from import analysis, `&v=<hash>`
74
+ * from the dep optimizer, `&used` from the SSR transform. An `endsWith('?shared-tree')` test
75
+ * matches none of those — and then the `.blp` fallback below misses too, because the id no
76
+ * longer ends in `.blp` either. The plugin would DECLINE the module, silently, and raw
77
+ * Blueprint source would reach whatever loader runs next. Rolldown, which is what this repo
78
+ * builds with, appends nothing, so the repo's own builds never show it.
79
+ */
80
+ function readId(id) {
81
+ const queryAt = id.indexOf('?');
82
+ const file = queryAt === -1 ? id : id.slice(0, queryAt);
83
+ if (!file.endsWith('.blp'))
84
+ return undefined;
85
+ if (queryAt === -1)
86
+ return { file, sharedTree: false };
87
+ return { file, sharedTree: new URLSearchParams(id.slice(queryAt + 1)).has(SHARED_TREE_PARAM) };
88
+ }
5
89
  export default function blueprintPlugin(options = {}) {
6
90
  const { minify = false, verbose = false } = options;
7
- // Cached only on SUCCESS, so the PATH walk costs one probe per plugin
8
- // instance instead of one per `.blp`. A MISS is deliberately re-probed:
9
- // installing the compiler and saving the file is how a watch session is
10
- // meant to recover, and caching "absent" would make that need a restart.
11
- let compiler = null;
12
91
  return {
13
92
  name: 'vite-plugin-blueprint',
93
+ // The query has to be resolved by hand: the default resolver stats the specifier, and
94
+ // no file is named `main-window.blp?shared-tree`. Resolving the path WITHOUT the query
95
+ // and putting it back is also what keeps the two exits separate modules — same file,
96
+ // two ids, two module records — so importing a template both ways in one bundle gets
97
+ // the XML and the tree rather than whichever was loaded first.
98
+ async resolveId(source, importer) {
99
+ const asked = readId(source);
100
+ if (asked === undefined || !asked.sharedTree)
101
+ return null;
102
+ const resolved = await this.resolve(asked.file, importer, { skipSelf: true });
103
+ // The WHOLE query goes back on, not just ours: anything the host added to the
104
+ // specifier is the host's, and dropping it here would resolve to a different
105
+ // module than the one it asked for.
106
+ return resolved === null ? null : `${resolved.id}${source.slice(source.indexOf('?'))}`;
107
+ },
14
108
  async load(id) {
15
- if (id.endsWith('.blp')) {
16
- compiler ??= resolveBlueprintCompiler();
17
- if (!compiler) {
18
- // Names the `.blp` that wanted it, so the diagnostic points at
19
- // a file the reader recognises rather than at the plugin.
20
- throw new BlueprintCompilerNotFoundError(id);
21
- }
22
- const resolved = compiler;
23
- try {
24
- // Compile .blp file and get XML output directly
25
- const { stdout } = await execa(resolved.file, [...resolved.prefixArgs, 'compile', id], {
26
- // Only ever an overlay (an MSYS2 PATH prepend); undefined
27
- // inherits the environment unchanged.
28
- env: resolved.env,
29
- });
30
- if (verbose)
31
- console.log(`Compiled ${id} (blueprint-compiler via ${resolved.source})`);
32
- let xmlContent = stdout;
33
- // Minify XML if option is enabled
34
- if (minify) {
35
- xmlContent = minifyXML(xmlContent);
36
- if (verbose)
37
- console.log(`Minified XML for ${id}`);
38
- }
39
- // Return the XML content as a string
40
- return `export default ${JSON.stringify(xmlContent)};`;
41
- }
42
- catch (error) {
43
- const detail = error instanceof Error ? error.stderr || error.message : String(error);
44
- throw new BlueprintCompileError(id, resolved.file, detail);
45
- }
109
+ const asked = readId(id);
110
+ if (asked === undefined)
111
+ return;
112
+ // ADR 0053 clause 5: the in-repo parser is authoritative for the build and
113
+ // `blueprint-compiler` is the oracle it is measured against. There is deliberately
114
+ // NO fallback to the binary. A silent one would put back the dependency clause 7
115
+ // exists to remove, and it would hide any disagreement behind whichever of the two
116
+ // happened to be installed — the build's output would depend on the host.
117
+ //
118
+ // The subset is a real limit while it lasts. A `.blp` reaching a construct the
119
+ // parser does not hold fails the build rather than compiling, and the error names
120
+ // the construct, the file and the line: `BlueprintSyntaxError` before an AST
121
+ // exists, `BlueprintEmitError` after. That is clause 3's declared trade against
122
+ // output that looks plausible and means something else.
123
+ const source = await readFile(asked.file, 'utf8');
124
+ const ast = parseBlueprint(source, asked.file);
125
+ if (asked.sharedTree) {
126
+ // THE LOSSY EXIT REFUSES ITS LOSSES RATHER THAN SHIPPING THEM.
127
+ //
128
+ // `projectToSharedNode` declares what it dropped — ADR 0053 clause 1 calls the
129
+ // losses "named at the seam rather than discovered downstream" — and this is the
130
+ // seam that has to act on the naming. A renderer handed a tree with losses builds
131
+ // a SMALLER UI than the `.blp` describes and cannot tell: a `bind` that never
132
+ // updates, an `Adw.Breakpoint` whose setters never fire. The template would then
133
+ // be complete on GTK and quietly partial everywhere else, which is worse than not
134
+ // sharing it at all, because the GTK build keeps saying it works.
135
+ //
136
+ // So the build stops here, with the file, the line and the kind of every loss —
137
+ // a porting task a reader can open, not a warning scrolled past. `minify` does
138
+ // not apply: it is an XML setting and this exit emits none.
139
+ const { node, lost } = projectToSharedNode(ast, { gtypeName });
140
+ if (lost.length > 0)
141
+ throw new BlueprintProjectionError(asked.file, lost);
142
+ if (verbose)
143
+ console.log(`Projected ${asked.file} (@gjsify/blueprint)`);
144
+ return `export default ${JSON.stringify(node)};`;
145
+ }
146
+ let xmlContent = emitGtkBuilderXml(ast, SEAMS);
147
+ if (verbose)
148
+ console.log(`Compiled ${asked.file} (@gjsify/blueprint)`);
149
+ // Minify XML if option is enabled
150
+ if (minify) {
151
+ xmlContent = minifyXML(xmlContent);
152
+ if (verbose)
153
+ console.log(`Minified XML for ${asked.file}`);
46
154
  }
155
+ // Return the XML content as a string
156
+ return `export default ${JSON.stringify(xmlContent)};`;
47
157
  },
48
158
  };
49
159
  }
@@ -0,0 +1,23 @@
1
+ import type { ProjectedLoss } from '@gjsify/blueprint';
2
+ /**
3
+ * A `.blp` asked for as a shared tree whose projection would have dropped something.
4
+ *
5
+ * WHY THIS IS A REFUSAL AND NOT A WARNING. The projection is lossy by construction (ADR 0053
6
+ * clause 1) and it says what it dropped. A build that emitted the tree anyway would hand a
7
+ * renderer a smaller UI than the source describes, with nothing downstream able to tell: the
8
+ * GTK build of the same file stays complete, so the template reads as shared and working while
9
+ * one surface silently renders less. That failure is expensive exactly because it is invisible,
10
+ * and a warning in a build log is invisible in the same way.
11
+ *
12
+ * WHY IT CARRIES THE LOSSES AND NOT JUST A SENTENCE. `lost` is the porting task: each entry is
13
+ * a construct and the line it sits on, so a consumer — a vite config, a CI step, a codemod —
14
+ * can group or count them without re-parsing the file. The message spells the same thing for a
15
+ * human, one loss per line, because a build failure is read before it is caught.
16
+ */
17
+ export declare class BlueprintProjectionError extends Error {
18
+ /** The `.blp` that was asked for, with no query attached. */
19
+ readonly file: string;
20
+ /** Every loss the projection declared, in the order it declared them. */
21
+ readonly lost: readonly ProjectedLoss[];
22
+ constructor(file: string, lost: readonly ProjectedLoss[]);
23
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A `.blp` asked for as a shared tree whose projection would have dropped something.
3
+ *
4
+ * WHY THIS IS A REFUSAL AND NOT A WARNING. The projection is lossy by construction (ADR 0053
5
+ * clause 1) and it says what it dropped. A build that emitted the tree anyway would hand a
6
+ * renderer a smaller UI than the source describes, with nothing downstream able to tell: the
7
+ * GTK build of the same file stays complete, so the template reads as shared and working while
8
+ * one surface silently renders less. That failure is expensive exactly because it is invisible,
9
+ * and a warning in a build log is invisible in the same way.
10
+ *
11
+ * WHY IT CARRIES THE LOSSES AND NOT JUST A SENTENCE. `lost` is the porting task: each entry is
12
+ * a construct and the line it sits on, so a consumer — a vite config, a CI step, a codemod —
13
+ * can group or count them without re-parsing the file. The message spells the same thing for a
14
+ * human, one loss per line, because a build failure is read before it is caught.
15
+ */
16
+ export class BlueprintProjectionError extends Error {
17
+ /** The `.blp` that was asked for, with no query attached. */
18
+ file;
19
+ /** Every loss the projection declared, in the order it declared them. */
20
+ lost;
21
+ constructor(file, lost) {
22
+ // One loss per line and the kind named before the place, because the kind decides
23
+ // whether this file can be shared at all and the line only says where to start.
24
+ const listed = lost.map((loss) => ` ${loss.kind} at ${file}:${loss.line}`).join('\n');
25
+ super(`${file} cannot be used as a shared tree: its projection drops ` +
26
+ `${lost.length} construct(s) the node shape has no spelling for.\n${listed}\n` +
27
+ 'The GtkBuilder-XML exit of the same file is unaffected — import it without ' +
28
+ '`?shared-tree` to build it for GTK. To share it, replace the constructs above ' +
29
+ 'with ones the shared node carries, or render this surface from its own tree.');
30
+ this.name = 'BlueprintProjectionError';
31
+ this.file = file;
32
+ this.lost = lost;
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gjsify/vite-plugin-blueprint",
3
- "version": "0.51.0",
4
- "description": "Compile GNOME Blueprint .blp templates to XML strings via blueprint-compiler. Vite / Rollup / Rolldown compatible.",
3
+ "version": "0.52.0",
4
+ "description": "Compile GNOME Blueprint .blp templates to GtkBuilder-XML strings with the in-repo @gjsify/blueprint parser: no blueprint-compiler, no spawn. Vite / Rollup / Rolldown compatible.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "module": "lib/index.js",
@@ -11,10 +11,6 @@
11
11
  "types": "./lib/index.d.ts",
12
12
  "default": "./lib/index.js"
13
13
  },
14
- "./resolve": {
15
- "types": "./lib/resolve-compiler.d.ts",
16
- "default": "./lib/resolve-compiler.js"
17
- },
18
14
  "./types": {
19
15
  "types": "./types/blueprint.d.ts"
20
16
  }
@@ -51,7 +47,7 @@
51
47
  ],
52
48
  "license": "MIT",
53
49
  "dependencies": {
54
- "execa": "^9.6.1",
50
+ "@gjsify/blueprint": "^0.52.0",
55
51
  "minify-xml": "^4.5.2"
56
52
  },
57
53
  "peerDependencies": {
@@ -67,8 +63,8 @@
67
63
  }
68
64
  },
69
65
  "devDependencies": {
70
- "@gjsify/cli": "^0.51.0",
71
- "@gjsify/unit": "^0.51.0",
66
+ "@gjsify/cli": "^0.52.0",
67
+ "@gjsify/unit": "^0.52.0",
72
68
  "@types/node": "^25.9.2",
73
69
  "typescript": "^6.0.3",
74
70
  "vite": "^8.1.3"
@@ -2,3 +2,20 @@ declare module '*.blp' {
2
2
  const content: string;
3
3
  export default content;
4
4
  }
5
+
6
+ /**
7
+ * The same file asked for as ADR 0051's authored-tree node — what `adwaita-web`'s
8
+ * `mountSharedTree` and the NativeScript port's `build` consume.
9
+ *
10
+ * A SECOND MODULE PATTERN AND NOT A WIDER `*.blp`, because the two exits are two shapes and a
11
+ * module has one: declaring `.blp` as `string | SharedNode` would make every existing consumer
12
+ * narrow past a value it cannot receive. `@gjsify/vite-plugin-blueprint` § SHARED_TREE_QUERY
13
+ * says why the exit is chosen by the specifier rather than by the build target.
14
+ *
15
+ * `SharedNode` is assignable to `@gjsify/adwaita-core`'s `SharedTreeNode` — the renderers'
16
+ * parameter — which is the property that makes this import mountable without a cast.
17
+ */
18
+ declare module '*.blp?shared-tree' {
19
+ const tree: import('@gjsify/blueprint').SharedNode;
20
+ export default tree;
21
+ }
@@ -1,84 +0,0 @@
1
- /** A spawnable blueprint-compiler, plus how it was found (for `verbose`). */
2
- export interface ResolvedBlueprintCompiler {
3
- /** The executable to spawn. */
4
- file: string;
5
- /** Arguments that must precede blueprint-compiler's own (e.g. the script path). */
6
- prefixArgs: string[];
7
- /** Environment overlay to spawn with, or undefined to inherit unchanged. */
8
- env?: Record<string, string>;
9
- source: 'env' | 'path' | 'msys2';
10
- }
11
- /**
12
- * The host facts this module decides on, taken as a PARAMETER rather than read
13
- * from ambient `process.*`.
14
- *
15
- * Every unit-test leg in this repo runs on Linux, so an ambient
16
- * `process.platform` puts the darwin and win32 branches — which are most of this
17
- * module and the only ones anybody gets wrong — out of reach of any test. This
18
- * is the purity `packages/infra/cli/src/utils/platform-check.ts` documents,
19
- * applied to the module whose entire job is a per-OS answer.
20
- */
21
- export interface BlueprintHost {
22
- /** `process.platform` of the host being answered for. */
23
- platform: NodeJS.Platform;
24
- /** That host's environment. */
25
- env: Record<string, string | undefined>;
26
- /** Does this path exist? `existsSync` in production, a map in a test. */
27
- exists: (path: string) => boolean;
28
- }
29
- /** The running process as a {@link BlueprintHost}. */
30
- export declare function currentBlueprintHost(): BlueprintHost;
31
- /**
32
- * Locate a usable blueprint-compiler, or null when there is none.
33
- *
34
- * Order: `BLUEPRINT_COMPILER` (an explicit answer always wins, and is the escape
35
- * hatch for an install none of the probes below know about) → `PATH` → an MSYS2
36
- * install on win32.
37
- *
38
- * A set-but-unusable override resolves to null rather than falling through to
39
- * the probes. Handing back a path that is not there sent its `ENOENT` into the
40
- * "the compiler EXISTS and refused the file" branch, which then blamed the
41
- * `.blp` for a typo in an environment variable; and quietly ignoring an explicit
42
- * instruction to run a second guess is not better. Null is what lets
43
- * {@link formatMissingBlueprintCompiler} say which of the two actually happened.
44
- */
45
- export declare function resolveBlueprintCompiler(host?: BlueprintHost): ResolvedBlueprintCompiler | null;
46
- /**
47
- * What to tell someone who has no blueprint-compiler.
48
- *
49
- * Names the install command for THIS platform rather than listing every one:
50
- * the reader is on one host and the other two lines are noise they have to
51
- * filter. The Linux arm is the exception that proves it — every Linux row of
52
- * `PM_PACKAGES` in `@gjsify/cli`'s `check-system-deps.ts` spells the package
53
- * identically, so splitting it per distro would be four lines carrying one word.
54
- *
55
- * `BLUEPRINT_COMPILER` is mentioned everywhere because it is the answer for any
56
- * install the probes miss — except when it is itself the problem, which gets its
57
- * own sentence: repeating "or set BLUEPRINT_COMPILER" at someone who just set it
58
- * is how a diagnostic loses their trust.
59
- */
60
- export declare function formatMissingBlueprintCompiler(host?: BlueprintHost): string;
61
- /**
62
- * No usable compiler on this host.
63
- *
64
- * Carries the whole user-facing sentence so the wording has exactly one home and
65
- * the plugin composes nothing: what this replaces was an `ExecaError` naming the
66
- * COMMAND, in the guest's own language, from inside a rolldown plugin stack
67
- * (#1098).
68
- */
69
- export declare class BlueprintCompilerNotFoundError extends Error {
70
- readonly blueprintPath: string;
71
- readonly name = "BlueprintCompilerNotFoundError";
72
- constructor(blueprintPath: string, host?: BlueprintHost);
73
- }
74
- /**
75
- * The compiler exists and refused the file, so this is a blueprint syntax or
76
- * type error and its own stderr is the useful part — never an install hint the
77
- * reader has already satisfied.
78
- */
79
- export declare class BlueprintCompileError extends Error {
80
- readonly blueprintPath: string;
81
- readonly compilerFile: string;
82
- readonly name = "BlueprintCompileError";
83
- constructor(blueprintPath: string, compilerFile: string, detail: string);
84
- }
@@ -1,219 +0,0 @@
1
- // Where is `blueprint-compiler`, and what do we say when it is nowhere?
2
- //
3
- // Split out of the plugin so it is answerable in a test without a compiler
4
- // installed, and because the answer is genuinely platform-shaped on Windows.
5
- //
6
- // The failure this replaces: the plugin called `execa('blueprint-compiler', …)`
7
- // and rethrew whatever came back, so a host without it got an ExecaError stack
8
- // naming the COMMAND THAT FAILED and nothing about what to install. Measured on
9
- // the win11-gjsify VM, that error was read as "blueprint-compiler is a GNOME
10
- // Python tool, so it is unavailable on Windows" — a conclusion that reached a
11
- // docs file and stopped anyone building a `.blp` showcase there for a day. It is
12
- // available; nothing said how to get it.
13
- import { existsSync } from 'node:fs';
14
- import { join } from 'node:path';
15
- /**
16
- * `existsSync` throws rather than answering `false` when the argument is not a
17
- * usable path at all (an embedded NUL, for one) — and an unreadable or malformed
18
- * PATH entry is not an answer about the command we are looking for. Same catch,
19
- * and same reason, as `isOnPath()` in `@gjsify/cli`'s `check-system-deps.ts`.
20
- */
21
- function existsSafe(path) {
22
- try {
23
- return existsSync(path);
24
- }
25
- catch {
26
- return false;
27
- }
28
- }
29
- /** The running process as a {@link BlueprintHost}. */
30
- export function currentBlueprintHost() {
31
- return { platform: process.platform, env: process.env, exists: existsSafe };
32
- }
33
- /**
34
- * MSYS2 install roots to probe on win32, in order.
35
- *
36
- * MSYS2 is the ONLY way to get blueprint-compiler onto Windows today, and not
37
- * for the reason the tool's Python-ness suggests: it is pure Python (PyPI ships
38
- * a `py3-none-any` wheel), but `blueprintcompiler/gir.py` reads TYPELIBS through
39
- * `GIRepository`, so it needs PyGObject — which publishes no Windows wheel, so
40
- * `pip install blueprint-compiler` dies building it from source. MSYS2 is where
41
- * Python, PyGObject and the typelibs arrive already fitted together.
42
- *
43
- * `C:\msys64` is the installer default; the `$USERPROFILE` entries cover the
44
- * extracted-archive install, which is what a host without admin rights gets;
45
- * `C:\tools\msys64` is Chocolatey's.
46
- */
47
- function msys2Roots(env) {
48
- return [
49
- env.MSYS2_ROOT,
50
- 'C:\\msys64',
51
- env.USERPROFILE && join(env.USERPROFILE, 'msys64'),
52
- env.USERPROFILE && join(env.USERPROFILE, 'Tools', 'msys64'),
53
- 'C:\\tools\\msys64',
54
- ].filter((root) => typeof root === 'string' && root.length > 0);
55
- }
56
- /**
57
- * MSYS2 environments, most-preferred first. `ucrt64` is MSYS2's own default for
58
- * new installs and links the modern Universal CRT; the others are probed so an
59
- * existing install is not rejected for being older.
60
- */
61
- const MSYS2_ENVS = ['ucrt64', 'mingw64', 'clang64'];
62
- /** Executable suffixes to try for a bare command name. Windows needs them; POSIX does not. */
63
- function exeSuffixes(platform) {
64
- return platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
65
- }
66
- /** First existing `<dir>/<cmd><suffix>`, or null. */
67
- function findOnPath(cmd, host) {
68
- const pathVar = host.env.PATH;
69
- if (!pathVar)
70
- return null;
71
- const sep = host.platform === 'win32' ? ';' : ':';
72
- for (const dir of pathVar.split(sep)) {
73
- if (!dir)
74
- continue;
75
- for (const suffix of exeSuffixes(host.platform)) {
76
- const candidate = join(dir, cmd + suffix);
77
- if (host.exists(candidate))
78
- return candidate;
79
- }
80
- }
81
- return null;
82
- }
83
- /**
84
- * Find an MSYS2-provided blueprint-compiler.
85
- *
86
- * MSYS2 installs it as a SHEBANG SCRIPT with no `.exe` beside it, which Windows
87
- * cannot execute — so this returns MSYS2's own `python.exe` with the script as
88
- * its first argument, rather than the script alone. `<env>/bin` is prepended to
89
- * PATH because the script imports PyGObject, which loads MSYS2's libglib and its
90
- * typelibs from there.
91
- *
92
- * Deliberately does NOT set `GI_TYPELIB_PATH` at the gjsify GTK runtime bundle:
93
- * those typelibs are gvsbuild's and name `glib-2.0-0.dll` in their
94
- * `shared_library` field while MSYS2 ships `libglib-2.0-0.dll`. Measured — it
95
- * fails with "Failed to load shared library", and the obvious repair (put both
96
- * on PATH) would load two GLib builds into one process, which is a worse problem
97
- * than the one it solves.
98
- */
99
- function findInMsys2(host) {
100
- if (host.platform !== 'win32')
101
- return null;
102
- for (const root of msys2Roots(host.env)) {
103
- for (const env of MSYS2_ENVS) {
104
- const bin = join(root, env, 'bin');
105
- const script = join(bin, 'blueprint-compiler');
106
- const python = join(bin, 'python.exe');
107
- if (!host.exists(script) || !host.exists(python))
108
- continue;
109
- return {
110
- file: python,
111
- prefixArgs: [script],
112
- // `;` is a win32 fact, not a host fact — this branch only runs there.
113
- env: { PATH: `${bin};${host.env.PATH ?? ''}` },
114
- source: 'msys2',
115
- };
116
- }
117
- }
118
- return null;
119
- }
120
- /** Does this override name a location, as opposed to a bare command to look up? */
121
- function looksLikePath(override) {
122
- return override.includes('/') || override.includes('\\');
123
- }
124
- /**
125
- * Locate a usable blueprint-compiler, or null when there is none.
126
- *
127
- * Order: `BLUEPRINT_COMPILER` (an explicit answer always wins, and is the escape
128
- * hatch for an install none of the probes below know about) → `PATH` → an MSYS2
129
- * install on win32.
130
- *
131
- * A set-but-unusable override resolves to null rather than falling through to
132
- * the probes. Handing back a path that is not there sent its `ENOENT` into the
133
- * "the compiler EXISTS and refused the file" branch, which then blamed the
134
- * `.blp` for a typo in an environment variable; and quietly ignoring an explicit
135
- * instruction to run a second guess is not better. Null is what lets
136
- * {@link formatMissingBlueprintCompiler} say which of the two actually happened.
137
- */
138
- export function resolveBlueprintCompiler(host = currentBlueprintHost()) {
139
- const override = host.env.BLUEPRINT_COMPILER;
140
- if (override) {
141
- if (looksLikePath(override)) {
142
- return host.exists(override) ? { file: override, prefixArgs: [], source: 'env' } : null;
143
- }
144
- // A bare command name is a legitimate override, and must still reach the
145
- // child through the same PATH walk any other command name gets.
146
- const onPath = findOnPath(override, host);
147
- return onPath ? { file: onPath, prefixArgs: [], source: 'env' } : null;
148
- }
149
- const onPath = findOnPath('blueprint-compiler', host);
150
- if (onPath) {
151
- return { file: onPath, prefixArgs: [], source: 'path' };
152
- }
153
- return findInMsys2(host);
154
- }
155
- /**
156
- * What to tell someone who has no blueprint-compiler.
157
- *
158
- * Names the install command for THIS platform rather than listing every one:
159
- * the reader is on one host and the other two lines are noise they have to
160
- * filter. The Linux arm is the exception that proves it — every Linux row of
161
- * `PM_PACKAGES` in `@gjsify/cli`'s `check-system-deps.ts` spells the package
162
- * identically, so splitting it per distro would be four lines carrying one word.
163
- *
164
- * `BLUEPRINT_COMPILER` is mentioned everywhere because it is the answer for any
165
- * install the probes miss — except when it is itself the problem, which gets its
166
- * own sentence: repeating "or set BLUEPRINT_COMPILER" at someone who just set it
167
- * is how a diagnostic loses their trust.
168
- */
169
- export function formatMissingBlueprintCompiler(host = currentBlueprintHost()) {
170
- const override = host.env.BLUEPRINT_COMPILER;
171
- if (override) {
172
- return (`BLUEPRINT_COMPILER is set to "${override}", which is not ${looksLikePath(override) ? 'a file that exists' : 'on PATH'}.\n` +
173
- ' Point it at a blueprint-compiler executable, or unset it to search PATH\n' +
174
- ' (and, on Windows, the usual MSYS2 install locations).');
175
- }
176
- const install = host.platform === 'win32'
177
- ? 'MSYS2 packages it prebuilt (it needs PyGObject, which has no Windows wheel, so pip cannot):\n' +
178
- ' pacman -S mingw-w64-ucrt-x86_64-blueprint-compiler mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita\n' +
179
- ' An MSYS2 install under C:\\msys64, %USERPROFILE%\\msys64, %USERPROFILE%\\Tools\\msys64 or\n' +
180
- ' C:\\tools\\msys64 is found automatically — it does NOT need to be on PATH.'
181
- : host.platform === 'darwin'
182
- ? 'brew install blueprint-compiler'
183
- : 'sudo dnf install blueprint-compiler (apt, pacman, zypper and apk package it under the same name)';
184
- return ('blueprint-compiler was not found — @gjsify/vite-plugin-blueprint needs it to\n' +
185
- ' compile .blp templates to GtkBuilder XML.\n' +
186
- ` ${install}\n` +
187
- ' Or set BLUEPRINT_COMPILER to its full path.');
188
- }
189
- /**
190
- * No usable compiler on this host.
191
- *
192
- * Carries the whole user-facing sentence so the wording has exactly one home and
193
- * the plugin composes nothing: what this replaces was an `ExecaError` naming the
194
- * COMMAND, in the guest's own language, from inside a rolldown plugin stack
195
- * (#1098).
196
- */
197
- export class BlueprintCompilerNotFoundError extends Error {
198
- blueprintPath;
199
- name = 'BlueprintCompilerNotFoundError';
200
- constructor(blueprintPath, host = currentBlueprintHost()) {
201
- super(`${blueprintPath}: ${formatMissingBlueprintCompiler(host)}`);
202
- this.blueprintPath = blueprintPath;
203
- }
204
- }
205
- /**
206
- * The compiler exists and refused the file, so this is a blueprint syntax or
207
- * type error and its own stderr is the useful part — never an install hint the
208
- * reader has already satisfied.
209
- */
210
- export class BlueprintCompileError extends Error {
211
- blueprintPath;
212
- compilerFile;
213
- name = 'BlueprintCompileError';
214
- constructor(blueprintPath, compilerFile, detail) {
215
- super(`${blueprintPath}: blueprint-compiler failed (${compilerFile})\n${detail}`);
216
- this.blueprintPath = blueprintPath;
217
- this.compilerFile = compilerFile;
218
- }
219
- }