@gjsify/cli 0.51.1 → 0.53.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 +539 -493
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +37 -1
- package/lib/bundler-pick.js +108 -17
- package/lib/cli-app.js +7 -2
- 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/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- 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/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +4 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +81 -5
- 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/self-update.js +2 -1
- package/lib/commands/ship.js +19 -1
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +2 -2
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +5 -19
- package/lib/utils/check-system-deps.js +31 -94
- package/lib/utils/dev-link.d.ts +225 -0
- package/lib/utils/dev-link.js +684 -0
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- 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 +33 -1
- package/lib/utils/install-backend-native.js +184 -20
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/oxc-resolve.js +42 -3
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- 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.d.ts +15 -1
- package/lib/utils/run-gjs.js +98 -13
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/gzip.js +1 -1
- 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 +197 -0
- package/lib/utils/ship/icons.js +399 -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/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +36 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -20
|
@@ -2,6 +2,8 @@ import type { LayoutMetadataInput } from './layout.js';
|
|
|
2
2
|
/** Where the two files live inside a bundle. Apple's names, not ours. */
|
|
3
3
|
export declare const BUNDLE_INFO_PLIST = "Contents/Info.plist";
|
|
4
4
|
export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
|
|
5
|
+
/** Where `CFBundleIconFile` is resolved: the bundle's resources directory, and nowhere else. */
|
|
6
|
+
export declare const BUNDLE_RESOURCES = "Contents/Resources";
|
|
5
7
|
/**
|
|
6
8
|
* Render `Contents/Info.plist`.
|
|
7
9
|
*
|
|
@@ -30,19 +32,12 @@ export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
|
|
|
30
32
|
* release). Emitting `version` twice would throw the distinction away on the one
|
|
31
33
|
* OS that has a field for it.
|
|
32
34
|
*
|
|
33
|
-
* KEYS DELIBERATELY ABSENT are listed in this module's header.
|
|
34
|
-
* is the one a reader will reach for first, and it is absent for a reason that
|
|
35
|
-
* outlives the key: M2a ships NO icon. `png2icns`, `icnsutil` and `iconutil` are
|
|
36
|
-
* all absent from this workstation and from the CI image, so an `.icns` written
|
|
37
|
-
* here could only be read back by a reader written here — `selfReading: true`,
|
|
38
|
-
* which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
|
|
39
|
-
* entering the CI image; until one does, the hicolor PNG/SVG the payload already
|
|
40
|
-
* carries stays the only icon, unread on macOS.
|
|
35
|
+
* KEYS DELIBERATELY ABSENT are listed in this module's header.
|
|
41
36
|
*
|
|
42
37
|
* A TWELFTH KEY, `ATSApplicationFontsPath`, is emitted when the bundle carries
|
|
43
38
|
* faces, and it is the first one here NOT cited to `refs/` — so the exception is
|
|
44
39
|
* stated rather than left to be noticed (ADR 0038). The rule this module opens
|
|
45
|
-
* with exists against DECORATION: the
|
|
40
|
+
* with exists against DECORATION: the four absent keys are cosmetic, nothing a
|
|
46
41
|
* reader here can open contains them, and emitting one would be a guess with no
|
|
47
42
|
* observable behind it. This key is a MECHANISM, and its citation is Apple's own
|
|
48
43
|
* *Information Property List Key Reference*, which states the scope in the terms
|
|
@@ -51,6 +46,21 @@ export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
|
|
|
51
46
|
* app and not for the system as a whole."* The value is a path relative to
|
|
52
47
|
* `Contents/Resources`.
|
|
53
48
|
*
|
|
49
|
+
* A THIRTEENTH, `CFBundleIconFile`, is the same shape of exception and closes the
|
|
50
|
+
* gap the previous version of this comment recorded: M2a shipped NO icon because
|
|
51
|
+
* nothing on Linux could read an `.icns` back. Two things changed. `icns.ts` now
|
|
52
|
+
* writes the container from rasters `icons.ts` renders through librsvg, and the
|
|
53
|
+
* readers were measured rather than assumed: CPython `struct` walks the element
|
|
54
|
+
* table in `verify-app-plist.py` (in CI), Pillow 12.3 opens all ten elements (on
|
|
55
|
+
* the workstation), `icns2png` reads three of ten and is NOT relied on, and
|
|
56
|
+
* `iconutil` on macOS is the reader with authority — a VM run, recorded with the
|
|
57
|
+
* change that lands this key. The citation is the same key reference: *"The name
|
|
58
|
+
* of the bundle's icon file, located in the Resources folder"*, and the
|
|
59
|
+
* measurement that motivates it is the released 0.8.0 bundle of one app on macOS
|
|
60
|
+
* 15.7 with no key, no `.icns` and the generic Finder icon. `CFBundleIconName` is
|
|
61
|
+
* NOT emitted: it names an asset-catalog entry (`Assets.car`), which this tree
|
|
62
|
+
* does not produce, and a name with no catalog is a dangling reference.
|
|
63
|
+
*
|
|
54
64
|
* It is also the only route there is. Pango on macOS is CoreText-backed, GTK is
|
|
55
65
|
* not built against fontconfig on that platform, and
|
|
56
66
|
* `pango_font_map_add_font_file()` answers `G_IO_ERROR_NOT_SUPPORTED` on the
|
|
@@ -63,7 +73,7 @@ export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
|
|
|
63
73
|
* Pango's CoreText map then holds the family, is unverified here — which is what
|
|
64
74
|
* `Layout.fontGap` says out loud rather than letting a green stage imply.
|
|
65
75
|
*/
|
|
66
|
-
export declare function renderInfoPlist(input: LayoutMetadataInput, fontsPath?: string): string;
|
|
76
|
+
export declare function renderInfoPlist(input: LayoutMetadataInput, fontsPath?: string, iconFile?: string): string;
|
|
67
77
|
/**
|
|
68
78
|
* Render `Contents/PkgInfo`: the package type and the signature, eight bytes, no
|
|
69
79
|
* newline.
|
package/lib/utils/ship/plist.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
// (`.github/ship-oracle/verify-modes.py`), and it parses the XML form.
|
|
15
15
|
//
|
|
16
16
|
// EVERY KEY BELOW IS CITED to a file in `refs/` that a real macOS toolchain
|
|
17
|
-
// produced or consumes
|
|
18
|
-
//
|
|
17
|
+
// produced or consumes, or — for the two MECHANISM keys, `ATSApplicationFontsPath`
|
|
18
|
+
// and `CFBundleIconFile` — to Apple's own key reference plus a measurement on a
|
|
19
|
+
// Mac. Keys that are merely plausible — `LSMinimumSystemVersion`,
|
|
20
|
+
// `NSHighResolutionCapable`, `LSApplicationCategoryType`,
|
|
19
21
|
// `NSHumanReadableCopyright` — are not emitted, because nothing a reader here can
|
|
20
22
|
// open contains them.
|
|
21
23
|
//
|
|
@@ -32,6 +34,8 @@
|
|
|
32
34
|
/** Where the two files live inside a bundle. Apple's names, not ours. */
|
|
33
35
|
export const BUNDLE_INFO_PLIST = 'Contents/Info.plist';
|
|
34
36
|
export const BUNDLE_PKGINFO = 'Contents/PkgInfo';
|
|
37
|
+
/** Where `CFBundleIconFile` is resolved: the bundle's resources directory, and nowhere else. */
|
|
38
|
+
export const BUNDLE_RESOURCES = 'Contents/Resources';
|
|
35
39
|
/**
|
|
36
40
|
* The four-character bundle type of an application, and the signature that says
|
|
37
41
|
* "none".
|
|
@@ -139,19 +143,12 @@ function arrayEntry(key, values) {
|
|
|
139
143
|
* release). Emitting `version` twice would throw the distinction away on the one
|
|
140
144
|
* OS that has a field for it.
|
|
141
145
|
*
|
|
142
|
-
* KEYS DELIBERATELY ABSENT are listed in this module's header.
|
|
143
|
-
* is the one a reader will reach for first, and it is absent for a reason that
|
|
144
|
-
* outlives the key: M2a ships NO icon. `png2icns`, `icnsutil` and `iconutil` are
|
|
145
|
-
* all absent from this workstation and from the CI image, so an `.icns` written
|
|
146
|
-
* here could only be read back by a reader written here — `selfReading: true`,
|
|
147
|
-
* which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
|
|
148
|
-
* entering the CI image; until one does, the hicolor PNG/SVG the payload already
|
|
149
|
-
* carries stays the only icon, unread on macOS.
|
|
146
|
+
* KEYS DELIBERATELY ABSENT are listed in this module's header.
|
|
150
147
|
*
|
|
151
148
|
* A TWELFTH KEY, `ATSApplicationFontsPath`, is emitted when the bundle carries
|
|
152
149
|
* faces, and it is the first one here NOT cited to `refs/` — so the exception is
|
|
153
150
|
* stated rather than left to be noticed (ADR 0038). The rule this module opens
|
|
154
|
-
* with exists against DECORATION: the
|
|
151
|
+
* with exists against DECORATION: the four absent keys are cosmetic, nothing a
|
|
155
152
|
* reader here can open contains them, and emitting one would be a guess with no
|
|
156
153
|
* observable behind it. This key is a MECHANISM, and its citation is Apple's own
|
|
157
154
|
* *Information Property List Key Reference*, which states the scope in the terms
|
|
@@ -160,6 +157,21 @@ function arrayEntry(key, values) {
|
|
|
160
157
|
* app and not for the system as a whole."* The value is a path relative to
|
|
161
158
|
* `Contents/Resources`.
|
|
162
159
|
*
|
|
160
|
+
* A THIRTEENTH, `CFBundleIconFile`, is the same shape of exception and closes the
|
|
161
|
+
* gap the previous version of this comment recorded: M2a shipped NO icon because
|
|
162
|
+
* nothing on Linux could read an `.icns` back. Two things changed. `icns.ts` now
|
|
163
|
+
* writes the container from rasters `icons.ts` renders through librsvg, and the
|
|
164
|
+
* readers were measured rather than assumed: CPython `struct` walks the element
|
|
165
|
+
* table in `verify-app-plist.py` (in CI), Pillow 12.3 opens all ten elements (on
|
|
166
|
+
* the workstation), `icns2png` reads three of ten and is NOT relied on, and
|
|
167
|
+
* `iconutil` on macOS is the reader with authority — a VM run, recorded with the
|
|
168
|
+
* change that lands this key. The citation is the same key reference: *"The name
|
|
169
|
+
* of the bundle's icon file, located in the Resources folder"*, and the
|
|
170
|
+
* measurement that motivates it is the released 0.8.0 bundle of one app on macOS
|
|
171
|
+
* 15.7 with no key, no `.icns` and the generic Finder icon. `CFBundleIconName` is
|
|
172
|
+
* NOT emitted: it names an asset-catalog entry (`Assets.car`), which this tree
|
|
173
|
+
* does not produce, and a name with no catalog is a dangling reference.
|
|
174
|
+
*
|
|
163
175
|
* It is also the only route there is. Pango on macOS is CoreText-backed, GTK is
|
|
164
176
|
* not built against fontconfig on that platform, and
|
|
165
177
|
* `pango_font_map_add_font_file()` answers `G_IO_ERROR_NOT_SUPPORTED` on the
|
|
@@ -172,7 +184,7 @@ function arrayEntry(key, values) {
|
|
|
172
184
|
* Pango's CoreText map then holds the family, is unverified here — which is what
|
|
173
185
|
* `Layout.fontGap` says out loud rather than letting a green stage imply.
|
|
174
186
|
*/
|
|
175
|
-
export function renderInfoPlist(input, fontsPath) {
|
|
187
|
+
export function renderInfoPlist(input, fontsPath, iconFile) {
|
|
176
188
|
const name = assertPlistText(input.name, 'CFBundleName', 'gjsify.ship.name');
|
|
177
189
|
const executable = assertPlistText(input.binaryName, 'CFBundleExecutable', 'gjsify.ship.binaryName');
|
|
178
190
|
const appId = assertPlistText(input.appId, 'CFBundleIdentifier', 'gjsify.ship.appId');
|
|
@@ -195,6 +207,11 @@ export function renderInfoPlist(input, fontsPath) {
|
|
|
195
207
|
stringEntry('CFBundleDevelopmentRegion', 'en'),
|
|
196
208
|
stringEntry('CFBundleDisplayName', name),
|
|
197
209
|
stringEntry('CFBundleExecutable', executable),
|
|
210
|
+
// Between Executable and Identifier is where the alphabet puts it. Absent,
|
|
211
|
+
// not empty, for a bundle that carries no icon (`kind: 'cli'`): an empty
|
|
212
|
+
// name is a file lookup that fails, which macOS answers with the same
|
|
213
|
+
// generic icon and no diagnostic.
|
|
214
|
+
...(iconFile === undefined ? [] : [stringEntry('CFBundleIconFile', iconFile)]),
|
|
198
215
|
stringEntry('CFBundleIdentifier', appId),
|
|
199
216
|
stringEntry('CFBundleInfoDictionaryVersion', '6.0'),
|
|
200
217
|
stringEntry('CFBundleName', name),
|
|
@@ -175,8 +175,9 @@ export function resolveShipSettings(input) {
|
|
|
175
175
|
const description = descriptionParagraphs(metadata.description) ?? [summary];
|
|
176
176
|
const sections = sectionsFor(metadata.categories);
|
|
177
177
|
if (kind === 'app' && discovered.iconFiles.length === 0) {
|
|
178
|
-
warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus and app stores
|
|
179
|
-
'
|
|
178
|
+
warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus and app stores ` +
|
|
179
|
+
'on Linux, and the darwin and windows layouts refuse to stage without one. Point ' +
|
|
180
|
+
'`gjsify.ship.icon` at an SVG (or a directory of sized PNGs).');
|
|
180
181
|
}
|
|
181
182
|
// Resolved here rather than in the packer, because the packer may run on a
|
|
182
183
|
// host with no `package.json` at all (ADR 0024 § A2) — and because the
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Win32Invocation } from './win32-command.js';
|
|
2
3
|
/**
|
|
3
4
|
* What the calling command does once this spawn settles.
|
|
4
5
|
*
|
|
@@ -82,6 +83,18 @@ export interface SpawnCompletion {
|
|
|
82
83
|
/** The child's stderr — only under `stdio: 'capture'`, `''` when it wrote none. */
|
|
83
84
|
stderr?: string;
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
|
|
88
|
+
* every other platform, and on win32 for anything already carrying a path or
|
|
89
|
+
* extension. `env` is the CHILD's, because its `PATH` is what must be searched.
|
|
90
|
+
*
|
|
91
|
+
* Exported for the synchronous PROBES that cannot go through
|
|
92
|
+
* {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
|
|
93
|
+
* runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
|
|
94
|
+
* Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
|
|
95
|
+
* `--runtime deno` was refused as "not installed" on a host where `deno` runs.
|
|
96
|
+
*/
|
|
97
|
+
export declare function toSpawnable(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv): Win32Invocation;
|
|
85
98
|
/**
|
|
86
99
|
* Run `cmd args…` to completion and resolve with its exit code and signal.
|
|
87
100
|
*
|
package/lib/utils/spawn.js
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
//
|
|
61
61
|
// ## What is spawned still matters — the Windows rewrite
|
|
62
62
|
//
|
|
63
|
-
// This helper also carries the Windows command rewrite (`
|
|
63
|
+
// This helper also carries the Windows command rewrite (`toSpawnable`), which is a
|
|
64
64
|
// second reason to come through it, and the one that applies OUTSIDE the CLI too:
|
|
65
65
|
//
|
|
66
66
|
// • a real executable (`git`, `gh`, `node`, `gjs`, `flatpak`, `process.execPath`)
|
|
@@ -93,10 +93,15 @@ function resolveEnv(opts) {
|
|
|
93
93
|
/**
|
|
94
94
|
* Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
|
|
95
95
|
* every other platform, and on win32 for anything already carrying a path or
|
|
96
|
-
* extension.
|
|
97
|
-
*
|
|
96
|
+
* extension. `env` is the CHILD's, because its `PATH` is what must be searched.
|
|
97
|
+
*
|
|
98
|
+
* Exported for the synchronous PROBES that cannot go through
|
|
99
|
+
* {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
|
|
100
|
+
* runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
|
|
101
|
+
* Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
|
|
102
|
+
* `--runtime deno` was refused as "not installed" on a host where `deno` runs.
|
|
98
103
|
*/
|
|
99
|
-
function
|
|
104
|
+
export function toSpawnable(cmd, args, env) {
|
|
100
105
|
const rewritten = resolveWin32Command(cmd, args, {
|
|
101
106
|
platform: process.platform,
|
|
102
107
|
env: env ?? process.env,
|
|
@@ -132,7 +137,7 @@ export function spawnToCompletion(cmd, args, opts) {
|
|
|
132
137
|
// must not touch it — see `SpawnToCompletionOptions.shell`.
|
|
133
138
|
const invocation = opts.shell
|
|
134
139
|
? { cmd, args: [...args], windowsVerbatimArguments: false }
|
|
135
|
-
:
|
|
140
|
+
: toSpawnable(cmd, args, env);
|
|
136
141
|
if (mustBlock(opts)) {
|
|
137
142
|
// `'inherit'` is NOT forwarded through `@gjsify/child_process`'s GJS
|
|
138
143
|
// spawnSync path (it drives a private main context via
|
|
@@ -23,9 +23,23 @@ export declare function spawnSupervised(cmd: string, cmdArgs: string[], env?: No
|
|
|
23
23
|
* "this project").
|
|
24
24
|
*/
|
|
25
25
|
export declare function spawnBundleSupervised(runtime: ExampleRuntime, bundlePath: string, cwd: string, subject: string): Promise<ChildProcess>;
|
|
26
|
+
/** One path the loop watches — a directory recursively, or the directory's own entries only. */
|
|
27
|
+
export interface WatchTarget {
|
|
28
|
+
path: string;
|
|
29
|
+
recursive: boolean;
|
|
30
|
+
}
|
|
26
31
|
export interface WatchLoopOptions {
|
|
27
|
-
/** Directory watched recursively. */
|
|
32
|
+
/** Directory watched recursively — the label and the default when `watch` is not given. */
|
|
28
33
|
dir: string;
|
|
34
|
+
/**
|
|
35
|
+
* Several paths instead of `dir` alone. `gjsify webext dev` needs it: its
|
|
36
|
+
* sources sit in sibling top-level directories next to `node_modules`, and a
|
|
37
|
+
* recursive watch on the project root costs one `Gio.FileMonitor` per
|
|
38
|
+
* directory under GJS, `node_modules` included.
|
|
39
|
+
*/
|
|
40
|
+
watch?: readonly WatchTarget[];
|
|
41
|
+
/** An absolute changed path the loop must not rebuild for (build output, caches). */
|
|
42
|
+
ignore?: (changed: string) => boolean;
|
|
29
43
|
/** How `dir` is spelled in the loop's own log lines. Defaults to `dir`. */
|
|
30
44
|
dirLabel?: string;
|
|
31
45
|
/** Prefix for the loop's log lines: `[<label>] …`. */
|
package/lib/utils/watch-loop.js
CHANGED
|
@@ -125,7 +125,7 @@ export function isSelfWrite(dir, output, filename) {
|
|
|
125
125
|
* fix, and a dev loop that dies on a syntax error is a dev loop nobody uses.
|
|
126
126
|
*/
|
|
127
127
|
export async function runWatchLoop(opts) {
|
|
128
|
-
const { dir, label, prepare, build, spawnChild, output, debounceMs = 200 } = opts;
|
|
128
|
+
const { dir, label, prepare, build, spawnChild, output, ignore, debounceMs = 200 } = opts;
|
|
129
129
|
// Say so BEFORE the first build: `gjsify run`'s in-process dispatch acts on
|
|
130
130
|
// this the moment the handler resolves, and a first build that throws still
|
|
131
131
|
// leaves the loop watching.
|
|
@@ -164,13 +164,16 @@ export async function runWatchLoop(opts) {
|
|
|
164
164
|
};
|
|
165
165
|
await rebuild();
|
|
166
166
|
let debounce = null;
|
|
167
|
-
const
|
|
168
|
-
|
|
167
|
+
const watchers = (opts.watch ?? [{ path: dir, recursive: true }]).map((target) => watch(target.path, { recursive: target.recursive }, (_event, filename) => {
|
|
168
|
+
const name = typeof filename === 'string' ? filename : null;
|
|
169
|
+
if (isSelfWrite(target.path, output, name))
|
|
170
|
+
return;
|
|
171
|
+
if (ignore && name !== null && ignore(resolve(target.path, name)))
|
|
169
172
|
return;
|
|
170
173
|
if (debounce)
|
|
171
174
|
clearTimeout(debounce);
|
|
172
175
|
debounce = setTimeout(() => void rebuild(), debounceMs);
|
|
173
|
-
});
|
|
176
|
+
}));
|
|
174
177
|
// Under GJS nothing else keeps us here. An `fs.watch` handle is a Gio file
|
|
175
178
|
// monitor, not a Node handle count, and the supervised spawn's own
|
|
176
179
|
// `ensureMainLoop()` declines to arm anything: it is reached from a
|
|
@@ -198,7 +201,8 @@ export async function runWatchLoop(opts) {
|
|
|
198
201
|
if (debounce)
|
|
199
202
|
clearTimeout(debounce);
|
|
200
203
|
child?.kill();
|
|
201
|
-
watcher
|
|
204
|
+
for (const watcher of watchers)
|
|
205
|
+
watcher.close();
|
|
202
206
|
process.exit(0);
|
|
203
207
|
};
|
|
204
208
|
process.on('SIGINT', shutdown);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ZipEntry } from '../ship/zip.js';
|
|
2
|
+
import type { RasterizeInput } from '../ship/icons.js';
|
|
3
|
+
import type { ResolvedWebext } from './config.js';
|
|
4
|
+
import type { WebextTarget } from './targets.js';
|
|
5
|
+
export interface BundleRequest {
|
|
6
|
+
entry: string;
|
|
7
|
+
outfile: string;
|
|
8
|
+
format: 'esm' | 'iife';
|
|
9
|
+
define: Record<string, string>;
|
|
10
|
+
minify: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WebextBuildOptions {
|
|
13
|
+
config: ResolvedWebext;
|
|
14
|
+
targets: readonly WebextTarget[];
|
|
15
|
+
/** Absolute; the target folders are `<outDir>/<target>`. */
|
|
16
|
+
outDir: string;
|
|
17
|
+
mode: 'production' | 'development';
|
|
18
|
+
/** Effective defines: the config's, then the command line's. */
|
|
19
|
+
define: Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Overwrite files instead of starting from an empty folder. `dev` needs it:
|
|
22
|
+
* deleting the folder `web-ext run` watches makes Firefox unload the
|
|
23
|
+
* extension mid-rebuild (measured in beifahrer).
|
|
24
|
+
*/
|
|
25
|
+
inPlace: boolean;
|
|
26
|
+
zip: boolean;
|
|
27
|
+
/** Bundle one entry. Injectable so the assembly is testable without a bundler. */
|
|
28
|
+
bundle: (request: BundleRequest) => Promise<void>;
|
|
29
|
+
rasterize?: (input: RasterizeInput) => Promise<Map<number, Uint8Array>>;
|
|
30
|
+
log?: (line: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface WebextBuildResult {
|
|
33
|
+
/** target id → absolute folder. */
|
|
34
|
+
folders: Map<string, string>;
|
|
35
|
+
/** target id → absolute zip path (only with `zip`). */
|
|
36
|
+
zips: Map<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/** One `SOURCE_DATE_EPOCH`-aware timestamp for every entry, so a rebuild zips to the same bytes. */
|
|
39
|
+
export declare function zipTimestamp(env?: NodeJS.ProcessEnv): number;
|
|
40
|
+
/** Every file under `dir`, as archive entries relative to it. */
|
|
41
|
+
export declare function zipEntriesOf(dir: string): ZipEntry[];
|
|
42
|
+
export declare function buildWebext(opts: WebextBuildOptions): Promise<WebextBuildResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// `gjsify webext build|zip|dev` — assemble one folder per target (ADR 0077 § 4).
|
|
2
|
+
//
|
|
3
|
+
// Every bundle is built ONCE into a staging directory and copied into each
|
|
4
|
+
// target; only the manifest and the icon files differ between targets. That is
|
|
5
|
+
// beifahrer's measured arrangement, and it is also what makes a target cheap:
|
|
6
|
+
// adding `edge-mv3` to a project costs a copy, not a rebuild.
|
|
7
|
+
import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
import { buildZip } from '../ship/zip.js';
|
|
10
|
+
import { renderWebextIcons, webextIconPaths, writeWebextIcons } from './icons.js';
|
|
11
|
+
import { checkManifest, composeManifest, loadManifestTemplate } from './manifest.js';
|
|
12
|
+
import { planPage } from './pages.js';
|
|
13
|
+
/** The fixed ZIP timestamp when SOURCE_DATE_EPOCH is unset: 1980-01-01, the format's floor. */
|
|
14
|
+
const ZIP_EPOCH_FLOOR = 315532800;
|
|
15
|
+
/** One `SOURCE_DATE_EPOCH`-aware timestamp for every entry, so a rebuild zips to the same bytes. */
|
|
16
|
+
export function zipTimestamp(env = process.env) {
|
|
17
|
+
const epoch = Number(env.SOURCE_DATE_EPOCH);
|
|
18
|
+
return Number.isFinite(epoch) && epoch >= ZIP_EPOCH_FLOOR ? epoch : ZIP_EPOCH_FLOOR;
|
|
19
|
+
}
|
|
20
|
+
/** Every file under `dir`, as archive entries relative to it. */
|
|
21
|
+
export function zipEntriesOf(dir) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const walk = (current) => {
|
|
24
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
25
|
+
const full = join(current, entry.name);
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
walk(full);
|
|
28
|
+
else
|
|
29
|
+
out.push({ path: relative(dir, full).split(/[\\/]/).join('/'), mode: 0o644, data: readFileSync(full) });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
walk(dir);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export async function buildWebext(opts) {
|
|
36
|
+
const { config, targets, outDir, define } = opts;
|
|
37
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
38
|
+
const minify = opts.mode === 'production' && config.minify;
|
|
39
|
+
const stage = join(outDir, '.stage');
|
|
40
|
+
rmSync(stage, { recursive: true, force: true });
|
|
41
|
+
mkdirSync(stage, { recursive: true });
|
|
42
|
+
try {
|
|
43
|
+
for (const [name, entry] of config.scripts) {
|
|
44
|
+
await opts.bundle({ entry, outfile: join(stage, `${name}.js`), format: 'iife', define, minify });
|
|
45
|
+
}
|
|
46
|
+
const written = new Map();
|
|
47
|
+
for (const [name, htmlPath] of config.pages) {
|
|
48
|
+
const plan = planPage(name, htmlPath, readFileSync(htmlPath, 'utf8'));
|
|
49
|
+
for (const script of plan.scripts) {
|
|
50
|
+
await opts.bundle({
|
|
51
|
+
entry: script.entry,
|
|
52
|
+
outfile: join(stage, script.output),
|
|
53
|
+
format: script.format,
|
|
54
|
+
define,
|
|
55
|
+
minify,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
for (const sheet of plan.stylesheets) {
|
|
59
|
+
const previous = written.get(sheet.output);
|
|
60
|
+
if (previous !== undefined && previous !== sheet.source) {
|
|
61
|
+
throw new Error(`gjsify webext: pages link ${previous} and ${sheet.source}, which would both be written as ` +
|
|
62
|
+
`${sheet.output}. Rename one.`);
|
|
63
|
+
}
|
|
64
|
+
written.set(sheet.output, sheet.source);
|
|
65
|
+
cpSync(sheet.source, join(stage, sheet.output));
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(join(stage, `${name}.html`), plan.html);
|
|
68
|
+
}
|
|
69
|
+
const icons = config.icons;
|
|
70
|
+
const rendered = icons ? await renderWebextIcons(icons, targets, opts.rasterize) : null;
|
|
71
|
+
const template = await loadManifestTemplate(config.manifest);
|
|
72
|
+
const result = { folders: new Map(), zips: new Map() };
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
const dir = join(outDir, target.id);
|
|
75
|
+
if (!opts.inPlace)
|
|
76
|
+
rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
mkdirSync(dir, { recursive: true });
|
|
78
|
+
// Public files first, so a built file of the same name wins.
|
|
79
|
+
if (config.publicDir)
|
|
80
|
+
cpSync(config.publicDir, dir, { recursive: true });
|
|
81
|
+
if (config.localesDir)
|
|
82
|
+
cpSync(config.localesDir, join(dir, '_locales'), { recursive: true });
|
|
83
|
+
cpSync(stage, dir, { recursive: true });
|
|
84
|
+
if (icons && rendered)
|
|
85
|
+
writeWebextIcons(rendered, icons, target, dir);
|
|
86
|
+
const ctx = {
|
|
87
|
+
target: target.id,
|
|
88
|
+
browser: target.browser,
|
|
89
|
+
manifestVersion: target.manifestVersion,
|
|
90
|
+
mode: opts.mode,
|
|
91
|
+
version: config.version,
|
|
92
|
+
define,
|
|
93
|
+
icons: (name) => {
|
|
94
|
+
if (!icons) {
|
|
95
|
+
throw new Error(`gjsify webext: the manifest asks for icon "${name}" and \`gjsify.webext.icons\` is not declared.`);
|
|
96
|
+
}
|
|
97
|
+
return webextIconPaths(icons, name, target);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const manifest = await composeManifest(template, ctx);
|
|
101
|
+
writeFileSync(join(dir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
102
|
+
const problems = checkManifest(manifest, target, dir);
|
|
103
|
+
if (problems.length > 0) {
|
|
104
|
+
throw new Error(`gjsify webext: ${target.id} would not load:\n - ${problems.join('\n - ')}`);
|
|
105
|
+
}
|
|
106
|
+
result.folders.set(target.id, dir);
|
|
107
|
+
log(`built ${relative(process.cwd(), dir) || dir}`);
|
|
108
|
+
if (opts.zip) {
|
|
109
|
+
const file = join(outDir, `${config.name}-${config.version}-${target.id}.zip`);
|
|
110
|
+
writeFileSync(file, buildZip(zipEntriesOf(dir), zipTimestamp()));
|
|
111
|
+
result.zips.set(target.id, file);
|
|
112
|
+
log(`zipped ${relative(process.cwd(), file) || file} (${Math.round(statSync(file).size / 1024)} KiB)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
rmSync(stage, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type WebextTarget } from './targets.js';
|
|
2
|
+
/** A source SVG per icon name: one file, or one per size ceiling (`{ "default": …, "32": … }`). */
|
|
3
|
+
export type WebextIconSource = string | Record<string, string>;
|
|
4
|
+
export interface WebextIconsConfig {
|
|
5
|
+
sizes?: number[];
|
|
6
|
+
sources: Record<string, WebextIconSource>;
|
|
7
|
+
/** Targets that receive the SVG files themselves instead of rendered PNGs. */
|
|
8
|
+
svg?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The block as a project writes it. */
|
|
11
|
+
export interface WebextConfig {
|
|
12
|
+
name?: string;
|
|
13
|
+
targets?: string[];
|
|
14
|
+
manifest?: string;
|
|
15
|
+
scripts?: Record<string, string>;
|
|
16
|
+
pages?: Record<string, string>;
|
|
17
|
+
icons?: WebextIconsConfig;
|
|
18
|
+
public?: string;
|
|
19
|
+
locales?: string;
|
|
20
|
+
outDir?: string;
|
|
21
|
+
define?: Record<string, string>;
|
|
22
|
+
watch?: string[];
|
|
23
|
+
minify?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolvedWebextIcons {
|
|
26
|
+
sizes: number[];
|
|
27
|
+
/** name → absolute SVG path per size. */
|
|
28
|
+
sources: Map<string, Map<number, string>>;
|
|
29
|
+
svgTargets: Set<string>;
|
|
30
|
+
}
|
|
31
|
+
/** The block after validation, with every path absolute. */
|
|
32
|
+
export interface ResolvedWebext {
|
|
33
|
+
root: string;
|
|
34
|
+
name: string;
|
|
35
|
+
version: string;
|
|
36
|
+
targets: WebextTarget[];
|
|
37
|
+
manifest: string;
|
|
38
|
+
scripts: Map<string, string>;
|
|
39
|
+
pages: Map<string, string>;
|
|
40
|
+
icons: ResolvedWebextIcons | null;
|
|
41
|
+
publicDir: string | null;
|
|
42
|
+
localesDir: string | null;
|
|
43
|
+
outDir: string;
|
|
44
|
+
define: Record<string, string>;
|
|
45
|
+
watch: string[];
|
|
46
|
+
minify: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare const DEFAULT_ICON_SIZES: number[];
|
|
49
|
+
interface PackageShape {
|
|
50
|
+
name?: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
gjsify?: {
|
|
53
|
+
webext?: unknown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The zip base name a package gets when it declares none: its npm name without the scope. */
|
|
57
|
+
export declare function defaultArtifactName(packageName: string | undefined): string;
|
|
58
|
+
/**
|
|
59
|
+
* Pick the SVG an icon uses at `size`: the smallest numeric ceiling that is
|
|
60
|
+
* `>= size`, else `default`. Returns undefined when neither covers the size.
|
|
61
|
+
*/
|
|
62
|
+
export declare function iconSourceForSize(source: WebextIconSource, size: number): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Validate `gjsify.webext` and resolve it against the package root.
|
|
65
|
+
*
|
|
66
|
+
* `exists` is injectable so the specs can describe a project without writing
|
|
67
|
+
* one to disk.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveWebextConfig(pkg: PackageShape, root: string, exists?: (path: string, kind: 'file' | 'dir') => boolean): ResolvedWebext;
|
|
70
|
+
/** Narrow the configured targets to the ones a command was asked for. */
|
|
71
|
+
export declare function selectTargets(config: ResolvedWebext, requested: readonly string[] | undefined): WebextTarget[];
|
|
72
|
+
export {};
|