@gjsify/cli 0.42.0 → 0.43.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/dist/cli.gjs.mjs +238 -210
- package/lib/actions/build.d.ts +6 -0
- package/lib/actions/build.js +64 -8
- package/lib/commands/ship.d.ts +2 -0
- package/lib/commands/ship.js +285 -93
- package/lib/types/config-data.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -0
- package/lib/utils/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -0
- package/lib/utils/ship/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +9 -6
- package/lib/utils/ship/depends.js +25 -3
- package/lib/utils/ship/discover.js +72 -6
- package/lib/utils/ship/formats.js +9 -1
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +76 -0
- package/lib/utils/ship/payload.js +91 -1
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +54 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.js +13 -0
- package/lib/utils/ship/stage-manifest.d.ts +154 -0
- package/lib/utils/ship/stage-manifest.js +408 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +91 -18
- package/lib/utils/ship/types.js +5 -0
- package/package.json +19 -19
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
/** The
|
|
1
|
+
/** The bundler-output fields this guard needs, from either engine. */
|
|
2
2
|
export interface GuardedChunk {
|
|
3
3
|
/** Emitted chunk name, for the error message. */
|
|
4
4
|
fileName?: string;
|
|
5
5
|
/** External specifiers the chunk STATICALLY imports (rollup/rolldown contract). */
|
|
6
6
|
imports?: readonly string[];
|
|
7
|
+
/** Emitted source, for the syntax check. Present on a written chunk on both engines. */
|
|
8
|
+
code?: string;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
|
|
@@ -20,5 +22,57 @@ export declare const GJS_ALLOWED_NODE_IMPORTS: readonly string[];
|
|
|
20
22
|
* allowlist. Sorted, so the error message and any test are stable.
|
|
21
23
|
*/
|
|
22
24
|
export declare function findDisallowedNodeImports(chunks: readonly GuardedChunk[], allowed?: readonly string[]): string[];
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Is `id` a BARE package specifier — the only shape GJS's ESM loader cannot resolve on
|
|
27
|
+
* its own?
|
|
28
|
+
*
|
|
29
|
+
* Relative and absolute paths name a file; anything carrying a URL scheme names a
|
|
30
|
+
* loader GJS either has (`gi:`, `file:`, `resource:`) or will report itself. `node:` is
|
|
31
|
+
* classified BEFORE this, because it has its own message and its own symptom.
|
|
32
|
+
* A win32 drive path (`C:\…`) matches the scheme test, which is the intended answer.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isBareSpecifier(id: string): boolean;
|
|
35
|
+
/** What the caller must tell the guard about this build's own promises. */
|
|
36
|
+
export interface GjsImportPolicy {
|
|
37
|
+
/**
|
|
38
|
+
* The target's own externals predicate — pass
|
|
39
|
+
* `createGjsExternalsPredicate(userExternal)`, the same one `externalsPlugin`
|
|
40
|
+
* enforces, so the guard and the build cannot disagree about what was declared.
|
|
41
|
+
* Omitted, nothing counts as declared.
|
|
42
|
+
*/
|
|
43
|
+
isExternal?: (id: string) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Every fileName the build emitted. `chunk.imports` lists sibling CHUNKS by fileName
|
|
46
|
+
* with no `./` prefix, which is indistinguishable from a bare package specifier — so
|
|
47
|
+
* the set is excluded by name rather than guessed at by shape.
|
|
48
|
+
*/
|
|
49
|
+
emitted?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The distinct bare specifiers `chunks` statically import that nothing in this build
|
|
53
|
+
* promised to provide. Sorted, so the error message and any test are stable.
|
|
54
|
+
*/
|
|
55
|
+
export declare function findUnresolvableBareImports(chunks: readonly GuardedChunk[], policy?: GjsImportPolicy): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Throw when a built `--app gjs` bundle statically imports something GJS cannot
|
|
58
|
+
* resolve — a bare `node:` builtin, or any other bare specifier this build did not
|
|
59
|
+
* declare external.
|
|
60
|
+
*
|
|
61
|
+
* The two are reported separately because they have different symptoms and different
|
|
62
|
+
* fixes: a `node:` specifier aborts with "Unsupported URI scheme", any other bare one
|
|
63
|
+
* with "Module not found", and the first means an alias target failed while the second
|
|
64
|
+
* means the dependency is simply not there.
|
|
65
|
+
*/
|
|
66
|
+
export declare function assertGjsBundleLoadable(chunks: readonly GuardedChunk[], outfile: string, policy?: GjsImportPolicy): void;
|
|
67
|
+
/**
|
|
68
|
+
* Throw when a written `--app gjs` chunk is not parseable JavaScript because raw JSX
|
|
69
|
+
* survived into it. See `@gjsify/rolldown-plugin-gjsify`'s `utils/jsx-survival.ts` for
|
|
70
|
+
* why the oracle is a parse and not a pattern, and for the measurement.
|
|
71
|
+
*
|
|
72
|
+
* Called ONLY when the effective JSX setting PRESERVES JSX, which is the only way it can
|
|
73
|
+
* reach an artifact: oxc compiles it otherwise. That is not a cost dodge but the precise
|
|
74
|
+
* condition — though the cost is real, and measured: parsing the repo's largest bundle
|
|
75
|
+
* (`dist/cli.gjs.mjs`, 6.8 MB) takes ~850 ms, which is not worth paying on every GJS
|
|
76
|
+
* build to re-answer a question the transform already settled.
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertGjsBundleParses(chunks: readonly GuardedChunk[], label: string): void;
|
|
@@ -37,6 +37,29 @@
|
|
|
37
37
|
// links the module, so one bad specifier kills the whole bundle. A dynamic
|
|
38
38
|
// `import('node:x')` throws lazily at its call site — a real bug, but not "the artifact
|
|
39
39
|
// cannot load", and not what this gate is for.
|
|
40
|
+
// ## `node:` was never the general case
|
|
41
|
+
//
|
|
42
|
+
// A bare `node:` specifier is one shape of ONE defect: the emitted module imports
|
|
43
|
+
// something the GJS loader cannot resolve. GJS's ESM loader has no node_modules walker
|
|
44
|
+
// and does not follow `package.json#exports`, so ANY bare specifier that survives into a
|
|
45
|
+
// `--app gjs` bundle is dead on arrival — `ImportError: Module not found: <specifier>`,
|
|
46
|
+
// again before one line runs. MEASURED: a `.tsx` entry with no JSX configuration makes
|
|
47
|
+
// oxc default to the automatic runtime with `importSource: 'react'`, the bundle carries
|
|
48
|
+
// `import { jsx } from "react/jsx-runtime"`, rolldown reports that as an
|
|
49
|
+
// `UNRESOLVED_IMPORT` **warning**, `gjsify build` exits 0, and `gjs -m` on the artifact
|
|
50
|
+
// dies with `ImportError: Module not found: react/jsx-runtime`. The `node:`-only guard
|
|
51
|
+
// saw none of it.
|
|
52
|
+
//
|
|
53
|
+
// The legitimate-vs-broken line is the same one `plugins/unresolved-workspace-import.ts`
|
|
54
|
+
// draws one layer up, and for the same reason a blanket "fail on any external" cannot be
|
|
55
|
+
// used: a DECLARED external is a promise the caller made and must be honoured. So the
|
|
56
|
+
// guard asks the target's OWN externals predicate (`createGjsExternalsPredicate`, the one
|
|
57
|
+
// `externalsPlugin` enforces) rather than a second list that can drift, and it ignores
|
|
58
|
+
// everything that is not a bare specifier — `gi://Gtk?version=4.0`, `file:`/`resource:`
|
|
59
|
+
// URLs and relative sibling-chunk paths all name something GJS can resolve on its own.
|
|
60
|
+
// `chunk.imports` also carries the fileNames of other EMITTED chunks, un-prefixed, so the
|
|
61
|
+
// emitted set is passed in and excluded by name.
|
|
62
|
+
import { locateSurvivingJsx, formatSurvivingJsx } from '@gjsify/rolldown-plugin-gjsify';
|
|
40
63
|
/**
|
|
41
64
|
* Bare `node:` specifiers a `--app gjs` bundle may keep as static imports.
|
|
42
65
|
*
|
|
@@ -64,18 +87,104 @@ export function findDisallowedNodeImports(chunks, allowed = GJS_ALLOWED_NODE_IMP
|
|
|
64
87
|
}
|
|
65
88
|
return [...found].sort();
|
|
66
89
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Is `id` a BARE package specifier — the only shape GJS's ESM loader cannot resolve on
|
|
92
|
+
* its own?
|
|
93
|
+
*
|
|
94
|
+
* Relative and absolute paths name a file; anything carrying a URL scheme names a
|
|
95
|
+
* loader GJS either has (`gi:`, `file:`, `resource:`) or will report itself. `node:` is
|
|
96
|
+
* classified BEFORE this, because it has its own message and its own symptom.
|
|
97
|
+
* A win32 drive path (`C:\…`) matches the scheme test, which is the intended answer.
|
|
98
|
+
*/
|
|
99
|
+
export function isBareSpecifier(id) {
|
|
100
|
+
if (id.startsWith('.') || id.startsWith('/') || id.startsWith('\\'))
|
|
101
|
+
return false;
|
|
102
|
+
return !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(id);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The distinct bare specifiers `chunks` statically import that nothing in this build
|
|
106
|
+
* promised to provide. Sorted, so the error message and any test are stable.
|
|
107
|
+
*/
|
|
108
|
+
export function findUnresolvableBareImports(chunks, policy = {}) {
|
|
109
|
+
const emitted = new Set(policy.emitted ?? []);
|
|
110
|
+
const found = new Set();
|
|
111
|
+
for (const chunk of chunks) {
|
|
112
|
+
for (const specifier of chunk.imports ?? []) {
|
|
113
|
+
// `node:` has its own diagnosis; classifying it here too would report one
|
|
114
|
+
// defect twice with two different fixes.
|
|
115
|
+
if (specifier.startsWith('node:'))
|
|
116
|
+
continue;
|
|
117
|
+
if (!isBareSpecifier(specifier))
|
|
118
|
+
continue;
|
|
119
|
+
if (emitted.has(specifier))
|
|
120
|
+
continue;
|
|
121
|
+
if (policy.isExternal?.(specifier))
|
|
122
|
+
continue;
|
|
123
|
+
found.add(specifier);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...found].sort();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Throw when a built `--app gjs` bundle statically imports something GJS cannot
|
|
130
|
+
* resolve — a bare `node:` builtin, or any other bare specifier this build did not
|
|
131
|
+
* declare external.
|
|
132
|
+
*
|
|
133
|
+
* The two are reported separately because they have different symptoms and different
|
|
134
|
+
* fixes: a `node:` specifier aborts with "Unsupported URI scheme", any other bare one
|
|
135
|
+
* with "Module not found", and the first means an alias target failed while the second
|
|
136
|
+
* means the dependency is simply not there.
|
|
137
|
+
*/
|
|
138
|
+
export function assertGjsBundleLoadable(chunks, outfile, policy = {}) {
|
|
139
|
+
const nodeOffenders = findDisallowedNodeImports(chunks);
|
|
140
|
+
if (nodeOffenders.length > 0) {
|
|
141
|
+
throw new Error(`gjsify build --app gjs: ${outfile} still imports ${nodeOffenders.length} bare \`node:\` module(s), ` +
|
|
142
|
+
'which stock GJS cannot resolve — it would fail at load with ' +
|
|
143
|
+
'"ImportError: Unsupported URI scheme for importing: node".\n' +
|
|
144
|
+
nodeOffenders.map((s) => ` - ${s}`).join('\n') +
|
|
145
|
+
'\n\nThese should have been aliased to their `@gjsify/*` polyfills. The usual cause is that the ' +
|
|
146
|
+
'alias TARGET could not be resolved, so the bundler externalised the import instead (look for ' +
|
|
147
|
+
'UNRESOLVED_IMPORT warnings above): e.g. a workspace checkout whose `@gjsify/*` packages have no ' +
|
|
148
|
+
'built `lib/`. Build the workspace first (`gjsify run build:infra`), or fix the resolution, then ' +
|
|
149
|
+
'rebuild.');
|
|
150
|
+
}
|
|
151
|
+
const bareOffenders = findUnresolvableBareImports(chunks, policy);
|
|
152
|
+
if (bareOffenders.length === 0)
|
|
71
153
|
return;
|
|
72
|
-
throw new Error(`gjsify build --app gjs: ${outfile}
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
154
|
+
throw new Error(`gjsify build --app gjs: ${outfile} statically imports ${bareOffenders.length} bare package ` +
|
|
155
|
+
'specifier(s) nothing in this build resolves or declares external. GJS has no node_modules walker ' +
|
|
156
|
+
'and does not follow `package.json#exports`, so it aborts the whole module graph at load with ' +
|
|
157
|
+
'"ImportError: Module not found: <specifier>".\n' +
|
|
158
|
+
bareOffenders.map((s) => ` - ${s}`).join('\n') +
|
|
159
|
+
'\n\nRolldown reports these as UNRESOLVED_IMPORT WARNINGS and re-emits the original specifier, ' +
|
|
160
|
+
'which is why the build reached this point at all. Likely causes:\n' +
|
|
161
|
+
' - the package is not installed — add it as a dependency and `gjsify install`.\n' +
|
|
162
|
+
' - a JSX entry with no JSX configuration: oxc then defaults to the automatic runtime with ' +
|
|
163
|
+
'`importSource: "react"`, so the bundle imports `react/jsx-runtime`. Point ' +
|
|
164
|
+
'`gjsify.bundler.transform.jsx.importSource` (or tsconfig `"jsxImportSource"`) at a runtime ' +
|
|
165
|
+
'this project HAS, or use `"jsx": "preserve"` plus the framework compiler that consumes it.\n' +
|
|
166
|
+
' - the specifier is meant to be provided by the host at runtime — then say so, with ' +
|
|
167
|
+
'`gjsify.bundler.external`.');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Throw when a written `--app gjs` chunk is not parseable JavaScript because raw JSX
|
|
171
|
+
* survived into it. See `@gjsify/rolldown-plugin-gjsify`'s `utils/jsx-survival.ts` for
|
|
172
|
+
* why the oracle is a parse and not a pattern, and for the measurement.
|
|
173
|
+
*
|
|
174
|
+
* Called ONLY when the effective JSX setting PRESERVES JSX, which is the only way it can
|
|
175
|
+
* reach an artifact: oxc compiles it otherwise. That is not a cost dodge but the precise
|
|
176
|
+
* condition — though the cost is real, and measured: parsing the repo's largest bundle
|
|
177
|
+
* (`dist/cli.gjs.mjs`, 6.8 MB) takes ~850 ms, which is not worth paying on every GJS
|
|
178
|
+
* build to re-answer a question the transform already settled.
|
|
179
|
+
*/
|
|
180
|
+
export function assertGjsBundleParses(chunks, label) {
|
|
181
|
+
for (const chunk of chunks) {
|
|
182
|
+
const code = chunk.code;
|
|
183
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
184
|
+
continue;
|
|
185
|
+
const found = locateSurvivingJsx(code);
|
|
186
|
+
if (found === null)
|
|
187
|
+
continue;
|
|
188
|
+
throw new Error(formatSurvivingJsx(found, chunk.fileName ? `${label} (${chunk.fileName})` : label));
|
|
189
|
+
}
|
|
81
190
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface JsxConfigInput {
|
|
2
|
+
/** `gjsify.bundler.transform.jsx`, exactly as configured (any of oxc's shapes). */
|
|
3
|
+
transformJsx?: unknown;
|
|
4
|
+
/** tsconfig `compilerOptions.jsx`. */
|
|
5
|
+
tsconfigJsx?: string | undefined;
|
|
6
|
+
/** tsconfig `compilerOptions.jsxImportSource` — names the runtime on its own. */
|
|
7
|
+
tsconfigJsxImportSource?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface JsxConfigVerdict {
|
|
10
|
+
/** Did anything at all name a JSX policy? `false` means oxc's react default applies. */
|
|
11
|
+
configured: boolean;
|
|
12
|
+
/** Will JSX survive the transform, so that a compiler downstream must consume it? */
|
|
13
|
+
preserves: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read the effective JSX policy.
|
|
17
|
+
*
|
|
18
|
+
* `configured` is deliberately satisfied by ANY value, `false` and an unrecognised string
|
|
19
|
+
* included: the gate's subject is a project that said nothing, not one whose setting this
|
|
20
|
+
* function fails to recognise. `transform.jsx: false` already fails the build loudly from
|
|
21
|
+
* inside oxc (measured: exit 1), which is a legitimate way to answer the question.
|
|
22
|
+
*/
|
|
23
|
+
export declare function describeJsxConfig(input: JsxConfigInput): JsxConfigVerdict;
|
|
24
|
+
/**
|
|
25
|
+
* Flatten Rolldown's three `input` shapes (string, array, name→path record) into the
|
|
26
|
+
* paths themselves. Takes `unknown` on purpose: the caller holds a post-glob-expansion
|
|
27
|
+
* `EntryPoints` from the bundler package, and this module has no business importing that
|
|
28
|
+
* type to look at strings.
|
|
29
|
+
*/
|
|
30
|
+
export declare function collectEntryPaths(entryPoints: unknown): string[];
|
|
31
|
+
/** The first entry point whose extension carries JSX, or `undefined`. */
|
|
32
|
+
export declare function findJsxEntryPoint(entryPoints: readonly string[]): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The refusal. Names the file, what the default would do to it, and the routes that answer
|
|
35
|
+
* the question — PRESERVE-plus-compiler first, because that is the one a GTK host wants:
|
|
36
|
+
* `@gjsify/gtk-host/jsx-runtime` is a TYPE surface whose `jsx()`/`jsxs()` THROW on purpose
|
|
37
|
+
* ("a TYPE surface, not an automatic JSX runtime"), so naming it as an `importSource`
|
|
38
|
+
* trades a build that fails for a build that dies at the first element. An error message
|
|
39
|
+
* that recommends the wrong fix is worse than one that recommends none.
|
|
40
|
+
*
|
|
41
|
+
* Scoped to `--app gjs` by its caller: on `--app node`/`--app browser` the react default
|
|
42
|
+
* is a legitimate answer for a project that has react installed, and refusing there would
|
|
43
|
+
* break real builds for a mistake they did not make.
|
|
44
|
+
*/
|
|
45
|
+
export declare function jsxConfigMissingError(entry: string): Error;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The pre-build JSX question: has this project configured JSX AT ALL, and does what it
|
|
2
|
+
// configured PRESERVE JSX?
|
|
3
|
+
//
|
|
4
|
+
// Neither answer is available from the bundler, and both decide whether a `--app gjs`
|
|
5
|
+
// build can produce anything loadable:
|
|
6
|
+
//
|
|
7
|
+
// - NOTHING configured. oxc's transformer then applies its own default — the AUTOMATIC
|
|
8
|
+
// runtime with `importSource: 'react'` — so a `.tsx` entry emits
|
|
9
|
+
// `import { jsx } from "react/jsx-runtime"`. Rolldown reports the miss as an
|
|
10
|
+
// `UNRESOLVED_IMPORT` WARNING, re-emits the specifier, and the build exits 0; `gjs -m`
|
|
11
|
+
// on the artifact then dies with `ImportError: Module not found: react/jsx-runtime`.
|
|
12
|
+
// MEASURED end to end on a two-line `.tsx` (exit 0, then exit 1 under gjs). The
|
|
13
|
+
// artifact gate in `gjs-bundle-guard.ts` catches it too — but AFTER a full bundle, and
|
|
14
|
+
// it can only name `react/jsx-runtime`, not the `.tsx` the reader has to fix. And the
|
|
15
|
+
// default is never right for this target: `react/jsx-runtime` builds React elements,
|
|
16
|
+
// which render NOTHING on a GTK host even on the project that happens to have react
|
|
17
|
+
// installed.
|
|
18
|
+
// - `preserve` configured. JSX is meant to survive the transform so a framework compiler
|
|
19
|
+
// can take it (`babel-preset-solid`, the Vue SFC compiler). Whether one is REGISTERED
|
|
20
|
+
// is not knowable here — a plugin declares no JSX capability — so this only reports
|
|
21
|
+
// that JSX may survive, and the post-bundle syntax check decides.
|
|
22
|
+
//
|
|
23
|
+
// Both `gjsify.bundler.transform.jsx` and tsconfig `compilerOptions.jsx` reach oxc, and
|
|
24
|
+
// the explicit `transform.jsx` WINS: measured with tsconfig `"jsx": "react-jsx"` +
|
|
25
|
+
// `"jsxImportSource": "solid-js"` and `bundler.transform.jsx: "preserve"` — the artifact
|
|
26
|
+
// kept raw JSX. Measured too, and the reason `preserve` is matched literally rather than
|
|
27
|
+
// by "TS spellings that keep JSX": tsconfig `"jsx": "react-native"`, which TypeScript
|
|
28
|
+
// documents as preserving, is NOT preserved by oxc — it compiled to runtime calls.
|
|
29
|
+
/** The JSX-bearing extensions oxc classifies as JSX-capable. */
|
|
30
|
+
const JSX_SOURCE_RE = /\.(?:jsx|tsx|mjsx|cjsx|mtsx|ctsx)$/i;
|
|
31
|
+
/**
|
|
32
|
+
* Read the effective JSX policy.
|
|
33
|
+
*
|
|
34
|
+
* `configured` is deliberately satisfied by ANY value, `false` and an unrecognised string
|
|
35
|
+
* included: the gate's subject is a project that said nothing, not one whose setting this
|
|
36
|
+
* function fails to recognise. `transform.jsx: false` already fails the build loudly from
|
|
37
|
+
* inside oxc (measured: exit 1), which is a legitimate way to answer the question.
|
|
38
|
+
*/
|
|
39
|
+
export function describeJsxConfig(input) {
|
|
40
|
+
const { transformJsx, tsconfigJsx, tsconfigJsxImportSource } = input;
|
|
41
|
+
const configured = transformJsx !== undefined || tsconfigJsx !== undefined || tsconfigJsxImportSource !== undefined;
|
|
42
|
+
const preserves = transformJsx !== undefined ? transformJsx === 'preserve' : tsconfigJsx === 'preserve';
|
|
43
|
+
return { configured, preserves };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Flatten Rolldown's three `input` shapes (string, array, name→path record) into the
|
|
47
|
+
* paths themselves. Takes `unknown` on purpose: the caller holds a post-glob-expansion
|
|
48
|
+
* `EntryPoints` from the bundler package, and this module has no business importing that
|
|
49
|
+
* type to look at strings.
|
|
50
|
+
*/
|
|
51
|
+
export function collectEntryPaths(entryPoints) {
|
|
52
|
+
if (typeof entryPoints === 'string')
|
|
53
|
+
return [entryPoints];
|
|
54
|
+
if (Array.isArray(entryPoints))
|
|
55
|
+
return entryPoints.filter((e) => typeof e === 'string');
|
|
56
|
+
if (entryPoints !== null && typeof entryPoints === 'object') {
|
|
57
|
+
return Object.values(entryPoints).filter((e) => typeof e === 'string');
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
/** The first entry point whose extension carries JSX, or `undefined`. */
|
|
62
|
+
export function findJsxEntryPoint(entryPoints) {
|
|
63
|
+
return entryPoints.find((entry) => JSX_SOURCE_RE.test(entry));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The refusal. Names the file, what the default would do to it, and the routes that answer
|
|
67
|
+
* the question — PRESERVE-plus-compiler first, because that is the one a GTK host wants:
|
|
68
|
+
* `@gjsify/gtk-host/jsx-runtime` is a TYPE surface whose `jsx()`/`jsxs()` THROW on purpose
|
|
69
|
+
* ("a TYPE surface, not an automatic JSX runtime"), so naming it as an `importSource`
|
|
70
|
+
* trades a build that fails for a build that dies at the first element. An error message
|
|
71
|
+
* that recommends the wrong fix is worse than one that recommends none.
|
|
72
|
+
*
|
|
73
|
+
* Scoped to `--app gjs` by its caller: on `--app node`/`--app browser` the react default
|
|
74
|
+
* is a legitimate answer for a project that has react installed, and refusing there would
|
|
75
|
+
* break real builds for a mistake they did not make.
|
|
76
|
+
*/
|
|
77
|
+
export function jsxConfigMissingError(entry) {
|
|
78
|
+
return new Error(`gjsify build --app gjs: ${entry} contains JSX, but nothing configures a JSX transform — not ` +
|
|
79
|
+
'`gjsify.bundler.transform.jsx`, and not tsconfig `compilerOptions.jsx`.\n\n' +
|
|
80
|
+
"Left unset, the bundler's transformer defaults to the automatic runtime with " +
|
|
81
|
+
'`importSource: "react"`, so the bundle would import `react/jsx-runtime`. GJS cannot resolve a ' +
|
|
82
|
+
'bare specifier at all (no node_modules walker, no `exports` map), so the build would exit 0 with ' +
|
|
83
|
+
'an UNRESOLVED_IMPORT warning and the artifact would abort at load with "ImportError: Module not ' +
|
|
84
|
+
'found: react/jsx-runtime" — and where react IS installed it would render React elements, which a ' +
|
|
85
|
+
'GTK host does nothing with.\n\n' +
|
|
86
|
+
'Configure one of:\n' +
|
|
87
|
+
' - PRESERVED JSX plus the framework compiler that consumes it — the route a GTK host wants.\n' +
|
|
88
|
+
' Set `"jsx": "preserve"` (in `gjsify.bundler.transform.jsx` or tsconfig\n' +
|
|
89
|
+
' `compilerOptions.jsx`) and register the compiler under `gjsify.bundler.plugins`:\n' +
|
|
90
|
+
' `babel-preset-solid` for Solid, the Vue SFC compiler for Vue. Pair it with tsconfig\n' +
|
|
91
|
+
' `"jsxImportSource": "@gjsify/gtk-host"` for the TYPES.\n' +
|
|
92
|
+
' - an automatic JSX runtime this project actually has:\n' +
|
|
93
|
+
' "bundler": { "transform": { "jsx": { "importSource": "<pkg exporting ./jsx-runtime>" } } }\n' +
|
|
94
|
+
' or the same in tsconfig: `"jsx": "react-jsx"` + `"jsxImportSource": "<pkg>"`. NOT\n' +
|
|
95
|
+
' `@gjsify/gtk-host` — its `/jsx-runtime` is a TYPE surface and throws when called.\n' +
|
|
96
|
+
' - `"jsx": false`, if this entry should contain no JSX at all: the transformer then\n' +
|
|
97
|
+
' reports the JSX itself instead of compiling it for a runtime that is not there.');
|
|
98
|
+
}
|
|
@@ -26,8 +26,31 @@ export interface ProbeOptions {
|
|
|
26
26
|
retryDelayMs?: number;
|
|
27
27
|
/** Injected delay (tests pass a no-op). Default: real `setTimeout`. */
|
|
28
28
|
sleep?: (ms: number) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Does this name exist on the registry? The PACKUMENT oracle — see
|
|
31
|
+
* {@link packumentExists}. Tests inject it; production uses the real fetch.
|
|
32
|
+
*/
|
|
33
|
+
exists?: (registry: string, name: string) => Promise<boolean | null>;
|
|
29
34
|
}
|
|
30
35
|
export declare const PROBE_RETRY_DELAY_MS = 400;
|
|
36
|
+
/**
|
|
37
|
+
* Does the registry serve a packument for this name?
|
|
38
|
+
*
|
|
39
|
+
* THE ORACLE, and the reason it exists: npm's trust endpoint answers `2xx` with an
|
|
40
|
+
* EMPTY LIST for a name that was never published. `classifyTrustList` reads that as
|
|
41
|
+
* `untrusted`, so `gjsify onboard` planned "already on npm, just needs trust" for a
|
|
42
|
+
* package that did not exist — it trusted the name, reported `0 to publish+trust`
|
|
43
|
+
* and published NOTHING. A bootstrap command reporting its own gap as closed, and
|
|
44
|
+
* it is why the v0.36.0 repair failed twice before anyone looked at the registry.
|
|
45
|
+
*
|
|
46
|
+
* `GET <registry>/<name>` is unauthenticated and needs no OTP, so it answers in the
|
|
47
|
+
* one situation that matters: a `--dry-run` before the maintainer has an OTP to
|
|
48
|
+
* hand, where every trust read is `401` and the plan is otherwise empty.
|
|
49
|
+
*
|
|
50
|
+
* `null` means UNDECIDED (network or an unexpected status) — never "absent". A
|
|
51
|
+
* bootstrap that publishes on a failed probe is worse than one that reports blocked.
|
|
52
|
+
*/
|
|
53
|
+
export declare function packumentExists(registry: string, name: string): Promise<boolean | null>;
|
|
31
54
|
/** Default probe concurrency — kept SMALL so a single token never bursts npm. */
|
|
32
55
|
export declare const DEFAULT_PROBE_CONCURRENCY = 4;
|
|
33
56
|
/** Map a classified trust state to the action the onboard sweep should take. */
|
|
@@ -11,6 +11,43 @@
|
|
|
11
11
|
import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
|
|
12
12
|
import { classifyTrustList, trustUrl } from './trust-registry.js';
|
|
13
13
|
export const PROBE_RETRY_DELAY_MS = 400;
|
|
14
|
+
/**
|
|
15
|
+
* Does the registry serve a packument for this name?
|
|
16
|
+
*
|
|
17
|
+
* THE ORACLE, and the reason it exists: npm's trust endpoint answers `2xx` with an
|
|
18
|
+
* EMPTY LIST for a name that was never published. `classifyTrustList` reads that as
|
|
19
|
+
* `untrusted`, so `gjsify onboard` planned "already on npm, just needs trust" for a
|
|
20
|
+
* package that did not exist — it trusted the name, reported `0 to publish+trust`
|
|
21
|
+
* and published NOTHING. A bootstrap command reporting its own gap as closed, and
|
|
22
|
+
* it is why the v0.36.0 repair failed twice before anyone looked at the registry.
|
|
23
|
+
*
|
|
24
|
+
* `GET <registry>/<name>` is unauthenticated and needs no OTP, so it answers in the
|
|
25
|
+
* one situation that matters: a `--dry-run` before the maintainer has an OTP to
|
|
26
|
+
* hand, where every trust read is `401` and the plan is otherwise empty.
|
|
27
|
+
*
|
|
28
|
+
* `null` means UNDECIDED (network or an unexpected status) — never "absent". A
|
|
29
|
+
* bootstrap that publishes on a failed probe is worse than one that reports blocked.
|
|
30
|
+
*/
|
|
31
|
+
export async function packumentExists(registry, name) {
|
|
32
|
+
const base = registry.endsWith('/') ? registry.slice(0, -1) : registry;
|
|
33
|
+
try {
|
|
34
|
+
// `Accept` narrows the response to the abbreviated packument: the full one
|
|
35
|
+
// for a package like `@gjsify/cli` is megabytes, and existence is all we ask.
|
|
36
|
+
const res = await fetch(`${base}/${name.replace('/', '%2f')}`, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
39
|
+
});
|
|
40
|
+
if (res.status === 404)
|
|
41
|
+
return false;
|
|
42
|
+
if (res.ok)
|
|
43
|
+
return true;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// A DNS or TLS failure is not evidence of absence.
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
14
51
|
/** Default probe concurrency — kept SMALL so a single token never bursts npm. */
|
|
15
52
|
export const DEFAULT_PROBE_CONCURRENCY = 4;
|
|
16
53
|
/** Map a classified trust state to the action the onboard sweep should take. */
|
|
@@ -48,6 +85,34 @@ export async function probeTrustState(request, ws, ctx, opts = {}) {
|
|
|
48
85
|
res = await request('GET', url);
|
|
49
86
|
state = classifyTrustList(res.status, res.json, classifyOpts);
|
|
50
87
|
}
|
|
88
|
+
// `untrusted` is the ONE ambiguous answer: the trust endpoint returns `2xx` with
|
|
89
|
+
// an empty list both for "published, nobody trusted it" and for "no such name".
|
|
90
|
+
// Only that case pays for the extra read — `trusted` cannot be a missing package
|
|
91
|
+
// and `unpublished` already has its answer.
|
|
92
|
+
// Two questions, two endpoints. The trust list answers "who may publish this",
|
|
93
|
+
// and onboard also needs "does this name exist at all" — which it can only
|
|
94
|
+
// INFER from the trust list, and infers wrongly in both ambiguous states:
|
|
95
|
+
//
|
|
96
|
+
// untrusted npm serves 2xx and an EMPTY list for a name that was never
|
|
97
|
+
// published — byte-identical to a real package that simply has
|
|
98
|
+
// no trusted publisher configured.
|
|
99
|
+
// auth-required a 401 says nothing whatsoever about existence. It is also
|
|
100
|
+
// where EVERY package lands in a `--dry-run` run before the
|
|
101
|
+
// maintainer has an OTP to hand, which is exactly the moment
|
|
102
|
+
// the plan is being read to decide whether to proceed.
|
|
103
|
+
//
|
|
104
|
+
// The packument answers the question directly and unauthenticated, and a 404
|
|
105
|
+
// there is proof no OTP could change. Only these two states pay for the extra
|
|
106
|
+
// read: `trusted` cannot be a missing package, and `unpublished` already has
|
|
107
|
+
// its answer from the endpoint it just asked.
|
|
108
|
+
if (state === 'untrusted' || state === 'auth-required') {
|
|
109
|
+
const exists = await (opts.exists ?? packumentExists)(registry, ws.name);
|
|
110
|
+
// ONLY a definitive 404 reclassifies. `null` — network failure, unexpected
|
|
111
|
+
// status — leaves the state exactly as it was: a bootstrap that publishes
|
|
112
|
+
// on a failed read is worse than one that reports itself blocked.
|
|
113
|
+
if (exists === false)
|
|
114
|
+
state = 'unpublished';
|
|
115
|
+
}
|
|
51
116
|
return { ws, registry, url, state, httpStatus: res.status, action: actionForState(state) };
|
|
52
117
|
}
|
|
53
118
|
/**
|
package/lib/utils/ship/deb.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
1
|
+
import { type PayloadEntry } from './payload.js';
|
|
2
|
+
import type { PackSettings } from './types.js';
|
|
3
3
|
export interface DebInputs {
|
|
4
|
-
settings:
|
|
4
|
+
settings: PackSettings;
|
|
5
5
|
/** The staged payload, prefix-relative. */
|
|
6
6
|
payload: readonly PayloadEntry[];
|
|
7
7
|
/** Install prefix, e.g. `/usr`. */
|
package/lib/utils/ship/deb.js
CHANGED
|
@@ -20,8 +20,9 @@ import { createArArchive } from './ar.js';
|
|
|
20
20
|
import { formatDebDepend } from './depends.js';
|
|
21
21
|
import { gzipDeterministic } from './gzip.js';
|
|
22
22
|
import { renderDebScripts } from './scripts.js';
|
|
23
|
+
import { readPayloadFacts } from './payload.js';
|
|
23
24
|
export async function buildDeb(inputs) {
|
|
24
|
-
const {
|
|
25
|
+
const { payload, mtime } = inputs;
|
|
25
26
|
const prefix = inputs.prefix.replace(/^\/+/, '');
|
|
26
27
|
const named = payload.map((entry) => ({ ...entry, tarPath: `./${posix.join(prefix, entry.path)}` }));
|
|
27
28
|
const directories = ancestorDirectories(named.map((entry) => entry.tarPath));
|
|
@@ -44,7 +45,10 @@ export async function buildDeb(inputs) {
|
|
|
44
45
|
{ name: './control', body: control, mode: 0o644, mtime },
|
|
45
46
|
{ name: './md5sums', body: md5sums, mode: 0o644, mtime },
|
|
46
47
|
];
|
|
47
|
-
|
|
48
|
+
// From the PAYLOAD, not the settings: the maintainer scripts refresh exactly the
|
|
49
|
+
// directories this package wrote into, and since ADR 0024 § A2 this packer also runs on a
|
|
50
|
+
// host that has the staged tree and no project to ask.
|
|
51
|
+
const maintainerScripts = renderDebScripts(readPayloadFacts(payload), inputs.prefix);
|
|
48
52
|
for (const [name, body] of Object.entries(maintainerScripts)) {
|
|
49
53
|
// dpkg refuses a maintainer script outside 0555–0775 with the mode in
|
|
50
54
|
// the error, so this is 0755 rather than "whatever the source had".
|
|
@@ -14,16 +14,19 @@ import type { FormatId } from './types.js';
|
|
|
14
14
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
15
15
|
*/
|
|
16
16
|
export declare const DEFAULT_GJS_FLOOR = "1.86";
|
|
17
|
-
/**
|
|
18
|
-
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
19
|
-
* Returns the warning lines; empty when the floor is fine.
|
|
20
|
-
*/
|
|
21
17
|
export declare function warnAboutGjsFloor(format: FormatId, floor: string): string[];
|
|
22
18
|
export interface DependsInputs {
|
|
23
19
|
/** GI namespaces the bundle imports, in `Ns-Version` spelling. */
|
|
24
20
|
namespaces: readonly string[];
|
|
25
|
-
/**
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Whether the payload installs into `share/icons/hicolor/`.
|
|
23
|
+
*
|
|
24
|
+
* Was `kind: 'app' | 'cli'`, which is a proxy for the question and not the question:
|
|
25
|
+
* `hicolor-icon-theme` is depended on because it OWNS that directory, so an app that
|
|
26
|
+
* installs no icon used to declare a dependency it never touched. Derived from the payload
|
|
27
|
+
* (`readPayloadFacts`), which is also the only thing the packing host has (ADR 0024 § A2).
|
|
28
|
+
*/
|
|
29
|
+
hasIcons: boolean;
|
|
27
30
|
/** Whether the payload installs GSettings schemas. */
|
|
28
31
|
hasSchemas: boolean;
|
|
29
32
|
/** User-supplied additions for this format. */
|
|
@@ -35,14 +35,34 @@
|
|
|
35
35
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
36
36
|
*/
|
|
37
37
|
export const DEFAULT_GJS_FLOOR = '1.86';
|
|
38
|
+
/**
|
|
39
|
+
* The package providing `glib-compile-schemas`, per format.
|
|
40
|
+
*
|
|
41
|
+
* A record, not `format === 'deb' ? … : …`. The ternary reads as a two-way
|
|
42
|
+
* choice and is really "deb, or ELSE rpm's package name" — measured on a third
|
|
43
|
+
* format, `dmg` got `glib2`, at exit 0, in the `Depends:` of a package that has
|
|
44
|
+
* no such thing. Adding a `FormatId` now fails to build here instead.
|
|
45
|
+
*/
|
|
46
|
+
const SCHEMA_COMPILER_PACKAGE = { deb: 'libglib2.0-bin', rpm: 'glib2' };
|
|
38
47
|
/** Debian's `gjs` versions per suite, as measured 2026-08-15 — see {@link warnAboutGjsFloor}. */
|
|
39
48
|
const DEBIAN_GJS = 'trixie ships 1.82.3 and forky 1.88.1; 1.84 and 1.86 were skipped';
|
|
40
49
|
/**
|
|
41
50
|
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
42
51
|
* Returns the warning lines; empty when the floor is fine.
|
|
43
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* Which formats this Debian-suite warning is ABOUT.
|
|
55
|
+
*
|
|
56
|
+
* A `Record<FormatId, …>` and not `format !== 'deb'`: the negative form answers
|
|
57
|
+
* for every format that will ever exist, and answers "stay quiet" — so a third
|
|
58
|
+
* format would inherit silence about a floor nobody has checked for its distro,
|
|
59
|
+
* with no compile error. As a record, adding a `FormatId` fails to build until
|
|
60
|
+
* someone decides. `rpm` is false because Fedora ships a current GJS, which is a
|
|
61
|
+
* measured fact about Fedora, not a default.
|
|
62
|
+
*/
|
|
63
|
+
const GJS_FLOOR_IS_DEBIAN_NEWS = { deb: true, rpm: false };
|
|
44
64
|
export function warnAboutGjsFloor(format, floor) {
|
|
45
|
-
if (format
|
|
65
|
+
if (!GJS_FLOOR_IS_DEBIAN_NEWS[format])
|
|
46
66
|
return [];
|
|
47
67
|
// Only a floor forky ACTUALLY satisfies is quiet. The first cut tested
|
|
48
68
|
// `>= 1.88.1` and therefore went silent for 1.90 and 2.0 as well — the
|
|
@@ -171,7 +191,9 @@ export function deriveDepends(format, inputs) {
|
|
|
171
191
|
'have to. (`gjsify.ship.depends.<format>` is for dependencies that are not typelibs at all — it ' +
|
|
172
192
|
'appends, and deliberately does not silence this.)');
|
|
173
193
|
}
|
|
174
|
-
|
|
194
|
+
// The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
|
|
195
|
+
// app/cli distinction that used to stand in for them.
|
|
196
|
+
if (inputs.hasIcons)
|
|
175
197
|
out.push('hicolor-icon-theme');
|
|
176
198
|
// The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
|
|
177
199
|
// (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
|
|
@@ -182,7 +204,7 @@ export function deriveDepends(format, inputs) {
|
|
|
182
204
|
// `Gio.Settings.new()` aborts the app — an install that succeeds and an app
|
|
183
205
|
// that does not start.
|
|
184
206
|
if (inputs.hasSchemas)
|
|
185
|
-
out.push(format
|
|
207
|
+
out.push(SCHEMA_COMPILER_PACKAGE[format]);
|
|
186
208
|
out.push(...inputs.extra);
|
|
187
209
|
// Set-dedupe keeps first-insertion order, so `gjs` stays first and the
|
|
188
210
|
// list is stable across runs.
|