@gjsify/cli 0.47.0 → 0.48.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 +86 -83
- package/lib/commands/affected-classify.js +30 -1
- package/lib/commands/dlx.js +7 -3
- package/lib/commands/foreach.js +9 -2
- package/lib/commands/publish.js +1 -8
- package/lib/commands/run.js +7 -2
- package/lib/commands/ship.js +32 -4
- package/lib/types/config-data.d.ts +37 -1
- package/lib/utils/base64.d.ts +34 -0
- package/lib/utils/base64.js +47 -0
- package/lib/utils/cli-runtime-closure.d.ts +61 -0
- package/lib/utils/cli-runtime-closure.js +54 -17
- package/lib/utils/double-dash-args.d.ts +8 -0
- package/lib/utils/double-dash-args.js +64 -0
- package/lib/utils/ship/changelog.d.ts +56 -0
- package/lib/utils/ship/changelog.js +166 -0
- package/lib/utils/ship/discover.d.ts +10 -1
- package/lib/utils/ship/discover.js +76 -2
- package/lib/utils/ship/entry-interpreter.d.ts +44 -0
- package/lib/utils/ship/entry-interpreter.js +109 -0
- package/lib/utils/ship/formats.js +5 -0
- package/lib/utils/ship/gi-namespaces.js +175 -12
- package/lib/utils/ship/plan.d.ts +5 -1
- package/lib/utils/ship/plan.js +21 -2
- package/lib/utils/ship/settings.d.ts +48 -2
- package/lib/utils/ship/settings.js +63 -3
- package/lib/utils/ship/stage-manifest.d.ts +22 -3
- package/lib/utils/ship/stage-manifest.js +40 -13
- package/lib/utils/ship/types.d.ts +13 -0
- package/package.json +19 -19
|
@@ -146,6 +146,27 @@ const SCRIPT_COUPLINGS = [
|
|
|
146
146
|
runE2E: true,
|
|
147
147
|
why: '@gjsify/create-app build → node scripts/process-template.mjs reads templates/',
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
// THE SAME SHAPE AS `.githooks/` ABOVE, one layer up: the guard's trigger names
|
|
151
|
+
// the TEST's path instead of the thing under test. `AppRegistry.runApplication`
|
|
152
|
+
// holds three lines observable only by RUNNING the loop —
|
|
153
|
+
// `registerBuiltinWidgets()`, the `toShellOptions` passthrough and
|
|
154
|
+
// `provideWindowChrome()` — and their one external observer is
|
|
155
|
+
// `tests/e2e/react-native-devtools`, which main.yml's `e2e` job now runs under
|
|
156
|
+
// xvfb + a session bus (#1550). Without this entry a change confined to
|
|
157
|
+
// `packages/framework/react-native/**` seeds its own workspace, the e2e tier
|
|
158
|
+
// stays OFF (it turns on for a `tests/e2e/**` touch), and the suite is silent
|
|
159
|
+
// for exactly the PRs it exists to catch. Measured on #1540: deleting
|
|
160
|
+
// `provideWindowChrome()` left format, lint, tsc and all in-process assertions
|
|
161
|
+
// green while the application drew two header bars.
|
|
162
|
+
//
|
|
163
|
+
// No extra seeds — the package is a workspace and already seeds itself. The
|
|
164
|
+
// TIER is the point, as for `.githooks/`.
|
|
165
|
+
match: /^packages\/framework\/react-native\//,
|
|
166
|
+
seeds: [],
|
|
167
|
+
runE2E: true,
|
|
168
|
+
why: 'tests/e2e/react-native-devtools is the only external observer of AppRegistry.runApplication',
|
|
169
|
+
},
|
|
149
170
|
];
|
|
150
171
|
export function classifyAndExpand(workspaces, changedFiles) {
|
|
151
172
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
@@ -255,7 +276,15 @@ export function classifyAndExpand(workspaces, changedFiles) {
|
|
|
255
276
|
global: false,
|
|
256
277
|
reason: `test-only (${remaining.length} file(s) in ${only})`,
|
|
257
278
|
workspaces: [only],
|
|
258
|
-
|
|
279
|
+
// `|| couplingRunE2E`: this shortcut skips CLOSURE EXPANSION, and a
|
|
280
|
+
// declared tier is not part of the closure. Dropping it here made a
|
|
281
|
+
// coupling's reach depend on the SHAPE of the changed path — the
|
|
282
|
+
// `.githooks/` entry survived only because a hook is not a spec file,
|
|
283
|
+
// while `packages/framework/react-native/src/widgets.spec.ts` fell
|
|
284
|
+
// through and turned the e2e tier back off. Measured by the vector
|
|
285
|
+
// "a react-native spec file ALSO turns the e2e tier on", which was RED
|
|
286
|
+
// when it was written (#1550).
|
|
287
|
+
runE2E: touchedE2E || couplingRunE2E,
|
|
259
288
|
runIntegration: touchedIntegration || isIntegrationWorkspace(only),
|
|
260
289
|
skipAll: false,
|
|
261
290
|
};
|
package/lib/commands/dlx.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
14
14
|
import { parseSpec } from '../utils/parse-spec.js';
|
|
15
15
|
import { resolveGjsEntry } from '../utils/resolve-gjs-entry.js';
|
|
16
|
+
import { doubleDashArgs } from '../utils/double-dash-args.js';
|
|
16
17
|
import { cacheDirFor, createCacheKey, getValidCachedPkg, makePrepareDir, resolveInstalledPkgDir, symlinkSwap, } from '../utils/dlx-cache.js';
|
|
17
18
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
18
19
|
export const dlxCommand = {
|
|
@@ -25,7 +26,11 @@ export const dlxCommand = {
|
|
|
25
26
|
// --help` shows ts-for-gir's --help instead of gjsify dlx's.
|
|
26
27
|
// Without `populate--`, the trailing `--help` is consumed at
|
|
27
28
|
// the gjsify level and the bundle never sees it.
|
|
28
|
-
|
|
29
|
+
// `parse-positional-numbers: false` alongside it: the `--` tail and the
|
|
30
|
+
// `[extraArgs..]` positional are the caller's argv, not numbers, and yargs
|
|
31
|
+
// otherwise retypes a bare `5` (#1531). `--cache-max-age` declares
|
|
32
|
+
// `type: 'number'` and is unaffected.
|
|
33
|
+
.parserConfiguration({ 'populate--': true, 'parse-positional-numbers': false })
|
|
29
34
|
.positional('spec', {
|
|
30
35
|
description: 'Package spec (`name`, `name@version`, `@scope/name@spec`, or local path).',
|
|
31
36
|
type: 'string',
|
|
@@ -84,8 +89,7 @@ export const dlxCommand = {
|
|
|
84
89
|
// number)[]`), so flags like `--help` reach the bundle untouched.
|
|
85
90
|
// Merge those into the positional extraArgs the splitter sees so
|
|
86
91
|
// both call shapes share one downstream path.
|
|
87
|
-
const
|
|
88
|
-
const extraArgsCombined = [...(args.extraArgs ?? []), ...passthroughDoubleDash];
|
|
92
|
+
const extraArgsCombined = [...(args.extraArgs ?? []), ...doubleDashArgs(args)];
|
|
89
93
|
const { binName, extraArgs } = splitBinAndArgs(pkgDir, args.binOrArg, extraArgsCombined);
|
|
90
94
|
const entry = resolveGjsEntry(pkgDir, binName);
|
|
91
95
|
if (entry.fromFallback) {
|
package/lib/commands/foreach.js
CHANGED
|
@@ -15,6 +15,7 @@ import { affectedClosure, buildDependencyGraph, discoverWorkspaces, filterWorksp
|
|
|
15
15
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
16
16
|
import { cliRuntimeClosure } from '../utils/cli-runtime-closure.js';
|
|
17
17
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
18
|
+
import { doubleDashArgs } from '../utils/double-dash-args.js';
|
|
18
19
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
19
20
|
// The ONE runner-selection rule, shared rather than re-derived. This file used to
|
|
20
21
|
// carry its own copy; `workspace.ts` grew the bootstrap-CLI branch and the copy
|
|
@@ -267,6 +268,12 @@ export const foreachCommand = {
|
|
|
267
268
|
// gjsify foreach --exec -- npm publish --tag latest
|
|
268
269
|
// without yargs grabbing --tag/--access/etc.
|
|
269
270
|
'populate--': true,
|
|
271
|
+
// …and keep that tail as the TEXT the caller typed. Without
|
|
272
|
+
// this yargs types a bare number in the array as a `number`,
|
|
273
|
+
// which is how `--verify-timeout 5 --tag latest` reached 209
|
|
274
|
+
// npm publishes as `--verify-timeout --tag latest` (#1531).
|
|
275
|
+
// Declared `type: 'number'` options (`--jobs`) are unaffected.
|
|
276
|
+
'parse-positional-numbers': false,
|
|
270
277
|
}),
|
|
271
278
|
handler: async (args) => {
|
|
272
279
|
// Walk up to the monorepo root — foreach is sometimes invoked
|
|
@@ -286,7 +293,7 @@ export const foreachCommand = {
|
|
|
286
293
|
// Script mode silently DROPPED everything after `--` (only --exec
|
|
287
294
|
// consumed args['--']). Forward them to each child invocation,
|
|
288
295
|
// matching `yarn workspaces foreach run <script> -- <flags>`.
|
|
289
|
-
const fromDoubleDash = (args
|
|
296
|
+
const fromDoubleDash = doubleDashArgs(args);
|
|
290
297
|
if (fromDoubleDash.length > 0)
|
|
291
298
|
cmdArgs = [...cmdArgs, ...fromDoubleDash];
|
|
292
299
|
}
|
|
@@ -294,7 +301,7 @@ export const foreachCommand = {
|
|
|
294
301
|
// With populate--:true, anything after the literal `--`
|
|
295
302
|
// separator lands in top-level args['--']. yargs DOES NOT
|
|
296
303
|
// attach it to args._ — it's a sibling array.
|
|
297
|
-
const fromDoubleDash = (args
|
|
304
|
+
const fromDoubleDash = doubleDashArgs(args);
|
|
298
305
|
if (fromDoubleDash.length > 0) {
|
|
299
306
|
if (!cmd) {
|
|
300
307
|
cmd = fromDoubleDash[0];
|
package/lib/commands/publish.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { readFileSync } from 'node:fs';
|
|
43
43
|
import { tmpdir } from 'node:os';
|
|
44
44
|
import { join, resolve } from 'node:path';
|
|
45
|
+
import { base64Encode } from '../utils/base64.js';
|
|
45
46
|
import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
|
|
46
47
|
import { buildPublishHeaders, escapePackageName } from '../utils/publish-headers.js';
|
|
47
48
|
import { packWorkspace } from './pack.js';
|
|
@@ -789,14 +790,6 @@ function buildPublishPayload(opts) {
|
|
|
789
790
|
payload.access = access;
|
|
790
791
|
return payload;
|
|
791
792
|
}
|
|
792
|
-
function base64Encode(bytes) {
|
|
793
|
-
let str = '';
|
|
794
|
-
const chunk = 0x8000;
|
|
795
|
-
for (let i = 0; i < bytes.length; i += chunk) {
|
|
796
|
-
str += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
797
|
-
}
|
|
798
|
-
return btoa(str);
|
|
799
|
-
}
|
|
800
793
|
function handleOidcFailure(err, packageName, asJson) {
|
|
801
794
|
if (err instanceof OidcUnavailableError) {
|
|
802
795
|
const msg = `gjsify publish: OIDC not available — ${err.message}`;
|
package/lib/commands/run.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// to disambiguate can pass `./<file>` explicitly.
|
|
16
16
|
import { readFileSync, statSync } from 'node:fs';
|
|
17
17
|
import { basename, delimiter, join, resolve } from 'node:path';
|
|
18
|
+
import { doubleDashArgs } from '../utils/double-dash-args.js';
|
|
18
19
|
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
19
20
|
import { runGjsBundle } from '../utils/run-gjs.js';
|
|
20
21
|
import { runRuntimeBundle } from '../utils/run-node.js';
|
|
@@ -93,6 +94,11 @@ export const runCommand = {
|
|
|
93
94
|
// Without this, anything after `--` lands in args._ and is
|
|
94
95
|
// silently dropped by the handler.
|
|
95
96
|
'populate--': true,
|
|
97
|
+
// …and hand that tail on as the TEXT the caller typed. Without
|
|
98
|
+
// this yargs types a bare number in the array — and in the
|
|
99
|
+
// `[args..]` positional — as a `number`, which the reader below
|
|
100
|
+
// used to drop outright (#1531).
|
|
101
|
+
'parse-positional-numbers': false,
|
|
96
102
|
// Forward UNKNOWN flags to the target without requiring `--`:
|
|
97
103
|
// gjsify run dist/app.gjs.mjs serve --port 8080 --year 2025
|
|
98
104
|
// Without this, the top-level `.strict()` (cli-app.ts) rejects
|
|
@@ -111,8 +117,7 @@ export const runCommand = {
|
|
|
111
117
|
// than silently discarding them (without it they land in args._ and
|
|
112
118
|
// are never forwarded to the child).
|
|
113
119
|
const positionalArgs = args.args ?? [];
|
|
114
|
-
const
|
|
115
|
-
const extraArgs = [...positionalArgs, ...doubleDashArgs];
|
|
120
|
+
const extraArgs = [...positionalArgs, ...doubleDashArgs(args)];
|
|
116
121
|
// `--node-script` (explicit) → run an UNBUNDLED Node-style script on the
|
|
117
122
|
// host runtime; under GJS it is bundled `--app gjs` on the fly first.
|
|
118
123
|
// Checked before `--runtime` and `--workspace`: it is a file mode, and
|
package/lib/commands/ship.js
CHANGED
|
@@ -42,13 +42,14 @@ import { isUnder, SHARE } from '../utils/ship/share-dirs.js';
|
|
|
42
42
|
import { compileSchemasForStage } from '../utils/ship/schemas.js';
|
|
43
43
|
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
44
44
|
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
45
|
+
import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
|
|
45
46
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
46
47
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
47
48
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
48
49
|
import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
49
50
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
50
51
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
51
|
-
import {
|
|
52
|
+
import { resolveShipApp, resolveShipBundle, resolveShipSettings, } from '../utils/ship/settings.js';
|
|
52
53
|
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
|
|
53
54
|
import { directorySize, readStage, writePayload, writeStage } from '../utils/ship/stage-writer.js';
|
|
54
55
|
const LOG = '[gjsify ship]';
|
|
@@ -280,12 +281,22 @@ async function assemble(args) {
|
|
|
280
281
|
assertCanPack(format);
|
|
281
282
|
if (!args['skip-build'])
|
|
282
283
|
await runProjectBuild(projectDir);
|
|
284
|
+
// THE OTHER HALF of the resolution above, and they are one decision: the
|
|
285
|
+
// runtime a target execs and the entry it hands that runtime (#1545). Two
|
|
286
|
+
// keys, resolved by two twin functions, compared by `assertEntryRunsUnder`
|
|
287
|
+
// as soon as both answers and the file exist.
|
|
288
|
+
const resolvedBundle = resolveShipBundle({ pkg, ship, layoutOs: layout.os });
|
|
289
|
+
if (resolvedBundle.overridden) {
|
|
290
|
+
console.log(`${LOG} entry for ${layout.name}: ${resolvedBundle.declared} — \`${resolvedBundle.key}\` ` +
|
|
291
|
+
'overrides `gjsify.main`.');
|
|
292
|
+
}
|
|
283
293
|
const discovered = discoverPayload({
|
|
284
294
|
projectDir,
|
|
285
295
|
pkg,
|
|
286
296
|
ship,
|
|
287
297
|
flatpakIcon: flatpak.icon,
|
|
288
|
-
declaredBundle:
|
|
298
|
+
declaredBundle: resolvedBundle.declared,
|
|
299
|
+
foreignBundles: resolvedBundle.foreign,
|
|
289
300
|
});
|
|
290
301
|
const { settings, metadata, warnings } = resolveShipSettings({
|
|
291
302
|
projectDir,
|
|
@@ -308,6 +319,21 @@ async function assemble(args) {
|
|
|
308
319
|
// the packing host.
|
|
309
320
|
for (const format of formats)
|
|
310
321
|
assertFormatCanRunInterpreter(format, settings.app);
|
|
322
|
+
// The entry, read ONCE: the pair check below and the namespace scan further
|
|
323
|
+
// down both ask what this file imports, and a bundle is the biggest file in
|
|
324
|
+
// the project.
|
|
325
|
+
const bundleSource = readFileSync(settings.bundlePath, 'utf-8');
|
|
326
|
+
// BEFORE anything is staged, for the reason above and one more: this is the
|
|
327
|
+
// refusal that says the artifact could never have started, and the run that
|
|
328
|
+
// reports it must not also print a stage path.
|
|
329
|
+
assertEntryRunsUnder({
|
|
330
|
+
interpreter: settings.app,
|
|
331
|
+
source: bundleSource,
|
|
332
|
+
entry: relative(projectDir, settings.bundlePath) || settings.bundlePath,
|
|
333
|
+
appKey: resolvedApp.key,
|
|
334
|
+
bundleKey: resolvedBundle.key,
|
|
335
|
+
layoutOs: layout.os,
|
|
336
|
+
});
|
|
311
337
|
const mtime = buildTimestamp(settings.bundlePath);
|
|
312
338
|
const metadataInputs = {
|
|
313
339
|
appId: settings.appId,
|
|
@@ -354,6 +380,8 @@ async function assemble(args) {
|
|
|
354
380
|
metainfo: translated.metainfo,
|
|
355
381
|
desktopEntry: translated.desktopEntry,
|
|
356
382
|
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
383
|
+
changelogText: settings.changelogFile === undefined ? undefined : readFileSync(settings.changelogFile, 'utf-8'),
|
|
384
|
+
mtime,
|
|
357
385
|
};
|
|
358
386
|
const outRoot = resolve(projectDir, settings.outDir);
|
|
359
387
|
const stageDir = join(outRoot, 'stage');
|
|
@@ -466,7 +494,7 @@ async function assemble(args) {
|
|
|
466
494
|
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
467
495
|
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
468
496
|
// tell which staged file is the entry.
|
|
469
|
-
const namespaces = scanGiNamespaces(
|
|
497
|
+
const namespaces = scanGiNamespaces(bundleSource);
|
|
470
498
|
if (args.verbose)
|
|
471
499
|
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
472
500
|
// PLACED, like the payload, and through the same map. `planOverlay` answers a
|
|
@@ -481,7 +509,7 @@ async function assemble(args) {
|
|
|
481
509
|
// overlay reaches it by the same route.
|
|
482
510
|
const overlay = new Map();
|
|
483
511
|
for (const format of formats) {
|
|
484
|
-
overlay.set(format.id, planOverlay(settings, format, stageInputs).map((file) => ({
|
|
512
|
+
overlay.set(format.id, (await planOverlay(settings, format, stageInputs)).map((file) => ({
|
|
485
513
|
...file,
|
|
486
514
|
path: place(layout, settings, file.path),
|
|
487
515
|
})));
|
|
@@ -464,14 +464,27 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
464
464
|
/**
|
|
465
465
|
* The built bundle `bin/<name>` executes. Default: `gjsify.main`, then
|
|
466
466
|
* `package.json#main`. Its whole directory is staged into `lib/<name>/`.
|
|
467
|
+
*
|
|
468
|
+
* A STRING is one entry for every target; a TABLE names one per OS, keyed
|
|
469
|
+
* like {@link ShipAppOptions} and resolved beside it. An application with two
|
|
470
|
+
* hosts has two bundles built from one source — `gjsify.app` decides which
|
|
471
|
+
* runtime a target execs, and this decides which of those bundles it hands
|
|
472
|
+
* that runtime (#1545).
|
|
467
473
|
*/
|
|
468
|
-
bundle?: string;
|
|
474
|
+
bundle?: string | ShipBundleOptions;
|
|
469
475
|
/** Icon file, or a directory of them. Sizes are read from the path or the filename. */
|
|
470
476
|
icon?: string;
|
|
471
477
|
/** A `*.gschema.xml` file, or a directory of them. */
|
|
472
478
|
schemas?: string;
|
|
473
479
|
/** Licence file. Default: the first of LICENSE, LICENSE.md, LICENSE.txt, COPYING. */
|
|
474
480
|
licenseFile?: string;
|
|
481
|
+
/**
|
|
482
|
+
* The project's own changelog, rendered into the `.deb`'s
|
|
483
|
+
* `share/doc/<package>/changelog.Debian.gz` (Debian Policy § 4.4).
|
|
484
|
+
* Default: the first of CHANGELOG.md, CHANGELOG, CHANGELOG.txt, NEWS.md,
|
|
485
|
+
* searched up to the repository root exactly as `licenseFile` is.
|
|
486
|
+
*/
|
|
487
|
+
changelogFile?: string;
|
|
475
488
|
/** deb `Section:`. Default: derived from `categories`. */
|
|
476
489
|
section?: string;
|
|
477
490
|
/** rpm `Group:`. Default: derived from `categories`. */
|
|
@@ -611,6 +624,29 @@ export interface ShipAppOptions {
|
|
|
611
624
|
/** What a Windows program directory carries. `node` is the only value its formats accept. */
|
|
612
625
|
win32?: 'gjs' | 'node';
|
|
613
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* `gjsify.ship.bundle` — the ENTRY per OS, the twin of {@link ShipAppOptions}.
|
|
629
|
+
*
|
|
630
|
+
* The two answer one question between them, and answering only half of it is
|
|
631
|
+
* what #1545 measured: `gjsify.ship.app.darwin: "node"` pointed the launcher at
|
|
632
|
+
* `node` while the payload stayed the project's `main` — a `--app gjs` bundle,
|
|
633
|
+
* because Linux is that project's primary target. Both halves were honoured,
|
|
634
|
+
* they contradicted each other, and the `.app` died on its first import.
|
|
635
|
+
*
|
|
636
|
+
* Same OS spelling as everywhere else in this block (`process.platform`), and
|
|
637
|
+
* the same fall-through: a target with no key here keeps `gjsify.main`. What it
|
|
638
|
+
* does NOT keep is another target's entry — a bundle declared for one OS is
|
|
639
|
+
* dropped from every other OS's payload, since no interpreter but that one can
|
|
640
|
+
* load it.
|
|
641
|
+
*/
|
|
642
|
+
export interface ShipBundleOptions {
|
|
643
|
+
/** The entry a `.deb`/`.rpm`/Flatpak installs. */
|
|
644
|
+
linux?: string;
|
|
645
|
+
/** The entry `<App>.app/Contents/Resources/lib/` carries. */
|
|
646
|
+
darwin?: string;
|
|
647
|
+
/** The entry the Windows program directory carries. */
|
|
648
|
+
win32?: string;
|
|
649
|
+
}
|
|
614
650
|
/**
|
|
615
651
|
* `gjsify.ship.sign` — an IDENTITY per OS, and never a certificate.
|
|
616
652
|
*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 for the CLI's own Node entry, which may not reach `@gjsify/buffer`.
|
|
3
|
+
*
|
|
4
|
+
* `@gjsify/buffer` exports exactly these two functions and this file duplicates
|
|
5
|
+
* them — deliberately, and the reason is a LAYER, not taste. The CLI's Node entry
|
|
6
|
+
* is the bootstrap artifact (ADR 0002): `gjsify install` links it before anything
|
|
7
|
+
* in this tree is built. `scripts/bootstrap-native-facades.mjs` emits `lib/esm`
|
|
8
|
+
* for its link-time runtime deps with plain `tsc`, and `buildCliRuntimeDeps()`
|
|
9
|
+
* resolves each one under `packages/infra/<name>`. `@gjsify/buffer` lives under
|
|
10
|
+
* `packages/node/` and its own `build` is `gjsify run build:gjsify && …`, i.e. it
|
|
11
|
+
* needs the BUNDLER — the artifact the bootstrap exists to produce. Importing it
|
|
12
|
+
* from here inverts that order, and the failure is not a type error: the ESM link
|
|
13
|
+
* fails at `bootstrap-native-facades`, so `build:infra` dies on every host before
|
|
14
|
+
* a single test runs.
|
|
15
|
+
*
|
|
16
|
+
* Measured: with `import { base64Encode } from '@gjsify/buffer'` in the CLI's Node
|
|
17
|
+
* entry, ten CI legs failed identically — "Missing at ESM link time:
|
|
18
|
+
* packages/infra/cli/node_modules/@gjsify/buffer/lib/esm/index.js, imported from
|
|
19
|
+
* lib/utils/ship/stage-manifest.js" — while all three REQUIRED checks stayed green,
|
|
20
|
+
* because `check-build-infra-order` holds the declaration order for TYPES and this
|
|
21
|
+
* is a runtime edge.
|
|
22
|
+
*
|
|
23
|
+
* So: one copy inside the CLI, imported by both call sites, rather than the two
|
|
24
|
+
* that prompted the deduplication in the first place. If the CLI ever gains a
|
|
25
|
+
* general way to reach `packages/node/*` at bootstrap time, this file is what
|
|
26
|
+
* should go.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Chunked because `String.fromCharCode(...bytes)` is a spread: a tarball-sized
|
|
30
|
+
* array blows the argument limit and throws `RangeError: Maximum call stack size
|
|
31
|
+
* exceeded`. 0x8000 is the size the CLI's publish path has always used.
|
|
32
|
+
*/
|
|
33
|
+
export declare function base64Encode(bytes: Uint8Array): string;
|
|
34
|
+
export declare function base64Decode(text: string): Uint8Array;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 for the CLI's own Node entry, which may not reach `@gjsify/buffer`.
|
|
3
|
+
*
|
|
4
|
+
* `@gjsify/buffer` exports exactly these two functions and this file duplicates
|
|
5
|
+
* them — deliberately, and the reason is a LAYER, not taste. The CLI's Node entry
|
|
6
|
+
* is the bootstrap artifact (ADR 0002): `gjsify install` links it before anything
|
|
7
|
+
* in this tree is built. `scripts/bootstrap-native-facades.mjs` emits `lib/esm`
|
|
8
|
+
* for its link-time runtime deps with plain `tsc`, and `buildCliRuntimeDeps()`
|
|
9
|
+
* resolves each one under `packages/infra/<name>`. `@gjsify/buffer` lives under
|
|
10
|
+
* `packages/node/` and its own `build` is `gjsify run build:gjsify && …`, i.e. it
|
|
11
|
+
* needs the BUNDLER — the artifact the bootstrap exists to produce. Importing it
|
|
12
|
+
* from here inverts that order, and the failure is not a type error: the ESM link
|
|
13
|
+
* fails at `bootstrap-native-facades`, so `build:infra` dies on every host before
|
|
14
|
+
* a single test runs.
|
|
15
|
+
*
|
|
16
|
+
* Measured: with `import { base64Encode } from '@gjsify/buffer'` in the CLI's Node
|
|
17
|
+
* entry, ten CI legs failed identically — "Missing at ESM link time:
|
|
18
|
+
* packages/infra/cli/node_modules/@gjsify/buffer/lib/esm/index.js, imported from
|
|
19
|
+
* lib/utils/ship/stage-manifest.js" — while all three REQUIRED checks stayed green,
|
|
20
|
+
* because `check-build-infra-order` holds the declaration order for TYPES and this
|
|
21
|
+
* is a runtime edge.
|
|
22
|
+
*
|
|
23
|
+
* So: one copy inside the CLI, imported by both call sites, rather than the two
|
|
24
|
+
* that prompted the deduplication in the first place. If the CLI ever gains a
|
|
25
|
+
* general way to reach `packages/node/*` at bootstrap time, this file is what
|
|
26
|
+
* should go.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Chunked because `String.fromCharCode(...bytes)` is a spread: a tarball-sized
|
|
30
|
+
* array blows the argument limit and throws `RangeError: Maximum call stack size
|
|
31
|
+
* exceeded`. 0x8000 is the size the CLI's publish path has always used.
|
|
32
|
+
*/
|
|
33
|
+
export function base64Encode(bytes) {
|
|
34
|
+
let str = '';
|
|
35
|
+
const chunk = 0x8000;
|
|
36
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
37
|
+
str += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
38
|
+
}
|
|
39
|
+
return btoa(str);
|
|
40
|
+
}
|
|
41
|
+
export function base64Decode(text) {
|
|
42
|
+
const binary = atob(text);
|
|
43
|
+
const bytes = new Uint8Array(binary.length);
|
|
44
|
+
for (let i = 0; i < binary.length; i++)
|
|
45
|
+
bytes[i] = binary.charCodeAt(i);
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
+
/**
|
|
3
|
+
* Literal module specifiers of `code`: static `import`/`export … from`, plus
|
|
4
|
+
* `import('<literal>')`.
|
|
5
|
+
*
|
|
6
|
+
* acorn rather than a regex — see the header. `allowHashBang` because the CLI's
|
|
7
|
+
* own entry is a bin (`#!/usr/bin/env node`), and a parse failure yields NO
|
|
8
|
+
* specifiers, which under-selects rather than mis-selects.
|
|
9
|
+
*/
|
|
10
|
+
export interface SpecifierNode {
|
|
11
|
+
type?: string;
|
|
12
|
+
value?: unknown;
|
|
13
|
+
expressions?: unknown[];
|
|
14
|
+
quasis?: Array<{
|
|
15
|
+
value?: {
|
|
16
|
+
cooked?: unknown;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The string a node names statically, or `null` when it is computed.
|
|
22
|
+
*
|
|
23
|
+
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
24
|
+
* skipping it is not hypothetical: the minifier rewrites `await
|
|
25
|
+
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
26
|
+
* silently loses every dynamic import in a built bundle.
|
|
27
|
+
*
|
|
28
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
29
|
+
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
30
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
31
|
+
* spellings above.
|
|
32
|
+
*/
|
|
33
|
+
export declare function staticStringValue(node: SpecifierNode | undefined): string | null;
|
|
34
|
+
/** One node of the walk: whatever acorn produced, seen as a bag of properties. */
|
|
35
|
+
export type AstNode = {
|
|
36
|
+
type?: string;
|
|
37
|
+
} & Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
40
|
+
*
|
|
41
|
+
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
42
|
+
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
43
|
+
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
44
|
+
* it load", and the second question has an answer the first cannot see — a
|
|
45
|
+
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
46
|
+
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
47
|
+
*
|
|
48
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
49
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
50
|
+
* that keeps a correct project buildable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function walkModuleAst(code: string, visit: (node: AstNode) => void): void;
|
|
53
|
+
/**
|
|
54
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
55
|
+
*
|
|
56
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
57
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
58
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
59
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
60
|
+
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
61
|
+
*/
|
|
62
|
+
export declare function importedSpecifier(node: AstNode): string | null;
|
|
2
63
|
export declare function moduleSpecifiers(code: string): string[];
|
|
3
64
|
export interface CliRuntimeClosureOptions {
|
|
4
65
|
/**
|
|
@@ -79,14 +79,19 @@ function splitWorkspaceSpecifier(specifier, byName) {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* The string a
|
|
82
|
+
* The string a node names statically, or `null` when it is computed.
|
|
83
83
|
*
|
|
84
84
|
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
85
85
|
* skipping it is not hypothetical: the minifier rewrites `await
|
|
86
86
|
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
87
87
|
* silently loses every dynamic import in a built bundle.
|
|
88
|
+
*
|
|
89
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
90
|
+
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
91
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
92
|
+
* spellings above.
|
|
88
93
|
*/
|
|
89
|
-
function
|
|
94
|
+
export function staticStringValue(node) {
|
|
90
95
|
if (node?.type === 'Literal')
|
|
91
96
|
return typeof node.value === 'string' ? node.value : null;
|
|
92
97
|
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
|
@@ -95,39 +100,71 @@ function literalSpecifier(node) {
|
|
|
95
100
|
}
|
|
96
101
|
return null;
|
|
97
102
|
}
|
|
98
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
105
|
+
*
|
|
106
|
+
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
107
|
+
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
108
|
+
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
109
|
+
* it load", and the second question has an answer the first cannot see — a
|
|
110
|
+
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
111
|
+
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
112
|
+
*
|
|
113
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
114
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
115
|
+
* that keeps a correct project buildable.
|
|
116
|
+
*/
|
|
117
|
+
export function walkModuleAst(code, visit) {
|
|
99
118
|
let ast;
|
|
100
119
|
try {
|
|
101
120
|
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
102
121
|
}
|
|
103
122
|
catch {
|
|
104
|
-
return
|
|
123
|
+
return;
|
|
105
124
|
}
|
|
106
|
-
const
|
|
107
|
-
const visit = (node) => {
|
|
125
|
+
const descend = (node) => {
|
|
108
126
|
if (!node || typeof node !== 'object')
|
|
109
127
|
return;
|
|
110
128
|
if (Array.isArray(node)) {
|
|
111
129
|
for (const child of node)
|
|
112
|
-
|
|
130
|
+
descend(child);
|
|
113
131
|
return;
|
|
114
132
|
}
|
|
115
133
|
const n = node;
|
|
116
|
-
|
|
117
|
-
n.type === 'ExportNamedDeclaration' ||
|
|
118
|
-
n.type === 'ExportAllDeclaration' ||
|
|
119
|
-
n.type === 'ImportExpression') {
|
|
120
|
-
const specifier = literalSpecifier(n.source);
|
|
121
|
-
if (specifier !== null)
|
|
122
|
-
out.push(specifier);
|
|
123
|
-
}
|
|
134
|
+
visit(n);
|
|
124
135
|
for (const [key, value] of Object.entries(n)) {
|
|
125
136
|
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
126
137
|
continue;
|
|
127
|
-
|
|
138
|
+
descend(value);
|
|
128
139
|
}
|
|
129
140
|
};
|
|
130
|
-
|
|
141
|
+
descend(ast);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
145
|
+
*
|
|
146
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
147
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
148
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
149
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
150
|
+
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
151
|
+
*/
|
|
152
|
+
export function importedSpecifier(node) {
|
|
153
|
+
if (node.type !== 'ImportDeclaration' &&
|
|
154
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
155
|
+
node.type !== 'ExportAllDeclaration' &&
|
|
156
|
+
node.type !== 'ImportExpression') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return staticStringValue(node.source);
|
|
160
|
+
}
|
|
161
|
+
export function moduleSpecifiers(code) {
|
|
162
|
+
const out = [];
|
|
163
|
+
walkModuleAst(code, (node) => {
|
|
164
|
+
const specifier = importedSpecifier(node);
|
|
165
|
+
if (specifier !== null)
|
|
166
|
+
out.push(specifier);
|
|
167
|
+
});
|
|
131
168
|
return out;
|
|
132
169
|
}
|
|
133
170
|
/** The file a relative specifier names, probing the extensionless spellings Node accepts. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything after the `--` separator, as an argv.
|
|
3
|
+
*
|
|
4
|
+
* @param args a yargs-parsed argument object from a command whose builder sets
|
|
5
|
+
* `parserConfiguration({ 'populate--': true, 'parse-positional-numbers': false })`
|
|
6
|
+
* @throws if an entry is not a string, i.e. if that configuration is missing
|
|
7
|
+
*/
|
|
8
|
+
export declare function doubleDashArgs(args: object): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The argv a caller wrote after `--`, read back off yargs.
|
|
2
|
+
//
|
|
3
|
+
// THE INCIDENT (#1531). `foreach.ts` and `run.ts` each read `args['--']` and
|
|
4
|
+
// narrowed it with `.filter((v): v is string => typeof v === 'string')`. That
|
|
5
|
+
// reads like a type narrowing and is a DELETION: yargs types a bare number in
|
|
6
|
+
// its `populate--` array as a `number`, so the value disappears and the option
|
|
7
|
+
// in front of it swallows the next flag. Measured against this repo's own
|
|
8
|
+
// yargs 18.0.0:
|
|
9
|
+
//
|
|
10
|
+
// in : gjsify publish --verify-timeout 5 --tag latest
|
|
11
|
+
// raw : ["gjsify","publish","--verify-timeout",5,"--tag","latest"]
|
|
12
|
+
// filter: ["gjsify","publish","--verify-timeout","--tag","latest"]
|
|
13
|
+
//
|
|
14
|
+
// That argv shipped: `npm:publish` passes `--verify-timeout 5 --tag latest`,
|
|
15
|
+
// and release run 33804937881 echoed `--verify-timeout --tag latest` 209 times
|
|
16
|
+
// — `--verify-timeout` taking `--tag` as its value, `latest` a stray
|
|
17
|
+
// positional, `--tag latest` gone. Nobody noticed because npm's default
|
|
18
|
+
// dist-tag IS `latest`, which is luck rather than design.
|
|
19
|
+
//
|
|
20
|
+
// TWO HALVES, AND THE FIRST ONE IS THE FIX. A coercion here (`String(v)`) would
|
|
21
|
+
// recover the value and still lose its SPELLING — measured against yargs 18.0.0,
|
|
22
|
+
// `1.0` comes back as 1, `0.10` as 0.1, `0x10` as 16 and `1e3` as 1000, and an
|
|
23
|
+
// argv is text, not arithmetic. (A leading zero is NOT in that set: `007` stays
|
|
24
|
+
// the string yargs was given. It reads like the obvious example and is the one
|
|
25
|
+
// shape here that never broke.) So the parse is corrected at
|
|
26
|
+
// the source: every command that sets `populate--` also sets
|
|
27
|
+
// `parse-positional-numbers: false`, which leaves `_`, the variadic positionals
|
|
28
|
+
// and the `--` tail as the strings the user typed while option values declared
|
|
29
|
+
// `type: 'number'` are still parsed (measured: `--jobs 4` stays `4` the number).
|
|
30
|
+
//
|
|
31
|
+
// This function is the second half. It does NOT coerce — with the parser
|
|
32
|
+
// configured as above a non-string cannot arrive, so one that does means the
|
|
33
|
+
// configuration was dropped, and that is the failure to name rather than to
|
|
34
|
+
// paper over. A silent `.filter()` over a parser-produced array is the wrong
|
|
35
|
+
// tool wherever the array is data rather than a union.
|
|
36
|
+
//
|
|
37
|
+
// ONE reader for all of it, because there were three and they disagreed:
|
|
38
|
+
// `foreach.ts` (twice) and `run.ts` filtered, `dlx.ts` coerced. The drifted
|
|
39
|
+
// copies are the ones that shipped the wrong argv.
|
|
40
|
+
/**
|
|
41
|
+
* Everything after the `--` separator, as an argv.
|
|
42
|
+
*
|
|
43
|
+
* @param args a yargs-parsed argument object from a command whose builder sets
|
|
44
|
+
* `parserConfiguration({ 'populate--': true, 'parse-positional-numbers': false })`
|
|
45
|
+
* @throws if an entry is not a string, i.e. if that configuration is missing
|
|
46
|
+
*/
|
|
47
|
+
export function doubleDashArgs(args) {
|
|
48
|
+
const raw = args['--'];
|
|
49
|
+
if (raw === undefined || raw === null)
|
|
50
|
+
return [];
|
|
51
|
+
if (!Array.isArray(raw)) {
|
|
52
|
+
throw new Error(`gjsify: internal error — yargs' \`--\` slot holds a ${typeof raw} rather than an array, so the ` +
|
|
53
|
+
'arguments after `--` cannot be forwarded. The command builder must set ' +
|
|
54
|
+
"`parserConfiguration({ 'populate--': true })`.");
|
|
55
|
+
}
|
|
56
|
+
return raw.map((value, index) => {
|
|
57
|
+
if (typeof value === 'string')
|
|
58
|
+
return value;
|
|
59
|
+
throw new Error(`gjsify: internal error — the argument after \`--\` at position ${index} arrived as a ` +
|
|
60
|
+
`${typeof value} (${String(value)}) instead of a string, so forwarding it would change what ` +
|
|
61
|
+
'the caller wrote. The command builder must set ' +
|
|
62
|
+
"`parserConfiguration({ 'parse-positional-numbers': false })` alongside `populate--`.");
|
|
63
|
+
});
|
|
64
|
+
}
|