@gjsify/vite-plugin-blueprint 0.40.0 → 0.41.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
@@ -14,7 +14,10 @@ npm install @gjsify/vite-plugin-blueprint
14
14
  yarn add @gjsify/vite-plugin-blueprint
15
15
  ```
16
16
 
17
- Requires `blueprint-compiler` to be installed on the system (e.g. `sudo dnf install blueprint-compiler`).
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.
18
21
 
19
22
  ## Usage
20
23
 
package/lib/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export interface BlueprintPluginOptions {
3
3
  minify?: boolean;
4
4
  verbose?: boolean;
5
5
  }
6
- export { resolveBlueprintCompiler, formatMissingBlueprintCompiler } from './resolve-compiler.js';
6
+ export { BlueprintCompileError, BlueprintCompilerNotFoundError, type BlueprintHost, currentBlueprintHost, formatMissingBlueprintCompiler, type ResolvedBlueprintCompiler, resolveBlueprintCompiler, } from './resolve-compiler.js';
7
7
  export default function blueprintPlugin(options?: BlueprintPluginOptions): Plugin;
package/lib/index.js CHANGED
@@ -1,29 +1,34 @@
1
1
  import { execa } from 'execa';
2
2
  import minifyXML from 'minify-xml';
3
- import { formatMissingBlueprintCompiler, resolveBlueprintCompiler } from './resolve-compiler.js';
4
- export { resolveBlueprintCompiler, formatMissingBlueprintCompiler } from './resolve-compiler.js';
3
+ import { BlueprintCompileError, BlueprintCompilerNotFoundError, resolveBlueprintCompiler, } from './resolve-compiler.js';
4
+ export { BlueprintCompileError, BlueprintCompilerNotFoundError, currentBlueprintHost, formatMissingBlueprintCompiler, resolveBlueprintCompiler, } from './resolve-compiler.js';
5
5
  export default function blueprintPlugin(options = {}) {
6
6
  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;
7
12
  return {
8
13
  name: 'vite-plugin-blueprint',
9
14
  async load(id) {
10
15
  if (id.endsWith('.blp')) {
11
- // Resolved per load rather than once at plugin construction, so a
12
- // compiler installed mid-watch is picked up without a restart —
13
- // and so the "missing" diagnostic names the .blp that wanted it.
14
- const compiler = resolveBlueprintCompiler();
16
+ compiler ??= resolveBlueprintCompiler();
15
17
  if (!compiler) {
16
- throw new Error(`${id}: ${formatMissingBlueprintCompiler()}`);
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);
17
21
  }
22
+ const resolved = compiler;
18
23
  try {
19
24
  // Compile .blp file and get XML output directly
20
- const { stdout } = await execa(compiler.file, [...compiler.prefixArgs, 'compile', id], {
25
+ const { stdout } = await execa(resolved.file, [...resolved.prefixArgs, 'compile', id], {
21
26
  // Only ever an overlay (an MSYS2 PATH prepend); undefined
22
27
  // inherits the environment unchanged.
23
- env: compiler.env,
28
+ env: resolved.env,
24
29
  });
25
30
  if (verbose)
26
- console.log(`Compiled ${id} (blueprint-compiler via ${compiler.source})`);
31
+ console.log(`Compiled ${id} (blueprint-compiler via ${resolved.source})`);
27
32
  let xmlContent = stdout;
28
33
  // Minify XML if option is enabled
29
34
  if (minify) {
@@ -35,12 +40,8 @@ export default function blueprintPlugin(options = {}) {
35
40
  return `export default ${JSON.stringify(xmlContent)};`;
36
41
  }
37
42
  catch (error) {
38
- // The compiler EXISTS and refused the file, so this is a
39
- // blueprint syntax/type error and its own stderr is the
40
- // useful part — surface that, not an install hint the reader
41
- // has already satisfied.
42
43
  const detail = error instanceof Error ? error.stderr || error.message : String(error);
43
- throw new Error(`${id}: blueprint-compiler failed (${compiler.file})\n${detail}`);
44
+ throw new BlueprintCompileError(id, resolved.file, detail);
44
45
  }
45
46
  }
46
47
  },
@@ -8,20 +8,77 @@ export interface ResolvedBlueprintCompiler {
8
8
  env?: Record<string, string>;
9
9
  source: 'env' | 'path' | 'msys2';
10
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;
11
31
  /**
12
32
  * Locate a usable blueprint-compiler, or null when there is none.
13
33
  *
14
34
  * Order: `BLUEPRINT_COMPILER` (an explicit answer always wins, and is the escape
15
35
  * hatch for an install none of the probes below know about) → `PATH` → an MSYS2
16
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.
17
44
  */
18
- export declare function resolveBlueprintCompiler(): ResolvedBlueprintCompiler | null;
45
+ export declare function resolveBlueprintCompiler(host?: BlueprintHost): ResolvedBlueprintCompiler | null;
19
46
  /**
20
47
  * What to tell someone who has no blueprint-compiler.
21
48
  *
22
49
  * Names the install command for THIS platform rather than listing every one:
23
50
  * the reader is on one host and the other two lines are noise they have to
24
- * filter. `BLUEPRINT_COMPILER` is mentioned everywhere because it is the answer
25
- * for any install the probes miss.
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.
26
59
  */
27
- export declare function formatMissingBlueprintCompiler(): string;
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
+ }
@@ -12,6 +12,24 @@
12
12
  // available; nothing said how to get it.
13
13
  import { existsSync } from 'node:fs';
14
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
+ }
15
33
  /**
16
34
  * MSYS2 install roots to probe on win32, in order.
17
35
  *
@@ -26,13 +44,15 @@ import { join } from 'node:path';
26
44
  * extracted-archive install, which is what a host without admin rights gets;
27
45
  * `C:\tools\msys64` is Chocolatey's.
28
46
  */
29
- const MSYS2_ROOTS = [
30
- process.env.MSYS2_ROOT,
31
- 'C:\\msys64',
32
- process.env.USERPROFILE && join(process.env.USERPROFILE, 'msys64'),
33
- process.env.USERPROFILE && join(process.env.USERPROFILE, 'Tools', 'msys64'),
34
- 'C:\\tools\\msys64',
35
- ].filter((root) => typeof root === 'string' && root.length > 0);
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
+ }
36
56
  /**
37
57
  * MSYS2 environments, most-preferred first. `ucrt64` is MSYS2's own default for
38
58
  * new installs and links the modern Universal CRT; the others are probed so an
@@ -40,25 +60,22 @@ const MSYS2_ROOTS = [
40
60
  */
41
61
  const MSYS2_ENVS = ['ucrt64', 'mingw64', 'clang64'];
42
62
  /** Executable suffixes to try for a bare command name. Windows needs them; POSIX does not. */
43
- const EXE_SUFFIXES = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
63
+ function exeSuffixes(platform) {
64
+ return platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
65
+ }
44
66
  /** First existing `<dir>/<cmd><suffix>`, or null. */
45
- function findOnPath(cmd) {
46
- const pathVar = process.env.PATH;
67
+ function findOnPath(cmd, host) {
68
+ const pathVar = host.env.PATH;
47
69
  if (!pathVar)
48
70
  return null;
49
- const sep = process.platform === 'win32' ? ';' : ':';
71
+ const sep = host.platform === 'win32' ? ';' : ':';
50
72
  for (const dir of pathVar.split(sep)) {
51
73
  if (!dir)
52
74
  continue;
53
- for (const suffix of EXE_SUFFIXES) {
75
+ for (const suffix of exeSuffixes(host.platform)) {
54
76
  const candidate = join(dir, cmd + suffix);
55
- try {
56
- if (existsSync(candidate))
57
- return candidate;
58
- }
59
- catch {
60
- // An unreadable PATH entry is not an answer about `cmd`.
61
- }
77
+ if (host.exists(candidate))
78
+ return candidate;
62
79
  }
63
80
  }
64
81
  return null;
@@ -79,67 +96,124 @@ function findOnPath(cmd) {
79
96
  * on PATH) would load two GLib builds into one process, which is a worse problem
80
97
  * than the one it solves.
81
98
  */
82
- function findInMsys2() {
83
- if (process.platform !== 'win32')
99
+ function findInMsys2(host) {
100
+ if (host.platform !== 'win32')
84
101
  return null;
85
- for (const root of MSYS2_ROOTS) {
102
+ for (const root of msys2Roots(host.env)) {
86
103
  for (const env of MSYS2_ENVS) {
87
104
  const bin = join(root, env, 'bin');
88
105
  const script = join(bin, 'blueprint-compiler');
89
106
  const python = join(bin, 'python.exe');
90
- try {
91
- if (!existsSync(script) || !existsSync(python))
92
- continue;
93
- }
94
- catch {
107
+ if (!host.exists(script) || !host.exists(python))
95
108
  continue;
96
- }
97
109
  return {
98
110
  file: python,
99
111
  prefixArgs: [script],
100
- env: { PATH: `${bin};${process.env.PATH ?? ''}` },
112
+ // `;` is a win32 fact, not a host fact — this branch only runs there.
113
+ env: { PATH: `${bin};${host.env.PATH ?? ''}` },
101
114
  source: 'msys2',
102
115
  };
103
116
  }
104
117
  }
105
118
  return null;
106
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
+ }
107
124
  /**
108
125
  * Locate a usable blueprint-compiler, or null when there is none.
109
126
  *
110
127
  * Order: `BLUEPRINT_COMPILER` (an explicit answer always wins, and is the escape
111
128
  * hatch for an install none of the probes below know about) → `PATH` → an MSYS2
112
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.
113
137
  */
114
- export function resolveBlueprintCompiler() {
115
- const override = process.env.BLUEPRINT_COMPILER;
138
+ export function resolveBlueprintCompiler(host = currentBlueprintHost()) {
139
+ const override = host.env.BLUEPRINT_COMPILER;
116
140
  if (override) {
117
- return { file: override, prefixArgs: [], source: 'env' };
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;
118
148
  }
119
- const onPath = findOnPath('blueprint-compiler');
149
+ const onPath = findOnPath('blueprint-compiler', host);
120
150
  if (onPath) {
121
151
  return { file: onPath, prefixArgs: [], source: 'path' };
122
152
  }
123
- return findInMsys2();
153
+ return findInMsys2(host);
124
154
  }
125
155
  /**
126
156
  * What to tell someone who has no blueprint-compiler.
127
157
  *
128
158
  * Names the install command for THIS platform rather than listing every one:
129
159
  * the reader is on one host and the other two lines are noise they have to
130
- * filter. `BLUEPRINT_COMPILER` is mentioned everywhere because it is the answer
131
- * for any install the probes miss.
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.
132
168
  */
133
- export function formatMissingBlueprintCompiler() {
134
- const install = process.platform === 'win32'
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'
135
177
  ? 'MSYS2 packages it prebuilt (it needs PyGObject, which has no Windows wheel, so pip cannot):\n' +
136
178
  ' pacman -S mingw-w64-ucrt-x86_64-blueprint-compiler mingw-w64-ucrt-x86_64-gtk4 mingw-w64-ucrt-x86_64-libadwaita\n' +
137
179
  ' An MSYS2 install under C:\\msys64, %USERPROFILE%\\msys64, %USERPROFILE%\\Tools\\msys64 or\n' +
138
180
  ' C:\\tools\\msys64 is found automatically — it does NOT need to be on PATH.'
139
- : process.platform === 'darwin'
181
+ : host.platform === 'darwin'
140
182
  ? 'brew install blueprint-compiler'
141
- : 'sudo dnf install blueprint-compiler (Debian/Ubuntu: sudo apt install blueprint-compiler)';
142
- return ('blueprint-compiler was not found, so a .blp template cannot be compiled.\n' +
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' +
143
186
  ` ${install}\n` +
144
187
  ' Or set BLUEPRINT_COMPILER to its full path.');
145
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/vite-plugin-blueprint",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Compile GNOME Blueprint .blp templates to XML strings via blueprint-compiler. Vite / Rollup / Rolldown compatible.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -24,9 +24,12 @@
24
24
  "types"
25
25
  ],
26
26
  "scripts": {
27
- "clear": "gjsify clear lib tsconfig.tsbuildinfo",
27
+ "clear": "gjsify clear lib tsconfig.tsbuildinfo test.node.mjs",
28
28
  "check": "gjsify tsc --noEmit",
29
- "build": "gjsify tsc"
29
+ "build": "gjsify tsc -p tsconfig.build.json",
30
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
31
+ "test": "gjsify run build:test:node && gjsify run test:node",
32
+ "test:node": "node test.node.mjs"
30
33
  },
31
34
  "repository": {
32
35
  "type": "git",
@@ -63,6 +66,8 @@
63
66
  }
64
67
  },
65
68
  "devDependencies": {
69
+ "@gjsify/cli": "^0.41.0",
70
+ "@gjsify/unit": "^0.41.0",
66
71
  "@types/node": "^25.9.2",
67
72
  "typescript": "^6.0.3",
68
73
  "vite": "^8.1.3"