@gjsify/cli 0.59.0 → 0.60.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.
@@ -0,0 +1,213 @@
1
+ // Do not publish a package whose dependency closure contains a version nobody can install.
2
+ //
3
+ // THE INCIDENT (v0.59.0 / v0.59.1). `@gjsify/adwaita-fonts@0.59.x` was accepted
4
+ // by npm (202) and never served — it sits in "Validating". The sweep published
5
+ // its dependents anyway: `publish-pins.ts` asks whether the NAME exists, and
6
+ // `adwaita-fonts` has existed since 0.5x, so the guard passed. `adwaita-web` and
7
+ // `adwaita-nativescript` 0.59.x went live pinning a version the registry does not
8
+ // serve, and are uninstallable. A name guard cannot see that; a VERSION guard can,
9
+ // at the price the name guard was designed to avoid — a version is eventually
10
+ // consistent for minutes after its own 2xx (see `publish-readback.ts`), so this
11
+ // one WAITS for the pin before it holds, and consults the status endpoint
12
+ // (`publish-status.ts`) so a version that is terminally unavailable does not cost
13
+ // the wait.
14
+ //
15
+ // The sweep is one `gjsify publish` PROCESS per package (`foreach --exec`), so a
16
+ // verdict reached by one process cannot reach the next in memory. The ledger is
17
+ // how it does: a JSONL file named by `GJSIFY_PUBLISH_LEDGER`, appended by every
18
+ // process that finds a version unserved or holds a package. A dependent of a held
19
+ // package then holds at once from the ledger instead of waiting out the budget
20
+ // again — but only for a TERMINAL class; a `validating` entry is re-probed, and a
21
+ // confirmed re-probe appends `served`. The end-of-job summary is rendered from
22
+ // the same file.
23
+ import { appendFileSync, existsSync, readFileSync } from 'node:fs';
24
+ import { REQUIRED_BLOCK } from './publish-pins.js';
25
+ import { verifyPublishedVersion } from './publish-readback.js';
26
+ import { classifyUnserved, describeClass, fetchVersionStatus, isTerminalClass, } from './publish-status.js';
27
+ /** Wait for a pin this long before holding. Covers the measured 252 s write lag (publish-readback.ts). */
28
+ export const DEFAULT_HOLD_WAIT_MS = 300_000;
29
+ /** `verifyPublishedVersion` needs a budget > 0; this makes it exactly one probe. */
30
+ const SINGLE_PROBE_BUDGET_MS = 1;
31
+ export function appendLedger(path, entry) {
32
+ if (!path)
33
+ return;
34
+ appendFileSync(path, `${JSON.stringify(entry)}\n`);
35
+ }
36
+ /** Tolerates a missing file (no entries yet) and skips unparseable lines rather than losing the rest. */
37
+ export function readLedger(path) {
38
+ if (!path || !existsSync(path))
39
+ return [];
40
+ const entries = [];
41
+ for (const line of readFileSync(path, 'utf-8').split('\n')) {
42
+ if (!line.trim())
43
+ continue;
44
+ try {
45
+ const parsed = JSON.parse(line);
46
+ if (parsed.ev === 'unserved' || parsed.ev === 'held' || parsed.ev === 'served')
47
+ entries.push(parsed);
48
+ }
49
+ catch {
50
+ /* a torn line from a killed process must not hide the entries around it */
51
+ }
52
+ }
53
+ return entries;
54
+ }
55
+ /**
56
+ * The version a rewritten pin asks for, when the spec names exactly one:
57
+ * `0.59.1`, `^0.59.1`, `~0.59.1`. `undefined` for a range, tag or URL — those are
58
+ * not decidable against one version and the name-level guard covers them.
59
+ */
60
+ export function pinVersion(spec) {
61
+ const match = /^[\^~]?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(spec.trim());
62
+ return match?.[1];
63
+ }
64
+ /**
65
+ * Every pin whose exact version the registry does not serve (after waiting for
66
+ * one that may be late), with the reason. Empty = safe to publish.
67
+ *
68
+ * Three cheap exits come first, in order: the ledger already knows this version
69
+ * is unserved or held; one packument probe finds it served (nine in ten); the
70
+ * status endpoint says it can never be served. Only a pin that is none of those —
71
+ * possibly just late — pays the wait.
72
+ */
73
+ export async function findUnavailablePins(input) {
74
+ const reasons = [];
75
+ const seen = new Map();
76
+ for (const pin of input.pins) {
77
+ const version = pinVersion(pin.spec);
78
+ if (!version) {
79
+ input.log?.(`gjsify publish: ${pin.name}@${pin.spec} names no single version — not checked for hold`);
80
+ continue;
81
+ }
82
+ const key = `${pin.name}@${version}`;
83
+ // An optional/peer edge is reported but never holds (see `holdsPublish`),
84
+ // so it does not earn the wait either: one probe.
85
+ const cacheKey = pin.block === REQUIRED_BLOCK ? key : `${key}#nowait`;
86
+ if (!seen.has(cacheKey)) {
87
+ const probe = pin.block === REQUIRED_BLOCK ? input : { ...input, waitBudgetMs: 0 };
88
+ seen.set(cacheKey, await judgePin(probe, pin.name, version));
89
+ }
90
+ const verdict = seen.get(cacheKey);
91
+ // One reason per EDGE: the same target in two blocks costs different things
92
+ // at install time, and the operator should see both.
93
+ if (verdict)
94
+ reasons.push({ ...verdict, block: pin.block });
95
+ }
96
+ return reasons;
97
+ }
98
+ async function judgePin(input, name, version) {
99
+ const base = { name, version, block: 'dependencies' };
100
+ // The LAST entry wins: a `served` entry supersedes the `unserved` one before it.
101
+ const known = [...input.ledger].reverse().find((e) => e.name === name && e.version === version);
102
+ if (known?.ev === 'served')
103
+ return null;
104
+ // Only a terminal class short-circuits. npm holds EVERY new version in
105
+ // `validating` for 30–90 s, and the publisher's own read-back runs inside
106
+ // that window, so a non-terminal entry is stale by the time a dependent
107
+ // looks (v0.60.0: 92 of 215 packages held on it). Fall through and re-probe.
108
+ if (known?.ev === 'unserved' && isTerminalClass(known.cls)) {
109
+ return { ...base, cls: known.cls, detail: known.detail };
110
+ }
111
+ if (known?.ev === 'held') {
112
+ return {
113
+ ...base,
114
+ cls: 'held',
115
+ detail: `was itself held — ${summariseReasons(known.reasons)}`,
116
+ };
117
+ }
118
+ const probeInput = {
119
+ registry: input.registry,
120
+ name,
121
+ version,
122
+ authorization: input.authorization,
123
+ fetchImpl: input.fetchImpl,
124
+ sleep: input.sleep,
125
+ now: input.now,
126
+ log: input.log,
127
+ };
128
+ let readback = await verifyPublishedVersion({
129
+ ...probeInput,
130
+ budgetMs: SINGLE_PROBE_BUDGET_MS,
131
+ });
132
+ if (readback.confirmed)
133
+ return confirmedServed(input, known, name, version);
134
+ const ask = () => fetchVersionStatus({
135
+ registry: input.registry,
136
+ name,
137
+ version,
138
+ token: input.statusToken,
139
+ fetchImpl: input.fetchImpl,
140
+ });
141
+ let classification = classifyUnserved(readback.verdict, await ask());
142
+ if (!classification.terminal && input.waitBudgetMs > 0) {
143
+ readback = await verifyPublishedVersion({
144
+ ...probeInput,
145
+ budgetMs: input.waitBudgetMs,
146
+ });
147
+ if (readback.confirmed)
148
+ return confirmedServed(input, known, name, version);
149
+ classification = classifyUnserved(readback.verdict, await ask());
150
+ }
151
+ return { ...base, cls: classification.cls, detail: classification.detail };
152
+ }
153
+ function confirmedServed(input, known, name, version) {
154
+ if (known?.ev === 'unserved')
155
+ appendLedger(input.ledgerPath, { ev: 'served', name, version });
156
+ return null;
157
+ }
158
+ /** Only a `dependencies` edge holds a publish, as in the name-level guard; the rest are reported. */
159
+ export const holdsPublish = (reason) => reason.block === REQUIRED_BLOCK;
160
+ export function summariseReasons(reasons) {
161
+ return reasons
162
+ .map((r) => `${r.name}@${r.version} is ${r.cls === 'held' ? 'held' : describeClass(r.cls)}`)
163
+ .join('; ');
164
+ }
165
+ /** Classify a version THIS process PUT and could not read back, and write it to the ledger. */
166
+ export async function recordUnserved(input) {
167
+ const status = await fetchVersionStatus({
168
+ registry: input.registry,
169
+ name: input.name,
170
+ version: input.version,
171
+ token: input.statusToken,
172
+ fetchImpl: input.fetchImpl,
173
+ });
174
+ const classification = classifyUnserved(input.verdict, status);
175
+ appendLedger(input.ledgerPath, {
176
+ ev: 'unserved',
177
+ name: input.name,
178
+ version: input.version,
179
+ cls: classification.cls,
180
+ detail: classification.detail,
181
+ });
182
+ return classification;
183
+ }
184
+ /**
185
+ * The job-summary markdown for a ledger: what is unserved and why, what was held
186
+ * and why, and the exact command that publishes the held set once their
187
+ * dependencies are installable. `undefined` when nothing needs saying.
188
+ */
189
+ export function renderLedgerSummary(entries, ref) {
190
+ const served = new Set(entries.filter((e) => e.ev === 'served').map((e) => `${e.name}@${e.version}`));
191
+ const unserved = entries.filter((e) => e.ev === 'unserved' && !served.has(`${e.name}@${e.version}`));
192
+ const held = entries.filter((e) => e.ev === 'held');
193
+ if (unserved.length === 0 && held.length === 0)
194
+ return undefined;
195
+ const lines = ['### npm versions that were accepted but are not installable', ''];
196
+ if (unserved.length > 0) {
197
+ lines.push('| Package | Version | State | Detail |', '| --- | --- | --- | --- |');
198
+ for (const e of unserved) {
199
+ lines.push(`| \`${e.name}\` | ${e.version} | ${describeClass(e.cls)} | ${e.detail} |`);
200
+ }
201
+ lines.push('');
202
+ }
203
+ if (held.length > 0) {
204
+ lines.push('### Held — NOT published in this run', '', 'Each package below depends on a version npm does not serve, so publishing it would have shipped a package that cannot be installed.', '', '| Package | Version | Because |', '| --- | --- | --- |');
205
+ for (const e of held)
206
+ lines.push(`| \`${e.name}\` | ${e.version} | ${summariseReasons(e.reasons)} |`);
207
+ lines.push('', 'Once the dependencies above are served by npm (a `validating` version appears on its own, or is approved/released by a maintainer), publish the held set with:', '', '```sh', `gh workflow run release.yml --ref ${ref} -f skip_publish=false -f only='${[...new Set(held.map((e) => e.name))].join(' ')}'`, '```');
208
+ if (entries.some((e) => e.ev === 'unserved' && (e.cls === 'blocked' || e.cls === 'deleted'))) {
209
+ lines.push('', 'A `blocked` or `deleted` version is a spent number: re-running the release cannot serve it, the package needs a version bump.');
210
+ }
211
+ }
212
+ return lines.join('\n');
213
+ }
@@ -0,0 +1,83 @@
1
+ import type { ReadbackVerdict } from './publish-readback.js';
2
+ /** The lifecycle states the status endpoint documents. */
3
+ export type VersionStatus = 'published' | 'validating' | 'staged' | 'blocked' | 'deleted';
4
+ export type StatusUnavailableReason =
5
+ /** No token to ask with — the OIDC-only CI case. */
6
+ 'no-credential'
7
+ /** 401: the token is wrong or lacks the scope. */
8
+ | 'rejected'
9
+ /** 403: the endpoint's per-package rollout gate is closed for this package. */
10
+ | 'not-enabled'
11
+ /** 404: the version does not exist, OR this token cannot see the package. Indistinguishable by design. */
12
+ | 'not-found'
13
+ /** 429 / 5xx / timeout / unparseable body. */
14
+ | 'error';
15
+ export type StatusProbe = {
16
+ state: 'known';
17
+ status: VersionStatus;
18
+ } | {
19
+ state: 'unavailable';
20
+ reason: StatusUnavailableReason;
21
+ detail: string;
22
+ };
23
+ export interface FetchVersionStatusInput {
24
+ registry: string;
25
+ name: string;
26
+ version: string;
27
+ /** A bare npm access token / granular access token. Absent → `no-credential`, no request is sent. */
28
+ token?: string;
29
+ probeTimeoutMs?: number;
30
+ fetchImpl?: typeof fetch;
31
+ }
32
+ /**
33
+ * The token to ask the status endpoint with: the dedicated `NPM_STATUS_TOKEN`
34
+ * first, then a `NODE_AUTH_TOKEN` that is a real token and not the
35
+ * `XXXXX-XXXXX-XXXXX-XXXXX` literal `actions/setup-node` injects when given a
36
+ * `registry-url`. Empty string and the placeholder both mean "none".
37
+ */
38
+ export declare function statusTokenFromEnv(env: Record<string, string | undefined>): string | undefined;
39
+ /** `GET {registry}/-/package/{name}/version/{version}/status`, classified. Never throws. */
40
+ export declare function fetchVersionStatus(input: FetchVersionStatusInput): Promise<StatusProbe>;
41
+ /**
42
+ * What a version the packument does not serve IS. The first five come from the
43
+ * registry's own status; the last two are what we can say without it.
44
+ */
45
+ export type UnservedClass =
46
+ /** Automated review has not finished; the version will appear — or turn `blocked`. */
47
+ 'validating'
48
+ /** Waits for a maintainer to approve it; nothing automatic will change this. */
49
+ | 'staged'
50
+ /** Failed automated validation. Will never be served. */
51
+ | 'blocked'
52
+ /** Deleted. Will never be served, and the number cannot be re-used. */
53
+ | 'deleted'
54
+ /** The registry says `published` but the install document does not carry it yet (write lag). */
55
+ | 'published-lagging'
56
+ /** The registry has no record of the version: the PUT did not land. */
57
+ | 'missing'
58
+ /** Not served and the status endpoint could not be asked; the read-back verdict is all there is. */
59
+ | 'unchecked';
60
+ export interface Classification {
61
+ cls: UnservedClass;
62
+ /** One line for the log and the summary. */
63
+ detail: string;
64
+ /**
65
+ * Waiting can no longer change the answer. `validating`, `published-lagging`
66
+ * and `unchecked` can still resolve on their own; the rest cannot.
67
+ */
68
+ terminal: boolean;
69
+ }
70
+ /** `classifyUnserved(...).terminal` for a class read back from the ledger. */
71
+ export declare function isTerminalClass(cls: UnservedClass): boolean;
72
+ /**
73
+ * Fold the registry's lifecycle status and the read-back verdict into ONE class.
74
+ *
75
+ * The status wins when it answered. When it did not, the read-back verdict is
76
+ * kept in the detail but never promoted to `missing`: `not-published` from the
77
+ * packument alone is exactly the observation a `validating` version produces, so
78
+ * "missing — re-publish" would send the operator into a 409. Only a status 404
79
+ * (the registry itself has no such version) earns `missing`.
80
+ */
81
+ export declare function classifyUnserved(verdict: ReadbackVerdict, probe: StatusProbe): Classification;
82
+ /** The class as a human label for tables. */
83
+ export declare function describeClass(cls: UnservedClass): string;
@@ -0,0 +1,193 @@
1
+ // What happened to a version npm ACCEPTED and the packument does not serve?
2
+ //
3
+ // THE INCIDENT (v0.59.0 / v0.59.1). A handful of packages — `@gjsify/adwaita-fonts`
4
+ // and five `@gjsify/example-*` — got HTTP 202 on the PUT and never appeared in the
5
+ // public packument; a re-PUT is answered 409. The npm website shows them as
6
+ // "Validating" ("Automated review hasn't finished. The version will remain
7
+ // unavailable until review completes."). Both read-back verdicts that existed
8
+ // for them — `not-published` and `recorded-not-served` — were wrong in spirit: the
9
+ // first told the operator to re-publish (answered 409), the second to wait (it
10
+ // would not come). The packument has no field that says which.
11
+ //
12
+ // The registry does: `GET /-/package/{name}/version/{version}/status` answers
13
+ // `published | validating | staged | blocked | deleted`. It accepts an npm access
14
+ // token or a granular access token with publish access to the package — NOT the
15
+ // short-lived OIDC exchange token the release sweep publishes with, and not
16
+ // anonymous requests (401). So in CI the answer is only available when a
17
+ // read-only token is provided as `NPM_STATUS_TOKEN`; without one this module says
18
+ // so (`no-credential`) instead of guessing, and the callers keep the old behaviour
19
+ // plus a hold, which needs no status to be safe.
20
+ import { escapePackageName } from './publish-headers.js';
21
+ const KNOWN_STATUSES = ['published', 'validating', 'staged', 'blocked', 'deleted'];
22
+ /**
23
+ * The token to ask the status endpoint with: the dedicated `NPM_STATUS_TOKEN`
24
+ * first, then a `NODE_AUTH_TOKEN` that is a real token and not the
25
+ * `XXXXX-XXXXX-XXXXX-XXXXX` literal `actions/setup-node` injects when given a
26
+ * `registry-url`. Empty string and the placeholder both mean "none".
27
+ */
28
+ export function statusTokenFromEnv(env) {
29
+ for (const candidate of [env['NPM_STATUS_TOKEN'], env['NODE_AUTH_TOKEN']]) {
30
+ const token = candidate?.trim();
31
+ if (token && !/^X+(-X+)+$/.test(token))
32
+ return token;
33
+ }
34
+ return undefined;
35
+ }
36
+ /** `GET {registry}/-/package/{name}/version/{version}/status`, classified. Never throws. */
37
+ export async function fetchVersionStatus(input) {
38
+ if (!input.token) {
39
+ return {
40
+ state: 'unavailable',
41
+ reason: 'no-credential',
42
+ detail: 'the status endpoint needs an npm access token (set NPM_STATUS_TOKEN); OIDC publish tokens are not accepted',
43
+ };
44
+ }
45
+ const doFetch = input.fetchImpl ?? ((...args) => globalThis.fetch(...args));
46
+ const registryClean = input.registry.endsWith('/') ? input.registry.slice(0, -1) : input.registry;
47
+ const url = `${registryClean}/-/package/${escapePackageName(input.name)}/version/${encodeURIComponent(input.version)}/status`;
48
+ let res;
49
+ try {
50
+ res = await doFetch(url, {
51
+ headers: {
52
+ accept: 'application/json',
53
+ authorization: `Bearer ${input.token}`,
54
+ },
55
+ signal: AbortSignal.timeout(input.probeTimeoutMs ?? 30_000),
56
+ });
57
+ }
58
+ catch (err) {
59
+ return {
60
+ state: 'unavailable',
61
+ reason: 'error',
62
+ detail: err instanceof Error ? err.message : String(err),
63
+ };
64
+ }
65
+ if (res.status === 401) {
66
+ return {
67
+ state: 'unavailable',
68
+ reason: 'rejected',
69
+ detail: '401 — the token is missing, invalid or expired',
70
+ };
71
+ }
72
+ if (res.status === 403) {
73
+ return {
74
+ state: 'unavailable',
75
+ reason: 'not-enabled',
76
+ detail: '403 — package version status is not enabled for this package',
77
+ };
78
+ }
79
+ if (res.status === 404) {
80
+ return {
81
+ state: 'unavailable',
82
+ reason: 'not-found',
83
+ detail: '404 — no such version, or the token cannot see this package',
84
+ };
85
+ }
86
+ if (!res.ok) {
87
+ return {
88
+ state: 'unavailable',
89
+ reason: 'error',
90
+ detail: `${res.status} ${res.statusText}`,
91
+ };
92
+ }
93
+ let body;
94
+ try {
95
+ body = await res.json();
96
+ }
97
+ catch (err) {
98
+ return {
99
+ state: 'unavailable',
100
+ reason: 'error',
101
+ detail: `status body is not JSON (${err instanceof Error ? err.message : String(err)})`,
102
+ };
103
+ }
104
+ const status = body?.status;
105
+ if (typeof status !== 'string' || !KNOWN_STATUSES.includes(status)) {
106
+ return {
107
+ state: 'unavailable',
108
+ reason: 'error',
109
+ detail: `unrecognised status ${JSON.stringify(status)}`,
110
+ };
111
+ }
112
+ return { state: 'known', status: status };
113
+ }
114
+ /** `classifyUnserved(...).terminal` for a class read back from the ledger. */
115
+ export function isTerminalClass(cls) {
116
+ return cls !== 'validating' && cls !== 'published-lagging' && cls !== 'unchecked';
117
+ }
118
+ /** Why the status could not be asked, in the words the `unchecked` line uses. */
119
+ function whyUnchecked(probe) {
120
+ return probe.reason === 'no-credential' ? 'status endpoint not asked: no NPM_STATUS_TOKEN' : probe.detail;
121
+ }
122
+ /**
123
+ * Fold the registry's lifecycle status and the read-back verdict into ONE class.
124
+ *
125
+ * The status wins when it answered. When it did not, the read-back verdict is
126
+ * kept in the detail but never promoted to `missing`: `not-published` from the
127
+ * packument alone is exactly the observation a `validating` version produces, so
128
+ * "missing — re-publish" would send the operator into a 409. Only a status 404
129
+ * (the registry itself has no such version) earns `missing`.
130
+ */
131
+ export function classifyUnserved(verdict, probe) {
132
+ if (probe.state === 'known') {
133
+ switch (probe.status) {
134
+ case 'validating':
135
+ return {
136
+ cls: 'validating',
137
+ detail: 'npm accepted the upload and its automated review has not finished',
138
+ terminal: false,
139
+ };
140
+ case 'staged':
141
+ return {
142
+ cls: 'staged',
143
+ detail: 'awaiting maintainer approval on npm',
144
+ terminal: true,
145
+ };
146
+ case 'blocked':
147
+ return {
148
+ cls: 'blocked',
149
+ detail: 'failed npm automated validation',
150
+ terminal: true,
151
+ };
152
+ case 'deleted':
153
+ return { cls: 'deleted', detail: 'deleted on npm', terminal: true };
154
+ case 'published':
155
+ return {
156
+ cls: 'published-lagging',
157
+ detail: 'npm reports it published; the install document does not serve it yet',
158
+ terminal: false,
159
+ };
160
+ }
161
+ }
162
+ if (probe.reason === 'not-found') {
163
+ return {
164
+ cls: 'missing',
165
+ detail: `the registry has no such version (${probe.detail}); the PUT did not land`,
166
+ terminal: true,
167
+ };
168
+ }
169
+ return {
170
+ cls: 'unchecked',
171
+ detail: `not served (read-back: ${verdict}); ${whyUnchecked(probe)}`,
172
+ terminal: false,
173
+ };
174
+ }
175
+ /** The class as a human label for tables. */
176
+ export function describeClass(cls) {
177
+ switch (cls) {
178
+ case 'validating':
179
+ return 'validating (npm automated review pending)';
180
+ case 'staged':
181
+ return 'staged (needs maintainer approval)';
182
+ case 'blocked':
183
+ return 'blocked (failed automated validation)';
184
+ case 'deleted':
185
+ return 'deleted';
186
+ case 'published-lagging':
187
+ return 'published, not yet served';
188
+ case 'missing':
189
+ return 'missing (never recorded)';
190
+ case 'unchecked':
191
+ return 'not served (status not checked)';
192
+ }
193
+ }
@@ -106,8 +106,9 @@ export interface ResolvedNodeGiPackage {
106
106
  * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
107
107
  *
108
108
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
109
- * `package.json` (the `exports` map is what resolves every subpath), every `.js`
110
- * at the package root (every `exports` target is one), and `overrides/`, which
109
+ * `package.json` (the `exports` map is what resolves every subpath, and itself an
110
+ * `exports` target for bundler hook discovery), every `.js` at the package root
111
+ * (every other `exports` target is one), and `overrides/`, which
111
112
  * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
112
113
  * REAL package's `exports` map, so a node-gi release that adds an entry outside
113
114
  * this set fails there rather than in a `.app` a stranger downloaded.
@@ -217,8 +217,9 @@ function resolveStagedPackage(specifier, options) {
217
217
  * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
218
218
  *
219
219
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
220
- * `package.json` (the `exports` map is what resolves every subpath), every `.js`
221
- * at the package root (every `exports` target is one), and `overrides/`, which
220
+ * `package.json` (the `exports` map is what resolves every subpath, and itself an
221
+ * `exports` target for bundler hook discovery), every `.js` at the package root
222
+ * (every other `exports` target is one), and `overrides/`, which
222
223
  * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
223
224
  * REAL package's `exports` map, so a node-gi release that adds an entry outside
224
225
  * this set fails there rather than in a `.app` a stranger downloaded.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,22 +142,22 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/blueprint": "^0.59.0",
146
- "@gjsify/buffer": "^0.59.0",
147
- "@gjsify/create-app": "^0.59.0",
148
- "@gjsify/node-globals": "^0.59.0",
149
- "@gjsify/node-polyfills": "^0.59.0",
150
- "@gjsify/npm-registry": "^0.59.0",
151
- "@gjsify/resolve-npm": "^0.59.0",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.59.0",
153
- "@gjsify/vite-plugin-blueprint": "^0.59.0",
154
- "@gjsify/rolldown-plugin-pnp": "^0.59.0",
155
- "@gjsify/semver": "^0.59.0",
156
- "@gjsify/tar": "^0.59.0",
157
- "@gjsify/tsc": "^0.59.0",
158
- "@gjsify/utils": "^0.59.0",
159
- "@gjsify/web-polyfills": "^0.59.0",
160
- "@gjsify/workspace": "^0.59.0",
145
+ "@gjsify/blueprint": "^0.60.0",
146
+ "@gjsify/buffer": "^0.60.0",
147
+ "@gjsify/create-app": "^0.60.0",
148
+ "@gjsify/node-globals": "^0.60.0",
149
+ "@gjsify/node-polyfills": "^0.60.0",
150
+ "@gjsify/npm-registry": "^0.60.0",
151
+ "@gjsify/resolve-npm": "^0.60.0",
152
+ "@gjsify/rolldown-plugin-gjsify": "^0.60.0",
153
+ "@gjsify/vite-plugin-blueprint": "^0.60.0",
154
+ "@gjsify/rolldown-plugin-pnp": "^0.60.0",
155
+ "@gjsify/semver": "^0.60.0",
156
+ "@gjsify/tar": "^0.60.0",
157
+ "@gjsify/tsc": "^0.60.0",
158
+ "@gjsify/utils": "^0.60.0",
159
+ "@gjsify/web-polyfills": "^0.60.0",
160
+ "@gjsify/workspace": "^0.60.0",
161
161
  "acorn": "^8.17.0",
162
162
  "cosmiconfig": "^9.0.2",
163
163
  "get-tsconfig": "^4.14.0",
@@ -166,13 +166,13 @@
166
166
  "yargs": "^18.0.0"
167
167
  },
168
168
  "devDependencies": {
169
- "@gjsify/unit": "^0.59.0",
169
+ "@gjsify/unit": "^0.60.0",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "^0.59.0",
175
- "@gjsify/rolldown-native": "^0.59.0"
174
+ "@gjsify/oxfmt-native": "^0.60.0",
175
+ "@gjsify/rolldown-native": "^0.60.0"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {
package/showcases.json CHANGED
@@ -1,48 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "showcases": [
4
- {
5
- "name": "canvas2d-fireworks",
6
- "category": "dom",
7
- "package": "@gjsify/example-dom-canvas2d-fireworks",
8
- "description": "Colorful fireworks Canvas 2D example with Adwaita controls"
9
- },
10
- {
11
- "name": "excalibur-jelly-jumper",
12
- "category": "dom",
13
- "package": "@gjsify/example-dom-excalibur-jelly-jumper",
14
- "description": "Excalibur.js jelly-jumper game"
15
- },
16
- {
17
- "name": "three-geometry-teapot",
18
- "category": "dom",
19
- "package": "@gjsify/example-dom-three-geometry-teapot",
20
- "description": "Three.js Utah teapot"
21
- },
22
- {
23
- "name": "three-loader-ldraw",
24
- "category": "dom",
25
- "package": "@gjsify/example-dom-three-loader-ldraw",
26
- "description": "Three.js LDraw viewer — loads packed .mpd models over fetch and steps through their building instructions"
27
- },
28
- {
29
- "name": "three-postprocessing-pixel",
30
- "category": "dom",
31
- "package": "@gjsify/example-dom-three-postprocessing-pixel",
32
- "description": "Three.js postprocessing pixel demo"
33
- },
34
- {
35
- "name": "webrtc-loopback",
36
- "category": "dom",
37
- "package": "@gjsify/example-dom-webrtc-loopback",
38
- "description": "WebRTC data-channel loopback: two local peers exchange offer/answer + ICE, then echo messages over an RTCDataChannel"
39
- },
40
- {
41
- "name": "minimalist-browser",
42
- "category": "dom",
43
- "package": "@gjsify/example-dom-minimalist-browser",
44
- "description": "Minimalist browser with URL bar + back/forward + iframe content area; same code runs in browser (real iframe) and GJS (IFrameBridge over WebKit.WebView)"
45
- },
46
4
  {
47
5
  "name": "express-webserver",
48
6
  "category": "node",