@mosaicast/plugin-sdk 0.3.0 → 0.4.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 +168 -3
- package/dist/index.d.ts +299 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +76 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +90 -9
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,11 @@ src/testing.ts TS: @mosaicast/plugin-sdk/testing — makeMockCtx
|
|
|
29
29
|
|
|
30
30
|
The contract version is a **single SemVer anchor** mirrored in four places that MUST move together
|
|
31
31
|
(CI enforces it): `build.gradle.kts` · `package.json` · `PlatformApi.VERSION` · `PLATFORM_API_VERSION`.
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
**How the host matches it:** core compares `major.minor` **exactly**. A plugin declaring `0.3.x` is
|
|
34
|
+
rejected the moment the host runs `0.4.0` — with the reason shown in the admin log viewer. While the SDK
|
|
35
|
+
is pre-1.0 a breaking change is therefore a **minor** bump (`0.3.0` → `0.4.0`), not a major one; from
|
|
36
|
+
`1.0.0` on, normal SemVer applies and breaking means major.
|
|
33
37
|
|
|
34
38
|
## Build & test
|
|
35
39
|
```bash
|
|
@@ -45,8 +49,8 @@ npm ci && npm run build # TypeScript: src → dist (.js + .d.ts)
|
|
|
45
49
|
- Released: from **GitHub Packages** (see below).
|
|
46
50
|
```kotlin
|
|
47
51
|
dependencies {
|
|
48
|
-
compileOnly("dev.mosaicast:plugin-api:0.
|
|
49
|
-
testImplementation("dev.mosaicast:plugin-testkit:0.
|
|
52
|
+
compileOnly("dev.mosaicast:plugin-api:0.4.0") // contract, provided by the host
|
|
53
|
+
testImplementation("dev.mosaicast:plugin-testkit:0.4.0") // test doubles only
|
|
50
54
|
}
|
|
51
55
|
```
|
|
52
56
|
Sources + Javadoc JARs give IDE hover docs automatically.
|
|
@@ -110,6 +114,167 @@ DELETE /api/plugins/{id}/data/{scopeType}/{scopeId}/{key}
|
|
|
110
114
|
|
|
111
115
|
> Roadmap: custom plugin-defined server routes may arrive in a later `platformApi` version; plugins use the doc store.
|
|
112
116
|
|
|
117
|
+
## Relational storage — `ctx.schema()` (since 0.4.0)
|
|
118
|
+
|
|
119
|
+
The doc store is the default and covers nearly everything. Declare a **schema** when you need what a JSON
|
|
120
|
+
document cannot give you: full-text search, revisions, backlinks. Any plugin may; most declare nothing.
|
|
121
|
+
|
|
122
|
+
You declare entities and fields in the manifest, and the **platform** provisions namespaced tables
|
|
123
|
+
(`plugin_<id>_*`) through its managed migration runner, dropping them when an admin purges the plugin.
|
|
124
|
+
**The plugin never writes DDL and never names a table:**
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
"storage": { "schema": { "page": {
|
|
128
|
+
"slug": "string:indexed:unique", "title": "string",
|
|
129
|
+
"markdown": "text:fulltext", "updatedAt": "timestamp:indexed" } } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```java
|
|
133
|
+
record Page(long id, String slug, String title, String markdown, Instant updatedAt) {}
|
|
134
|
+
|
|
135
|
+
SchemaStore schema = ctx.schema(); // null unless the manifest declares one
|
|
136
|
+
|
|
137
|
+
long id = schema.insert("page", Map.of(
|
|
138
|
+
"slug", "getting-started", "title", "Getting started",
|
|
139
|
+
"markdown", "# Hello", "updatedAt", Instant.now()));
|
|
140
|
+
|
|
141
|
+
Optional<Page> byId = schema.find("page", id, Page.class);
|
|
142
|
+
List<Page> recent = schema.select("page",
|
|
143
|
+
Criteria.all().orderBy("updatedAt", Direction.DESC).limit(20), Page.class);
|
|
144
|
+
List<Page> hits = schema.search("page", "markdown", "lighthouse", Criteria.all(), Page.class);
|
|
145
|
+
long total = schema.count("page", Criteria.all());
|
|
146
|
+
schema.update("page", id, Map.of("markdown", "# Hello again"));
|
|
147
|
+
schema.delete("page", Criteria.where("slug", Op.EQ, "getting-started"));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- Everything is addressed by **declared entity and field name — never SQL, never a table**. That is the
|
|
151
|
+
scoping guarantee: reaching another plugin's tables isn't blocked, it's inexpressible. The host binds
|
|
152
|
+
every value as a JDBC parameter and checks every field name against your manifest.
|
|
153
|
+
- Rows map to **your own record types**, component names matching field names — the same convention as
|
|
154
|
+
`store().get(...)` and `config().get(...)`. Each entity carries a platform-assigned `long id`.
|
|
155
|
+
- `Criteria` is immutable; predicates combine with **AND** (no `or` in 0.4.0). `search` needs a field
|
|
156
|
+
declared `:fulltext`; for a plain match use `Op.LIKE` with `select`.
|
|
157
|
+
- Naming an undeclared entity or field throws `IllegalArgumentException` — against the test kit too, so a
|
|
158
|
+
manifest that drifted from the code fails in your tests.
|
|
159
|
+
|
|
160
|
+
Test it with `FakeSchemaStore`, declaring the same entities your manifest does. Its `search` is a
|
|
161
|
+
substring match, **not** Postgres full-text: no stemming, no ranking. Assert on which rows come back, not
|
|
162
|
+
on their order.
|
|
163
|
+
|
|
164
|
+
## Logging — `ctx.logger()` / `ctx.log()` (since 0.4.0)
|
|
165
|
+
|
|
166
|
+
```java
|
|
167
|
+
ctx.logger().info("indexed {} pages", count); // org.slf4j.Logger, named plugin.<pluginId>
|
|
168
|
+
```
|
|
169
|
+
```ts
|
|
170
|
+
ctx.log('warn', `no board for episode ${ctx.scope.id}`);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A plain SLF4J `Logger` on the backend: authors know the API, and parameterised messages and throwables
|
|
174
|
+
come free. **Attribution rides in the logger name**, which is why the host hands you a named logger rather
|
|
175
|
+
than offering `log(level, message)` — the name travels with the event, so output is still attributed to
|
|
176
|
+
your plugin from a thread you started or from an `onSchedule` task, where a thread-local MDC arrives
|
|
177
|
+
empty. Don't build your own `LoggerFactory.getLogger(...)`: it won't sit under the `plugin.` prefix and
|
|
178
|
+
the host can't attribute it.
|
|
179
|
+
|
|
180
|
+
The host stores `info` and above, surfaces `warn` and above in the admin log viewer, and **rate-limits**
|
|
181
|
+
this path. On the frontend, `ctx.log` replaces POSTing to `/api/plugins/{id}/log` through `ctx.api`.
|
|
182
|
+
`FakePluginContext.logger()` returns a `RecordingLogger`, so a test asserts on logging the way it asserts
|
|
183
|
+
on stored documents; `makeMockCtx` collects `ctx.logs`.
|
|
184
|
+
|
|
185
|
+
## Consent & the manifest (core-owned schema)
|
|
186
|
+
|
|
187
|
+
Core runs **banner-free** — strictly necessary cookies need no consent. The only reason a visitor sees a
|
|
188
|
+
consent prompt is a plugin that asked for one, so treat it as the cost it is.
|
|
189
|
+
|
|
190
|
+
### `ctx.consent` (frontend)
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
if (ctx.consent.has('analytics')) mountChart(root);
|
|
194
|
+
else button.onclick = async () => {
|
|
195
|
+
if (await ctx.consent.request('analytics')) mountChart(root); // from a user gesture, never on mount
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const off = ctx.consent.onChange(() => rerender()); // fires on every change
|
|
199
|
+
return off; // detach on cleanup
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`request(category)` opens the host's settings for that one purpose and resolves with the visitor's answer
|
|
203
|
+
(`false` if dismissed). `granted()` lists everything currently granted. `onChange` fires on **every**
|
|
204
|
+
change — including a withdrawal made in the settings page while you are mounted — and returns an
|
|
205
|
+
unsubscribe. Since 0.4.0 `filter`, `route`, `locale` and `player.on` return one too.
|
|
206
|
+
|
|
207
|
+
**Services describe, categories decide.** The manifest is per-service (`provider`, `privacyUrl`,
|
|
208
|
+
`thirdCountryTransfer`, each `storage[]` item) but every `ctx.consent` method takes a **category**. The
|
|
209
|
+
per-service detail exists so the notice can name who stores what for how long; what the visitor toggles is
|
|
210
|
+
the category. So if two plugins each declare an `analytics` service with different providers, the visitor
|
|
211
|
+
sees **one** decision listing both plugins, and granting it grants both — there is no way to accept one
|
|
212
|
+
provider and refuse the other. If you need your own services to be refusable independently, declare them
|
|
213
|
+
under **different categories** (a plugin-declared category is fine; the shell just has no translated label
|
|
214
|
+
for it). That is the only lever the contract gives you.
|
|
215
|
+
|
|
216
|
+
**`necessary` is never asked about.** `has('necessary')` is always `true` and the host never prompts for
|
|
217
|
+
it — it is the category the core itself uses, and a banner-free site stays banner-free. Declaring a
|
|
218
|
+
service `necessary` means "this loads unconditionally"; use it only for what genuinely cannot be refused.
|
|
219
|
+
|
|
220
|
+
**Concurrent `request()` calls.** A page of plugin tiles will produce them, so the contract is explicit:
|
|
221
|
+
there is **one consent surface host-wide** (a second call joins the one in flight rather than opening
|
|
222
|
+
another); the visitor decides **all** categories in one interaction, so other categories may move too;
|
|
223
|
+
every call resolves exactly once and is never dropped; and grants are **shared across plugins** — another
|
|
224
|
+
plugin's accepted `request('analytics')` resolves yours too. Don't serialize calls or build a queue, and
|
|
225
|
+
re-read `has(...)` after a change instead of caching what a request resolved with.
|
|
226
|
+
|
|
227
|
+
### `consent.services[]` in `plugin.json`
|
|
228
|
+
|
|
229
|
+
The manifest type is **core-owned** — the SDK has no manifest type and will not grow one — but this is
|
|
230
|
+
where plugin authors look, so the required shape is documented here. **From `0.4.0` the legacy
|
|
231
|
+
`{ "categories": [...], "externalSources": [...] }` form is rejected** and the plugin will not load.
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
"consent": {
|
|
235
|
+
"services": [{
|
|
236
|
+
"id": "plausible",
|
|
237
|
+
"name": "Plausible Analytics",
|
|
238
|
+
"provider": "Plausible Insights OÜ",
|
|
239
|
+
"category": "necessary | functional | analytics | <plugin-declared>",
|
|
240
|
+
"privacyUrl": "https://plausible.io/privacy",
|
|
241
|
+
"hosts": ["https://plausible.example"],
|
|
242
|
+
"thirdCountryTransfer": false,
|
|
243
|
+
"storage": [
|
|
244
|
+
{ "name": "plausible_ignore", "type": "cookie | localStorage | sessionStorage",
|
|
245
|
+
"purpose": "Remembers that you opted out of statistics", "duration": "persistent | session | 12 months" }
|
|
246
|
+
]
|
|
247
|
+
}]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Field | Meaning |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `id` | Stable identifier for this service within your plugin. |
|
|
254
|
+
| `name` | The service as a visitor would recognise it, e.g. "Plausible Analytics". |
|
|
255
|
+
| `provider` | The **legal entity** operating it — the company name, not your plugin's. |
|
|
256
|
+
| `category` | The consent category the service falls under; what `ctx.consent.has(...)` gates on. **`necessary` is always granted and never prompted for.** Two services sharing a category are decided together. |
|
|
257
|
+
| `privacyUrl` | The provider's own privacy policy. |
|
|
258
|
+
| `hosts` | Every origin the service is contacted on. **Also the CSP allow-list** — see below. |
|
|
259
|
+
| `thirdCountryTransfer` | Whether personal data leaves the EU/EEA. |
|
|
260
|
+
| `storage[]` | Each item the service stores on the device: `name`, `type`, `purpose`, `duration`. |
|
|
261
|
+
|
|
262
|
+
**Why the shape changed.** A cookie notice that satisfies §25 TDDDG / Art. 5(3) ePD has to name each
|
|
263
|
+
stored item, what it is for, how long it lasts, who the provider is, and whether data leaves the country.
|
|
264
|
+
Category slugs and bare hostnames cannot produce that — and they force the notice to talk about "plugins"
|
|
265
|
+
to visitors who only care about cookies and named companies.
|
|
266
|
+
|
|
267
|
+
**`hosts` doubles as the CSP allow-list.** Core widens `script-src` / `frame-src` / `connect-src` by
|
|
268
|
+
exactly these origins. An origin you did not declare **stays blocked even after consent is given** — if a
|
|
269
|
+
third-party embed silently fails to load with consent granted, an undeclared host is the first thing to
|
|
270
|
+
check.
|
|
271
|
+
|
|
272
|
+
**Authoring help:** the SDK exports `ConsentServiceDeclaration` (and `ConsentStorageDeclaration`) as a
|
|
273
|
+
**documentation-only** type. Nothing in the SDK reads `plugin.json` or validates it — the host owns and
|
|
274
|
+
enforces the manifest — but typing a literal against it gives you completion and catches a typo before
|
|
275
|
+
core rejects the plugin at load, which is otherwise your first feedback. If the type and core ever
|
|
276
|
+
disagree, **core wins**; treat the mismatch as an SDK bug. The manifest as a whole stays core-owned.
|
|
277
|
+
|
|
113
278
|
## Releasing (maintainers)
|
|
114
279
|
Publishing is automated and fires on a **published GitHub Release**, not on PR merge
|
|
115
280
|
(`.github/workflows/release.yml`).
|
package/dist/index.d.ts
CHANGED
|
@@ -11,12 +11,30 @@
|
|
|
11
11
|
* The plugin contract version (SemVer).
|
|
12
12
|
*
|
|
13
13
|
* Mirror of the Java `dev.mosaicast.plugin.api.PlatformApi.VERSION` constant and the npm package
|
|
14
|
-
* version
|
|
15
|
-
*
|
|
14
|
+
* version — **these move together**, and CI fails the build if they drift.
|
|
15
|
+
*
|
|
16
|
+
* The host compares a plugin manifest's `platformApi` against this value on **`major.minor` exactly** and
|
|
17
|
+
* rejects a mismatch at startup (ARCHITECTURE §7.2). While the SDK is pre-1.0 a breaking change is
|
|
18
|
+
* therefore a *minor* bump; from `1.0.0` on, breaking means major.
|
|
16
19
|
*/
|
|
17
|
-
export declare const PLATFORM_API_VERSION: '0.
|
|
20
|
+
export declare const PLATFORM_API_VERSION: '0.4.0';
|
|
18
21
|
/** A user's role (ARCHITECTURE §8.5). Anonymous visitors have no role (`user` is `null`). */
|
|
19
22
|
export type Role = 'admin' | 'podcaster' | 'fan';
|
|
23
|
+
/**
|
|
24
|
+
* The severity of a {@link PluginContext.log} call.
|
|
25
|
+
*
|
|
26
|
+
* Mirrors the SLF4J levels a plugin backend gets through the Java `ctx.logger()`, minus `trace` — a
|
|
27
|
+
* browser has no use for it, and the host would drop it anyway.
|
|
28
|
+
*/
|
|
29
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
30
|
+
/**
|
|
31
|
+
* Unsubscribes a callback registered through one of the `onChange` handles.
|
|
32
|
+
*
|
|
33
|
+
* Call it when your component tears down — the cleanup callback returned from your
|
|
34
|
+
* {@link MosaicastRender} is the natural place. A subscription that outlives its render keeps a
|
|
35
|
+
* reference to a detached DOM tree and keeps firing into it.
|
|
36
|
+
*/
|
|
37
|
+
export type Unsubscribe = () => void;
|
|
20
38
|
/** The level plus concrete entity a view is scoped to (ARCHITECTURE §6.1). */
|
|
21
39
|
export interface Scope {
|
|
22
40
|
/** The scope level. */
|
|
@@ -200,15 +218,230 @@ export interface DisplaySnapshot {
|
|
|
200
218
|
* @returns the resolved artwork URL, or `undefined` when neither the episode nor the feed declares one
|
|
201
219
|
*/
|
|
202
220
|
export declare function resolveArtwork(snapshot: DisplaySnapshot): string | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* The consent gate for anything that stores data on the visitor's device or talks to a third party
|
|
223
|
+
* (ARCHITECTURE §12.5).
|
|
224
|
+
*
|
|
225
|
+
* Consent is **denied until granted**. The core itself runs banner-free — strictly necessary cookies need
|
|
226
|
+
* no consent — so the only reason a visitor ever sees a consent prompt is a plugin that asked for one.
|
|
227
|
+
* Treat that as the cost it is.
|
|
228
|
+
*
|
|
229
|
+
* The categories are declared per service in your manifest's `consent.services[]` (`necessary`,
|
|
230
|
+
* `functional`, `analytics`, or one you declare yourself). The host builds the cookie notice from those
|
|
231
|
+
* declarations, and the `hosts` you list there are also the CSP allow-list: **an origin you did not
|
|
232
|
+
* declare stays blocked even after consent is given.**
|
|
233
|
+
*
|
|
234
|
+
* ## The decision is per category, not per service
|
|
235
|
+
*
|
|
236
|
+
* `consent.services[]` is richly per-service — `provider`, `privacyUrl`, `thirdCountryTransfer`, each
|
|
237
|
+
* `storage[]` item — but every method here takes a **category**. That asymmetry is deliberate and worth
|
|
238
|
+
* stating plainly, because the schema implies otherwise: **services describe, categories decide.** The
|
|
239
|
+
* per-service detail exists so the notice can name who stores what for how long; the thing a visitor
|
|
240
|
+
* actually toggles is the category.
|
|
241
|
+
*
|
|
242
|
+
* The consequence: if two plugins each declare a service under `analytics` with different providers, the
|
|
243
|
+
* visitor sees **one** decision listing both plugins, and granting it grants both. There is no way to
|
|
244
|
+
* consent to one provider and withhold the other. Do not build a UI that implies otherwise, and don't
|
|
245
|
+
* assume `has('analytics')` says anything about *which* provider was accepted — it says the category was.
|
|
246
|
+
*
|
|
247
|
+
* If you need a visitor to be able to accept one of your services and refuse another, declare them under
|
|
248
|
+
* **different categories** (a plugin-declared category is allowed, it just has no translated label in the
|
|
249
|
+
* shell). That is the only lever the contract gives you.
|
|
250
|
+
*
|
|
251
|
+
* ## `necessary` is never asked about
|
|
252
|
+
*
|
|
253
|
+
* `has('necessary')` is **always `true`** and the host never prompts for it — it is the category the core
|
|
254
|
+
* itself uses, and a banner-free site stays banner-free. Declaring a service as `necessary` therefore
|
|
255
|
+
* means "this loads unconditionally"; use it only for what genuinely cannot be refused, and expect
|
|
256
|
+
* `request('necessary')` to resolve `true` without showing the visitor anything.
|
|
257
|
+
*
|
|
258
|
+
* @example The click-to-load placeholder this exists for
|
|
259
|
+
* ```ts
|
|
260
|
+
* function render({ ctx, root }: { ctx: PluginContext; root: HTMLElement }) {
|
|
261
|
+
* if (ctx.consent.has('analytics')) {
|
|
262
|
+
* mountChart(root);
|
|
263
|
+
* return;
|
|
264
|
+
* }
|
|
265
|
+
*
|
|
266
|
+
* const button = document.createElement('button');
|
|
267
|
+
* button.textContent = 'Load chart (sets a cookie)';
|
|
268
|
+
* button.onclick = async () => {
|
|
269
|
+
* if (await ctx.consent.request('analytics')) mountChart(root);
|
|
270
|
+
* };
|
|
271
|
+
* root.append(button);
|
|
272
|
+
*
|
|
273
|
+
* // Someone may flip the purpose in the settings page while this is mounted.
|
|
274
|
+
* return ctx.consent.onChange(() => rerender());
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export interface ConsentApi {
|
|
279
|
+
/**
|
|
280
|
+
* Whether the visitor has granted a consent category.
|
|
281
|
+
*
|
|
282
|
+
* Check it before every load of a consent-requiring resource, not once at startup — consent can be
|
|
283
|
+
* withdrawn mid-session from the host's settings page.
|
|
284
|
+
*
|
|
285
|
+
* @param category the consent category, as declared on a service in your manifest
|
|
286
|
+
* @returns `true` if the category is currently granted
|
|
287
|
+
*/
|
|
288
|
+
has(category: string): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Every category currently granted.
|
|
291
|
+
*
|
|
292
|
+
* For rendering a summary ("statistics: on, embeds: off"). Prefer {@link has} for a single gate.
|
|
293
|
+
*
|
|
294
|
+
* Includes `necessary`, which is always granted. These are categories, not service ids — a granted
|
|
295
|
+
* category covers every service declared under it, across all plugins.
|
|
296
|
+
*
|
|
297
|
+
* @returns the granted category names, in no guaranteed order
|
|
298
|
+
*/
|
|
299
|
+
granted(): string[];
|
|
300
|
+
/**
|
|
301
|
+
* Asks the visitor to grant a category, and resolves with their answer.
|
|
302
|
+
*
|
|
303
|
+
* The host opens its consent settings for this one purpose and resolves once the visitor decides;
|
|
304
|
+
* dismissing it resolves `false`. **Call this from a user gesture** — the click on your placeholder —
|
|
305
|
+
* and never on mount: an unprompted call turns a banner-free site into one with a banner, which is
|
|
306
|
+
* exactly what §12.5 is arranged to avoid.
|
|
307
|
+
*
|
|
308
|
+
* Resolving `true` means the category is granted from that moment on; it does not load anything for
|
|
309
|
+
* you. Load the resource yourself afterwards.
|
|
310
|
+
*
|
|
311
|
+
* ## What a caller may rely on
|
|
312
|
+
*
|
|
313
|
+
* A page full of plugin tiles will produce concurrent calls, so the contract is explicit about them:
|
|
314
|
+
*
|
|
315
|
+
* - **There is one consent surface, host-wide.** Calling this does not open a dialog of your own, and a
|
|
316
|
+
* second call while one is already open does not open a second — it joins the one in flight. You are
|
|
317
|
+
* asking the host to surface *its* settings, not opening a modal.
|
|
318
|
+
* - **The visitor decides every category at once.** The host's settings cover all declared categories,
|
|
319
|
+
* so one interaction can change several. Your promise still resolves with the state of *the category
|
|
320
|
+
* you asked for* — but other categories may have moved too, which is why {@link onChange} fires for
|
|
321
|
+
* every change rather than only yours.
|
|
322
|
+
* - **Every call resolves exactly once, and always.** Concurrent calls are never dropped or left
|
|
323
|
+
* pending: each resolves when the visitor completes the decision, including calls made while the
|
|
324
|
+
* surface was already open.
|
|
325
|
+
* - **Grants are shared across plugins.** If another plugin's `request('analytics')` is what the
|
|
326
|
+
* visitor accepted, your pending `request('analytics')` resolves `true` too — the decision is per
|
|
327
|
+
* category and site-wide, not per plugin (see the note on granularity above).
|
|
328
|
+
*
|
|
329
|
+
* So: don't serialize your calls, don't build a queue, and don't assume the visitor only answered you.
|
|
330
|
+
* Re-read {@link has} after any change rather than caching what a request resolved with.
|
|
331
|
+
*
|
|
332
|
+
* @param category the consent category to ask for
|
|
333
|
+
* @returns whether the category is granted after the visitor decided
|
|
334
|
+
*/
|
|
335
|
+
request(category: string): Promise<boolean>;
|
|
336
|
+
/**
|
|
337
|
+
* Subscribes to consent changes.
|
|
338
|
+
*
|
|
339
|
+
* Fires on **every** change to any category — including one made in the settings page while your
|
|
340
|
+
* component is mounted, and including a withdrawal. It carries no payload: re-read {@link has} or
|
|
341
|
+
* {@link granted} for the current state.
|
|
342
|
+
*
|
|
343
|
+
* @param cb called after each change
|
|
344
|
+
* @returns an {@link Unsubscribe} — return it from your render callback so the subscription dies with
|
|
345
|
+
* the component
|
|
346
|
+
*/
|
|
347
|
+
onChange(cb: () => void): Unsubscribe;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* One item a service stores on the visitor's device, as declared in `plugin.json`.
|
|
351
|
+
*
|
|
352
|
+
* Part of {@link ConsentServiceDeclaration} — see the caveats there before using either type.
|
|
353
|
+
*/
|
|
354
|
+
export interface ConsentStorageDeclaration {
|
|
355
|
+
/** The cookie or storage key exactly as it appears on the device, e.g. `plausible_ignore`. */
|
|
356
|
+
name: string;
|
|
357
|
+
/** Where it is stored. */
|
|
358
|
+
type: 'cookie' | 'localStorage' | 'sessionStorage';
|
|
359
|
+
/** What it is for, in language a visitor reads — not an internal description. */
|
|
360
|
+
purpose: string;
|
|
361
|
+
/** How long it lasts: `session`, `persistent`, or a human duration such as `12 months`. */
|
|
362
|
+
duration: string;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The shape of one entry in your manifest's `consent.services[]`.
|
|
366
|
+
*
|
|
367
|
+
* **This type is documentation, not enforcement.** The manifest is owned and validated by the host — the
|
|
368
|
+
* SDK does not read `plugin.json`, and nothing here runs at build or load time. It exists so an author
|
|
369
|
+
* writing the declaration gets IDE completion and catches a typo before core rejects the plugin at load,
|
|
370
|
+
* which is otherwise the first feedback you get. Two consequences worth knowing:
|
|
371
|
+
*
|
|
372
|
+
* - **The host is authoritative.** If this type and core disagree, core wins. It is kept in step by hand,
|
|
373
|
+
* so treat a mismatch as a bug in the SDK rather than permission to ignore the host.
|
|
374
|
+
* - **It is not a manifest type.** The manifest as a whole stays core-owned and the SDK will not grow one;
|
|
375
|
+
* this covers a single nested shape that got deep enough in 0.4.0 to be worth typing.
|
|
376
|
+
*
|
|
377
|
+
* Use it by typing a literal you keep next to your manifest, or as a reference while writing the JSON:
|
|
378
|
+
*
|
|
379
|
+
* ```ts
|
|
380
|
+
* const services: ConsentServiceDeclaration[] = [{
|
|
381
|
+
* id: 'plausible',
|
|
382
|
+
* name: 'Plausible Analytics',
|
|
383
|
+
* provider: 'Plausible Insights OÜ',
|
|
384
|
+
* category: 'analytics',
|
|
385
|
+
* privacyUrl: 'https://plausible.io/privacy',
|
|
386
|
+
* hosts: ['https://plausible.example'],
|
|
387
|
+
* thirdCountryTransfer: false,
|
|
388
|
+
* storage: [{
|
|
389
|
+
* name: 'plausible_ignore', type: 'localStorage',
|
|
390
|
+
* purpose: 'Remembers that you opted out of statistics', duration: 'persistent',
|
|
391
|
+
* }],
|
|
392
|
+
* }];
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
export interface ConsentServiceDeclaration {
|
|
396
|
+
/** Stable identifier for this service within your plugin. */
|
|
397
|
+
id: string;
|
|
398
|
+
/** The service as a visitor would recognise it, e.g. `Plausible Analytics`. */
|
|
399
|
+
name: string;
|
|
400
|
+
/** The **legal entity** operating the service — the company, not your plugin. */
|
|
401
|
+
provider: string;
|
|
402
|
+
/**
|
|
403
|
+
* The consent category this service falls under, and therefore what {@link ConsentApi.has} gates on.
|
|
404
|
+
*
|
|
405
|
+
* `necessary` is never prompted for and always granted. Remember that the category — not the service —
|
|
406
|
+
* is what the visitor decides: two services sharing a category are accepted or refused together.
|
|
407
|
+
*/
|
|
408
|
+
category: 'necessary' | 'functional' | 'analytics' | (string & {});
|
|
409
|
+
/** The provider's own privacy policy. */
|
|
410
|
+
privacyUrl: string;
|
|
411
|
+
/**
|
|
412
|
+
* Every origin the service is contacted on, scheme included (`https://plausible.example`).
|
|
413
|
+
*
|
|
414
|
+
* **Also the CSP allow-list**: core widens `script-src`/`frame-src`/`connect-src` by exactly these, so
|
|
415
|
+
* an undeclared or bare-hostname origin stays blocked even after consent is given.
|
|
416
|
+
*/
|
|
417
|
+
hosts: string[];
|
|
418
|
+
/** Whether personal data leaves the EU/EEA. */
|
|
419
|
+
thirdCountryTransfer: boolean;
|
|
420
|
+
/** Each item the service stores on the visitor's device. */
|
|
421
|
+
storage: ConsentStorageDeclaration[];
|
|
422
|
+
}
|
|
203
423
|
/**
|
|
204
424
|
* Everything a frontend plugin is given, set by the host on the mounted custom element
|
|
205
425
|
* (ARCHITECTURE §7.5). This is the **entire** interface a plugin author must learn.
|
|
206
426
|
*/
|
|
207
427
|
export interface PluginContext {
|
|
208
|
-
/**
|
|
428
|
+
/**
|
|
429
|
+
* The scope this plugin instance is mounted in. For the `episode` scope, `scope.id` is the episode's
|
|
430
|
+
* **public slug** (e.g. `the-sample-cast-s01e06`) — the same id used in its URL and as the doc-store
|
|
431
|
+
* partition (`data/episode/{slug}/…`), not the internal UUID.
|
|
432
|
+
*/
|
|
209
433
|
scope: Scope;
|
|
210
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* The episode ids in scope, resolved (and access-filtered) by the host — the public **slugs** (the same
|
|
436
|
+
* values used in URLs and doc-store paths). Pair with {@link episodeLabels} for display.
|
|
437
|
+
*/
|
|
211
438
|
episodes: string[];
|
|
439
|
+
/**
|
|
440
|
+
* Human-readable labels for {@link episodes}, keyed by slug (e.g. `"S01E06 · The Lighthouse…"`). The host
|
|
441
|
+
* builds them from the feed's season/episode + title; use them in pickers so users see titles, not slugs.
|
|
442
|
+
* Optional: absent (or partial) when the host does not provide a label for a given episode.
|
|
443
|
+
*/
|
|
444
|
+
episodeLabels?: Record<string, string>;
|
|
212
445
|
/** Present on the `episode` scope: lifecycle status of the current episode. */
|
|
213
446
|
episode?: {
|
|
214
447
|
status: 'PLANNED' | 'PUBLISHED' | 'WITHDRAWN';
|
|
@@ -224,31 +457,68 @@ export interface PluginContext {
|
|
|
224
457
|
* uses via `ctx.store()`. See {@link PluginApiClient} for the endpoint shape and access rules.
|
|
225
458
|
*/
|
|
226
459
|
api: PluginApiClient;
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Writes one line to the host's log, attributed to this plugin.
|
|
462
|
+
*
|
|
463
|
+
* The counterpart of the backend's `ctx.logger()`, and the **only** supported way for a frontend
|
|
464
|
+
* component to log to the host — do not POST to `/api/plugins/{id}/log` through {@link api}, which is a
|
|
465
|
+
* data endpoint. The host attributes the entry to your plugin, stores `info` and above, surfaces `warn`
|
|
466
|
+
* and above in the admin log viewer, and rate-limits this path: a render loop logging per frame will be
|
|
467
|
+
* throttled, not stored.
|
|
468
|
+
*
|
|
469
|
+
* Messages are read by site operators, not by you — they land next to core's own output. Keep them
|
|
470
|
+
* short, and keep personal data out of them.
|
|
471
|
+
*
|
|
472
|
+
* @param level the severity
|
|
473
|
+
* @param message the message; already-formatted, since there is no placeholder syntax here
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* ctx.log('warn', `no board for episode ${ctx.scope.id}`);
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
log(level: LogLevel, message: string): void;
|
|
481
|
+
/** Cookie/consent gate for third-party resources — see {@link ConsentApi} (ARCHITECTURE §12.5). */
|
|
482
|
+
consent: ConsentApi;
|
|
483
|
+
/**
|
|
484
|
+
* Read-only access to the host's URL filter state (ARCHITECTURE §6.1).
|
|
485
|
+
*
|
|
486
|
+
* Plugins *consume* filters, they never define them: the axes (season, tags, sorting) belong to the
|
|
487
|
+
* host and live in the URL. `onChange` returns an {@link Unsubscribe}.
|
|
488
|
+
*/
|
|
233
489
|
filter: {
|
|
234
490
|
current(): FilterState;
|
|
235
|
-
onChange(cb: (f: FilterState) => void):
|
|
491
|
+
onChange(cb: (f: FilterState) => void): Unsubscribe;
|
|
236
492
|
};
|
|
237
|
-
/**
|
|
493
|
+
/**
|
|
494
|
+
* Player position + control, for sync plugins (ARCHITECTURE §6.5).
|
|
495
|
+
*
|
|
496
|
+
* `on` returns an {@link Unsubscribe} — player events outlive a single render, so detaching matters
|
|
497
|
+
* here more than anywhere else on this context.
|
|
498
|
+
*/
|
|
238
499
|
player: {
|
|
239
500
|
currentTime(): number;
|
|
240
501
|
seekTo(s: number): void;
|
|
241
|
-
on(ev: string, cb: (...args: unknown[]) => void):
|
|
502
|
+
on(ev: string, cb: (...args: unknown[]) => void): Unsubscribe;
|
|
242
503
|
};
|
|
243
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* The subpath under `/p/<pluginId>/`, for deep-linkable plugin content (ARCHITECTURE §6.4).
|
|
506
|
+
*
|
|
507
|
+
* `onChange` returns an {@link Unsubscribe}.
|
|
508
|
+
*/
|
|
244
509
|
route: {
|
|
245
510
|
path: string;
|
|
246
|
-
onChange(cb: (p: string) => void):
|
|
511
|
+
onChange(cb: (p: string) => void): Unsubscribe;
|
|
247
512
|
};
|
|
248
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* The active UI locale (ARCHITECTURE §12.7).
|
|
515
|
+
*
|
|
516
|
+
* `onChange` returns an {@link Unsubscribe}. {@link createPluginI18n} subscribes to this for you and
|
|
517
|
+
* hands back a `dispose` to undo it.
|
|
518
|
+
*/
|
|
249
519
|
locale: {
|
|
250
520
|
current(): string;
|
|
251
|
-
onChange(cb: (l: string) => void):
|
|
521
|
+
onChange(cb: (l: string) => void): Unsubscribe;
|
|
252
522
|
};
|
|
253
523
|
/** Core listening progress in seconds, or `null` if unknown (ARCHITECTURE §6.5). */
|
|
254
524
|
progress: {
|
|
@@ -306,6 +576,14 @@ export interface PluginI18n {
|
|
|
306
576
|
t(key: string, params?: Record<string, string | number>): string;
|
|
307
577
|
/** The currently active locale code. */
|
|
308
578
|
readonly locale: string;
|
|
579
|
+
/**
|
|
580
|
+
* Detaches the translator from `ctx.locale`.
|
|
581
|
+
*
|
|
582
|
+
* A translator subscribes to locale changes for its whole life. Call this when the component that owns
|
|
583
|
+
* it goes away — from the cleanup callback your render returns — or the subscription keeps a dead
|
|
584
|
+
* translator alive.
|
|
585
|
+
*/
|
|
586
|
+
dispose(): void;
|
|
309
587
|
}
|
|
310
588
|
/**
|
|
311
589
|
* Creates a plugin-local translator bound to the host's active locale (ARCHITECTURE §12.7).
|
|
@@ -314,6 +592,9 @@ export interface PluginI18n {
|
|
|
314
592
|
* change — the same convention as the shell, no extra i18n library required. English (`en`) is the
|
|
315
593
|
* source language and the fallback.
|
|
316
594
|
*
|
|
595
|
+
* The translator subscribes to `locale.onChange` for as long as it lives — call {@link PluginI18n.dispose}
|
|
596
|
+
* from your render's cleanup callback when the component goes away.
|
|
597
|
+
*
|
|
317
598
|
* @param catalogs catalogs keyed by locale code
|
|
318
599
|
* @param locale the host locale handle, i.e. `ctx.locale`; its `onChange` drives re-selection
|
|
319
600
|
* @returns a translator whose `t` and `locale` reflect the currently active locale
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
* The plugin contract version (SemVer).
|
|
14
14
|
*
|
|
15
15
|
* Mirror of the Java `dev.mosaicast.plugin.api.PlatformApi.VERSION` constant and the npm package
|
|
16
|
-
* version
|
|
17
|
-
*
|
|
16
|
+
* version — **these move together**, and CI fails the build if they drift.
|
|
17
|
+
*
|
|
18
|
+
* The host compares a plugin manifest's `platformApi` against this value on **`major.minor` exactly** and
|
|
19
|
+
* rejects a mismatch at startup (ARCHITECTURE §7.2). While the SDK is pre-1.0 a breaking change is
|
|
20
|
+
* therefore a *minor* bump; from `1.0.0` on, breaking means major.
|
|
18
21
|
*/
|
|
19
|
-
export const PLATFORM_API_VERSION = '0.
|
|
22
|
+
export const PLATFORM_API_VERSION = '0.4.0';
|
|
20
23
|
/**
|
|
21
24
|
* The artwork to display for an episode: its own {@link DisplaySnapshot.imageUrl} if present, otherwise
|
|
22
25
|
* the {@link DisplaySnapshot.feedImageUrl feed cover}, otherwise `undefined`.
|
|
@@ -120,19 +123,27 @@ function interpolate(template, params) {
|
|
|
120
123
|
* change — the same convention as the shell, no extra i18n library required. English (`en`) is the
|
|
121
124
|
* source language and the fallback.
|
|
122
125
|
*
|
|
126
|
+
* The translator subscribes to `locale.onChange` for as long as it lives — call {@link PluginI18n.dispose}
|
|
127
|
+
* from your render's cleanup callback when the component goes away.
|
|
128
|
+
*
|
|
123
129
|
* @param catalogs catalogs keyed by locale code
|
|
124
130
|
* @param locale the host locale handle, i.e. `ctx.locale`; its `onChange` drives re-selection
|
|
125
131
|
* @returns a translator whose `t` and `locale` reflect the currently active locale
|
|
126
132
|
*/
|
|
127
133
|
export function createPluginI18n(catalogs, locale) {
|
|
128
134
|
let active = locale.current();
|
|
129
|
-
locale.onChange((l) => {
|
|
135
|
+
const unsubscribe = locale.onChange((l) => {
|
|
130
136
|
active = l;
|
|
131
137
|
});
|
|
132
138
|
return {
|
|
133
139
|
get locale() {
|
|
134
140
|
return active;
|
|
135
141
|
},
|
|
142
|
+
dispose() {
|
|
143
|
+
// Optional call: a host built against 0.3.x returns nothing here, and a translator that cannot be
|
|
144
|
+
// disposed is better than one that throws while a component is tearing down.
|
|
145
|
+
unsubscribe?.();
|
|
146
|
+
},
|
|
136
147
|
t(key, params) {
|
|
137
148
|
const template = catalogs[active]?.[key] ?? catalogs[SOURCE_LOCALE]?.[key] ?? key;
|
|
138
149
|
return interpolate(template, params);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,qDAAqD;AAErD;;;;;;;;GAQG;AAEH
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
|
-
import type { PluginApiClient, PluginContext, ThemeTokens } from './index.js';
|
|
9
|
+
import type { ConsentApi, LogLevel, PluginApiClient, PluginContext, ThemeTokens } from './index.js';
|
|
10
|
+
/** One recorded {@link PluginContext.log} call. */
|
|
11
|
+
export interface LogRecord {
|
|
12
|
+
/** The severity it was logged at. */
|
|
13
|
+
level: LogLevel;
|
|
14
|
+
/** The logged message. */
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
10
17
|
/** One recorded {@link MockApiClient} call. */
|
|
11
18
|
export interface RecordedCall {
|
|
12
19
|
/** HTTP method, lowercase. */
|
|
@@ -26,8 +33,66 @@ export interface MockApiClient extends PluginApiClient {
|
|
|
26
33
|
*/
|
|
27
34
|
responses: Record<string, unknown>;
|
|
28
35
|
}
|
|
36
|
+
/** A {@link ConsentApi} whose grants a test can drive. */
|
|
37
|
+
export interface MockConsent extends ConsentApi {
|
|
38
|
+
/** Grants a category and notifies subscribers. Granting an already-granted category does nothing. */
|
|
39
|
+
grant(category: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Withdraws a category and notifies subscribers, as the host's settings page can mid-session.
|
|
42
|
+
*
|
|
43
|
+
* Revoking `necessary` does nothing — the host cannot withdraw it either.
|
|
44
|
+
*/
|
|
45
|
+
revoke(category: string): void;
|
|
46
|
+
/** Every category passed to {@link ConsentApi.request}, in order. */
|
|
47
|
+
readonly requests: string[];
|
|
48
|
+
/**
|
|
49
|
+
* What {@link ConsentApi.request} resolves with — i.e. what the visitor decides.
|
|
50
|
+
*
|
|
51
|
+
* `false` by default: the visitor says no. Set it to `true` to test the accepted path, which also
|
|
52
|
+
* grants the category as the host would.
|
|
53
|
+
*/
|
|
54
|
+
autoGrantOnRequest: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Builds a {@link ConsentApi} for tests, **denying everything** unless told otherwise.
|
|
58
|
+
*
|
|
59
|
+
* Deny is the default on purpose. The host denies until granted, so a component written against a
|
|
60
|
+
* permissive mock is a component whose placeholder path was never exercised — and that path is the entire
|
|
61
|
+
* point of the consent contract (ARCHITECTURE §12.5).
|
|
62
|
+
*
|
|
63
|
+
* The one exception is `necessary`, which is granted from the start and cannot be revoked in the host
|
|
64
|
+
* either — it is the category the core itself uses.
|
|
65
|
+
*
|
|
66
|
+
* @param initial categories granted from the start, on top of `necessary`
|
|
67
|
+
* @returns a consent double with `grant`/`revoke` and a recorded `requests` list
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const consent = makeMockConsent();
|
|
72
|
+
* const ctx = makeMockCtx({ consent });
|
|
73
|
+
*
|
|
74
|
+
* mount(ctx); // renders the click-to-load placeholder
|
|
75
|
+
* consent.autoGrantOnRequest = true;
|
|
76
|
+
* await clickPlaceholder();
|
|
77
|
+
* expect(consent.requests).toEqual(['analytics']);
|
|
78
|
+
*
|
|
79
|
+
* consent.revoke('analytics'); // fires onChange; the component should go back to the placeholder
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function makeMockConsent(initial?: string[]): MockConsent;
|
|
29
83
|
/** Default theme tokens (neutral light values) used when a test does not override them. */
|
|
30
84
|
export declare const DEFAULT_THEME: ThemeTokens;
|
|
85
|
+
/** A {@link PluginContext} wired for tests: the mock `api` and the recorded `log` calls are reachable. */
|
|
86
|
+
export type MockPluginContext = PluginContext & {
|
|
87
|
+
/** The recording client every `api` call goes through. */
|
|
88
|
+
api: MockApiClient;
|
|
89
|
+
/**
|
|
90
|
+
* Every {@link PluginContext.log} call, in order.
|
|
91
|
+
*
|
|
92
|
+
* Stays empty if you override `log` yourself — the recording lives in the default implementation.
|
|
93
|
+
*/
|
|
94
|
+
logs: LogRecord[];
|
|
95
|
+
};
|
|
31
96
|
/** Overrides accepted by {@link makeMockCtx}. */
|
|
32
97
|
export interface MockCtxOverrides extends Partial<Omit<PluginContext, 'api'>> {
|
|
33
98
|
/** Canned responses for the mock `api`, keyed as described on {@link MockApiClient.responses}. */
|
|
@@ -38,14 +103,18 @@ export interface MockCtxOverrides extends Partial<Omit<PluginContext, 'api'>> {
|
|
|
38
103
|
/**
|
|
39
104
|
* Builds a {@link PluginContext} wired with in-memory doubles for unit tests.
|
|
40
105
|
*
|
|
41
|
-
* Defaults: `site` scope, no episodes, anonymous user, all consent denied
|
|
106
|
+
* Defaults: `site` scope, no episodes, anonymous user, **all consent denied** (a {@link makeMockConsent}
|
|
107
|
+
* double — pass your own to drive grants), empty filter, a player
|
|
42
108
|
* parked at 0s, empty route, `en` locale, unknown progress, and {@link DEFAULT_THEME}. Pass overrides to
|
|
43
|
-
* change any field; the returned `api` is a {@link MockApiClient} recording every call
|
|
109
|
+
* change any field; the returned `api` is a {@link MockApiClient} recording every call, and `logs`
|
|
110
|
+
* collects every `ctx.log(...)`.
|
|
111
|
+
*
|
|
112
|
+
* `episodeLabels` is deliberately **absent** by default. It is optional on the real context and the host
|
|
113
|
+
* may supply it partially, so a component that renders labels has to survive their absence — the mock
|
|
114
|
+
* makes you face that unless you pass labels in.
|
|
44
115
|
*
|
|
45
116
|
* @param overrides partial context plus optional `apiResponses`
|
|
46
|
-
* @returns a full context whose `api` is a {@link MockApiClient}
|
|
117
|
+
* @returns a full context whose `api` is a {@link MockApiClient} and whose `log` calls are recorded
|
|
47
118
|
*/
|
|
48
|
-
export declare function makeMockCtx(overrides?: MockCtxOverrides):
|
|
49
|
-
api: MockApiClient;
|
|
50
|
-
};
|
|
119
|
+
export declare function makeMockCtx(overrides?: MockCtxOverrides): MockPluginContext;
|
|
51
120
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,+CAA+C;AAC/C,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,UAAU,EAEV,QAAQ,EACR,eAAe,EACf,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,qCAAqC;IACrC,KAAK,EAAE,QAAQ,CAAC;IAChB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+CAA+C;AAC/C,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAuBD,0DAA0D;AAC1D,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,qGAAqG;IACrG,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,MAAM,EAAO,GAAG,WAAW,CAwCnE;AAED,2FAA2F;AAC3F,eAAO,MAAM,aAAa,EAAE,WAS3B,CAAC;AAEF,0GAA0G;AAC1G,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG;IAC9C,0DAA0D;IAC1D,GAAG,EAAE,aAAa,CAAC;IACnB;;;;OAIG;IACH,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,gBAAiB,SAAQ,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;IAC3E,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,kFAAkF;IAClF,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,SAAS,GAAE,gBAAqB,GAAG,iBAAiB,CA2B/E"}
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 The Mosaicast Authors
|
|
3
|
+
/** The unsubscribe returned by the inert `onChange` doubles — there is nothing to detach. */
|
|
4
|
+
const noop = () => { };
|
|
3
5
|
function makeMockApi(responses) {
|
|
4
6
|
const calls = [];
|
|
5
7
|
const resolve = (method, path, body) => {
|
|
@@ -17,6 +19,72 @@ function makeMockApi(responses) {
|
|
|
17
19
|
};
|
|
18
20
|
return api;
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds a {@link ConsentApi} for tests, **denying everything** unless told otherwise.
|
|
24
|
+
*
|
|
25
|
+
* Deny is the default on purpose. The host denies until granted, so a component written against a
|
|
26
|
+
* permissive mock is a component whose placeholder path was never exercised — and that path is the entire
|
|
27
|
+
* point of the consent contract (ARCHITECTURE §12.5).
|
|
28
|
+
*
|
|
29
|
+
* The one exception is `necessary`, which is granted from the start and cannot be revoked in the host
|
|
30
|
+
* either — it is the category the core itself uses.
|
|
31
|
+
*
|
|
32
|
+
* @param initial categories granted from the start, on top of `necessary`
|
|
33
|
+
* @returns a consent double with `grant`/`revoke` and a recorded `requests` list
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const consent = makeMockConsent();
|
|
38
|
+
* const ctx = makeMockCtx({ consent });
|
|
39
|
+
*
|
|
40
|
+
* mount(ctx); // renders the click-to-load placeholder
|
|
41
|
+
* consent.autoGrantOnRequest = true;
|
|
42
|
+
* await clickPlaceholder();
|
|
43
|
+
* expect(consent.requests).toEqual(['analytics']);
|
|
44
|
+
*
|
|
45
|
+
* consent.revoke('analytics'); // fires onChange; the component should go back to the placeholder
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function makeMockConsent(initial = []) {
|
|
49
|
+
// `necessary` is what the core itself uses: always granted, never prompted for. Seeding it means a
|
|
50
|
+
// component gated on it behaves in tests the way it will in the host.
|
|
51
|
+
const granted = new Set(['necessary', ...initial]);
|
|
52
|
+
const subscribers = new Set();
|
|
53
|
+
const requests = [];
|
|
54
|
+
const notify = () => {
|
|
55
|
+
subscribers.forEach((cb) => cb());
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
requests,
|
|
59
|
+
autoGrantOnRequest: false,
|
|
60
|
+
has: (category) => granted.has(category),
|
|
61
|
+
granted: () => [...granted],
|
|
62
|
+
request(category) {
|
|
63
|
+
requests.push(category);
|
|
64
|
+
if (this.autoGrantOnRequest && !granted.has(category)) {
|
|
65
|
+
granted.add(category);
|
|
66
|
+
notify();
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve(granted.has(category));
|
|
69
|
+
},
|
|
70
|
+
grant(category) {
|
|
71
|
+
if (!granted.has(category)) {
|
|
72
|
+
granted.add(category);
|
|
73
|
+
notify();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
revoke(category) {
|
|
77
|
+
// The host cannot withdraw `necessary`, so neither can the double.
|
|
78
|
+
if (category !== 'necessary' && granted.delete(category)) {
|
|
79
|
+
notify();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
onChange(cb) {
|
|
83
|
+
subscribers.add(cb);
|
|
84
|
+
return () => subscribers.delete(cb);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
20
88
|
/** Default theme tokens (neutral light values) used when a test does not override them. */
|
|
21
89
|
export const DEFAULT_THEME = {
|
|
22
90
|
bg: '#ffffff',
|
|
@@ -31,30 +99,43 @@ export const DEFAULT_THEME = {
|
|
|
31
99
|
/**
|
|
32
100
|
* Builds a {@link PluginContext} wired with in-memory doubles for unit tests.
|
|
33
101
|
*
|
|
34
|
-
* Defaults: `site` scope, no episodes, anonymous user, all consent denied
|
|
102
|
+
* Defaults: `site` scope, no episodes, anonymous user, **all consent denied** (a {@link makeMockConsent}
|
|
103
|
+
* double — pass your own to drive grants), empty filter, a player
|
|
35
104
|
* parked at 0s, empty route, `en` locale, unknown progress, and {@link DEFAULT_THEME}. Pass overrides to
|
|
36
|
-
* change any field; the returned `api` is a {@link MockApiClient} recording every call
|
|
105
|
+
* change any field; the returned `api` is a {@link MockApiClient} recording every call, and `logs`
|
|
106
|
+
* collects every `ctx.log(...)`.
|
|
107
|
+
*
|
|
108
|
+
* `episodeLabels` is deliberately **absent** by default. It is optional on the real context and the host
|
|
109
|
+
* may supply it partially, so a component that renders labels has to survive their absence — the mock
|
|
110
|
+
* makes you face that unless you pass labels in.
|
|
37
111
|
*
|
|
38
112
|
* @param overrides partial context plus optional `apiResponses`
|
|
39
|
-
* @returns a full context whose `api` is a {@link MockApiClient}
|
|
113
|
+
* @returns a full context whose `api` is a {@link MockApiClient} and whose `log` calls are recorded
|
|
40
114
|
*/
|
|
41
115
|
export function makeMockCtx(overrides = {}) {
|
|
42
116
|
const { apiResponses, api, ...rest } = overrides;
|
|
43
117
|
const mockApi = api ?? makeMockApi(apiResponses ?? {});
|
|
44
118
|
const filter = rest.filter?.current() ?? {};
|
|
119
|
+
const logs = [];
|
|
45
120
|
const base = {
|
|
46
121
|
scope: { type: 'site', id: 'main' },
|
|
47
122
|
episodes: [],
|
|
48
123
|
user: null,
|
|
49
124
|
api: mockApi,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
125
|
+
logs,
|
|
126
|
+
log: (level, message) => {
|
|
127
|
+
logs.push({ level, message });
|
|
128
|
+
},
|
|
129
|
+
consent: makeMockConsent(),
|
|
130
|
+
// The no-op handles still return a working unsubscribe, so a component that detaches on cleanup
|
|
131
|
+
// behaves the same against the mock as against the host.
|
|
132
|
+
filter: { current: () => filter, onChange: () => noop },
|
|
133
|
+
player: { currentTime: () => 0, seekTo: () => { }, on: () => noop },
|
|
134
|
+
route: { path: '', onChange: () => noop },
|
|
135
|
+
locale: { current: () => 'en', onChange: () => noop },
|
|
55
136
|
progress: { get: () => Promise.resolve(null) },
|
|
56
137
|
theme: DEFAULT_THEME,
|
|
57
138
|
};
|
|
58
|
-
return { ...base, ...rest, api: mockApi };
|
|
139
|
+
return { ...base, ...rest, api: mockApi, logs };
|
|
59
140
|
}
|
|
60
141
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,qDAAqD;
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mosaicast/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Versioned plugin contract for Mosaicast: the frontend PluginContext types, a Web Component base helper, an i18n helper, plus a test kit under the /testing subpath.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|