@meith/marketplace 0.21.1 → 0.22.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/package.json +2 -2
- package/src/build-info.ts +1 -21
- package/src/cache.ts +0 -19
- package/src/fetch.ts +0 -18
- package/src/range.ts +0 -29
- package/src/refresh.ts +0 -15
- package/src/schema.ts +0 -31
- package/src/status.ts +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/marketplace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@meith/theme-kit": "0.
|
|
22
|
+
"@meith/theme-kit": "0.22.0"
|
|
23
23
|
}
|
|
24
24
|
}
|
package/src/build-info.ts
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
import { parseApiVersion, THEME_API_VERSION } from '@meith/theme-kit'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* This board's own release version, checked against a listing's `meith`
|
|
5
|
-
* range. Duplicated the same way `CODE_VERSION` already is across
|
|
6
|
-
* apps/cli/src/upgrade.ts, apps/community/src/server/upgrade-notice.ts and
|
|
7
|
-
* packages/create-meith/src/bin.ts — see docs/release.md — and kept honest
|
|
8
|
-
* by the same `scripts/release-check.mjs`.
|
|
9
|
-
*/
|
|
10
|
-
export const MEITH_VERSION = '0.21.1'
|
|
3
|
+
export const MEITH_VERSION = '0.22.0'
|
|
11
4
|
|
|
12
|
-
/**
|
|
13
|
-
* The theme-kit major this build implements, read from the single place
|
|
14
|
-
* that already declares it rather than duplicated as a literal.
|
|
15
|
-
*/
|
|
16
5
|
export const THEME_API_MAJOR = parseApiVersion(THEME_API_VERSION).major
|
|
17
6
|
|
|
18
|
-
/**
|
|
19
|
-
* The plugin-kit major this build implements. Unlike theme-kit, plugin-kit
|
|
20
|
-
* exports no equivalent constant — a plugin's own `apiVersion` is a free
|
|
21
|
-
* string nothing currently checks (see docs/plugin-api.md's Versioning
|
|
22
|
-
* section, which describes the policy without a constant enforcing it).
|
|
23
|
-
* Both kits are pre-1.0, so this is 0 until plugin-kit ships a major of its
|
|
24
|
-
* own; bump it by hand alongside that, the same way this file's other
|
|
25
|
-
* constant is bumped by hand at every release.
|
|
26
|
-
*/
|
|
27
7
|
export const PLUGIN_API_MAJOR = 0
|
package/src/cache.ts
CHANGED
|
@@ -2,8 +2,6 @@ import type { MarketplaceFeed } from './schema'
|
|
|
2
2
|
|
|
3
3
|
export interface CachedMarketplace {
|
|
4
4
|
readonly feed: MarketplaceFeed | null
|
|
5
|
-
/** The feed URL `feed` was actually fetched from — a listing's screenshot
|
|
6
|
-
* path is resolved against this, not against today's setting. */
|
|
7
5
|
readonly sourceUrl: string | null
|
|
8
6
|
readonly fetchedAt: Date | null
|
|
9
7
|
readonly error: string | null
|
|
@@ -18,18 +16,6 @@ export const EMPTY_CACHE: CachedMarketplace = {
|
|
|
18
16
|
errorAt: null,
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
/**
|
|
22
|
-
* The cached feed and the daily task's own bookkeeping — one row, whichever
|
|
23
|
-
* infrastructure package backs it (see packages/db's `marketplace-repo.ts`).
|
|
24
|
-
* `claimNotified` is what makes an update notify administrators exactly once
|
|
25
|
-
* per (plugin, version), ever, even when the daily task and an admin's
|
|
26
|
-
* "Refresh" click race on the same newly-seen version — independent of
|
|
27
|
-
* whether the notification itself has since been read, which a bare
|
|
28
|
-
* `dedupeKey` on the notification service is not (its coalescing only holds
|
|
29
|
-
* while a notification is unread — see packages/notifications). See
|
|
30
|
-
* docs/marketplace.md for why the claim is a single atomic step rather than
|
|
31
|
-
* a check followed by a write.
|
|
32
|
-
*/
|
|
33
19
|
export interface MarketplaceCacheRepository {
|
|
34
20
|
read(): Promise<CachedMarketplace>
|
|
35
21
|
saveFeed(input: {
|
|
@@ -38,10 +24,5 @@ export interface MarketplaceCacheRepository {
|
|
|
38
24
|
readonly fetchedAt: Date
|
|
39
25
|
}): Promise<void>
|
|
40
26
|
saveError(input: { readonly message: string; readonly at: Date }): Promise<void>
|
|
41
|
-
/**
|
|
42
|
-
* Atomically records (key, version) as notified and reports whether this
|
|
43
|
-
* call is the one that newly claimed it — `false` means some other caller
|
|
44
|
-
* (a concurrent refresh) already has, and this one must not notify.
|
|
45
|
-
*/
|
|
46
27
|
claimNotified(key: string, version: string): Promise<boolean>
|
|
47
28
|
}
|
package/src/fetch.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one place this package touches the network — and only ever server-side:
|
|
3
|
-
* the daily task and the admin "Refresh" action both call `refreshCatalog`
|
|
4
|
-
* (see refresh.ts), never the browser. Node's built-in `fetch` is enough; no
|
|
5
|
-
* HTTP client dependency is added for this.
|
|
6
|
-
*/
|
|
7
1
|
export interface FetchFeedResult {
|
|
8
2
|
readonly ok: boolean
|
|
9
|
-
/** Parsed JSON body, present only when `ok` is true. */
|
|
10
3
|
readonly body: unknown
|
|
11
|
-
/** A short, loggable reason, present only when `ok` is false. */
|
|
12
4
|
readonly error: string | null
|
|
13
5
|
}
|
|
14
6
|
|
|
@@ -18,11 +10,9 @@ const MAX_BODY_BYTES = 2_000_000
|
|
|
18
10
|
export interface FetchFeedOptions {
|
|
19
11
|
readonly url: string
|
|
20
12
|
readonly timeoutMs?: number
|
|
21
|
-
/** Swappable for tests; defaults to the platform's global `fetch`. */
|
|
22
13
|
readonly fetchImpl?: typeof fetch
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
/** See `docs/marketplace.md#outbound-fetches-do-not-follow-redirects`. */
|
|
26
16
|
export async function readCappedBody(
|
|
27
17
|
response: Response,
|
|
28
18
|
maxBytes: number,
|
|
@@ -62,14 +52,6 @@ export async function readCappedBody(
|
|
|
62
52
|
return combined
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
/**
|
|
66
|
-
* Fetches and JSON-parses a marketplace feed. Never throws: an unreachable
|
|
67
|
-
* host, a non-200 response (a redirect included — see
|
|
68
|
-
* `docs/marketplace.md#outbound-fetches-do-not-follow-redirects`), an
|
|
69
|
-
* oversized body or invalid JSON are all reported as `{ ok: false, error }`
|
|
70
|
-
* — the board with no outbound network is meant to fail quietly here, not
|
|
71
|
-
* crash the task that called this.
|
|
72
|
-
*/
|
|
73
55
|
export async function fetchMarketplaceFeed(options: FetchFeedOptions): Promise<FetchFeedResult> {
|
|
74
56
|
const fetchImpl = options.fetchImpl ?? fetch
|
|
75
57
|
const controller = new AbortController()
|
package/src/range.ts
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version comparison and the "meith" range check.
|
|
3
|
-
*
|
|
4
|
-
* `marketplace/schema.json` documents the range syntax but the generator
|
|
5
|
-
* only checks that it *parses* — it is never evaluated against a real
|
|
6
|
-
* version there (see docs/marketplace.md). This is that evaluation, written
|
|
7
|
-
* for the first time here: comparators `>=`, `<=`, `>`, `<` or `=` (default
|
|
8
|
-
* `=`) against a version of one to three numeric parts, space-separated to
|
|
9
|
-
* mean AND.
|
|
10
|
-
*
|
|
11
|
-
* Precision is handled differently for the two families, which is a
|
|
12
|
-
* deliberate reading rather than something the docs pin down:
|
|
13
|
-
*
|
|
14
|
-
* - `=` **truncates** the board version to the comparator's own precision,
|
|
15
|
-
* so `=1` means "meith major 1, whatever the minor and patch" and `=1.2`
|
|
16
|
-
* means "1.2.x". That is what "pin to a major or minor" has to mean.
|
|
17
|
-
* - `>=`, `<=`, `>`, `<` **zero-pad** the comparator to three parts before
|
|
18
|
-
* comparing the full version against it, so `<1` means "strictly before
|
|
19
|
-
* 1.0.0" and `>1.2` means "newer than 1.2.0" (1.2.5 included) rather than
|
|
20
|
-
* "1.3.0 or later". That is the ordinary meaning of a version bound.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
1
|
export type Semver = readonly [number, number, number]
|
|
24
2
|
|
|
25
3
|
const SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/
|
|
@@ -39,7 +17,6 @@ function compareParts(a: readonly number[], b: readonly number[]): number {
|
|
|
39
17
|
return 0
|
|
40
18
|
}
|
|
41
19
|
|
|
42
|
-
/** -1 if `a` is older than `b`, 0 if equal, 1 if newer. Both must be full semver. */
|
|
43
20
|
export function compareSemver(a: string, b: string): number {
|
|
44
21
|
const left = parseSemver(a)
|
|
45
22
|
const right = parseSemver(b)
|
|
@@ -69,7 +46,6 @@ function parseComparator(token: string): Comparator | null {
|
|
|
69
46
|
return { op, parts }
|
|
70
47
|
}
|
|
71
48
|
|
|
72
|
-
/** Parses a `meith` range string, or null when it does not parse. */
|
|
73
49
|
export function parseMeithRange(range: string): readonly Comparator[] | null {
|
|
74
50
|
const trimmed = range.trim()
|
|
75
51
|
if (trimmed === '') return null
|
|
@@ -101,11 +77,6 @@ function satisfiesComparator(comparator: Comparator, version: Semver): boolean {
|
|
|
101
77
|
}
|
|
102
78
|
}
|
|
103
79
|
|
|
104
|
-
/**
|
|
105
|
-
* Whether `version` (major.minor.patch) satisfies every comparator in
|
|
106
|
-
* `range`. An unparseable range or version is never satisfied — an honest
|
|
107
|
-
* "no", not a thrown error, since this runs against untrusted feed data.
|
|
108
|
-
*/
|
|
109
80
|
export function satisfiesMeithRange(range: string, version: string): boolean {
|
|
110
81
|
const comparators = parseMeithRange(range)
|
|
111
82
|
if (comparators === null) return false
|
package/src/refresh.ts
CHANGED
|
@@ -8,13 +8,7 @@ export interface RefreshCatalogInput {
|
|
|
8
8
|
readonly url: string
|
|
9
9
|
readonly repository: MarketplaceCacheRepository
|
|
10
10
|
readonly build: BuildInfo
|
|
11
|
-
/** How this build resolves a listing's key against what it compiled in. */
|
|
12
11
|
readonly resolveInstalled: (listing: MarketplaceListing) => InstalledEntry | null
|
|
13
|
-
/**
|
|
14
|
-
* Called once per newly-detected (plugin, version) update, after it has
|
|
15
|
-
* already been claimed as notified — see docs/marketplace.md for why the
|
|
16
|
-
* claim happens first, and what that means if this throws.
|
|
17
|
-
*/
|
|
18
12
|
readonly notifyUpdate: (listing: MarketplaceListing) => Promise<void>
|
|
19
13
|
readonly now?: () => Date
|
|
20
14
|
readonly fetchImpl?: typeof fetch
|
|
@@ -28,15 +22,6 @@ export interface RefreshCatalogResult {
|
|
|
28
22
|
readonly error: string | null
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
* Fetches, validates and caches the feed, then raises the update
|
|
33
|
-
* notification for any plugin whose new version this board has not already
|
|
34
|
-
* notified about. This is the one function both the daily task and the
|
|
35
|
-
* admin "Refresh" button call — see docs/marketplace.md — so there is
|
|
36
|
-
* exactly one place that decides what counts as a successful refresh, and
|
|
37
|
-
* the two can run concurrently: `claimNotified` is what keeps a race between
|
|
38
|
-
* them from raising the same (plugin, version) update twice.
|
|
39
|
-
*/
|
|
40
25
|
export async function refreshCatalog(input: RefreshCatalogInput): Promise<RefreshCatalogResult> {
|
|
41
26
|
const now = input.now ?? (() => new Date())
|
|
42
27
|
|
package/src/schema.ts
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shape of a listing on the wire — mirroring `marketplace/schema.json`
|
|
3
|
-
* and `scripts/marketplace-gen.mjs`, which this package never imports (a
|
|
4
|
-
* board-side consumer and the generator that produces the feed it consumes
|
|
5
|
-
* are deliberately separate concerns; see docs/marketplace.md). One
|
|
6
|
-
* difference is load-bearing: `marketplace/schema.json` validates a
|
|
7
|
-
* *listing file* before it is merged, where `screenshots` is a bare
|
|
8
|
-
* filename. What a board actually fetches is the *emitted* feed at
|
|
9
|
-
* `/marketplace/v1.json`, where `buildFeed()` has already rewritten every
|
|
10
|
-
* screenshot into an absolute site path (`/marketplace/screenshots/x.png`)
|
|
11
|
-
* and wrapped the listings in `{ schema, listings }`. `validateFeed` below
|
|
12
|
-
* validates that served shape — same fields, same rules, screenshots
|
|
13
|
-
* checked as paths rather than bare names — which is "the same schema
|
|
14
|
-
* shape" the issue asks for once the wrapper and the path rewrite are
|
|
15
|
-
* accounted for. `packages/marketplace/src/schema.test.ts` cross-checks the
|
|
16
|
-
* required field list against `marketplace/schema.json` itself so the two
|
|
17
|
-
* cannot silently drift apart.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
1
|
export type ListingKind = 'plugin' | 'theme'
|
|
21
2
|
|
|
22
3
|
export interface MarketplaceListing {
|
|
@@ -52,15 +33,9 @@ export const REQUIRED_LISTING_FIELDS = [
|
|
|
52
33
|
'licence',
|
|
53
34
|
] as const
|
|
54
35
|
|
|
55
|
-
/**
|
|
56
|
-
* Mirrors KEY_PATTERN in scripts/marketplace-gen.mjs and
|
|
57
|
-
* packages/plugin-kit/src/plugin.ts. Exported so
|
|
58
|
-
* scripts/marketplace-gen.test.ts can pin all three against each other.
|
|
59
|
-
*/
|
|
60
36
|
export const KEY_PATTERN = /^[a-z][a-z0-9-]{1,39}$/
|
|
61
37
|
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/
|
|
62
38
|
const PACKAGE_PATTERN = /^(@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/
|
|
63
|
-
/** The served feed's screenshots are absolute site paths, not bare filenames — see this file's own header. */
|
|
64
39
|
const SCREENSHOT_PATH_PATTERN = /^\/marketplace\/screenshots\/[a-z0-9][a-z0-9-]*\.png$/
|
|
65
40
|
const KINDS = new Set<string>(['plugin', 'theme'])
|
|
66
41
|
|
|
@@ -148,12 +123,6 @@ function validateListing(entry: unknown, index: number): readonly string[] {
|
|
|
148
123
|
return errors
|
|
149
124
|
}
|
|
150
125
|
|
|
151
|
-
/**
|
|
152
|
-
* Validates the document a board fetches from `/marketplace/v1.json` (or a
|
|
153
|
-
* self-hosted mirror of it) against the same shape MEI-79's schema defines.
|
|
154
|
-
* Never throws — a malformed feed is exactly the case this function exists
|
|
155
|
-
* to report, not to crash the fetch task over.
|
|
156
|
-
*/
|
|
157
126
|
export function validateFeed(raw: unknown): FeedValidation {
|
|
158
127
|
if (!isRecord(raw)) {
|
|
159
128
|
return { ok: false, feed: null, errors: ['the feed must be a JSON object'] }
|
package/src/status.ts
CHANGED
|
@@ -16,21 +16,16 @@ export interface CompatibilityCheck {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface BuildInfo {
|
|
19
|
-
/** This board's own release version — `CODE_VERSION`, checked against `meith`. */
|
|
20
19
|
readonly meithVersion: string
|
|
21
|
-
/** The plugin-kit major this build implements, checked against a plugin listing's `apiVersion`. */
|
|
22
20
|
readonly pluginApiMajor: number
|
|
23
|
-
/** The theme-kit major this build implements, checked against a theme listing's `apiVersion`. */
|
|
24
21
|
readonly themeApiMajor: number
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export interface CompatibilityResult {
|
|
28
25
|
readonly compatible: boolean
|
|
29
|
-
/** Set exactly when `compatible` is false — why, in words a board operator can act on. */
|
|
30
26
|
readonly reason: string | null
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
/** Checks a listing's declared `apiVersion` and `meith` range against this build. */
|
|
34
29
|
export function checkCompatibility(
|
|
35
30
|
listing: CompatibilityCheck,
|
|
36
31
|
build: BuildInfo,
|
|
@@ -54,10 +49,8 @@ export function checkCompatibility(
|
|
|
54
49
|
return { compatible: false, reason: reasons.join('; ') }
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
/** What this build already knows about a listing's key, if anything. */
|
|
58
52
|
export interface InstalledEntry {
|
|
59
53
|
readonly enabled: boolean
|
|
60
|
-
/** The compiled version to compare the feed against, or null when there is none to compare. */
|
|
61
54
|
readonly version: string | null
|
|
62
55
|
}
|
|
63
56
|
|
|
@@ -67,25 +60,9 @@ export interface ListingStatusInput extends CompatibilityCheck {
|
|
|
67
60
|
|
|
68
61
|
export interface ListingStatusResult {
|
|
69
62
|
readonly status: ListingStatus
|
|
70
|
-
/** Set exactly when `status` is 'incompatible'. */
|
|
71
63
|
readonly incompatibleReason: string | null
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
/**
|
|
75
|
-
* The five statuses the Browse tab renders, computed against what this build
|
|
76
|
-
* actually contains — never against what installing would do, because this
|
|
77
|
-
* screen does not install anything.
|
|
78
|
-
*
|
|
79
|
-
* An installed plugin or theme that is merely *running* is never recomputed
|
|
80
|
-
* to 'incompatible' on the strength of the catalog's opinion of its current
|
|
81
|
-
* listing — what is already active keeps being reported as active. Incompat-
|
|
82
|
-
* ibility only gates two things: a listing nothing has installed, and an
|
|
83
|
-
* update this board would otherwise offer. That is also why a theme (whose
|
|
84
|
-
* compiled version this build cannot see — `defineTheme` carries none) never
|
|
85
|
-
* reports 'update-available': there is nothing reliable to compare against,
|
|
86
|
-
* so it settles on active/disabled once compatibility has been read for the
|
|
87
|
-
* not-installed case.
|
|
88
|
-
*/
|
|
89
66
|
export function computeListingStatus(
|
|
90
67
|
input: ListingStatusInput,
|
|
91
68
|
build: BuildInfo,
|