@gjsify/cli 0.64.17-next.292 → 0.64.18
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 +190 -175
- package/lib/actions/build.d.ts +2 -0
- package/lib/actions/build.js +19 -3
- package/lib/cli-app.js +3 -1
- package/lib/commands/android-icons.d.ts +9 -0
- package/lib/commands/android-icons.js +164 -0
- package/lib/commands/credits.d.ts +9 -0
- package/lib/commands/credits.js +73 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/types/config-data.d.ts +26 -0
- package/lib/utils/credits.d.ts +53 -0
- package/lib/utils/credits.js +275 -0
- package/lib/utils/ship/android-icons.d.ts +109 -0
- package/lib/utils/ship/android-icons.js +215 -0
- package/package.json +21 -22
package/lib/actions/build.d.ts
CHANGED
|
@@ -110,6 +110,8 @@ export declare class BuildAction {
|
|
|
110
110
|
* its typelibs can still be fixed by installing a package; an APK carries its
|
|
111
111
|
* whole GI stack or does without it.
|
|
112
112
|
*/
|
|
113
|
+
/** The app's `dependencies` + `devDependencies` names; `[]` without a readable manifest. */
|
|
114
|
+
private appDependencyNames;
|
|
113
115
|
private assertNativescriptGiSelected;
|
|
114
116
|
/**
|
|
115
117
|
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
package/lib/actions/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
|
-
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, scanGiNamespaces, planGiSelection, WorkerEntries, bundleSuffix, } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
|
+
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, scanGiNamespaces, planGiSelection, planGiAddons, WorkerEntries, bundleSuffix, } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
@@ -454,7 +454,17 @@ export class BuildAction {
|
|
|
454
454
|
* its typelibs can still be fixed by installing a package; an APK carries its
|
|
455
455
|
* whole GI stack or does without it.
|
|
456
456
|
*/
|
|
457
|
-
|
|
457
|
+
/** The app's `dependencies` + `devDependencies` names; `[]` without a readable manifest. */
|
|
458
|
+
async appDependencyNames() {
|
|
459
|
+
try {
|
|
460
|
+
const manifest = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf-8'));
|
|
461
|
+
return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})];
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async assertNativescriptGiSelected(result, outfile, outdir) {
|
|
458
468
|
const label = outfile ?? outdir ?? 'the nativescript bundle';
|
|
459
469
|
const scanned = new Set();
|
|
460
470
|
for (const item of result.output ?? []) {
|
|
@@ -478,6 +488,12 @@ export class BuildAction {
|
|
|
478
488
|
// the namespaces but not which file they were read from.
|
|
479
489
|
throw new Error(`gjsify build: ${label}: ${error.message}`);
|
|
480
490
|
}
|
|
491
|
+
try {
|
|
492
|
+
planGiAddons({ selected: selection.selected, installed: await this.appDependencyNames() });
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
throw new Error(`gjsify build: ${label}: ${error.message}`);
|
|
496
|
+
}
|
|
481
497
|
for (const namespace of selection.unscanned) {
|
|
482
498
|
console.warn(`[gjsify] --app nativescript: nativescript.giNamespaces declares ${namespace}, which ` +
|
|
483
499
|
`${label} does not appear to import. The APK carries it anyway — drop it if nothing ` +
|
|
@@ -916,7 +932,7 @@ export class BuildAction {
|
|
|
916
932
|
// package-build time (ADR 0117), so there is no host to fall back on: the
|
|
917
933
|
// alternative to this gate is an APK that installs and throws at the import.
|
|
918
934
|
if (app === 'nativescript') {
|
|
919
|
-
this.assertNativescriptGiSelected(writeResult, outfile, outdir);
|
|
935
|
+
await this.assertNativescriptGiSelected(writeResult, outfile, outdir);
|
|
920
936
|
}
|
|
921
937
|
// Browser HTML entry: emit the processed `index.html` beside the bundle
|
|
922
938
|
// (engine-portable post-step — the native rolldown engine has no
|
package/lib/cli-app.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import yargs from 'yargs';
|
|
19
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
20
20
|
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
-
import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, releaseNotesCommand as releaseNotes, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, releaseNotesCommand as releaseNotes, androidIconsCommand as androidIcons, creditsCommand as credits, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
|
|
22
22
|
import { APP_NAME } from './constants.js';
|
|
23
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -118,6 +118,8 @@ export async function runCli(argv) {
|
|
|
118
118
|
.command(publish.command, publish.description, publish.builder, publish.handler)
|
|
119
119
|
.command(whoami.command, whoami.description, whoami.builder, whoami.handler)
|
|
120
120
|
.command(releaseNotes.command, releaseNotes.description, releaseNotes.builder, releaseNotes.handler)
|
|
121
|
+
.command(androidIcons.command, androidIcons.description, androidIcons.builder, androidIcons.handler)
|
|
122
|
+
.command(credits.command, credits.description, credits.builder, credits.handler)
|
|
121
123
|
.command(login.command, login.description, login.builder, login.handler)
|
|
122
124
|
.command(logout.command, logout.description, logout.builder, logout.handler)
|
|
123
125
|
.command(trust.command, trust.description, trust.builder, trust.handler)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface AndroidIconsOptions {
|
|
3
|
+
resources?: string;
|
|
4
|
+
icon?: string;
|
|
5
|
+
background?: string;
|
|
6
|
+
check?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const androidIconsCommand: Command<unknown, AndroidIconsOptions>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// `gjsify android-icons [--resources <dir>] [--icon <svg>] [--background <colour>] [--check]`
|
|
2
|
+
//
|
|
3
|
+
// Writes the Android launcher icon and splash logo of a NativeScript app from the
|
|
4
|
+
// ONE app icon the GNOME way already declares, so no app hand-maintains rasters.
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { Config } from '../config.js';
|
|
9
|
+
import { ANDROID_ICONS_STAMP, buildStamp, checkStamp, hashInputs, parseMetainfoBranding, pickIconSources, planAndroidIcons, resolveBackground, resolveResourcesName, symbolicSibling, wrapperSvg, } from '../utils/ship/android-icons.js';
|
|
10
|
+
import { rasterizeSvg } from '../utils/ship/icons.js';
|
|
11
|
+
const RES_PREFIX = 'Android/src/main/res';
|
|
12
|
+
function walk(dir, out = []) {
|
|
13
|
+
if (!existsSync(dir))
|
|
14
|
+
return out;
|
|
15
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
const full = join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory())
|
|
18
|
+
walk(full, out);
|
|
19
|
+
else
|
|
20
|
+
out.push(full);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function discoverIcon(projectDir, flag) {
|
|
25
|
+
if (flag !== undefined) {
|
|
26
|
+
const icon = resolve(projectDir, flag);
|
|
27
|
+
if (!existsSync(icon))
|
|
28
|
+
throw new Error(`the icon ${flag} does not exist`);
|
|
29
|
+
const sibling = symbolicSibling(icon);
|
|
30
|
+
return { icon, symbolic: existsSync(sibling) ? sibling : null };
|
|
31
|
+
}
|
|
32
|
+
// The app's own `data/` first, then the sibling GNOME package of a monorepo.
|
|
33
|
+
const roots = [join(projectDir, 'data', 'icons'), ...siblingIconRoots(projectDir)];
|
|
34
|
+
for (const root of roots) {
|
|
35
|
+
const found = pickIconSources(walk(root)
|
|
36
|
+
.filter((f) => f.toLowerCase().endsWith('.svg'))
|
|
37
|
+
.sort());
|
|
38
|
+
if (found.icon !== null)
|
|
39
|
+
return { icon: found.icon, symbolic: found.symbolic };
|
|
40
|
+
}
|
|
41
|
+
throw new Error('no app icon found; pass --icon <svg> or set `gjsify.androidIcons.icon`');
|
|
42
|
+
}
|
|
43
|
+
function siblingIconRoots(projectDir) {
|
|
44
|
+
const parent = dirname(projectDir);
|
|
45
|
+
if (!existsSync(parent))
|
|
46
|
+
return [];
|
|
47
|
+
return readdirSync(parent)
|
|
48
|
+
.sort()
|
|
49
|
+
.map((name) => join(parent, name, 'data', 'icons'))
|
|
50
|
+
.filter((dir) => dir !== join(projectDir, 'data', 'icons') && existsSync(dir) && statSync(dir).isDirectory());
|
|
51
|
+
}
|
|
52
|
+
function findMetainfoColor(iconPath, projectDir) {
|
|
53
|
+
const roots = new Set([join(projectDir, 'data')]);
|
|
54
|
+
// `<pkg>/data/icons/hicolor/scalable/apps/<id>.svg` → `<pkg>/data`
|
|
55
|
+
roots.add(resolve(dirname(iconPath), '..', '..', '..', '..'));
|
|
56
|
+
for (const root of roots) {
|
|
57
|
+
const files = walk(join(root, 'metainfo'))
|
|
58
|
+
.filter((f) => /\.metainfo\.xml(\.in)?$/.test(f))
|
|
59
|
+
.sort();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const color = parseMetainfoBranding(readFileSync(file, 'utf8'));
|
|
62
|
+
if (color !== null)
|
|
63
|
+
return color;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
export const androidIconsCommand = {
|
|
69
|
+
command: 'android-icons',
|
|
70
|
+
description: 'Generate the Android launcher icon and splash logo of a NativeScript app from its one app icon.',
|
|
71
|
+
builder: (yargs) => yargs
|
|
72
|
+
.option('resources', {
|
|
73
|
+
description: 'The App_Resources directory. Default: `gjsify.androidIcons.resources`, else `appResourcesPath` of nativescript.config.ts, else App_Resources.',
|
|
74
|
+
type: 'string',
|
|
75
|
+
})
|
|
76
|
+
.option('icon', {
|
|
77
|
+
description: 'The scalable app icon SVG. Default: `gjsify.androidIcons.icon`, else the hicolor icon under data/icons (here or in a sibling package).',
|
|
78
|
+
type: 'string',
|
|
79
|
+
})
|
|
80
|
+
.option('background', {
|
|
81
|
+
description: 'Adaptive-icon background colour. Default: `gjsify.androidIcons.background`, else the metainfo light `<branding>` colour, else white.',
|
|
82
|
+
type: 'string',
|
|
83
|
+
})
|
|
84
|
+
.option('check', {
|
|
85
|
+
description: 'Write nothing; exit 1 when the generated files are missing or the inputs changed.',
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
default: false,
|
|
88
|
+
}),
|
|
89
|
+
handler: async (args) => {
|
|
90
|
+
const projectDir = process.cwd();
|
|
91
|
+
try {
|
|
92
|
+
const config = (await new Config().forCommand(projectDir)).androidIcons ?? {};
|
|
93
|
+
const nsConfigPath = ['nativescript.config.ts', 'nativescript.config.js']
|
|
94
|
+
.map((name) => join(projectDir, name))
|
|
95
|
+
.find((file) => existsSync(file));
|
|
96
|
+
const resourcesName = resolveResourcesName({
|
|
97
|
+
flag: args.resources,
|
|
98
|
+
configured: config.resources,
|
|
99
|
+
nativescriptConfig: nsConfigPath === undefined ? null : readFileSync(nsConfigPath, 'utf8'),
|
|
100
|
+
});
|
|
101
|
+
const resources = resolve(projectDir, resourcesName);
|
|
102
|
+
const { icon, symbolic } = discoverIcon(projectDir, args.icon ?? config.icon);
|
|
103
|
+
const background = resolveBackground({
|
|
104
|
+
flag: args.background,
|
|
105
|
+
configured: config.background,
|
|
106
|
+
fromMetainfo: findMetainfoColor(icon, projectDir),
|
|
107
|
+
});
|
|
108
|
+
const iconBytes = new Uint8Array(readFileSync(icon));
|
|
109
|
+
const symbolicBytes = symbolic === null ? null : new Uint8Array(readFileSync(symbolic));
|
|
110
|
+
const inputs = hashInputs({ icon: iconBytes, symbolic: symbolicBytes, background });
|
|
111
|
+
const plan = planAndroidIcons({ background, monochrome: symbolicBytes !== null });
|
|
112
|
+
const stampPath = join(resources, ANDROID_ICONS_STAMP);
|
|
113
|
+
if (args.check === true) {
|
|
114
|
+
const verdict = checkStamp(existsSync(stampPath) ? readFileSync(stampPath, 'utf8') : null, inputs, (rel) => existsSync(join(resources, ...rel.split('/'))));
|
|
115
|
+
if (!verdict.current) {
|
|
116
|
+
process.stderr.write(`gjsify android-icons: ${resourcesName} ${verdict.reason}; run \`gjsify android-icons\` and commit it\n`);
|
|
117
|
+
return process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const res = join(resources, ...RES_PREFIX.split('/'));
|
|
122
|
+
const tmp = mkdtempSync(join(tmpdir(), 'gjsify-android-icons-'));
|
|
123
|
+
try {
|
|
124
|
+
const sources = new Map([['plain', icon]]);
|
|
125
|
+
for (const kind of ['round', 'foreground']) {
|
|
126
|
+
const file = join(tmp, `${kind}.svg`);
|
|
127
|
+
writeFileSync(file, wrapperSvg(kind, iconBytes, background));
|
|
128
|
+
sources.set(kind, file);
|
|
129
|
+
}
|
|
130
|
+
if (symbolicBytes !== null) {
|
|
131
|
+
const file = join(tmp, 'monochrome.svg');
|
|
132
|
+
writeFileSync(file, wrapperSvg('monochrome', symbolicBytes, background));
|
|
133
|
+
sources.set('monochrome', file);
|
|
134
|
+
}
|
|
135
|
+
for (const [kind, svg] of sources) {
|
|
136
|
+
const wanted = plan.png.filter((p) => kind === 'plain' ? p.kind === 'legacy' || p.kind === 'splash' : p.kind === kind);
|
|
137
|
+
const sizes = [...new Set(wanted.map((p) => p.size))];
|
|
138
|
+
const rendered = await rasterizeSvg({ svg, sizes, command: 'gjsify android-icons' });
|
|
139
|
+
for (const spec of wanted) {
|
|
140
|
+
const target = join(res, spec.path);
|
|
141
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
142
|
+
writeFileSync(target, rendered.get(spec.size));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
for (const spec of plan.text) {
|
|
150
|
+
const target = join(res, spec.path);
|
|
151
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
152
|
+
writeFileSync(target, spec.content);
|
|
153
|
+
}
|
|
154
|
+
// The template's vector would be dead weight showing the NativeScript "N".
|
|
155
|
+
rmSync(join(res, 'drawable', 'ic_launcher_foreground.xml'), { force: true });
|
|
156
|
+
writeFileSync(stampPath, `${JSON.stringify(buildStamp(inputs, plan, RES_PREFIX), null, 2)}\n`);
|
|
157
|
+
process.stdout.write(`gjsify android-icons: wrote ${plan.png.length + plan.text.length} files under ${relative(projectDir, res).split(sep).join('/')} from ${relative(projectDir, icon)}\n`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
process.stderr.write(`gjsify android-icons: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
161
|
+
return process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// `gjsify credits [--output <file>] [--overrides <file>] [--check | --stdout]`
|
|
2
|
+
//
|
|
3
|
+
// Derives who to thank from the git history and writes it as JSON, so an About
|
|
4
|
+
// dialog or a README reads names from one file instead of a hand-kept list.
|
|
5
|
+
import { Config } from '../config.js';
|
|
6
|
+
import { DEFAULT_CREDITS_IGNORE, buildCredits, isStale, parseOverrides, readCommits, serializeCredits, } from '../utils/credits.js';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
const DEFAULT_OUTPUT = 'credits.json';
|
|
10
|
+
const DEFAULT_OVERRIDES = 'credits.overrides.json';
|
|
11
|
+
export const creditsCommand = {
|
|
12
|
+
command: 'credits',
|
|
13
|
+
description: 'Write the project’s contributors (developers, translators per language) from the git history as JSON, names only.',
|
|
14
|
+
builder: (yargs) => yargs
|
|
15
|
+
.option('output', {
|
|
16
|
+
alias: 'o',
|
|
17
|
+
description: `File to write. Default: \`gjsify.credits.output\`, else ${DEFAULT_OUTPUT}.`,
|
|
18
|
+
type: 'string',
|
|
19
|
+
})
|
|
20
|
+
.option('overrides', {
|
|
21
|
+
description: `Hand-kept additions and exclusions. Default: \`gjsify.credits.overrides\`, else ${DEFAULT_OVERRIDES} when it exists.`,
|
|
22
|
+
type: 'string',
|
|
23
|
+
})
|
|
24
|
+
.option('check', {
|
|
25
|
+
description: 'Write nothing; exit 1 when the file on disk differs from what the history gives.',
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
default: false,
|
|
28
|
+
})
|
|
29
|
+
.option('stdout', {
|
|
30
|
+
description: 'Print the JSON instead of writing it.',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
})
|
|
34
|
+
.conflicts('check', 'stdout'),
|
|
35
|
+
handler: async (args) => {
|
|
36
|
+
const projectDir = process.cwd();
|
|
37
|
+
try {
|
|
38
|
+
const config = (await new Config().forCommand(projectDir)).credits ?? {};
|
|
39
|
+
const output = resolve(projectDir, args.output ?? config.output ?? DEFAULT_OUTPUT);
|
|
40
|
+
const overridesName = args.overrides ?? config.overrides;
|
|
41
|
+
const overridesPath = resolve(projectDir, overridesName ?? DEFAULT_OVERRIDES);
|
|
42
|
+
let overrides;
|
|
43
|
+
if (existsSync(overridesPath)) {
|
|
44
|
+
overrides = parseOverrides(readFileSync(overridesPath, 'utf8'), overridesName ?? DEFAULT_OVERRIDES);
|
|
45
|
+
}
|
|
46
|
+
else if (overridesName !== undefined) {
|
|
47
|
+
throw new Error(`cannot read the overrides file ${overridesName}`);
|
|
48
|
+
}
|
|
49
|
+
const credits = buildCredits(readCommits(projectDir), {
|
|
50
|
+
ignore: config.ignore ?? DEFAULT_CREDITS_IGNORE,
|
|
51
|
+
overrides,
|
|
52
|
+
});
|
|
53
|
+
const text = serializeCredits(credits);
|
|
54
|
+
if (args.stdout === true) {
|
|
55
|
+
process.stdout.write(text);
|
|
56
|
+
}
|
|
57
|
+
else if (args.check === true) {
|
|
58
|
+
const existing = existsSync(output) ? readFileSync(output, 'utf8') : null;
|
|
59
|
+
if (isStale(existing, credits)) {
|
|
60
|
+
process.stderr.write(`gjsify credits: ${args.output ?? config.output ?? DEFAULT_OUTPUT} is ${existing === null ? 'missing' : 'stale'}; run \`gjsify credits\` and commit it\n`);
|
|
61
|
+
return process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
writeFileSync(output, text);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
process.stderr.write(`gjsify credits: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
70
|
+
return process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export * from './workspace.js';
|
|
|
23
23
|
export * from './pack.js';
|
|
24
24
|
export * from './publish.js';
|
|
25
25
|
export * from './release-notes.js';
|
|
26
|
+
export * from './android-icons.js';
|
|
27
|
+
export * from './credits.js';
|
|
26
28
|
export * from './whoami.js';
|
|
27
29
|
export * from './login.js';
|
|
28
30
|
export * from './logout.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export * from './workspace.js';
|
|
|
23
23
|
export * from './pack.js';
|
|
24
24
|
export * from './publish.js';
|
|
25
25
|
export * from './release-notes.js';
|
|
26
|
+
export * from './android-icons.js';
|
|
27
|
+
export * from './credits.js';
|
|
26
28
|
export * from './whoami.js';
|
|
27
29
|
export * from './login.js';
|
|
28
30
|
export * from './logout.js';
|
|
@@ -223,6 +223,10 @@ export interface ConfigData {
|
|
|
223
223
|
site?: {
|
|
224
224
|
metainfo?: string;
|
|
225
225
|
};
|
|
226
|
+
/** Config for `gjsify credits`. CLI flags override these. */
|
|
227
|
+
credits?: ConfigDataCredits;
|
|
228
|
+
/** Config for `gjsify android-icons`. CLI flags override these. */
|
|
229
|
+
androidIcons?: ConfigDataAndroidIcons;
|
|
226
230
|
/**
|
|
227
231
|
* Config for `gjsify format` / `lint` / `fix`. A thin shell: oxc's own
|
|
228
232
|
* `.oxfmtrc.json` / `.oxlintrc.json` are the real configuration files.
|
|
@@ -233,6 +237,28 @@ export interface ConfigData {
|
|
|
233
237
|
/** Config for the `nativescript` target — what the APK carries (ADR 0117). */
|
|
234
238
|
nativescript?: ConfigDataNativescript;
|
|
235
239
|
}
|
|
240
|
+
/** `gjsify.androidIcons` — where the NativeScript App_Resources live and what the icon is drawn from. */
|
|
241
|
+
export interface ConfigDataAndroidIcons {
|
|
242
|
+
/** The App_Resources directory. Default: `appResourcesPath` of nativescript.config.ts, else `App_Resources`. */
|
|
243
|
+
resources?: string;
|
|
244
|
+
/** The scalable app icon SVG. Default: the hicolor icon under `data/icons`. */
|
|
245
|
+
icon?: string;
|
|
246
|
+
/** Adaptive-icon background, `#rrggbb`. Default: the metainfo light branding colour, else white. */
|
|
247
|
+
background?: string;
|
|
248
|
+
}
|
|
249
|
+
/** `gjsify.credits` — where `gjsify credits` writes and what it leaves out. */
|
|
250
|
+
export interface ConfigDataCredits {
|
|
251
|
+
/** File to write. Default `credits.json`. */
|
|
252
|
+
output?: string;
|
|
253
|
+
/** Hand-kept additions and exclusions. Default `credits.overrides.json`, read when it exists. */
|
|
254
|
+
overrides?: string;
|
|
255
|
+
/**
|
|
256
|
+
* Patterns for people who are not contributors (bots, AI co-authors, translation services),
|
|
257
|
+
* matched against the name and the e-mail with `*` as the only wildcard. REPLACES the built-in
|
|
258
|
+
* list rather than extending it, so a project can also take a name off it.
|
|
259
|
+
*/
|
|
260
|
+
ignore?: string[];
|
|
261
|
+
}
|
|
236
262
|
/**
|
|
237
263
|
* What a NativeScript build decides that no other target has to.
|
|
238
264
|
*
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface CreditsPerson {
|
|
2
|
+
name: string;
|
|
3
|
+
email: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CreditsCommit {
|
|
6
|
+
author: CreditsPerson;
|
|
7
|
+
/** `Co-authored-by:` trailers, already run through `.mailmap`. */
|
|
8
|
+
coAuthors: CreditsPerson[];
|
|
9
|
+
files: string[];
|
|
10
|
+
}
|
|
11
|
+
/** The `credits.json` schema. `supporters` has no source but the overrides file. */
|
|
12
|
+
export interface Credits {
|
|
13
|
+
developers: string[];
|
|
14
|
+
translators: Record<string, string[]>;
|
|
15
|
+
supporters: string[];
|
|
16
|
+
}
|
|
17
|
+
/** Hand-maintained additions and removals; every key is optional. */
|
|
18
|
+
export interface CreditsOverrides {
|
|
19
|
+
developers?: string[];
|
|
20
|
+
translators?: Record<string, string[]>;
|
|
21
|
+
supporters?: string[];
|
|
22
|
+
/** Patterns (see {@link isIgnored}) matched against names; removes a person from every list. */
|
|
23
|
+
exclude?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Per language, patterns that remove a person from that language's translators only; `*` means
|
|
26
|
+
* every language. Applied to what the history yields, before the `translators` additions above.
|
|
27
|
+
*/
|
|
28
|
+
excludeTranslators?: Record<string, string[]>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Patterns for people who are not contributors. `*` is the only wildcard and a
|
|
32
|
+
* pattern must match the WHOLE name or the WHOLE e-mail, case-insensitively.
|
|
33
|
+
* Weblate commits as "Weblate (bot)" with the real translator in a trailer, so
|
|
34
|
+
* the bot is dropped and the trailer is what counts.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DEFAULT_CREDITS_IGNORE: readonly string[];
|
|
37
|
+
export declare function isIgnored(person: CreditsPerson, patterns: readonly string[]): boolean;
|
|
38
|
+
/** `Name <mail>` → parts. A trailer without an address keeps the whole value as the name. */
|
|
39
|
+
export declare function parsePerson(value: string): CreditsPerson;
|
|
40
|
+
export declare function buildCredits(commits: readonly CreditsCommit[], options?: {
|
|
41
|
+
ignore?: readonly string[];
|
|
42
|
+
overrides?: CreditsOverrides;
|
|
43
|
+
}): Credits;
|
|
44
|
+
/** The `git log` format `parseGitLog` reads: author, address, co-author trailers, then the file names. */
|
|
45
|
+
export declare const GIT_LOG_ARGS: string[];
|
|
46
|
+
/** Splits `git log` output into commits; co-author trailers are NOT mapped yet. */
|
|
47
|
+
export declare function parseGitLog(output: string): CreditsCommit[];
|
|
48
|
+
/** The repo's non-merge commits with `.mailmap` applied to authors and co-authors. */
|
|
49
|
+
export declare function readCommits(cwd: string): CreditsCommit[];
|
|
50
|
+
export declare function parseOverrides(text: string, where?: string): CreditsOverrides;
|
|
51
|
+
export declare function serializeCredits(credits: Credits): string;
|
|
52
|
+
/** True when `existing` (the file on disk, or `null` when absent) is not what `credits` serializes to. */
|
|
53
|
+
export declare function isStale(existing: string | null, credits: Credits): boolean;
|