@gjsify/cli 0.51.1 → 0.52.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 +505 -490
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected-classify.d.ts +7 -1
- package/lib/commands/affected-classify.js +66 -9
- package/lib/commands/affected-script-refs.d.ts +20 -0
- package/lib/commands/affected-script-refs.js +218 -0
- package/lib/commands/affected.js +24 -2
- package/lib/commands/flatpak/diff.js +8 -2
- package/lib/commands/flatpak/init.js +4 -1
- package/lib/commands/flatpak/sources.js +4 -3
- package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
- package/lib/commands/flatpak/sync-flathub.js +149 -7
- package/lib/commands/flatpak/utils.d.ts +10 -0
- package/lib/commands/flatpak/utils.js +10 -0
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +76 -0
- package/lib/commands/link.d.ts +13 -0
- package/lib/commands/link.js +243 -0
- package/lib/commands/onboard.js +18 -2
- package/lib/commands/publish.d.ts +34 -0
- package/lib/commands/publish.js +153 -8
- package/lib/commands/ship.js +19 -1
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/check-system-deps.d.ts +1 -17
- package/lib/utils/check-system-deps.js +10 -68
- package/lib/utils/dev-link.d.ts +222 -0
- package/lib/utils/dev-link.js +632 -0
- package/lib/utils/hang-watchdog.d.ts +95 -0
- package/lib/utils/hang-watchdog.js +194 -0
- package/lib/utils/install-backend-native.d.ts +14 -0
- package/lib/utils/install-backend-native.js +91 -13
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/publish-pins.d.ts +38 -0
- package/lib/utils/publish-pins.js +47 -0
- package/lib/utils/publish-readback.d.ts +103 -17
- package/lib/utils/publish-readback.js +326 -47
- package/lib/utils/run-gjs.js +82 -6
- package/lib/utils/ship/icns.d.ts +14 -0
- package/lib/utils/ship/icns.js +83 -0
- package/lib/utils/ship/ico.d.ts +25 -0
- package/lib/utils/ship/ico.js +120 -0
- package/lib/utils/ship/icons.d.ts +191 -0
- package/lib/utils/ship/icons.js +398 -0
- package/lib/utils/ship/launcher.js +16 -1
- package/lib/utils/ship/layout.d.ts +32 -0
- package/lib/utils/ship/layout.js +112 -38
- package/lib/utils/ship/msi.d.ts +1 -1
- package/lib/utils/ship/msi.js +30 -5
- package/lib/utils/ship/pe-launcher.d.ts +18 -0
- package/lib/utils/ship/pe-launcher.js +125 -4
- package/lib/utils/ship/plan.d.ts +1 -34
- package/lib/utils/ship/plan.js +7 -56
- package/lib/utils/ship/plist.d.ts +20 -10
- package/lib/utils/ship/plist.js +29 -12
- package/lib/utils/ship/settings.js +3 -2
- package/package.json +20 -20
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// `gjsify link <checkout>` / `gjsify unlink` — develop a consumer against a local
|
|
2
|
+
// gjsify checkout without waiting for an npm release, and without changing a byte
|
|
3
|
+
// of what the consumer commits.
|
|
4
|
+
//
|
|
5
|
+
// The model, the trap it closes and why `--immutable` refuses it are the header of
|
|
6
|
+
// `utils/dev-link.ts` and ADR 0065. This file is the surface: two commands, the
|
|
7
|
+
// output that makes an active link visible, and the undo.
|
|
8
|
+
//
|
|
9
|
+
// `unlink` REINSTALLS by default. Removing a link leaves a hole where the registry
|
|
10
|
+
// copy used to be, and a command that promises to undo itself must not hand back a
|
|
11
|
+
// tree that no longer resolves; `--no-install` is there for the case where the next
|
|
12
|
+
// step is an install anyway.
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { relative, resolve } from 'node:path';
|
|
15
|
+
import { applyDevLinks, consumerKnownNames, DEV_LINK_FILE, devLinkPath, ensureLocallyIgnored, formatUnbuiltDevLinks, planDevLinks, prepareDevLinks, readDevLinkOverride, removeDevLinkOverride, removeDevLinks, removeLocalIgnore, resolveCheckoutWorkspaces, retireDevLinks, scanDevLinks, unbuiltDevLinks, writeDevLinkOverride, } from '../utils/dev-link.js';
|
|
16
|
+
export const linkCommand = {
|
|
17
|
+
command: 'link <checkout>',
|
|
18
|
+
description: 'Link this project against a local gjsify checkout for development: its workspace packages replace the installed registry copies, recorded in a git-ignored .gjsify-link.json that every later `gjsify install` re-applies. Undo with `gjsify unlink`.',
|
|
19
|
+
builder: (yargs) => yargs
|
|
20
|
+
.positional('checkout', {
|
|
21
|
+
description: 'Path to the gjsify checkout to link from (a repo whose package.json declares "workspaces").',
|
|
22
|
+
type: 'string',
|
|
23
|
+
demandOption: true,
|
|
24
|
+
})
|
|
25
|
+
.option('packages', {
|
|
26
|
+
description: "Name glob selecting which of the checkout's workspace packages take part (repeatable; default: all of them). Matched against the package NAME with the same single-segment dialect as `gjsify foreach --include`, so a whole scope is `@gjsify/*` — a bare `*` matches no scoped name. Only packages this project actually depends on are ever linked.",
|
|
27
|
+
type: 'string',
|
|
28
|
+
array: true,
|
|
29
|
+
})
|
|
30
|
+
.option('dry-run', {
|
|
31
|
+
description: 'Print the links that would be made and write nothing.',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
handler: (args) => {
|
|
36
|
+
const consumerRoot = process.cwd();
|
|
37
|
+
const dryRun = args['dry-run'] ?? false;
|
|
38
|
+
const checkout = resolve(consumerRoot, args.checkout);
|
|
39
|
+
const overridePath = devLinkPath(consumerRoot);
|
|
40
|
+
// Validated BEFORE anything is written: a `.gjsify-link.json` naming a
|
|
41
|
+
// directory that is not a gjsify checkout would fail on every later install
|
|
42
|
+
// instead of on the command that created it.
|
|
43
|
+
const workspaces = resolveCheckoutWorkspaces(checkout, overridePath);
|
|
44
|
+
// No `--packages` means every workspace package, spelled as the EMPTY list:
|
|
45
|
+
// `['*']` would select nothing, because the glob dialect's `*` does not
|
|
46
|
+
// cross a `/` and every name here is scoped (utils/dev-link.ts).
|
|
47
|
+
const patterns = args.packages ?? [];
|
|
48
|
+
const links = planDevLinks({
|
|
49
|
+
consumerRoot,
|
|
50
|
+
workspaces,
|
|
51
|
+
patterns,
|
|
52
|
+
knownNames: consumerKnownNames(consumerRoot),
|
|
53
|
+
});
|
|
54
|
+
if (links.length === 0) {
|
|
55
|
+
console.error(`gjsify link: nothing to link — ${checkout} has ${workspaces.length} workspace package(s), ` +
|
|
56
|
+
`none of which ${consumerRoot} depends on${patterns.length === 0 ? '' : ` and matches ${patterns.join(', ')}`}.\n` +
|
|
57
|
+
`A link that links nothing is a silent no-op, so this is an error. Check the --packages ` +
|
|
58
|
+
`pattern, or run \`gjsify install\` first so the dependency tree exists.`);
|
|
59
|
+
return process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const label = `gjsify link${dryRun ? ' (dry-run)' : ''}`;
|
|
62
|
+
console.log(`${label} → ${checkout}`);
|
|
63
|
+
console.log(`${' '.repeat(label.length)} in ${consumerRoot}`);
|
|
64
|
+
for (const link of links) {
|
|
65
|
+
console.log(` ${link.name} → ${relative(checkout, link.target) || '.'}`);
|
|
66
|
+
}
|
|
67
|
+
// REFUSES rather than warns, and it refuses before anything is written.
|
|
68
|
+
// A link to an unbuilt package is not a smaller link, it is a tree whose
|
|
69
|
+
// first import fails — and the earlier draft's warning was printed once,
|
|
70
|
+
// at link time, while `gjsify install` re-applied the same link on every
|
|
71
|
+
// later run in silence (`utils/dev-link.ts`, `assertDevLinksBuilt`).
|
|
72
|
+
const unbuilt = unbuiltDevLinks(links);
|
|
73
|
+
if (unbuilt.length > 0) {
|
|
74
|
+
console.error(`\n${formatUnbuiltDevLinks(unbuilt)}`);
|
|
75
|
+
return process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// A dry run that hides the REMOVALS is the half-report this feature keeps
|
|
78
|
+
// being caught by: retiring a link is the destructive half of a re-link,
|
|
79
|
+
// and it is the half worth seeing before it happens.
|
|
80
|
+
const sweep = sweepCheckouts(consumerRoot, checkout);
|
|
81
|
+
const keep = new Set(links.map((l) => l.name));
|
|
82
|
+
const stale = scanDevLinks(consumerRoot, sweep).filter((link) => !keep.has(link.name));
|
|
83
|
+
for (const link of stale)
|
|
84
|
+
console.log(` retire ${link.name} (no longer selected)`);
|
|
85
|
+
if (dryRun) {
|
|
86
|
+
console.log(`\n${label}: nothing written. Drop --dry-run to apply.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// RE-linking is a re-SELECTION, not an addition: whatever the previous
|
|
90
|
+
// override linked and this one does not is removed here. Without it a
|
|
91
|
+
// narrower `--packages` left an orphan symlink out of `node_modules` that
|
|
92
|
+
// no later command would ever name, and every `gjsify install` after it
|
|
93
|
+
// aborted (utils/dev-link.ts, `scanDevLinks`). Both checkouts are swept,
|
|
94
|
+
// because the new override may point somewhere else entirely.
|
|
95
|
+
const retired = retireDevLinks(consumerRoot, sweep, keep);
|
|
96
|
+
const written = applyDevLinks(links);
|
|
97
|
+
writeDevLinkOverride(consumerRoot, { version: 1, checkout, packages: patterns });
|
|
98
|
+
const ignored = ensureLocallyIgnored(consumerRoot);
|
|
99
|
+
console.log(`\n ${written.length} link(s) written, ${links.length - written.length} already current.`);
|
|
100
|
+
// Counted against what was PLANNED, not just reported: `removeDevLinks`
|
|
101
|
+
// refuses a real directory sitting where a link belongs, and a retire that
|
|
102
|
+
// silently did less than it announced is the same half-report again.
|
|
103
|
+
if (stale.length > 0) {
|
|
104
|
+
console.log(` ${retired.length} of ${stale.length} retired link(s) removed.`);
|
|
105
|
+
}
|
|
106
|
+
console.log(` override: ${overridePath}`);
|
|
107
|
+
if (ignored === 'added')
|
|
108
|
+
console.log(` git: ${DEV_LINK_FILE} added to .git/info/exclude (local, never committed)`);
|
|
109
|
+
if (ignored === 'no-git')
|
|
110
|
+
console.log(` git: ${consumerRoot} is not a git repository — nothing to ignore`);
|
|
111
|
+
if (ignored === 'unreadable-git') {
|
|
112
|
+
console.warn(` ! git: ${consumerRoot} has a .git POINTER that does not parse, so ${DEV_LINK_FILE} is NOT ignored. ` +
|
|
113
|
+
`Add it to .gitignore by hand, or the next \`git add -A\` commits your local override.`);
|
|
114
|
+
}
|
|
115
|
+
console.log(`\nEvery \`gjsify install\` here now re-applies these links and says so.\n` +
|
|
116
|
+
`\`gjsify install --immutable\` refuses while the override exists. Undo: \`gjsify unlink\`.`);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
export const unlinkCommand = {
|
|
120
|
+
command: 'unlink',
|
|
121
|
+
description: 'Undo `gjsify link`: remove the development links and the .gjsify-link.json override, then reinstall the registry copies.',
|
|
122
|
+
builder: (yargs) => yargs
|
|
123
|
+
.option('dry-run', {
|
|
124
|
+
description: 'Print what would be removed and change nothing.',
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
default: false,
|
|
127
|
+
})
|
|
128
|
+
.option('install', {
|
|
129
|
+
description: 'Reinstall after unlinking, to put the registry copies back where the links were. On by default; --no-install leaves the holes for a later `gjsify install`.',
|
|
130
|
+
type: 'boolean',
|
|
131
|
+
default: true,
|
|
132
|
+
}),
|
|
133
|
+
handler: async (args) => {
|
|
134
|
+
const consumerRoot = process.cwd();
|
|
135
|
+
const dryRun = args['dry-run'] ?? false;
|
|
136
|
+
const overridePath = devLinkPath(consumerRoot);
|
|
137
|
+
if (!existsSync(overridePath)) {
|
|
138
|
+
console.log(`gjsify unlink: no development link in ${consumerRoot} (no ${DEV_LINK_FILE}).`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// TWO readers, because the plan alone is not a census of what is linked
|
|
142
|
+
// (utils/dev-link.ts, `scanDevLinks`). A checkout that has since been
|
|
143
|
+
// deleted must still be unlinkable, so the dead-target refusal is caught
|
|
144
|
+
// here rather than propagated.
|
|
145
|
+
//
|
|
146
|
+
// The checkout is read separately from the PLAN, because the plan is the
|
|
147
|
+
// part that can fail (a checkout that has since been deleted, an unbuilt
|
|
148
|
+
// one) and the checkout path is what the tree sweep below needs.
|
|
149
|
+
const checkouts = [];
|
|
150
|
+
try {
|
|
151
|
+
const checkout = readDevLinkOverride(consumerRoot)?.checkout;
|
|
152
|
+
if (checkout)
|
|
153
|
+
checkouts.push(checkout);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Unparseable override: the sweep finds nothing, the plan below says
|
|
157
|
+
// why, and the override is still removed.
|
|
158
|
+
}
|
|
159
|
+
let links = [];
|
|
160
|
+
try {
|
|
161
|
+
links = prepareDevLinks(consumerRoot)?.links ?? [];
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.warn(`gjsify unlink: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
165
|
+
` Removing the override anyway; any dangling links are cleaned by the reinstall.`);
|
|
166
|
+
}
|
|
167
|
+
const label = `gjsify unlink${dryRun ? ' (dry-run)' : ''}`;
|
|
168
|
+
console.log(`${label} in ${consumerRoot}`);
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
const names = new Set(links.map((l) => l.name));
|
|
171
|
+
for (const link of scanDevLinks(consumerRoot, checkouts))
|
|
172
|
+
names.add(link.name);
|
|
173
|
+
for (const name of [...names].sort())
|
|
174
|
+
console.log(` would remove link ${name}`);
|
|
175
|
+
console.log(` would remove ${overridePath}`);
|
|
176
|
+
console.log(` would remove ${DEV_LINK_FILE} entry from .git/info/exclude (if this module wrote it)`);
|
|
177
|
+
console.log(`\n${label}: nothing changed. Drop --dry-run to apply.`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// ORDER IS THE FIX. The links go first, the override LAST: the override is
|
|
181
|
+
// the only record of which checkout to sweep, so removing it while a link
|
|
182
|
+
// is still standing destroys the escape route — measured, that left
|
|
183
|
+
// `gjsify unlink` reporting success, the reinstall aborting on the orphan,
|
|
184
|
+
// and no command able to name it again.
|
|
185
|
+
//
|
|
186
|
+
// And the sweep is by TREE, not by plan: `removeDevLinks(links)` only ever
|
|
187
|
+
// visits what the CURRENT override selects, which is exactly how the orphan
|
|
188
|
+
// survived in the first place.
|
|
189
|
+
const removed = removeDevLinks(links);
|
|
190
|
+
const kept = new Set(removed.map((l) => l.name));
|
|
191
|
+
for (const link of retireDevLinks(consumerRoot, checkouts, new Set())) {
|
|
192
|
+
if (kept.has(link.name))
|
|
193
|
+
continue;
|
|
194
|
+
removed.push(link);
|
|
195
|
+
}
|
|
196
|
+
for (const link of removed)
|
|
197
|
+
console.log(` unlinked ${link.name}`);
|
|
198
|
+
// Nothing may point out of `node_modules` any more, or the reinstall below
|
|
199
|
+
// dies on it. If something does, the override STAYS: it is what a later
|
|
200
|
+
// `gjsify unlink` needs, and a half-undone tree with no record of the
|
|
201
|
+
// checkout is the wedge this whole block exists to prevent.
|
|
202
|
+
const stragglers = scanDevLinks(consumerRoot, checkouts);
|
|
203
|
+
if (stragglers.length > 0) {
|
|
204
|
+
console.error(`gjsify unlink: ${stragglers.length} link(s) still point into a checkout and could not be removed:\n` +
|
|
205
|
+
stragglers.map((l) => ` ${l.name} ${l.linkPath} → ${l.target}`).join('\n') +
|
|
206
|
+
`\n ${overridePath} is kept so this stays undoable. Remove the paths above and re-run.`);
|
|
207
|
+
return process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
removeDevLinkOverride(consumerRoot);
|
|
210
|
+
console.log(` removed ${overridePath}`);
|
|
211
|
+
// The `.git/info/exclude` block is part of what `link` wrote, so it is part
|
|
212
|
+
// of what `unlink` owes back. Leaving it behind made the undo incomplete in
|
|
213
|
+
// a way nobody notices until they read the file and wonder what wrote it.
|
|
214
|
+
if (removeLocalIgnore(consumerRoot) === 'removed') {
|
|
215
|
+
console.log(` removed ${DEV_LINK_FILE} entry from .git/info/exclude`);
|
|
216
|
+
}
|
|
217
|
+
if (args.install === false) {
|
|
218
|
+
console.log(`\n ${removed.length} link(s) removed. Run \`gjsify install\` to restore the registry copies.`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.log(`\n ${removed.length} link(s) removed — reinstalling to restore the registry copies.\n`);
|
|
222
|
+
const { runCli } = await import('../cli-app.js');
|
|
223
|
+
await runCli(['install']);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Which checkouts a re-link has to sweep: the one it is about to record, plus the
|
|
228
|
+
* one the CURRENT override names when that is a different directory.
|
|
229
|
+
*
|
|
230
|
+
* Every failure reading the old override is swallowed. A malformed or dead one
|
|
231
|
+
* must not stop a fresh `gjsify link` from fixing the situation, which is usually
|
|
232
|
+
* exactly why it is being run.
|
|
233
|
+
*/
|
|
234
|
+
function sweepCheckouts(consumerRoot, checkout) {
|
|
235
|
+
let previous = null;
|
|
236
|
+
try {
|
|
237
|
+
previous = readDevLinkOverride(consumerRoot)?.checkout ?? null;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
previous = null;
|
|
241
|
+
}
|
|
242
|
+
return previous && previous !== checkout ? [previous, checkout] : [checkout];
|
|
243
|
+
}
|
package/lib/commands/onboard.js
CHANGED
|
@@ -464,6 +464,11 @@ export const onboardCommand = {
|
|
|
464
464
|
};
|
|
465
465
|
// 4a. Bookkeeping-only rows, and the serial publish path.
|
|
466
466
|
const trustOnly = [];
|
|
467
|
+
// What THIS sweep has already put on the registry. A name is not served
|
|
468
|
+
// the instant its PUT is accepted, so the pin guard in `publishWorkspace`
|
|
469
|
+
// must not ask the registry about a package this loop created moments
|
|
470
|
+
// ago — target before dependent is the order, and the order is kept here.
|
|
471
|
+
const publishedHere = new Set();
|
|
467
472
|
for (let i = 0; i < plans.length; i++) {
|
|
468
473
|
const p = plans[i];
|
|
469
474
|
if (p.action === 'skip') {
|
|
@@ -494,6 +499,7 @@ export const onboardCommand = {
|
|
|
494
499
|
continue;
|
|
495
500
|
}
|
|
496
501
|
const pub = await publishWorkspace({
|
|
502
|
+
assumePresent: [...publishedHere],
|
|
497
503
|
wsDir: p.ws.location,
|
|
498
504
|
tag: 'latest',
|
|
499
505
|
access: args.access,
|
|
@@ -514,7 +520,13 @@ export const onboardCommand = {
|
|
|
514
520
|
failed++;
|
|
515
521
|
continue;
|
|
516
522
|
}
|
|
517
|
-
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}`
|
|
523
|
+
log(` published ${pub.name}@${'version' in pub ? pub.version : ''}` +
|
|
524
|
+
('readback' in pub && pub.readback?.confirmed
|
|
525
|
+
? ` (verified on ${pub.registry} — ${pub.readback.attempts} probe(s))`
|
|
526
|
+
: pub.action === 'skipped-untrusted-new'
|
|
527
|
+
? ''
|
|
528
|
+
: ' (UNVERIFIED)'));
|
|
529
|
+
publishedHere.add(pub.name);
|
|
518
530
|
await configureTrust(p, i, pub.action !== 'skipped-untrusted-new');
|
|
519
531
|
}
|
|
520
532
|
// 4b. The trust-only packages, concurrently.
|
|
@@ -670,7 +682,11 @@ function describePublishFailure(pub) {
|
|
|
670
682
|
return (`npm ${pub.claim === 'already-published' ? 'reported it already published' : 'accepted the upload'} ` +
|
|
671
683
|
`(HTTP ${pub.putStatus}) but ${pub.name}@${pub.version} did not ` +
|
|
672
684
|
`resolve on ${pub.registry} after ${pub.readback.attempts} probe(s) over ` +
|
|
673
|
-
`${(pub.readback.elapsedMs / 1000).toFixed(1)}s`);
|
|
685
|
+
`${(pub.readback.elapsedMs / 1000).toFixed(1)}s [${pub.readback.verdict}: ${pub.readback.verdictDetail}]`);
|
|
686
|
+
case 'blocked-missing-dependency':
|
|
687
|
+
return (`not published — ${pub.missing.length} required pin(s) name a package ${pub.registry} does not ` +
|
|
688
|
+
`have: ${pub.missing.map((m) => `${m.block}.${m.name}@${m.spec}`).join(', ')}. Bootstrap the ` +
|
|
689
|
+
`target BEFORE its dependent; that order is a correctness property`);
|
|
674
690
|
case 'error':
|
|
675
691
|
return `HTTP ${pub.status} ${pub.statusText}`;
|
|
676
692
|
default:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
import { type Diagnose404Result } from '../utils/publish-diagnose.js';
|
|
3
3
|
import { type ReadbackResult } from '../utils/publish-readback.js';
|
|
4
|
+
import { type WorkspacePin } from '../utils/publish-pins.js';
|
|
4
5
|
import { OtpProvider } from '../utils/npm-otp.js';
|
|
5
6
|
interface PublishOptions {
|
|
6
7
|
path?: string;
|
|
@@ -29,6 +30,18 @@ export interface PublishWorkspaceInput {
|
|
|
29
30
|
tolerate: boolean;
|
|
30
31
|
/** Skip an un-bootstrapped new package on the OIDC path. */
|
|
31
32
|
tolerateUntrustedNew: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Names this CALLER has published in this same process, which the pin guard
|
|
35
|
+
* therefore does not ask the registry about.
|
|
36
|
+
*
|
|
37
|
+
* A brand-new name is not served the instant its PUT is accepted — the same
|
|
38
|
+
* eventual consistency that makes a version gate unsafe — so a sweep that
|
|
39
|
+
* bootstraps a target and its dependent in one run would otherwise block the
|
|
40
|
+
* dependent on a name it had just created itself. `gjsify onboard` is that
|
|
41
|
+
* sweep. The release sweep is `foreach --exec`, one process per package, so
|
|
42
|
+
* its children pass nothing and every edge is a real registry question.
|
|
43
|
+
*/
|
|
44
|
+
assumePresent?: readonly string[];
|
|
32
45
|
/** `--trusted` (force OIDC) / `undefined` | `'auto'` (auto-detect). */
|
|
33
46
|
trustedFlag: boolean | 'auto' | undefined;
|
|
34
47
|
/** Registry override. Default: `$npm_config_registry` → scope-aware npmrc → npmjs. */
|
|
@@ -94,6 +107,7 @@ export type PublishOutcome = {
|
|
|
94
107
|
name: string;
|
|
95
108
|
version: string;
|
|
96
109
|
status: number;
|
|
110
|
+
registry: string;
|
|
97
111
|
/** The read-back that CONFIRMED the conflicting version. */
|
|
98
112
|
readback?: ReadbackResult;
|
|
99
113
|
} | {
|
|
@@ -101,6 +115,26 @@ export type PublishOutcome = {
|
|
|
101
115
|
action: 'skipped-untrusted-new';
|
|
102
116
|
name: string;
|
|
103
117
|
version: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The tarball was NOT sent, because a required intra-repo pin in it names a
|
|
121
|
+
* package the registry does not have (#1713).
|
|
122
|
+
*
|
|
123
|
+
* `ok` is the TOLERANCE, not the health: under `--tolerate-untrusted-new`
|
|
124
|
+
* this is a skip the sweep continues past, because a fatal child stops
|
|
125
|
+
* `foreach --exec` mid-roster and leaves a PARTIAL publish. Either way the
|
|
126
|
+
* broken tarball stays off the registry, which is the difference from the
|
|
127
|
+
* shape this outcome exists to end — where the dependency was skipped, exit
|
|
128
|
+
* 0, and its dependent shipped a pin npm could not resolve.
|
|
129
|
+
*/
|
|
130
|
+
| {
|
|
131
|
+
ok: boolean;
|
|
132
|
+
action: 'blocked-missing-dependency';
|
|
133
|
+
name: string;
|
|
134
|
+
version: string;
|
|
135
|
+
registry: string;
|
|
136
|
+
tolerated: boolean;
|
|
137
|
+
missing: WorkspacePin[];
|
|
104
138
|
} | {
|
|
105
139
|
ok: false;
|
|
106
140
|
action: 'otp-required';
|
package/lib/commands/publish.js
CHANGED
|
@@ -48,7 +48,8 @@ import { buildPublishHeaders, escapePackageName } from '../utils/publish-headers
|
|
|
48
48
|
import { packWorkspace } from './pack.js';
|
|
49
49
|
import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailableError } from '../utils/npm-oidc.js';
|
|
50
50
|
import { diagnose404, is404DiagnosticCandidate } from '../utils/publish-diagnose.js';
|
|
51
|
-
import { DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
51
|
+
import { DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, probePackageName, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
52
|
+
import { collectWorkspacePins, REQUIRED_BLOCK } from '../utils/publish-pins.js';
|
|
52
53
|
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
53
54
|
import { OtpProvider, withOtpRetry, isOtpChallenge } from '../utils/npm-otp.js';
|
|
54
55
|
import { promptLine } from '../utils/prompt.js';
|
|
@@ -81,7 +82,7 @@ export const publishCommand = {
|
|
|
81
82
|
default: false,
|
|
82
83
|
})
|
|
83
84
|
.option('verify-timeout', {
|
|
84
|
-
description: 'Seconds to keep asking the registry for the just-published version before giving up. npm answers 2xx to a write it has only ACCEPTED: in the v0.46.0 release 19 of 199 packages were recorded 56-252s after their 2xx and one was never recorded at all, so a shorter window turns normal queueing into a false red. `0` disables the read-back — for a registry with no packument read path.',
|
|
85
|
+
description: 'Seconds to keep asking the registry for the just-published version before giving up. npm answers 2xx to a write it has only ACCEPTED: in the v0.46.0 release 19 of 199 packages were recorded 56-252s after their 2xx and one was never recorded at all, so a shorter window turns normal queueing into a false red. `0` disables the read-back — for a registry with no packument read path; the success line then reads `UNVERIFIED` and says so, because a publish nothing checked must never look like one that was checked.',
|
|
85
86
|
type: 'number',
|
|
86
87
|
default: DEFAULT_VERIFY_BUDGET_MS / 1000,
|
|
87
88
|
})
|
|
@@ -305,6 +306,55 @@ export async function publishWorkspace(input) {
|
|
|
305
306
|
// Base headers (npm-command: publish routing header etc.). The OTP is added
|
|
306
307
|
// per-PUT by `doPut` so `withOtpRetry` can retry with different codes.
|
|
307
308
|
const headers = buildPublishHeaders(url, { npmrc });
|
|
309
|
+
// 5. Do not PUT a tarball whose required intra-repo pins point at a name
|
|
310
|
+
// this registry does not have (#1713).
|
|
311
|
+
//
|
|
312
|
+
// The question is PRESENCE OF THE NAME, never a version, and that is what
|
|
313
|
+
// makes it safe to gate on: a version is eventually consistent for
|
|
314
|
+
// minutes after its own 2xx, so a version gate would manufacture a red on
|
|
315
|
+
// a publish that worked. A name does not move once its first version
|
|
316
|
+
// lands. `unknown` — a 5xx, a timeout, an auth wall — is not `absent` and
|
|
317
|
+
// never blocks.
|
|
318
|
+
const assumePresent = new Set(input.assumePresent ?? []);
|
|
319
|
+
const pins = collectWorkspacePins(pkg, rewrittenPkg).filter((pin) => !assumePresent.has(pin.name));
|
|
320
|
+
if (pins.length > 0) {
|
|
321
|
+
const probed = await Promise.all(pins.map(async (pin) => ({
|
|
322
|
+
pin,
|
|
323
|
+
presence: await probePackageName({
|
|
324
|
+
registry,
|
|
325
|
+
name: pin.name,
|
|
326
|
+
authorization: headers['authorization'],
|
|
327
|
+
}),
|
|
328
|
+
})));
|
|
329
|
+
const missing = [];
|
|
330
|
+
for (const { pin, presence } of probed) {
|
|
331
|
+
if (presence.state === 'present')
|
|
332
|
+
continue;
|
|
333
|
+
const where = `${pin.block}.${pin.name}@${pin.spec}`;
|
|
334
|
+
if (presence.state === 'unknown') {
|
|
335
|
+
console.error(`gjsify publish: could not establish whether ${pin.name} exists (${presence.detail}) — not blocking on ${where}`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (pin.block !== REQUIRED_BLOCK) {
|
|
339
|
+
// npm and pnpm skip an unresolvable optional edge in silence;
|
|
340
|
+
// Yarn refuses the whole install. Loud, not fatal.
|
|
341
|
+
console.error(`gjsify publish: ${where} names a package ${registry} does not have — npm installs around it, Yarn (\`YN0035\`) does not`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
missing.push(pin);
|
|
345
|
+
}
|
|
346
|
+
if (missing.length > 0) {
|
|
347
|
+
return {
|
|
348
|
+
ok: tolerateUntrustedNew,
|
|
349
|
+
action: 'blocked-missing-dependency',
|
|
350
|
+
name: packed.name,
|
|
351
|
+
version: packed.version,
|
|
352
|
+
registry,
|
|
353
|
+
tolerated: tolerateUntrustedNew,
|
|
354
|
+
missing,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
308
358
|
// Trusted Publishing (OIDC) path — `--trusted` forces it, `undefined`
|
|
309
359
|
// auto-detects from the GitHub OIDC env. Unused by `gjsify onboard` (local
|
|
310
360
|
// token auth), preserved verbatim for `gjsify publish` in CI.
|
|
@@ -414,7 +464,7 @@ export async function publishWorkspace(input) {
|
|
|
414
464
|
//
|
|
415
465
|
// NOT CONFIRMED YET — the other 19. Here the read-back waits out
|
|
416
466
|
// npm's own write queue, and those residual lags SUM TO 2554 s: a
|
|
417
|
-
// sweep that waited them all out
|
|
467
|
+
// sweep that waited them all out under the fatal default would have
|
|
418
468
|
// added ~43 min to that 33 min job. That single number decides the
|
|
419
469
|
// shape below.
|
|
420
470
|
//
|
|
@@ -438,7 +488,7 @@ export async function publishWorkspace(input) {
|
|
|
438
488
|
//
|
|
439
489
|
// Hence BOTH, split along that cost. The read-back runs per package
|
|
440
490
|
// everywhere and is FATAL by default — that is what turns the
|
|
441
|
-
// incident's green single-package job red, for up to
|
|
491
|
+
// incident's green single-package job red, for up to 600 s of
|
|
442
492
|
// patience on a job that otherwise ran 59 s. The
|
|
443
493
|
// 199-package sweep passes `--verify-defer --verify-timeout 5`: it
|
|
444
494
|
// still CONFIRMS nine packages in ten and still names the suspect
|
|
@@ -458,7 +508,7 @@ export async function publishWorkspace(input) {
|
|
|
458
508
|
// decoration: it is the only client-side measurement of when a
|
|
459
509
|
// version became RESOLVABLE, as against `time[version]`, which is
|
|
460
510
|
// when the registry recorded the write. The two are assumed equal by
|
|
461
|
-
// the 4.2-minute figure and the
|
|
511
|
+
// the 4.2-minute figure and the default window, and nothing had ever
|
|
462
512
|
// measured the gap. The pre-registered comparison — and what each
|
|
463
513
|
// outcome would change — is in #1509's description; this line, under
|
|
464
514
|
// GJSIFY_PUBLISH_DEBUG (which release.yml sets), is its input. Note
|
|
@@ -534,6 +584,7 @@ export async function publishWorkspace(input) {
|
|
|
534
584
|
name: packed.name,
|
|
535
585
|
version: packed.version,
|
|
536
586
|
status: res.status,
|
|
587
|
+
registry: registryClean,
|
|
537
588
|
readback,
|
|
538
589
|
};
|
|
539
590
|
}
|
|
@@ -568,9 +619,68 @@ export async function publishWorkspace(input) {
|
|
|
568
619
|
* re-checks the same set afterwards. It is policy, so it lives here and not in
|
|
569
620
|
* {@link publishWorkspace}, which stays a pure fact-finder.
|
|
570
621
|
*/
|
|
622
|
+
/**
|
|
623
|
+
* What the success line SAYS it established — the half of the read-back that
|
|
624
|
+
* was missing.
|
|
625
|
+
*
|
|
626
|
+
* AN INSTRUMENT STATES WHAT IT MEASURED, and `+ name@version` states nothing.
|
|
627
|
+
* It is the identical string from a CLI that polled the registry and read a
|
|
628
|
+
* tarball back, from one run with `--verify-timeout 0`, and from any
|
|
629
|
+
* `@gjsify/cli` older than v0.47.0, which had no read-back at all. That is not a
|
|
630
|
+
* hypothetical: ts-for-gir's v5.1.0 release (run 34899018849) printed twelve
|
|
631
|
+
* `+ <name>@5.1.0` lines with `GJSIFY_PUBLISH_DEBUG=1` set and not one
|
|
632
|
+
* `response:` or read-back line among them, because the job's
|
|
633
|
+
* `PATH="$WS_PATH/node_modules/.bin:$PATH"` put that workspace's pinned
|
|
634
|
+
* `@gjsify/cli@^0.44.0` ahead of the 0.51.1 it had just bootstrapped. Three of
|
|
635
|
+
* the twelve were not on the registry. The marker could not have told anyone,
|
|
636
|
+
* and nobody could have known to look.
|
|
637
|
+
*
|
|
638
|
+
* So every terminal success marker now carries its verification clause, and an
|
|
639
|
+
* unverified one says UNVERIFIED in a word a log sweep can grep for. A bare
|
|
640
|
+
* `+ name@version` with no clause is now itself the signature of a CLI too old
|
|
641
|
+
* to check.
|
|
642
|
+
*/
|
|
643
|
+
function describeVerification(readback, registry) {
|
|
644
|
+
if (!readback?.confirmed) {
|
|
645
|
+
return {
|
|
646
|
+
verified: false,
|
|
647
|
+
clause: '(UNVERIFIED — read-back disabled by --verify-timeout 0)',
|
|
648
|
+
json: null,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const seconds = (readback.elapsedMs / 1000).toFixed(1);
|
|
652
|
+
return {
|
|
653
|
+
verified: true,
|
|
654
|
+
clause: `(verified on ${registry} — ${readback.attempts} probe(s), ${seconds}s)`,
|
|
655
|
+
json: {
|
|
656
|
+
registry,
|
|
657
|
+
verdict: readback.verdict,
|
|
658
|
+
probes: readback.attempts,
|
|
659
|
+
elapsedMs: readback.elapsedMs,
|
|
660
|
+
tarball: readback.last.state === 'present' ? readback.last.tarball : null,
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* An unverified SUCCESS gets an Actions annotation too.
|
|
666
|
+
*
|
|
667
|
+
* `--verify-timeout 0` is a legitimate escape hatch for a registry with no
|
|
668
|
+
* packument read path, and it restores exactly the behaviour every incident in
|
|
669
|
+
* this file came out of. A disabled verifier that announces itself is a
|
|
670
|
+
* decision; one that stays quiet is the original defect wearing a hat — and the
|
|
671
|
+
* channel is the one `--verify-defer` already uses, beside the job rather than
|
|
672
|
+
* in a log nobody re-reads. stderr, because `--json` owns stdout.
|
|
673
|
+
*/
|
|
674
|
+
function warnUnverified(name, version, registry) {
|
|
675
|
+
if (!process.env.GITHUB_ACTIONS)
|
|
676
|
+
return;
|
|
677
|
+
process.stderr.write(`::warning title=Publish unverified::${name}@${version} was published with the read-back disabled ` +
|
|
678
|
+
`(--verify-timeout 0) — nothing confirmed that ${registry} serves it\n`);
|
|
679
|
+
}
|
|
571
680
|
function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
572
681
|
switch (outcome.action) {
|
|
573
682
|
case 'published': {
|
|
683
|
+
const verification = describeVerification(outcome.readback, outcome.registry);
|
|
574
684
|
const out = {
|
|
575
685
|
ok: true,
|
|
576
686
|
name: outcome.name,
|
|
@@ -580,11 +690,15 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
580
690
|
integrity: outcome.integrity,
|
|
581
691
|
tag: outcome.tag,
|
|
582
692
|
registry: outcome.registry,
|
|
693
|
+
verified: verification.verified,
|
|
694
|
+
verification: verification.json,
|
|
583
695
|
};
|
|
584
696
|
if (asJson)
|
|
585
697
|
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
586
698
|
else
|
|
587
|
-
process.stdout.write(`+ ${outcome.name}@${outcome.version}\n`);
|
|
699
|
+
process.stdout.write(`+ ${outcome.name}@${outcome.version} ${verification.clause}\n`);
|
|
700
|
+
if (!verification.verified)
|
|
701
|
+
warnUnverified(outcome.name, outcome.version, outcome.registry);
|
|
588
702
|
return;
|
|
589
703
|
}
|
|
590
704
|
case 'publish-unconfirmed': {
|
|
@@ -602,6 +716,9 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
602
716
|
elapsedMs: outcome.readback.elapsedMs,
|
|
603
717
|
readbackUrl: outcome.readback.url,
|
|
604
718
|
readbackState: outcome.readback.last.state,
|
|
719
|
+
verdict: outcome.readback.verdict,
|
|
720
|
+
verdictDetail: outcome.readback.verdictDetail,
|
|
721
|
+
recordedAt: outcome.readback.recordedAt ?? null,
|
|
605
722
|
deferred: deferUnconfirmed,
|
|
606
723
|
}, null, 2)}\n`);
|
|
607
724
|
}
|
|
@@ -619,24 +736,31 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
619
736
|
const claimed = outcome.claim === 'already-published' ? 'was reported already published' : 'was accepted';
|
|
620
737
|
process.stderr.write(`::warning title=Publish unconfirmed::${outcome.name}@${outcome.version} ${claimed} ` +
|
|
621
738
|
`(HTTP ${outcome.putStatus}) but did not resolve on ${outcome.registry} within ` +
|
|
622
|
-
`${(outcome.readback.elapsedMs / 1000).toFixed(1)}s —
|
|
739
|
+
`${(outcome.readback.elapsedMs / 1000).toFixed(1)}s [${outcome.readback.verdict}] — ` +
|
|
740
|
+
'the release-closure job must confirm it\n');
|
|
623
741
|
}
|
|
624
742
|
return;
|
|
625
743
|
}
|
|
626
744
|
return process.exit(1);
|
|
627
745
|
}
|
|
628
746
|
case 'republish-tolerated': {
|
|
747
|
+
const verification = describeVerification(outcome.readback, outcome.registry);
|
|
629
748
|
const out = {
|
|
630
749
|
ok: true,
|
|
631
750
|
action: 'republish-tolerated',
|
|
632
751
|
name: outcome.name,
|
|
633
752
|
version: outcome.version,
|
|
634
753
|
status: outcome.status,
|
|
754
|
+
registry: outcome.registry,
|
|
755
|
+
verified: verification.verified,
|
|
756
|
+
verification: verification.json,
|
|
635
757
|
};
|
|
636
758
|
if (asJson)
|
|
637
759
|
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
638
760
|
else
|
|
639
|
-
process.stdout.write(`= ${outcome.name}@${outcome.version} (already published, tolerated)\n`);
|
|
761
|
+
process.stdout.write(`= ${outcome.name}@${outcome.version} (already published, tolerated) ${verification.clause}\n`);
|
|
762
|
+
if (!verification.verified)
|
|
763
|
+
warnUnverified(outcome.name, outcome.version, outcome.registry);
|
|
640
764
|
return;
|
|
641
765
|
}
|
|
642
766
|
case 'skipped-untrusted-new': {
|
|
@@ -654,6 +778,27 @@ function reportPublishOutcome(outcome, asJson, deferUnconfirmed = false) {
|
|
|
654
778
|
}
|
|
655
779
|
return;
|
|
656
780
|
}
|
|
781
|
+
case 'blocked-missing-dependency': {
|
|
782
|
+
const edges = outcome.missing.map((m) => `${m.block}.${m.name}@${m.spec}`).join(', ');
|
|
783
|
+
if (asJson) {
|
|
784
|
+
process.stdout.write(`${JSON.stringify({
|
|
785
|
+
ok: outcome.ok,
|
|
786
|
+
action: 'blocked-missing-dependency',
|
|
787
|
+
name: outcome.name,
|
|
788
|
+
version: outcome.version,
|
|
789
|
+
registry: outcome.registry,
|
|
790
|
+
tolerated: outcome.tolerated,
|
|
791
|
+
missing: outcome.missing,
|
|
792
|
+
}, null, 2)}\n`);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
process.stdout.write(`~ ${outcome.name}@${outcome.version} (NOT published — ${outcome.missing.length} required pin(s) name a package ${outcome.registry} does not have: ${edges})\n`);
|
|
796
|
+
}
|
|
797
|
+
console.error(`gjsify publish: every consumer install of ${outcome.name} would fail with \`npm error 404\`, on every package manager — a required edge is not skippable. Bootstrap the target first (docs/publishing.md § New \`@gjsify/*\` package), then re-run. See #1713.`);
|
|
798
|
+
if (!outcome.tolerated)
|
|
799
|
+
return process.exit(1);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
657
802
|
case 'otp-required': {
|
|
658
803
|
console.error(`gjsify publish: npm requires a 2FA one-time code — re-run with \`--otp <code>\``);
|
|
659
804
|
return process.exit(1);
|
package/lib/commands/ship.js
CHANGED
|
@@ -35,6 +35,7 @@ import { discoverPayload } from '../utils/ship/discover.js';
|
|
|
35
35
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
36
36
|
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
37
37
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
38
|
+
import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
|
|
38
39
|
import { localizeMetadata } from '../utils/ship/localize-metadata.js';
|
|
39
40
|
import { assertHostCanFinish, assertKindCanPack, assertToolsInstalled, canCarryTicket, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
|
|
40
41
|
import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout, LAYOUT_NAMES, } from '../utils/ship/layout.js';
|
|
@@ -433,7 +434,24 @@ async function assemble(args) {
|
|
|
433
434
|
workDir: join(outRoot, 'schemas'),
|
|
434
435
|
})),
|
|
435
436
|
];
|
|
436
|
-
|
|
437
|
+
// THE APPLICATION ICON, rendered once for the sizes this layout's writers
|
|
438
|
+
// embed — six for the `.exe`'s resource directory, seven for the `.icns` —
|
|
439
|
+
// and nothing for a CLI or for Linux, where the theme carries the SVG the
|
|
440
|
+
// project declared. `resolveAppIcon` REFUSES an app that has no source for a
|
|
441
|
+
// size rather than staging without it: the released artifacts this closes
|
|
442
|
+
// shipped for a year with the generic icon on two platforms, and the only
|
|
443
|
+
// reason was that nothing said so (`utils/ship/icons.ts`).
|
|
444
|
+
const icon = settings.kind === 'app' && layout.icon !== undefined
|
|
445
|
+
? await resolveAppIcon({
|
|
446
|
+
iconFiles: settings.iconFiles,
|
|
447
|
+
appId: settings.appId,
|
|
448
|
+
sizes: layout.icon.sizes,
|
|
449
|
+
target: layout.icon.writes,
|
|
450
|
+
})
|
|
451
|
+
: undefined;
|
|
452
|
+
if (icon !== undefined)
|
|
453
|
+
console.log(`${LOG} icon: ${iconSizes(icon).join('/')} px, ${icon.source}`);
|
|
454
|
+
const staged = placeStage(layout, { ...settings, icon }, planned, runtime.files);
|
|
437
455
|
writeStage(stageDir, staged);
|
|
438
456
|
console.log(`${LOG} staged ${staged.length} file(s) for ${layout.name} in ${relative(projectDir, stageDir)}/`);
|
|
439
457
|
if (args.verbose)
|
|
@@ -771,7 +771,7 @@ export interface ConfigDataFlatpak extends AppMetadata {
|
|
|
771
771
|
command?: string;
|
|
772
772
|
/** Finish-args (capabilities). Default depends on `runtime` + `--cli-only`. */
|
|
773
773
|
finishArgs?: string[];
|
|
774
|
-
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g.
|
|
774
|
+
/** Extra Flatpak modules prepended before the app's own meson/simple module (e.g. a GI library the runtime lacks). */
|
|
775
775
|
extraModules?: unknown[];
|
|
776
776
|
/**
|
|
777
777
|
* Full replacement for the manifest's `modules` array: used verbatim, with
|