@vormio/plugin-sdk 0.0.7-rc.1 → 0.0.9-rc.1
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 +49 -4
- package/dist/bridge.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/manifest.js +1 -1
- package/dist/report.js +4 -0
- package/dist/runtime.d.ts +11 -2
- package/dist/runtime.js +15 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @vormio/plugin-sdk
|
|
2
2
|
|
|
3
|
-
The manifest schema, runtime helpers and packaging command for writing Vormio plugins, plus the commands that publish a signed plugin catalog. The package is versioned with Vormio-Core (currently 0.0.
|
|
3
|
+
The manifest schema, runtime helpers and packaging command for writing Vormio plugins, plus the commands that publish a signed plugin catalog. The package is versioned with Vormio-Core (currently 0.0.9) and published to npm under the MIT license: `npm install @vormio/plugin-sdk@0.0.9`.
|
|
4
4
|
|
|
5
|
-
Plugin contract version: `apiVersion:
|
|
5
|
+
Plugin contract version: `apiVersion: 4`. During development (0.0.x) there is no cross-version compatibility promise: the host loads only plugins with the same `apiVersion` as its own.
|
|
6
6
|
|
|
7
7
|
## What a plugin can do
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ A plugin is a folder: a `manifest.json` at the root plus the built static files.
|
|
|
28
28
|
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
|
-
"apiVersion":
|
|
31
|
+
"apiVersion": 4,
|
|
32
32
|
"id": "acme.model-viewer",
|
|
33
33
|
"version": "0.1.0",
|
|
34
34
|
"name": "Model Viewer",
|
|
@@ -117,6 +117,7 @@ analyzer(async (input) => {
|
|
|
117
117
|
|
|
118
118
|
- Pages that do not use these helpers can call `window.vormio` directly; see the `PluginBridge` type in `@vormio/plugin-sdk/bridge`. A thumbnail handler returns the PNG as an `ArrayBuffer`, and an analyzer handler (`onAnalyzeRequest`) returns the composition report; either can return `{ notApplicable: true }` instead, and a viewer calls `notApplicable(input)`.
|
|
119
119
|
- While a plugin view has focus, unmodified Esc, ← and → are handled by the host (back to the grid, previous, next); the plugin does not receive these keys.
|
|
120
|
+
- `viewer(show, { focus })` also receives the host's requests to show a subresource of the shown file; see [Focusing a subresource](#focusing-a-subresource).
|
|
120
121
|
|
|
121
122
|
## Files that are not your format
|
|
122
123
|
|
|
@@ -162,6 +163,7 @@ interface CompositionReport {
|
|
|
162
163
|
from?: string // Key of the referencing subresource; omitted means the file itself
|
|
163
164
|
uri?: string // Points to another file: the raw address written in the file
|
|
164
165
|
key?: string // Points to a subresource in this file; give exactly one of uri and key
|
|
166
|
+
uriKey?: string // With uri: a subresource of that file, by the key its report gives it
|
|
165
167
|
role: string // Display role, e.g. "Base color texture"
|
|
166
168
|
evidence?: string // Location in the file, e.g. "/images/2/uri"
|
|
167
169
|
inferred?: string // Not written in the file: how you inferred it, e.g. "Same name as the skeleton"
|
|
@@ -174,7 +176,7 @@ interface CompositionReport {
|
|
|
174
176
|
```
|
|
175
177
|
|
|
176
178
|
- The host validates reports with `compositionReportSchema` (`parseReport` also checks the size); plugins can check themselves against the same schema before returning. The report serialized as JSON is at most 4 MiB (counted in UTF-8 bytes). Names and keys are at most 200 characters, kinds and roles 80, bases of inference 120, evidence 300, diagnostics 500 and addresses 2,048. Names, kinds, roles, bases of inference and evidence must not contain control characters.
|
|
177
|
-
- The report is invalid if it has duplicate keys, a `parent`, `from` or `key` that points to a missing subresource, a cyclic hierarchy, a reference with both or neither of `uri` and `key`, or an `inferred` reference with a `key`.
|
|
179
|
+
- The report is invalid if it has duplicate keys, a `parent`, `from` or `key` that points to a missing subresource, a cyclic hierarchy, a reference with both or neither of `uri` and `key`, a `uriKey` without a `uri`, or an `inferred` reference with a `key`. A `uriKey` is not checked against this report: it names a subresource of the other file.
|
|
178
180
|
- Write `uri` exactly as it appears in the file; do not resolve it yourself. The host decodes percent-encoding once, ignores everything after `?` and `#`, treats `\` as `/`, resolves it relative to the analyzed file and classifies it: a file in the linked folder, out of bounds (`..` leaving the linked folder), embedded (`data:`), external (absolute paths, drive letters, other schemes) or invalid (undecodable, containing control characters, or pointing to a folder). Whether the target exists is decided by the host at query time.
|
|
179
181
|
- For embedded data (`data:`), report only the header up to the comma, e.g. `data:image/png;base64`. The host classifies it as embedded by prefix and does not store the content; a full `data:` address often exceeds 2,048 characters and would invalidate the whole report.
|
|
180
182
|
- Reporting a reference does not require reading the target file, so you do not need `companionFormats` just to report references. Declare them only when the analysis actually reads companion files.
|
|
@@ -204,6 +206,49 @@ analyzer(async (input) => {
|
|
|
204
206
|
- The inference may depend on the file's name. When an analyzed file with inferred references is renamed or moved, the host analyzes it again.
|
|
205
207
|
- Do not report the same target both as written and as inferred.
|
|
206
208
|
|
|
209
|
+
### References to subresources of other files
|
|
210
|
+
|
|
211
|
+
When a format looks an object up by name in another file, report the object itself: give the file's address in `uri` and the object's key in that file's report in `uriKey`. A Spine skeleton's attachment uses the region of the same name in the paired atlas:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
{
|
|
215
|
+
from: 'attachments/default/head/head',
|
|
216
|
+
uri: 'hero.atlas',
|
|
217
|
+
uriKey: 'region:head-front', // The key your atlas analyzer gives that region
|
|
218
|
+
role: 'Region',
|
|
219
|
+
inferred: 'Same name as the skeleton', // The atlas itself is inferred
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- A key that other files refer to is a contract within your plugin: derive it from the format's own lookup rule (for Spine, the region name), so that it stays the same when the target file is exported again. Never use a position in a list.
|
|
224
|
+
- The host connects the two reports only when the same build of your plugin analyzed both files (same plugin ID and package). Your analyzers for both formats must be in one plugin.
|
|
225
|
+
- Do not read the target file to check that the subresource exists. The host decides at query time, from the target's current report, and the status follows when the target is analyzed again:
|
|
226
|
+
|
|
227
|
+
| The target file | Written reference | Inferred reference |
|
|
228
|
+
| --------------------------------------------------------------------------------- | --------------------------- | ------------------------ |
|
|
229
|
+
| Does not exist | Missing | Not found |
|
|
230
|
+
| Its report has the key | Resolved | Resolved |
|
|
231
|
+
| Its complete report lacks the key | Missing | Not found |
|
|
232
|
+
| Its partial report lacks the key | Unknown (it may be omitted) | Unknown |
|
|
233
|
+
| Has no report, an outdated one, a failed one, or one from another plugin or build | Unknown, with the reason | Unknown, with the reason |
|
|
234
|
+
|
|
235
|
+
- Keys follow the limits of `key`: at most 200 characters. Do not put the key in the address: the host ignores everything after `?` and `#`.
|
|
236
|
+
|
|
237
|
+
## Focusing a subresource
|
|
238
|
+
|
|
239
|
+
When the user picks a subresource in the inspector, or jumps to a subresource of another file, the host asks the viewer to show it:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
viewer(showAtlas, {
|
|
243
|
+
focus: (key, input) => highlightRegion(key),
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- The key is from your own analyzer's report: the host asks only when the viewer and the shown file's analysis come from the same build of your plugin.
|
|
248
|
+
- `focus` is called only after the viewer reported the file ready. A request about a file the viewer has moved away from is dropped. The same key can come again when the user asks again; show it again.
|
|
249
|
+
- Find it if you can, for example by selecting, framing or highlighting it; ignore keys you cannot find. The host does not wait for an answer.
|
|
250
|
+
- Pages that do not use `viewer()` subscribe with `window.vormio.onFocus(({ url, key }) => …)` and compare `url` with the input they show.
|
|
251
|
+
|
|
207
252
|
## Packaging and development
|
|
208
253
|
|
|
209
254
|
The package declares `"sideEffects": false`: when a plugin page uses only the runtime helpers, bundlers (esbuild, Vite, Rollup, webpack) drop the manifest schema and its zod dependency, leaving about 1 KB of runtime code for a minimal page. Node-side packaging and validation are imported from `@vormio/plugin-sdk/node`.
|
package/dist/bridge.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface ReportReference {
|
|
|
35
35
|
uri?: string;
|
|
36
36
|
/** A subresource of the analyzed file. */
|
|
37
37
|
key?: string;
|
|
38
|
+
/**
|
|
39
|
+
* A subresource of the file at `uri`, by the key that file's report gives it; only with `uri`.
|
|
40
|
+
* The host connects it when the same build of your plugin analyzed that file.
|
|
41
|
+
*/
|
|
42
|
+
uriKey?: string;
|
|
38
43
|
/** Display label, such as "Base color texture". */
|
|
39
44
|
role: string;
|
|
40
45
|
/** Where in the file the reference is, such as "/images/2/uri". */
|
|
@@ -84,6 +89,15 @@ export interface PluginBridge {
|
|
|
84
89
|
* preview.
|
|
85
90
|
*/
|
|
86
91
|
notApplicable(input: PluginInput): void;
|
|
92
|
+
/**
|
|
93
|
+
* Called when the host asks the viewer to show a subresource of the input with `url`, by its key
|
|
94
|
+
* in your analyzer's report: only after `ready` for that input, and again whenever it is asked
|
|
95
|
+
* again. Returns the function that unsubscribes.
|
|
96
|
+
*/
|
|
97
|
+
onFocus(listener: (focus: {
|
|
98
|
+
url: string;
|
|
99
|
+
key: string;
|
|
100
|
+
}) => void): () => void;
|
|
87
101
|
/**
|
|
88
102
|
* Thumbnail pages: handles each request from the host and returns PNG bytes no larger than
|
|
89
103
|
* `maxEdge` on either side, or `{ notApplicable: true }`. (Blobs cannot cross the bridge;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { contributionsOf, manifestSchema, missingEntries, parseManifest, permiss
|
|
|
2
2
|
export { checkPackageFiles, checkPackagePath, packageExtensions, packageLimits, type PackageFile, } from './package-rules.ts';
|
|
3
3
|
export { compositionReportSchema, parseReport, reportLimits, type ReportResult } from './report.ts';
|
|
4
4
|
export type { ReportDiagnostic, ReportReference, ReportSubresource } from './bridge.ts';
|
|
5
|
-
export { analyzer, bridge, isNotApplicable, notApplicable, readCompanion, readInput, thumbnail, viewer, type CompanionResult, type CompanionStatus, type CompositionReport, type NotApplicable, type PluginBridge, type PluginInput, } from './runtime.ts';
|
|
5
|
+
export { analyzer, bridge, isNotApplicable, notApplicable, readCompanion, readInput, thumbnail, viewer, type CompanionResult, type CompanionStatus, type CompositionReport, type NotApplicable, type PluginBridge, type PluginInput, type ViewerOptions, } from './runtime.ts';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
/** The plugin contract version this SDK describes. Hosts load only plugins with this version. */
|
|
3
|
-
export declare const pluginApiVersion =
|
|
3
|
+
export declare const pluginApiVersion = 4;
|
|
4
4
|
declare const contributionSchema: z.ZodObject<{
|
|
5
5
|
id: z.ZodString;
|
|
6
6
|
entry: z.ZodString;
|
|
@@ -13,7 +13,7 @@ export declare const pluginIdSchema: z.ZodString;
|
|
|
13
13
|
/** A semantic version without build metadata. */
|
|
14
14
|
export declare const versionSchema: z.ZodString;
|
|
15
15
|
export declare const manifestSchema: z.ZodObject<{
|
|
16
|
-
apiVersion: z.ZodLiteral<
|
|
16
|
+
apiVersion: z.ZodLiteral<4>;
|
|
17
17
|
id: z.ZodString;
|
|
18
18
|
version: z.ZodString;
|
|
19
19
|
name: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { checkPackagePath } from "./package-rules.js";
|
|
3
3
|
/** The plugin contract version this SDK describes. Hosts load only plugins with this version. */
|
|
4
|
-
export const pluginApiVersion =
|
|
4
|
+
export const pluginApiVersion = 4;
|
|
5
5
|
const unique = (values) => new Set(values).size === values.length;
|
|
6
6
|
const extension = z.string().regex(/^[a-z0-9]{1,16}$/, 'Use a lower-case extension without a dot');
|
|
7
7
|
const extensions = (min) => z.array(extension).min(min).max(64).refine(unique, 'Extensions must not repeat');
|
package/dist/report.js
CHANGED
|
@@ -25,6 +25,7 @@ const referenceSchema = z
|
|
|
25
25
|
from: key.optional(),
|
|
26
26
|
uri: z.string().min(1).max(2048).optional(),
|
|
27
27
|
key: key.optional(),
|
|
28
|
+
uriKey: key.optional(),
|
|
28
29
|
role: plain(80).min(1),
|
|
29
30
|
evidence: plain(300).optional(),
|
|
30
31
|
inferred: plain(120).min(1).optional(),
|
|
@@ -34,6 +35,9 @@ const referenceSchema = z
|
|
|
34
35
|
})
|
|
35
36
|
.refine((reference) => reference.inferred === undefined || reference.uri !== undefined, {
|
|
36
37
|
message: 'Only a reference with a uri can be inferred',
|
|
38
|
+
})
|
|
39
|
+
.refine((reference) => reference.uriKey === undefined || reference.uri !== undefined, {
|
|
40
|
+
message: 'Give uriKey only with uri',
|
|
37
41
|
});
|
|
38
42
|
const diagnosticSchema = z.strictObject({
|
|
39
43
|
severity: z.enum(['error', 'warning', 'info']),
|
package/dist/runtime.d.ts
CHANGED
|
@@ -15,13 +15,22 @@ export declare function bridge(): PluginBridge;
|
|
|
15
15
|
export declare function readCompanion(input: PluginInput, relativePath: string): Promise<CompanionResult>;
|
|
16
16
|
/** Reads the input's own bytes. */
|
|
17
17
|
export declare function readInput(input: PluginInput): Promise<ArrayBuffer>;
|
|
18
|
+
/** What a viewer can do besides showing its input. */
|
|
19
|
+
export interface ViewerOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Shows a subresource of the shown input, by its key in your analyzer's report, for example by
|
|
22
|
+
* highlighting it. Called only once the input is shown, and again each time the host asks again;
|
|
23
|
+
* ignore keys you cannot find.
|
|
24
|
+
*/
|
|
25
|
+
focus?: (key: string, input: PluginInput) => void;
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Runs a viewer: `show` is called for the first input and again whenever the host moves to another
|
|
20
29
|
* file, with the previous call's signal aborted. Resolving reports ready, resolving with
|
|
21
30
|
* `notApplicable()` reports the file is not this viewer's format, and throwing reports the error
|
|
22
|
-
* message.
|
|
31
|
+
* message. `options.focus` receives the host's requests to show a subresource of the shown input.
|
|
23
32
|
*/
|
|
24
|
-
export declare function viewer(show: (input: PluginInput, signal: AbortSignal) => Promise<void | NotApplicable
|
|
33
|
+
export declare function viewer(show: (input: PluginInput, signal: AbortSignal) => Promise<void | NotApplicable>, options?: ViewerOptions): void;
|
|
25
34
|
/** Runs a thumbnail provider; a canvas is converted to PNG. `render` may return `notApplicable()`. */
|
|
26
35
|
export declare function thumbnail(render: (input: PluginInput, size: {
|
|
27
36
|
maxEdge: number;
|
package/dist/runtime.js
CHANGED
|
@@ -49,13 +49,16 @@ export async function readInput(input) {
|
|
|
49
49
|
* Runs a viewer: `show` is called for the first input and again whenever the host moves to another
|
|
50
50
|
* file, with the previous call's signal aborted. Resolving reports ready, resolving with
|
|
51
51
|
* `notApplicable()` reports the file is not this viewer's format, and throwing reports the error
|
|
52
|
-
* message.
|
|
52
|
+
* message. `options.focus` receives the host's requests to show a subresource of the shown input.
|
|
53
53
|
*/
|
|
54
|
-
export function viewer(show) {
|
|
54
|
+
export function viewer(show, options = {}) {
|
|
55
55
|
const host = bridge();
|
|
56
56
|
let current;
|
|
57
|
+
/** The input the viewer last reported ready, until it moves to another. */
|
|
58
|
+
let shown;
|
|
57
59
|
const run = async (input) => {
|
|
58
60
|
current?.abort();
|
|
61
|
+
shown = undefined;
|
|
59
62
|
const controller = new AbortController();
|
|
60
63
|
current = controller;
|
|
61
64
|
try {
|
|
@@ -64,8 +67,10 @@ export function viewer(show) {
|
|
|
64
67
|
return;
|
|
65
68
|
if (isNotApplicable(result))
|
|
66
69
|
host.notApplicable(input);
|
|
67
|
-
else
|
|
70
|
+
else {
|
|
71
|
+
shown = input;
|
|
68
72
|
host.ready(input);
|
|
73
|
+
}
|
|
69
74
|
}
|
|
70
75
|
catch (error) {
|
|
71
76
|
if (!controller.signal.aborted)
|
|
@@ -73,6 +78,13 @@ export function viewer(show) {
|
|
|
73
78
|
}
|
|
74
79
|
};
|
|
75
80
|
host.onInput((input) => void run(input));
|
|
81
|
+
const { focus } = options;
|
|
82
|
+
if (focus)
|
|
83
|
+
host.onFocus(({ url, key }) => {
|
|
84
|
+
// A request about a file the viewer has moved away from is void.
|
|
85
|
+
if (shown?.url === url)
|
|
86
|
+
focus(key, shown);
|
|
87
|
+
});
|
|
76
88
|
void host.getInput().then(run);
|
|
77
89
|
}
|
|
78
90
|
/** Runs a thumbnail provider; a canvas is converted to PNG. `render` may return `notApplicable()`. */
|