@gjsify/vite-plugin-blueprint 0.54.0 → 0.56.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 +2 -1
- package/lib/index.js +78 -8
- package/lib/projection-error.d.ts +18 -0
- package/lib/projection-error.js +21 -0
- package/package.json +4 -4
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
|
-
|
|
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
|
+
}
|
package/lib/projection-error.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.56.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.
|
|
50
|
+
"@gjsify/blueprint": "^0.56.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.
|
|
67
|
-
"@gjsify/unit": "^0.
|
|
66
|
+
"@gjsify/cli": "^0.56.0",
|
|
67
|
+
"@gjsify/unit": "^0.56.0",
|
|
68
68
|
"@types/node": "^25.9.2",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
70
|
"vite": "^8.1.3"
|