@gjsify/vite-plugin-blueprint 0.51.1 → 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 +19 -5
- package/lib/index.d.ts +3 -1
- package/lib/index.js +149 -39
- package/lib/projection-error.d.ts +23 -0
- package/lib/projection-error.js +34 -0
- package/package.json +5 -9
- package/types/blueprint.d.ts +17 -0
- package/lib/resolve-compiler.d.ts +0 -84
- package/lib/resolve-compiler.js +0 -219
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
|
|
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
|
-
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
4
|
-
"description": "Compile GNOME Blueprint .blp templates to XML strings
|
|
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
|
-
"
|
|
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.
|
|
71
|
-
"@gjsify/unit": "^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"
|
package/types/blueprint.d.ts
CHANGED
|
@@ -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
|
-
}
|
package/lib/resolve-compiler.js
DELETED
|
@@ -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
|
-
}
|