@gjsify/vite-plugin-blueprint 0.53.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 +10 -1
- package/lib/index.js +122 -9
- package/lib/projection-error.d.ts +18 -0
- package/lib/projection-error.js +21 -0
- package/package.json +4 -4
- package/types/blueprint.d.ts +13 -0
package/lib/index.d.ts
CHANGED
|
@@ -2,8 +2,17 @@ import { type Plugin } from 'vite';
|
|
|
2
2
|
export interface BlueprintPluginOptions {
|
|
3
3
|
minify?: boolean;
|
|
4
4
|
verbose?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Write each `.blp`'s `x.d.blp.ts` sidecar beside it during `load` (ADR 0088 § 4). On by
|
|
7
|
+
* default: the types are what the named exports are FOR, and a build that emits `build()`
|
|
8
|
+
* without them leaves every consumer on the ambient `*.blp` wildcard.
|
|
9
|
+
*
|
|
10
|
+
* A `.blp` under `node_modules` is never written, whatever this says — see `writeSidecar`.
|
|
11
|
+
*/
|
|
12
|
+
sidecars?: boolean;
|
|
5
13
|
}
|
|
6
|
-
export { BlueprintProjectionError } from './projection-error.js';
|
|
14
|
+
export { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
|
|
15
|
+
export type { RefusedUse } from './projection-error.js';
|
|
7
16
|
/** The specifier an import site writes: `./x.blp?shared-tree`. */
|
|
8
17
|
export declare const SHARED_TREE_QUERY = "?shared-tree";
|
|
9
18
|
export default function blueprintPlugin(options?: BlueprintPluginOptions): Plugin;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname } from 'node:path';
|
|
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';
|
|
3
6
|
import minifyXML from 'minify-xml';
|
|
4
7
|
// Blueprint's OWN two error types are not re-exported here, and that is the shape rather than
|
|
5
8
|
// an omission. What a parse or an emit throws is `@gjsify/blueprint`'s `BlueprintSyntaxError` /
|
|
@@ -13,8 +16,8 @@ import minifyXML from 'minify-xml';
|
|
|
13
16
|
// the file fine and the projection hands back a tree plus the list of what it cost; deciding
|
|
14
17
|
// that such a tree may not reach a renderer is this plugin's decision, taken at the seam where
|
|
15
18
|
// 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';
|
|
19
|
+
export { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
|
|
20
|
+
import { BlueprintProjectionError, RendererRefusalError } from './projection-error.js';
|
|
18
21
|
/**
|
|
19
22
|
* The five seams `@gjsify/blueprint`'s emitter reaches introspection through, answered by that
|
|
20
23
|
* same package out of the `@girs` vocabulary.
|
|
@@ -63,6 +66,12 @@ const SEAMS = Object.freeze({
|
|
|
63
66
|
* "NO blueprintPlugin" comment was one half of.
|
|
64
67
|
*/
|
|
65
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';
|
|
66
75
|
/** The specifier an import site writes: `./x.blp?shared-tree`. */
|
|
67
76
|
export const SHARED_TREE_QUERY = `?${SHARED_TREE_PARAM}`;
|
|
68
77
|
/**
|
|
@@ -84,10 +93,91 @@ function readId(id) {
|
|
|
84
93
|
return undefined;
|
|
85
94
|
if (queryAt === -1)
|
|
86
95
|
return { file, sharedTree: false };
|
|
87
|
-
|
|
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 }) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write the sidecar beside its `.blp`, unless nothing would change.
|
|
102
|
+
*
|
|
103
|
+
* ONLY ON A DIFFERENCE, for two reasons that both bite in watch mode: a write with identical
|
|
104
|
+
* bytes still moves the mtime, which the watcher sees as a change and turns into a rebuild
|
|
105
|
+
* loop; and a `.blp` whose sidecar is already correct should leave the working tree untouched,
|
|
106
|
+
* so `git status` after a build stays a statement about what the author changed.
|
|
107
|
+
*
|
|
108
|
+
* NEVER UNDER `node_modules`. A `.blp` shipped inside a dependency is not this project's to
|
|
109
|
+
* edit, and the tree may be read-only or a store symlink. A failure to write anywhere else is
|
|
110
|
+
* reported and not thrown: the sidecar is the developer's convenience and
|
|
111
|
+
* `scripts/check-blueprint-sidecars.mjs` is the enforcement, so a read-only checkout should
|
|
112
|
+
* still build.
|
|
113
|
+
*/
|
|
114
|
+
async function writeSidecar(file, text, verbose) {
|
|
115
|
+
if (file.includes('node_modules'))
|
|
116
|
+
return;
|
|
117
|
+
const path = sidecarPathFor(file);
|
|
118
|
+
try {
|
|
119
|
+
if ((await readFile(path, 'utf8').catch(() => undefined)) === text)
|
|
120
|
+
return;
|
|
121
|
+
await writeFile(path, text, 'utf8');
|
|
122
|
+
if (verbose)
|
|
123
|
+
console.log(`Wrote ${path} (@gjsify/vite-plugin-blueprint)`);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.warn(`vite-plugin-blueprint: could not write ${path}: ${error.message}`);
|
|
127
|
+
}
|
|
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);
|
|
88
178
|
}
|
|
89
179
|
export default function blueprintPlugin(options = {}) {
|
|
90
|
-
const { minify = false, verbose = false } = options;
|
|
180
|
+
const { minify = false, verbose = false, sidecars = true } = options;
|
|
91
181
|
return {
|
|
92
182
|
name: 'vite-plugin-blueprint',
|
|
93
183
|
// The query has to be resolved by hand: the default resolver stats the specifier, and
|
|
@@ -136,9 +226,11 @@ export default function blueprintPlugin(options = {}) {
|
|
|
136
226
|
// So the build stops here, with the file, the line and the kind of every loss —
|
|
137
227
|
// a porting task a reader can open, not a warning scrolled past. `minify` does
|
|
138
228
|
// not apply: it is an XML setting and this exit emits none.
|
|
139
|
-
const { node, lost } = projectToSharedNode(ast, { gtypeName });
|
|
229
|
+
const { node, lost, uses } = projectToSharedNode(ast, { gtypeName });
|
|
140
230
|
if (lost.length > 0)
|
|
141
231
|
throw new BlueprintProjectionError(asked.file, lost);
|
|
232
|
+
if (asked.renderer !== undefined)
|
|
233
|
+
await refuseForRenderer(this, asked.file, asked.renderer, uses);
|
|
142
234
|
if (verbose)
|
|
143
235
|
console.log(`Projected ${asked.file} (@gjsify/blueprint)`);
|
|
144
236
|
return `export default ${JSON.stringify(node)};`;
|
|
@@ -152,8 +244,29 @@ export default function blueprintPlugin(options = {}) {
|
|
|
152
244
|
if (verbose)
|
|
153
245
|
console.log(`Minified XML for ${asked.file}`);
|
|
154
246
|
}
|
|
155
|
-
//
|
|
156
|
-
|
|
247
|
+
// ADR 0088's named exports, beside the `default` that does not move. The module text
|
|
248
|
+
// is `@gjsify/blueprint`'s, not this file's, because the sidecar DECLARES what this
|
|
249
|
+
// implements and the two have to be written together — `typed-exports.mjs` § the
|
|
250
|
+
// MODULE a bundler gets says why they share a file.
|
|
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
|
+
//
|
|
259
|
+
// The sidecar is written AFTER the module is emitted, so a `.blp` the derivation
|
|
260
|
+
// refuses leaves no sidecar behind claiming exports the module does not have.
|
|
261
|
+
const moduleText = emitTypedModule(ast, xmlContent, format);
|
|
262
|
+
// `basename` from `node:path`, and this plugin is the one caller for which the HOST is
|
|
263
|
+
// the authority: a vite/rolldown plugin only ever runs in the Node process driving the
|
|
264
|
+
// build, on the machine whose paths `asked.file` is a path on. `@gjsify/blueprint`
|
|
265
|
+
// cannot do this itself — see `typed-exports.mjs` § `emitTypedSidecar`.
|
|
266
|
+
if (sidecars) {
|
|
267
|
+
await writeSidecar(asked.file, emitTypedSidecar(ast, basename(asked.file), format), verbose);
|
|
268
|
+
}
|
|
269
|
+
return moduleText;
|
|
157
270
|
},
|
|
158
271
|
};
|
|
159
272
|
}
|
|
@@ -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.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.
|
|
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.
|
|
67
|
-
"@gjsify/unit": "^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"
|
package/types/blueprint.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FALLBACK, and only the fallback, since ADR 0088.
|
|
3
|
+
*
|
|
4
|
+
* A `.blp` with an `x.d.blp.ts` sidecar beside it is typed by that sidecar instead — measured on
|
|
5
|
+
* TypeScript 6.0.3, a sidecar read under `allowArbitraryExtensions` wins over this wildcard. This
|
|
6
|
+
* stays because the migration is per-file and must be: a `.blp` with no sidecar keeps typing as
|
|
7
|
+
* the XML string it has always been, so nothing had to move when the named exports landed.
|
|
8
|
+
*
|
|
9
|
+
* It is also what makes the missing-sidecar case LOUD rather than silent. A consumer writing
|
|
10
|
+
* `import { build } from './x.blp'` against a file with no sidecar lands here, where only
|
|
11
|
+
* `default` exists, and `tsc` reports TS2614 naming each missing member — which is why
|
|
12
|
+
* `scripts/check-blueprint-sidecars.mjs` does not check for that case itself.
|
|
13
|
+
*/
|
|
1
14
|
declare module '*.blp' {
|
|
2
15
|
const content: string;
|
|
3
16
|
export default content;
|