@meith/marketplace 0.21.2 → 0.23.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/marketplace",
3
- "version": "0.21.2",
3
+ "version": "0.23.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.21.2"
22
+ "@meith/theme-kit": "0.23.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.2'
3
+ export const MEITH_VERSION = '0.23.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,