@gjsify/vite-plugin-blueprint 0.54.0 → 0.55.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/lib/index.d.ts CHANGED
@@ -11,7 +11,8 @@ export interface BlueprintPluginOptions {
11
11
  */
12
12
  sidecars?: boolean;
13
13
  }
14
- export { BlueprintProjectionError } from './projection-error.js';
14
+ export { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
15
+ export type { RefusedUse } from './projection-error.js';
15
16
  /** The specifier an import site writes: `./x.blp?shared-tree`. */
16
17
  export declare const SHARED_TREE_QUERY = "?shared-tree";
17
18
  export default function blueprintPlugin(options?: BlueprintPluginOptions): Plugin;
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
- import { basename } from 'node:path';
2
+ import { basename, dirname } from 'node:path';
3
3
  import { accessibilityElement, accessibilityValue, emitGtkBuilderXml, emitTypedModule, emitTypedSidecar, enumOrFlagsTypeOf, gtypeName, parseBlueprint, projectToSharedNode, resolveIdent, sidecarPathFor, } from '@gjsify/blueprint';
4
+ import { emitFormatForTree } from '@gjsify/blueprint/oxfmt';
5
+ import { pathToFileURL } from 'node:url';
4
6
  import minifyXML from 'minify-xml';
5
7
  // Blueprint's OWN two error types are not re-exported here, and that is the shape rather than
6
8
  // an omission. What a parse or an emit throws is `@gjsify/blueprint`'s `BlueprintSyntaxError` /
@@ -14,8 +16,8 @@ import minifyXML from 'minify-xml';
14
16
  // the file fine and the projection hands back a tree plus the list of what it cost; deciding
15
17
  // that such a tree may not reach a renderer is this plugin's decision, taken at the seam where
16
18
  // a build target is known. So it is declared where it is thrown.
17
- export { BlueprintProjectionError } from './projection-error.js';
18
- import { BlueprintProjectionError } from './projection-error.js';
19
+ export { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
20
+ import { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
19
21
  /**
20
22
  * The five seams `@gjsify/blueprint`'s emitter reaches introspection through, answered by that
21
23
  * same package out of the `@girs` vocabulary.
@@ -64,6 +66,12 @@ const SEAMS = Object.freeze({
64
66
  * "NO blueprintPlugin" comment was one half of.
65
67
  */
66
68
  const SHARED_TREE_PARAM = 'shared-tree';
69
+ /**
70
+ * `for=<renderer>` names the renderer an import is checked against (ADR 0093 § 2), by the
71
+ * package name without `@gjsify/`: `./x.blp?shared-tree&for=adwaita-web`. It is chosen at the
72
+ * import site for ADR 0070 § 1's reason — a target does not determine a renderer.
73
+ */
74
+ const RENDERER_PARAM = 'for';
67
75
  /** The specifier an import site writes: `./x.blp?shared-tree`. */
68
76
  export const SHARED_TREE_QUERY = `?${SHARED_TREE_PARAM}`;
69
77
  /**
@@ -85,7 +93,9 @@ function readId(id) {
85
93
  return undefined;
86
94
  if (queryAt === -1)
87
95
  return { file, sharedTree: false };
88
- return { file, sharedTree: new URLSearchParams(id.slice(queryAt + 1)).has(SHARED_TREE_PARAM) };
96
+ const params = new URLSearchParams(id.slice(queryAt + 1));
97
+ const renderer = params.get(RENDERER_PARAM) ?? undefined;
98
+ return { file, sharedTree: params.has(SHARED_TREE_PARAM), ...(renderer === undefined ? {} : { renderer }) };
89
99
  }
90
100
  /**
91
101
  * Write the sidecar beside its `.blp`, unless nothing would change.
@@ -116,6 +126,56 @@ async function writeSidecar(file, text, verbose) {
116
126
  console.warn(`vite-plugin-blueprint: could not write ${path}: ${error.message}`);
117
127
  }
118
128
  }
129
+ /**
130
+ * The emitter options for the tree a `.blp` lives in, memoized per directory.
131
+ *
132
+ * The walk touches the filesystem, and watch mode re-enters `load` for every changed file in a
133
+ * tree that may hold hundreds of `.blp` — so one read per distinct directory, not per file. The
134
+ * walk itself is `emitFormatForTree`'s, shared with the drift gate and `gjsify blueprint types`,
135
+ * because three answers to "which `.oxfmtrc` does the formatter read" is how the 4-space indent
136
+ * reached a `tabWidth: 2` consumer in the first place.
137
+ *
138
+ * Cached for the life of the PROCESS, which is the declared limit: an `.oxfmtrc` edited while a
139
+ * dev server is already running is picked up on the next start, not on the next rebuild. That is
140
+ * the same refresh the formatter itself gets, so a sidecar written after such an edit is the one
141
+ * the running formatter is no longer using — and `check-blueprint-sidecars.mjs` re-derives from
142
+ * disk either way, so the committed bytes stay honest.
143
+ */
144
+ const formatCache = new Map();
145
+ const formatFor = (dir) => {
146
+ const cached = formatCache.get(dir);
147
+ if (cached !== undefined)
148
+ return cached;
149
+ const format = emitFormatForTree(dir);
150
+ formatCache.set(dir, format);
151
+ return format;
152
+ };
153
+ /**
154
+ * Intersect the constructs a file uses with the table of the renderer it is imported for, and
155
+ * throw {@link RendererRefusalError} for each one that renderer refuses (ADR 0093 § 2).
156
+ *
157
+ * The table is resolved from the `.blp`'s own location, so the renderer found is the one the
158
+ * project depends on, and read as pure data: `./capabilities` never reaches the renderer's
159
+ * runtime. A name that resolves to nothing, or a table with no row for a used kind, is an error
160
+ * and not permission.
161
+ */
162
+ async function refuseForRenderer(context, file, renderer, uses) {
163
+ const specifier = `@gjsify/${renderer}/capabilities`;
164
+ const resolved = await context.resolve(specifier, file, { skipSelf: true });
165
+ if (resolved === null) {
166
+ throw new Error(`${file}: \`for=${renderer}\` names no renderer — ${specifier} does not resolve from here.`);
167
+ }
168
+ const { capabilities } = (await import(pathToFileURL(resolved.id).href));
169
+ const refused = uses.flatMap((use) => {
170
+ const capability = capabilities[use.kind];
171
+ if (capability === undefined) {
172
+ throw new Error(`${specifier} declares no row for the construct '${use.kind}'.`);
173
+ }
174
+ return capability === 'implemented' ? [] : [{ ...use, reason: capability.refused }];
175
+ });
176
+ if (refused.length > 0)
177
+ throw new RendererRefusalError(file, renderer, refused);
178
+ }
119
179
  export default function blueprintPlugin(options = {}) {
120
180
  const { minify = false, verbose = false, sidecars = true } = options;
121
181
  return {
@@ -166,9 +226,11 @@ export default function blueprintPlugin(options = {}) {
166
226
  // So the build stops here, with the file, the line and the kind of every loss —
167
227
  // a porting task a reader can open, not a warning scrolled past. `minify` does
168
228
  // not apply: it is an XML setting and this exit emits none.
169
- const { node, lost } = projectToSharedNode(ast, { gtypeName });
229
+ const { node, lost, uses } = projectToSharedNode(ast, { gtypeName });
170
230
  if (lost.length > 0)
171
231
  throw new BlueprintProjectionError(asked.file, lost);
232
+ if (asked.renderer !== undefined)
233
+ await refuseForRenderer(this, asked.file, asked.renderer, uses);
172
234
  if (verbose)
173
235
  console.log(`Projected ${asked.file} (@gjsify/blueprint)`);
174
236
  return `export default ${JSON.stringify(node)};`;
@@ -187,15 +249,23 @@ export default function blueprintPlugin(options = {}) {
187
249
  // implements and the two have to be written together — `typed-exports.mjs` § the
188
250
  // MODULE a bundler gets says why they share a file.
189
251
  //
252
+ // The format is the PROJECT's `.oxfmtrc`, resolved once per directory beside the
253
+ // `.blp`, and it is what makes this sidecar survive the consumer's own
254
+ // `gjsify format --check`: the emitter once hardcoded a 4-space indent, which is this
255
+ // repository's value and nobody else's (ADR 0088 § Consequences). Same walk as the
256
+ // drift gate and `gjsify blueprint types`, so the three cannot answer differently.
257
+ const format = formatFor(dirname(asked.file));
258
+ //
190
259
  // The sidecar is written AFTER the module is emitted, so a `.blp` the derivation
191
260
  // refuses leaves no sidecar behind claiming exports the module does not have.
192
- const moduleText = emitTypedModule(ast, xmlContent);
261
+ const moduleText = emitTypedModule(ast, xmlContent, format);
193
262
  // `basename` from `node:path`, and this plugin is the one caller for which the HOST is
194
263
  // the authority: a vite/rolldown plugin only ever runs in the Node process driving the
195
264
  // build, on the machine whose paths `asked.file` is a path on. `@gjsify/blueprint`
196
265
  // cannot do this itself — see `typed-exports.mjs` § `emitTypedSidecar`.
197
- if (sidecars)
198
- await writeSidecar(asked.file, emitTypedSidecar(ast, basename(asked.file)), verbose);
266
+ if (sidecars) {
267
+ await writeSidecar(asked.file, emitTypedSidecar(ast, basename(asked.file), format), verbose);
268
+ }
199
269
  return moduleText;
200
270
  },
201
271
  };
@@ -21,3 +21,21 @@ export declare class BlueprintProjectionError extends Error {
21
21
  readonly lost: readonly ProjectedLoss[];
22
22
  constructor(file: string, lost: readonly ProjectedLoss[]);
23
23
  }
24
+ /** One construct a named renderer refuses, with the line it sits on and the table's reason. */
25
+ export interface RefusedUse {
26
+ kind: string;
27
+ line: number;
28
+ reason: string;
29
+ }
30
+ /**
31
+ * A `.blp` imported with `?shared-tree&for=<renderer>` that uses constructs the renderer's
32
+ * `./capabilities` table refuses (ADR 0093 § 2).
33
+ *
34
+ * A subclass of {@link BlueprintProjectionError} so one `instanceof` catches both refusals; `lost`
35
+ * is empty, because the projection itself was whole — the renderer is what says no.
36
+ */
37
+ export declare class RendererRefusalError extends BlueprintProjectionError {
38
+ readonly renderer: string;
39
+ readonly refused: readonly RefusedUse[];
40
+ constructor(file: string, renderer: string, refused: readonly RefusedUse[]);
41
+ }
@@ -32,3 +32,24 @@ export class BlueprintProjectionError extends Error {
32
32
  this.lost = lost;
33
33
  }
34
34
  }
35
+ /**
36
+ * A `.blp` imported with `?shared-tree&for=<renderer>` that uses constructs the renderer's
37
+ * `./capabilities` table refuses (ADR 0093 § 2).
38
+ *
39
+ * A subclass of {@link BlueprintProjectionError} so one `instanceof` catches both refusals; `lost`
40
+ * is empty, because the projection itself was whole — the renderer is what says no.
41
+ */
42
+ export class RendererRefusalError extends BlueprintProjectionError {
43
+ renderer;
44
+ refused;
45
+ constructor(file, renderer, refused) {
46
+ super(file, []);
47
+ const listed = refused.map((use) => ` ${use.kind} at ${file}:${use.line} — ${use.reason}`).join('\n');
48
+ this.message =
49
+ `${file} cannot be rendered by ${renderer}: ${refused.length} construct(s) it refuses.\n${listed}\n` +
50
+ 'The GtkBuilder-XML exit of the same file is unaffected.';
51
+ this.name = 'RendererRefusalError';
52
+ this.renderer = renderer;
53
+ this.refused = refused;
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/vite-plugin-blueprint",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
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",
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "license": "MIT",
49
49
  "dependencies": {
50
- "@gjsify/blueprint": "^0.54.0",
50
+ "@gjsify/blueprint": "^0.55.0",
51
51
  "minify-xml": "^4.5.2"
52
52
  },
53
53
  "peerDependencies": {
@@ -63,8 +63,8 @@
63
63
  }
64
64
  },
65
65
  "devDependencies": {
66
- "@gjsify/cli": "^0.54.0",
67
- "@gjsify/unit": "^0.54.0",
66
+ "@gjsify/cli": "^0.55.0",
67
+ "@gjsify/unit": "^0.55.0",
68
68
  "@types/node": "^25.9.2",
69
69
  "typescript": "^6.0.3",
70
70
  "vite": "^8.1.3"