@gjsify/cli 0.59.1 → 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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.59.1",
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.1",
146
- "@gjsify/buffer": "^0.59.1",
147
- "@gjsify/create-app": "^0.59.1",
148
- "@gjsify/node-globals": "^0.59.1",
149
- "@gjsify/node-polyfills": "^0.59.1",
150
- "@gjsify/npm-registry": "^0.59.1",
151
- "@gjsify/resolve-npm": "^0.59.1",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.59.1",
153
- "@gjsify/vite-plugin-blueprint": "^0.59.1",
154
- "@gjsify/rolldown-plugin-pnp": "^0.59.1",
155
- "@gjsify/semver": "^0.59.1",
156
- "@gjsify/tar": "^0.59.1",
157
- "@gjsify/tsc": "^0.59.1",
158
- "@gjsify/utils": "^0.59.1",
159
- "@gjsify/web-polyfills": "^0.59.1",
160
- "@gjsify/workspace": "^0.59.1",
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.1",
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.1",
175
- "@gjsify/rolldown-native": "^0.59.1"
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",