@gjsify/cli 0.48.0 → 0.50.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.
@@ -3,7 +3,7 @@
3
3
  // as `utils/copy-targets.ts`, and for the same reason: a stager that discovers
4
4
  // its own inputs can only be tested by building a real project.
5
5
  import { renderDebianChangelog } from './changelog.js';
6
- import { gzipDeterministic } from './gzip.js';
6
+ import { gzipDeterministic, POLICY_MAX_COMPRESSION } from './gzip.js';
7
7
  import { renderMimePackage } from './mime.js';
8
8
  import { SHARE } from './share-dirs.js';
9
9
  import { basename, extname, posix } from 'node:path';
@@ -174,12 +174,19 @@ export async function planOverlay(settings, format, inputs) {
174
174
  // gzip performed on the packing host would put a second compressor's output in
175
175
  // an artifact whose every other byte came from the first. `gzipDeterministic`
176
176
  // zeroes the header stamp for the usual reason (utils/ship/gzip.ts).
177
+ //
178
+ // It is also the one gzip member in the artifact whose bytes cannot vary with
179
+ // the packing host, for the same reason: the two hosts' zlibs disagree (that
180
+ // file's header measurement), and this member is compressed once, here.
177
181
  if (format.changelogDest !== undefined) {
178
182
  const changelog = renderDebianChangelog(settings, inputs.changelogText, inputs.mtime);
179
183
  files.push({
180
184
  path: format.changelogDest(settings.binaryName),
181
185
  mode: 0o644,
182
- source: { kind: 'bytes', data: await gzipDeterministic(new TextEncoder().encode(changelog)) },
186
+ source: {
187
+ kind: 'bytes',
188
+ data: await gzipDeterministic(new TextEncoder().encode(changelog), POLICY_MAX_COMPRESSION),
189
+ },
183
190
  });
184
191
  }
185
192
  return files;
@@ -233,19 +240,27 @@ function renderDebianCopyright(settings, licenseText) {
233
240
  * Icons land in the hicolor theme, named after the app id — that name is what
234
241
  * the desktop entry's `Icon=` and the MetaInfo `<id>` both point at, so a file
235
242
  * keeping its source basename installs an icon nothing ever looks up.
243
+ *
244
+ * A SYMBOLIC icon keeps the `-symbolic` half of that name, and for the same
245
+ * reason rather than as an exception: the name GTK resolves a symbolic icon by
246
+ * is `<app id>-symbolic`, so renaming the file to the bare id installs an icon
247
+ * nothing ever looks up — one sentence, applied to both lookups.
236
248
  */
237
249
  function planIcons(settings) {
238
250
  const out = [];
239
251
  const seen = new Map();
240
252
  for (const icon of settings.iconFiles) {
241
253
  const ext = extname(icon);
242
- const dir = iconSizeDir(icon);
243
- const path = `${SHARE.icons}/${dir}/apps/${settings.appId}${ext}`;
254
+ const dir = iconThemeDir(icon);
255
+ const name = isSymbolicIcon(icon) ? `${settings.appId}-symbolic` : settings.appId;
256
+ const path = `${SHARE.icons}/${dir}/apps/${name}${ext}`;
244
257
  const previous = seen.get(path);
245
258
  if (previous !== undefined) {
246
259
  throw new Error(`gjsify ship: ${icon} and ${previous} both install as ${path}. ` +
247
- 'Icon size is read from the path (`.../128x128/...`) or the filename (`icon-128.png`); ' +
248
- 'give them distinguishable sizes or point `gjsify.ship.icon` at a single file.');
260
+ 'An icon is keyed on its context and its size: `symbolic` is read from a `symbolic/` ' +
261
+ 'path component or a `-symbolic` filename, the size from the path (`.../128x128/...`) ' +
262
+ 'or the filename (`icon-128.png`). Give them distinguishable sizes, or point ' +
263
+ '`gjsify.ship.icon` at a single file.');
249
264
  }
250
265
  seen.set(path, icon);
251
266
  out.push({ path, mode: 0o644, source: { kind: 'file', path: icon } });
@@ -253,11 +268,47 @@ function planIcons(settings) {
253
268
  return out;
254
269
  }
255
270
  /**
256
- * The hicolor subdirectory for an icon: `scalable` for SVG, otherwise the
257
- * pixel size read from a `<n>x<n>` path component or a trailing number in the
258
- * filename.
271
+ * Whether an icon belongs in the theme's SYMBOLIC context rather than at a size.
272
+ *
273
+ * `symbolic` is a directory of its own in a hicolor theme and NOT a size value.
274
+ * Measured in `refs/adwaita-icon-theme/index.theme`, which lists `symbolic/apps`
275
+ * in `Directories=` beside `scalable/apps` and `16x16/apps` and gives it its own
276
+ * section — `Context=Applications`, `Size=16`, `MinSize=8`, `MaxSize=512`,
277
+ * `Type=Scalable` — against the scalable row's `Size=128`. The freedesktop icon
278
+ * theme specification has no notion of `symbolic` at all: a theme expresses it by
279
+ * giving the directory a section, which is exactly why reading the EXTENSION
280
+ * cannot see it, and why this question has to be asked before the size one.
281
+ *
282
+ * TWO SIGNALS, the same pair `iconThemeDir` already reads for a size: the
283
+ * directory an author put the file in, and the name they gave it. GTK resolves a
284
+ * symbolic icon by the `-symbolic` name suffix, so an author who wrote the name
285
+ * has said as much as one who wrote the path.
286
+ *
287
+ * SVG ONLY, because the symbolic directory is declared `Type=Scalable`: the
288
+ * raster form GTK's own `gtk-encode-symbolic-svg` produces
289
+ * (`<name>-symbolic.symbolic.png`) is installed at a SIZE instead. So a PNG keeps
290
+ * answering the size question, and one that cannot answer it stays refused rather
291
+ * than quietly becoming a scalable icon that does not scale.
292
+ */
293
+ export function isSymbolicIcon(iconPath) {
294
+ if (extname(iconPath).toLowerCase() !== '.svg')
295
+ return false;
296
+ if (/(?:^|[\\/])symbolic[\\/]/.test(iconPath))
297
+ return true;
298
+ return basename(iconPath, extname(iconPath)).endsWith('-symbolic');
299
+ }
300
+ /**
301
+ * The hicolor subdirectory for an icon: the `symbolic` CONTEXT, else `scalable`
302
+ * for an SVG, else the pixel size read from a `<n>x<n>` path component or a
303
+ * trailing number in the filename.
304
+ *
305
+ * Named for the theme DIRECTORY rather than a size from the moment the first of
306
+ * those answers stopped being one: `iconSizeDir` returning `symbolic` would be a
307
+ * name every caller has to read past.
259
308
  */
260
- export function iconSizeDir(iconPath) {
309
+ export function iconThemeDir(iconPath) {
310
+ if (isSymbolicIcon(iconPath))
311
+ return 'symbolic';
261
312
  if (extname(iconPath).toLowerCase() === '.svg')
262
313
  return 'scalable';
263
314
  const square = /(?:^|[\\/])(\d{1,4})x\1(?:[\\/]|$)/.exec(iconPath);
@@ -1,3 +1,18 @@
1
+ export interface TokenizeOptions {
2
+ /**
3
+ * Accept an unquoted `*`/`?` as an ordinary token character.
4
+ *
5
+ * Off by default, and the default is about EXECUTION: a caller that RUNS
6
+ * the tokens must not pass `*.tsbuildinfo` through unexpanded, because the
7
+ * shell it replaces would have expanded it first. A caller that only READS
8
+ * the targets — `utils/package-inputs.ts`, asking which paths a package
9
+ * declares it produces — wants the pattern itself, and `gjsify clear`
10
+ * expands the same two characters in its own last segment anyway
11
+ * (`utils/clear-targets.ts`). Nothing else moves: every operator,
12
+ * substitution, brace and tilde still returns `null`.
13
+ */
14
+ allowGlobs?: boolean;
15
+ }
1
16
  /**
2
17
  * Tokenize `cmd`, or return `null` if the string contains anything the shell
3
18
  * would treat specially — operators (`&& | ; < > &`), substitutions (`$(...)` /
@@ -7,10 +22,10 @@
7
22
  * single literal token, exactly as the shell would hand it to `gjsify` (which
8
23
  * does its own glob expansion).
9
24
  */
10
- export declare function tokenizeSimpleCommand(cmd: string): string[] | null;
25
+ export declare function tokenizeSimpleCommand(cmd: string, options?: TokenizeOptions): string[] | null;
11
26
  /**
12
27
  * The argv of a script that is a single `gjsify <subcommand> …` command — i.e.
13
28
  * the tokens after `gjsify` — or `null` when it is anything else. `null` is the
14
29
  * signal to hand the literal to a shell.
15
30
  */
16
- export declare function gjsifyCommandArgv(literal: string): string[] | null;
31
+ export declare function gjsifyCommandArgv(literal: string, options?: TokenizeOptions): string[] | null;
@@ -6,6 +6,10 @@
6
6
  // deliberately conservative in ONE direction — a `null` costs the caller its
7
7
  // fast path, a wrong token list would run a different command than the script
8
8
  // says.
9
+ /** Characters that make a string something only a shell may execute. */
10
+ const SHELL_SPECIAL = '|&;<>`$()\\\n\r*?{}[]~#!';
11
+ /** The same set minus `*` and `?` — see {@link TokenizeOptions.allowGlobs}. */
12
+ const SHELL_SPECIAL_ALLOWING_GLOBS = SHELL_SPECIAL.replace('*', '').replace('?', '');
9
13
  /**
10
14
  * Tokenize `cmd`, or return `null` if the string contains anything the shell
11
15
  * would treat specially — operators (`&& | ; < > &`), substitutions (`$(...)` /
@@ -15,7 +19,8 @@
15
19
  * single literal token, exactly as the shell would hand it to `gjsify` (which
16
20
  * does its own glob expansion).
17
21
  */
18
- export function tokenizeSimpleCommand(cmd) {
22
+ export function tokenizeSimpleCommand(cmd, options = {}) {
23
+ const special = options.allowGlobs === true ? SHELL_SPECIAL_ALLOWING_GLOBS : SHELL_SPECIAL;
19
24
  const tokens = [];
20
25
  let cur = '';
21
26
  let has = false;
@@ -53,7 +58,7 @@ export function tokenizeSimpleCommand(cmd) {
53
58
  }
54
59
  continue;
55
60
  }
56
- if ('|&;<>`$()\\\n\r*?{}[]~#!'.includes(c))
61
+ if (special.includes(c))
57
62
  return null;
58
63
  cur += c;
59
64
  has = true;
@@ -69,8 +74,8 @@ export function tokenizeSimpleCommand(cmd) {
69
74
  * the tokens after `gjsify` — or `null` when it is anything else. `null` is the
70
75
  * signal to hand the literal to a shell.
71
76
  */
72
- export function gjsifyCommandArgv(literal) {
73
- const tokens = tokenizeSimpleCommand(literal);
77
+ export function gjsifyCommandArgv(literal, options = {}) {
78
+ const tokens = tokenizeSimpleCommand(literal, options);
74
79
  if (!tokens || tokens.length < 2 || tokens[0] !== 'gjsify')
75
80
  return null;
76
81
  return tokens.slice(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,21 +142,21 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/buffer": "^0.48.0",
146
- "@gjsify/create-app": "^0.48.0",
147
- "@gjsify/node-globals": "^0.48.0",
148
- "@gjsify/node-polyfills": "^0.48.0",
149
- "@gjsify/npm-registry": "^0.48.0",
150
- "@gjsify/resolve-npm": "^0.48.0",
151
- "@gjsify/rolldown-plugin-gjsify": "^0.48.0",
152
- "@gjsify/vite-plugin-blueprint": "^0.48.0",
153
- "@gjsify/rolldown-plugin-pnp": "^0.48.0",
154
- "@gjsify/semver": "^0.48.0",
155
- "@gjsify/tar": "^0.48.0",
156
- "@gjsify/tsc": "^0.48.0",
157
- "@gjsify/utils": "^0.48.0",
158
- "@gjsify/web-polyfills": "^0.48.0",
159
- "@gjsify/workspace": "^0.48.0",
145
+ "@gjsify/buffer": "^0.50.0",
146
+ "@gjsify/create-app": "^0.50.0",
147
+ "@gjsify/node-globals": "^0.50.0",
148
+ "@gjsify/node-polyfills": "^0.50.0",
149
+ "@gjsify/npm-registry": "^0.50.0",
150
+ "@gjsify/resolve-npm": "^0.50.0",
151
+ "@gjsify/rolldown-plugin-gjsify": "^0.50.0",
152
+ "@gjsify/vite-plugin-blueprint": "^0.50.0",
153
+ "@gjsify/rolldown-plugin-pnp": "^0.50.0",
154
+ "@gjsify/semver": "^0.50.0",
155
+ "@gjsify/tar": "^0.50.0",
156
+ "@gjsify/tsc": "^0.50.0",
157
+ "@gjsify/utils": "^0.50.0",
158
+ "@gjsify/web-polyfills": "^0.50.0",
159
+ "@gjsify/workspace": "^0.50.0",
160
160
  "acorn": "^8.17.0",
161
161
  "cosmiconfig": "^9.0.2",
162
162
  "get-tsconfig": "^4.14.0",
@@ -165,13 +165,13 @@
165
165
  "yargs": "^18.0.0"
166
166
  },
167
167
  "devDependencies": {
168
- "@gjsify/unit": "^0.48.0",
168
+ "@gjsify/unit": "^0.50.0",
169
169
  "@types/yargs": "^17.0.35",
170
170
  "typescript": "^6.0.3"
171
171
  },
172
172
  "peerDependencies": {
173
- "@gjsify/oxfmt-native": "^0.48.0",
174
- "@gjsify/rolldown-native": "^0.48.0"
173
+ "@gjsify/oxfmt-native": "^0.50.0",
174
+ "@gjsify/rolldown-native": "^0.50.0"
175
175
  },
176
176
  "peerDependenciesMeta": {
177
177
  "@gjsify/oxfmt-native": {