@gjsify/cli 0.1.10 → 0.1.11
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/lib/actions/build.d.ts +5 -0
- package/lib/actions/build.js +31 -1
- package/lib/commands/build.js +6 -0
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +40 -3
- package/lib/commands/gettext.d.ts +14 -0
- package/lib/commands/gettext.js +201 -0
- package/lib/commands/gresource.d.ts +9 -0
- package/lib/commands/gresource.js +82 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/config.js +2 -0
- package/lib/index.js +3 -2
- package/lib/types/cli-build-options.d.ts +6 -0
- package/lib/types/config-data.d.ts +4 -0
- package/package.json +11 -10
- package/src/actions/build.ts +32 -1
- package/src/commands/build.ts +6 -0
- package/src/commands/create.ts +49 -7
- package/src/commands/gettext.ts +258 -0
- package/src/commands/gresource.ts +97 -0
- package/src/commands/index.ts +3 -1
- package/src/config.ts +1 -0
- package/src/index.ts +12 -2
- package/src/types/cli-build-options.ts +6 -0
- package/src/types/config-data.ts +4 -0
package/lib/actions/build.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export declare class BuildAction {
|
|
|
26
26
|
* The auto path is handled in `buildApp` via the two-pass build.
|
|
27
27
|
*/
|
|
28
28
|
private resolveGlobalsInject;
|
|
29
|
+
/**
|
|
30
|
+
* Post-processing: prepend GJS shebang and mark the output file executable.
|
|
31
|
+
* Only runs for GJS app builds with a resolvable single outfile.
|
|
32
|
+
*/
|
|
33
|
+
private applyShebang;
|
|
29
34
|
/** Application mode */
|
|
30
35
|
buildApp(app?: App): Promise<BuildResult<{
|
|
31
36
|
format: "esm" | "cjs";
|
package/lib/actions/build.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { build } from 'esbuild';
|
|
2
2
|
import { gjsifyPlugin } from '@gjsify/esbuild-plugin-gjsify';
|
|
3
3
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/esbuild-plugin-gjsify/globals';
|
|
4
|
-
import { dirname, extname } from 'path';
|
|
4
|
+
import { dirname, extname } from 'node:path';
|
|
5
|
+
import { chmod, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m\n';
|
|
5
7
|
export class BuildAction {
|
|
6
8
|
configData;
|
|
7
9
|
constructor(configData = {}) {
|
|
@@ -105,6 +107,28 @@ export class BuildAction {
|
|
|
105
107
|
}
|
|
106
108
|
return injectPath ?? undefined;
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Post-processing: prepend GJS shebang and mark the output file executable.
|
|
112
|
+
* Only runs for GJS app builds with a resolvable single outfile.
|
|
113
|
+
*/
|
|
114
|
+
async applyShebang(outfile, verbose) {
|
|
115
|
+
if (!outfile) {
|
|
116
|
+
if (verbose)
|
|
117
|
+
console.warn('[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const content = await readFile(outfile, 'utf-8');
|
|
121
|
+
if (content.startsWith('#!')) {
|
|
122
|
+
if (verbose)
|
|
123
|
+
console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await writeFile(outfile, GJS_SHEBANG + content);
|
|
127
|
+
}
|
|
128
|
+
await chmod(outfile, 0o755);
|
|
129
|
+
if (verbose)
|
|
130
|
+
console.debug(`[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`);
|
|
131
|
+
}
|
|
108
132
|
/** Application mode */
|
|
109
133
|
async buildApp(app = 'gjs') {
|
|
110
134
|
const { verbose, esbuild, typescript, exclude, library: pgk } = this.configData;
|
|
@@ -140,6 +164,9 @@ export class BuildAction {
|
|
|
140
164
|
}),
|
|
141
165
|
],
|
|
142
166
|
});
|
|
167
|
+
if (app === 'gjs' && this.configData.shebang) {
|
|
168
|
+
await this.applyShebang(esbuild?.outfile, verbose);
|
|
169
|
+
}
|
|
143
170
|
return [result];
|
|
144
171
|
}
|
|
145
172
|
// --- Explicit list (no `auto` token) or none mode ---
|
|
@@ -157,6 +184,9 @@ export class BuildAction {
|
|
|
157
184
|
}),
|
|
158
185
|
]
|
|
159
186
|
});
|
|
187
|
+
if (app === 'gjs' && this.configData.shebang) {
|
|
188
|
+
await this.applyShebang(esbuild?.outfile, verbose);
|
|
189
|
+
}
|
|
160
190
|
return [result];
|
|
161
191
|
}
|
|
162
192
|
async start(buildType = { app: 'gjs' }) {
|
package/lib/commands/build.js
CHANGED
|
@@ -91,6 +91,12 @@ export const buildCommand = {
|
|
|
91
91
|
type: 'string',
|
|
92
92
|
normalize: true,
|
|
93
93
|
default: 'auto'
|
|
94
|
+
})
|
|
95
|
+
.option('shebang', {
|
|
96
|
+
description: "Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output and mark it executable (chmod 755). Only applies to GJS app builds with a single --outfile.",
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
normalize: true,
|
|
99
|
+
default: false
|
|
94
100
|
});
|
|
95
101
|
},
|
|
96
102
|
handler: async (args) => {
|
package/lib/commands/create.d.ts
CHANGED
package/lib/commands/create.js
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
|
-
import { createProject } from '@gjsify/create-app';
|
|
1
|
+
import { createProject, discoverTemplates } from '@gjsify/create-app';
|
|
2
|
+
import { promptTemplate } from '@gjsify/create-app/prompt';
|
|
2
3
|
export const createCommand = {
|
|
3
4
|
command: 'create [project-name]',
|
|
4
5
|
description: 'Scaffold a new Gjsify project in a new directory.',
|
|
5
6
|
builder: (yargs) => {
|
|
6
|
-
|
|
7
|
+
const templates = discoverTemplates();
|
|
8
|
+
const templateChoices = templates.map((t) => t.name);
|
|
9
|
+
return yargs
|
|
10
|
+
.positional('project-name', {
|
|
7
11
|
describe: 'Name of the project directory to create',
|
|
8
12
|
type: 'string',
|
|
9
13
|
default: 'my-gjs-app',
|
|
14
|
+
})
|
|
15
|
+
.option('template', {
|
|
16
|
+
alias: 't',
|
|
17
|
+
describe: 'Template to scaffold from',
|
|
18
|
+
type: 'string',
|
|
19
|
+
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
20
|
+
})
|
|
21
|
+
.option('force', {
|
|
22
|
+
alias: 'f',
|
|
23
|
+
describe: 'Scaffold into a non-empty directory',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
default: false,
|
|
26
|
+
})
|
|
27
|
+
.option('install', {
|
|
28
|
+
describe: 'Run npm install after scaffolding',
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
default: false,
|
|
10
31
|
});
|
|
11
32
|
},
|
|
12
33
|
handler: async (args) => {
|
|
13
|
-
|
|
34
|
+
let template = args.template;
|
|
35
|
+
if (!template) {
|
|
36
|
+
const templates = discoverTemplates();
|
|
37
|
+
if (!process.stdin.isTTY) {
|
|
38
|
+
const list = templates.map((t) => t.name).join(', ');
|
|
39
|
+
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const picked = await promptTemplate(templates);
|
|
43
|
+
template = picked.name;
|
|
44
|
+
}
|
|
45
|
+
await createProject({
|
|
46
|
+
projectName: args['project-name'],
|
|
47
|
+
template,
|
|
48
|
+
force: args.force,
|
|
49
|
+
install: args.install,
|
|
50
|
+
});
|
|
14
51
|
},
|
|
15
52
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
type GettextFormat = 'mo' | 'xml' | 'desktop' | 'json';
|
|
3
|
+
interface GettextOptions {
|
|
4
|
+
poDir: string;
|
|
5
|
+
outDir: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
format?: GettextFormat;
|
|
8
|
+
metainfo?: string;
|
|
9
|
+
filename?: string;
|
|
10
|
+
removeXmlComments?: boolean;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const gettextCommand: Command<any, GettextOptions>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
async function listLanguages(poDir) {
|
|
7
|
+
const entries = await readdir(poDir);
|
|
8
|
+
return entries
|
|
9
|
+
.filter((name) => name.endsWith('.po') && !name.startsWith('.'))
|
|
10
|
+
.map((name) => name.slice(0, -3))
|
|
11
|
+
.sort();
|
|
12
|
+
}
|
|
13
|
+
function stripXmlComments(source) {
|
|
14
|
+
return source.replace(/<!--[\s\S]*?-->/g, '');
|
|
15
|
+
}
|
|
16
|
+
async function ensureDir(path) {
|
|
17
|
+
await mkdir(path, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
async function fileExists(path) {
|
|
20
|
+
try {
|
|
21
|
+
await stat(path);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function compileBulkXml(opts) {
|
|
29
|
+
const outputFile = join(opts.outDir, opts.filename);
|
|
30
|
+
await ensureDir(opts.outDir);
|
|
31
|
+
const args = [
|
|
32
|
+
`--output-file=${outputFile}`,
|
|
33
|
+
'--xml',
|
|
34
|
+
`--template=${opts.template}`,
|
|
35
|
+
'-d',
|
|
36
|
+
opts.poDir,
|
|
37
|
+
];
|
|
38
|
+
if (opts.verbose) {
|
|
39
|
+
console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
|
|
40
|
+
}
|
|
41
|
+
await execFileAsync('msgfmt', args);
|
|
42
|
+
if (opts.removeXmlComments) {
|
|
43
|
+
const content = await readFile(outputFile, 'utf-8');
|
|
44
|
+
await writeFile(outputFile, stripXmlComments(content));
|
|
45
|
+
}
|
|
46
|
+
if (opts.verbose) {
|
|
47
|
+
console.log(`[gjsify gettext] wrote ${outputFile}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function compilePerLanguage(opts) {
|
|
51
|
+
const languages = await listLanguages(opts.poDir);
|
|
52
|
+
if (languages.length === 0) {
|
|
53
|
+
console.warn(`[gjsify gettext] no .po files found in ${opts.poDir}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const lang of languages) {
|
|
57
|
+
const poFile = join(opts.poDir, `${lang}.po`);
|
|
58
|
+
const langDir = opts.format === 'mo'
|
|
59
|
+
? join(opts.outDir, lang, 'LC_MESSAGES')
|
|
60
|
+
: join(opts.outDir, lang);
|
|
61
|
+
await ensureDir(langDir);
|
|
62
|
+
const outputFile = join(langDir, opts.filename);
|
|
63
|
+
// msgfmt produces the binary .mo format by default — there is no
|
|
64
|
+
// `--mo` flag (only --xml, --desktop, --properties-output, ...).
|
|
65
|
+
const args = [`--output-file=${outputFile}`];
|
|
66
|
+
if (opts.format !== 'mo') {
|
|
67
|
+
args.push(`--${opts.format}`);
|
|
68
|
+
}
|
|
69
|
+
args.push(poFile);
|
|
70
|
+
if (opts.verbose) {
|
|
71
|
+
console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
|
|
72
|
+
}
|
|
73
|
+
await execFileAsync('msgfmt', args);
|
|
74
|
+
}
|
|
75
|
+
if (opts.verbose) {
|
|
76
|
+
console.log(`[gjsify gettext] compiled ${languages.length} language(s) into ${opts.outDir}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function defaultFilename(domain, format, metainfoTemplate) {
|
|
80
|
+
switch (format) {
|
|
81
|
+
case 'mo':
|
|
82
|
+
return `${domain}.mo`;
|
|
83
|
+
case 'json':
|
|
84
|
+
return `${domain}.json`;
|
|
85
|
+
case 'desktop':
|
|
86
|
+
return `${domain}.desktop`;
|
|
87
|
+
case 'xml': {
|
|
88
|
+
// Mirror the template filename but without the trailing `.in` (convention for
|
|
89
|
+
// pre-processed metainfo templates: `org.foo.Bar.metainfo.xml.in`).
|
|
90
|
+
if (metainfoTemplate) {
|
|
91
|
+
const base = metainfoTemplate.replace(/\.in$/, '');
|
|
92
|
+
const slash = base.lastIndexOf('/');
|
|
93
|
+
return slash >= 0 ? base.slice(slash + 1) : base;
|
|
94
|
+
}
|
|
95
|
+
return `${domain}.xml`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export const gettextCommand = {
|
|
100
|
+
command: 'gettext <poDir> <outDir>',
|
|
101
|
+
description: 'Compile gettext .po files to .mo (per-language locale tree) or substitute a metainfo template via msgfmt --xml.',
|
|
102
|
+
builder: (yargs) => {
|
|
103
|
+
return yargs
|
|
104
|
+
.positional('poDir', {
|
|
105
|
+
description: 'Directory containing <lang>.po files',
|
|
106
|
+
type: 'string',
|
|
107
|
+
normalize: true,
|
|
108
|
+
demandOption: true,
|
|
109
|
+
})
|
|
110
|
+
.positional('outDir', {
|
|
111
|
+
description: 'Output directory (locale tree for --format=mo, plain dir for xml/desktop/json)',
|
|
112
|
+
type: 'string',
|
|
113
|
+
normalize: true,
|
|
114
|
+
demandOption: true,
|
|
115
|
+
})
|
|
116
|
+
.option('domain', {
|
|
117
|
+
description: 'Text domain / application ID (e.g. `org.pixelrpg.maker`)',
|
|
118
|
+
type: 'string',
|
|
119
|
+
normalize: true,
|
|
120
|
+
demandOption: true,
|
|
121
|
+
})
|
|
122
|
+
.option('format', {
|
|
123
|
+
description: 'Output format',
|
|
124
|
+
type: 'string',
|
|
125
|
+
choices: ['mo', 'xml', 'desktop', 'json'],
|
|
126
|
+
default: 'mo',
|
|
127
|
+
})
|
|
128
|
+
.option('metainfo', {
|
|
129
|
+
description: 'For --format=xml: path to the template (`.metainfo.xml.in`) used as msgfmt --template',
|
|
130
|
+
type: 'string',
|
|
131
|
+
normalize: true,
|
|
132
|
+
})
|
|
133
|
+
.option('filename', {
|
|
134
|
+
description: 'Override the output filename (defaults to <domain>.<ext>)',
|
|
135
|
+
type: 'string',
|
|
136
|
+
normalize: true,
|
|
137
|
+
})
|
|
138
|
+
.option('remove-xml-comments', {
|
|
139
|
+
description: 'For --format=xml: strip XML comments from the compiled output',
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
default: true,
|
|
142
|
+
})
|
|
143
|
+
.option('verbose', {
|
|
144
|
+
description: 'Print each msgfmt invocation',
|
|
145
|
+
type: 'boolean',
|
|
146
|
+
default: false,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
handler: async (args) => {
|
|
150
|
+
const poDir = resolve(args.poDir);
|
|
151
|
+
const outDir = resolve(args.outDir);
|
|
152
|
+
const domain = args.domain;
|
|
153
|
+
const format = args.format ?? 'mo';
|
|
154
|
+
const metainfo = args.metainfo ? resolve(args.metainfo) : undefined;
|
|
155
|
+
const filename = args.filename ?? defaultFilename(domain, format, metainfo);
|
|
156
|
+
const verbose = !!args.verbose;
|
|
157
|
+
const removeXmlComments = !!args['remove-xml-comments'];
|
|
158
|
+
if (!(await fileExists(poDir))) {
|
|
159
|
+
console.error(`[gjsify gettext] PO directory does not exist: ${poDir}`);
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
if (format === 'xml' && metainfo) {
|
|
165
|
+
await compileBulkXml({
|
|
166
|
+
poDir,
|
|
167
|
+
outDir,
|
|
168
|
+
domain,
|
|
169
|
+
template: metainfo,
|
|
170
|
+
filename,
|
|
171
|
+
removeXmlComments,
|
|
172
|
+
verbose,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
if (format === 'xml') {
|
|
177
|
+
console.warn('[gjsify gettext] --format=xml without --metainfo: falling back to per-language XML files');
|
|
178
|
+
}
|
|
179
|
+
await compilePerLanguage({
|
|
180
|
+
poDir,
|
|
181
|
+
outDir,
|
|
182
|
+
domain,
|
|
183
|
+
format,
|
|
184
|
+
filename,
|
|
185
|
+
verbose,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (err?.code === 'ENOENT') {
|
|
191
|
+
console.error('[gjsify gettext] msgfmt not found. Install it via your distro (package: gettext).');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
if (err?.stderr)
|
|
195
|
+
process.stderr.write(err.stderr);
|
|
196
|
+
console.error(`[gjsify gettext] msgfmt failed${err?.code !== undefined ? ` (exit ${err.code})` : ''}`);
|
|
197
|
+
}
|
|
198
|
+
process.exitCode = typeof err?.code === 'number' ? err.code : 1;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
/**
|
|
7
|
+
* Derive a default `.gresource` target path from the XML descriptor.
|
|
8
|
+
* `foo.gresource.xml` → `foo.gresource` in the same directory.
|
|
9
|
+
*/
|
|
10
|
+
function defaultTargetFor(xmlPath) {
|
|
11
|
+
const ext = extname(xmlPath);
|
|
12
|
+
const stem = basename(xmlPath, ext);
|
|
13
|
+
return resolve(dirname(xmlPath), stem);
|
|
14
|
+
}
|
|
15
|
+
export const gresourceCommand = {
|
|
16
|
+
command: 'gresource <xml>',
|
|
17
|
+
description: 'Compile a GResource XML descriptor into a binary .gresource bundle (wraps `glib-compile-resources`).',
|
|
18
|
+
builder: (yargs) => {
|
|
19
|
+
return yargs
|
|
20
|
+
.positional('xml', {
|
|
21
|
+
description: 'Path to the .gresource.xml descriptor',
|
|
22
|
+
type: 'string',
|
|
23
|
+
normalize: true,
|
|
24
|
+
demandOption: true,
|
|
25
|
+
})
|
|
26
|
+
.option('sourcedir', {
|
|
27
|
+
description: 'Directory containing the resource files referenced by <xml>',
|
|
28
|
+
type: 'string',
|
|
29
|
+
normalize: true,
|
|
30
|
+
})
|
|
31
|
+
.option('target', {
|
|
32
|
+
alias: 't',
|
|
33
|
+
description: 'Output .gresource file (default: <xml-without-.xml> next to <xml>)',
|
|
34
|
+
type: 'string',
|
|
35
|
+
normalize: true,
|
|
36
|
+
})
|
|
37
|
+
.option('verbose', {
|
|
38
|
+
description: 'Print the underlying glib-compile-resources invocation',
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
default: false,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
handler: async (args) => {
|
|
44
|
+
const xmlPath = resolve(args.xml);
|
|
45
|
+
const target = args.target ? resolve(args.target) : defaultTargetFor(xmlPath);
|
|
46
|
+
const sourcedir = args.sourcedir
|
|
47
|
+
? resolve(args.sourcedir)
|
|
48
|
+
: dirname(xmlPath);
|
|
49
|
+
const cmdArgs = [
|
|
50
|
+
`--sourcedir=${sourcedir}`,
|
|
51
|
+
`--target=${target}`,
|
|
52
|
+
xmlPath,
|
|
53
|
+
];
|
|
54
|
+
if (args.verbose) {
|
|
55
|
+
console.log(`[gjsify gresource] glib-compile-resources ${cmdArgs.join(' ')}`);
|
|
56
|
+
}
|
|
57
|
+
// Ensure the target directory exists — glib-compile-resources writes
|
|
58
|
+
// a temporary file next to the target and fails with ENOENT otherwise.
|
|
59
|
+
await mkdir(dirname(target), { recursive: true });
|
|
60
|
+
try {
|
|
61
|
+
const { stdout, stderr } = await execFileAsync('glib-compile-resources', cmdArgs);
|
|
62
|
+
if (stdout)
|
|
63
|
+
process.stdout.write(stdout);
|
|
64
|
+
if (stderr)
|
|
65
|
+
process.stderr.write(stderr);
|
|
66
|
+
if (args.verbose) {
|
|
67
|
+
console.log(`[gjsify gresource] wrote ${target}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err?.code === 'ENOENT') {
|
|
72
|
+
console.error('[gjsify gresource] glib-compile-resources not found. Install it via your distro (package: glib2-devel / libglib2.0-dev).');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (err?.stderr)
|
|
76
|
+
process.stderr.write(err.stderr);
|
|
77
|
+
console.error(`[gjsify gresource] glib-compile-resources failed${err?.code !== undefined ? ` (exit ${err.code})` : ''}`);
|
|
78
|
+
}
|
|
79
|
+
process.exitCode = typeof err?.code === 'number' ? err.code : 1;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -71,6 +71,8 @@ export class Config {
|
|
|
71
71
|
configData.consoleShim = cliArgs.consoleShim;
|
|
72
72
|
if (cliArgs.globals !== undefined)
|
|
73
73
|
configData.globals = cliArgs.globals;
|
|
74
|
+
if (cliArgs.shebang !== undefined)
|
|
75
|
+
configData.shebang = cliArgs.shebang;
|
|
74
76
|
merge(configData.library ??= {}, pkg, configData.library);
|
|
75
77
|
merge(configData.typescript ??= {}, tsConfig, configData.typescript);
|
|
76
78
|
merge(configData.esbuild ??= {}, {
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import yargs from 'yargs';
|
|
3
3
|
import { hideBin } from 'yargs/helpers';
|
|
4
|
-
import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create } from './commands/index.js';
|
|
4
|
+
import { buildCommand as build, runCommand as run, infoCommand as info, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, } from './commands/index.js';
|
|
5
5
|
import { APP_NAME } from './constants.js';
|
|
6
6
|
void yargs(hideBin(process.argv))
|
|
7
7
|
.scriptName(APP_NAME)
|
|
8
8
|
.strict()
|
|
9
|
-
// .usage(Config.usage)
|
|
10
9
|
.command(create.command, create.description, create.builder, create.handler)
|
|
11
10
|
.command(build.command, build.description, build.builder, build.handler)
|
|
12
11
|
.command(run.command, run.description, run.builder, run.handler)
|
|
13
12
|
.command(info.command, info.description, info.builder, info.handler)
|
|
14
13
|
.command(check.command, check.description, check.builder, check.handler)
|
|
15
14
|
.command(showcase.command, showcase.description, showcase.builder, showcase.handler)
|
|
15
|
+
.command(gresource.command, gresource.description, gresource.builder, gresource.handler)
|
|
16
|
+
.command(gettext.command, gettext.description, gettext.builder, gettext.handler)
|
|
16
17
|
.demandCommand(1)
|
|
17
18
|
.help().argv;
|
|
@@ -54,4 +54,10 @@ export interface CliBuildOptions {
|
|
|
54
54
|
* bundle. Only applies to GJS app builds.
|
|
55
55
|
*/
|
|
56
56
|
globals?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output file and mark
|
|
59
|
+
* it executable (chmod 755). Only applies to GJS app builds with a single
|
|
60
|
+
* `--outfile`. Default: false.
|
|
61
|
+
*/
|
|
62
|
+
shebang?: boolean;
|
|
57
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,15 +23,16 @@
|
|
|
23
23
|
"cli"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@gjsify/create-app": "^0.1.
|
|
27
|
-
"@gjsify/esbuild-plugin-gjsify": "^0.1.
|
|
28
|
-
"@gjsify/example-dom-
|
|
29
|
-
"@gjsify/example-dom-
|
|
30
|
-
"@gjsify/example-dom-
|
|
31
|
-
"@gjsify/example-dom-three-
|
|
32
|
-
"@gjsify/example-
|
|
33
|
-
"@gjsify/node-
|
|
34
|
-
"@gjsify/
|
|
26
|
+
"@gjsify/create-app": "^0.1.11",
|
|
27
|
+
"@gjsify/esbuild-plugin-gjsify": "^0.1.11",
|
|
28
|
+
"@gjsify/example-dom-adwaita-package-builder": "^0.1.11",
|
|
29
|
+
"@gjsify/example-dom-canvas2d-fireworks": "^0.1.11",
|
|
30
|
+
"@gjsify/example-dom-excalibur-jelly-jumper": "^0.1.11",
|
|
31
|
+
"@gjsify/example-dom-three-geometry-teapot": "^0.1.11",
|
|
32
|
+
"@gjsify/example-dom-three-postprocessing-pixel": "^0.1.11",
|
|
33
|
+
"@gjsify/example-node-express-webserver": "^0.1.11",
|
|
34
|
+
"@gjsify/node-polyfills": "^0.1.11",
|
|
35
|
+
"@gjsify/web-polyfills": "^0.1.11",
|
|
35
36
|
"cosmiconfig": "^9.0.1",
|
|
36
37
|
"esbuild": "^0.28.0",
|
|
37
38
|
"get-tsconfig": "^4.13.7",
|
package/src/actions/build.ts
CHANGED
|
@@ -3,7 +3,10 @@ import type { App } from '@gjsify/esbuild-plugin-gjsify';
|
|
|
3
3
|
import { build, BuildOptions, BuildResult } from 'esbuild';
|
|
4
4
|
import { gjsifyPlugin } from '@gjsify/esbuild-plugin-gjsify';
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals } from '@gjsify/esbuild-plugin-gjsify/globals';
|
|
6
|
-
import { dirname, extname } from 'path';
|
|
6
|
+
import { dirname, extname } from 'node:path';
|
|
7
|
+
import { chmod, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
|
|
9
|
+
const GJS_SHEBANG = '#!/usr/bin/env -S gjs -m\n';
|
|
7
10
|
|
|
8
11
|
export class BuildAction {
|
|
9
12
|
constructor(readonly configData: ConfigData = {}) {
|
|
@@ -123,6 +126,26 @@ export class BuildAction {
|
|
|
123
126
|
return injectPath ?? undefined;
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Post-processing: prepend GJS shebang and mark the output file executable.
|
|
131
|
+
* Only runs for GJS app builds with a resolvable single outfile.
|
|
132
|
+
*/
|
|
133
|
+
private async applyShebang(outfile: string | undefined, verbose: boolean | undefined): Promise<void> {
|
|
134
|
+
if (!outfile) {
|
|
135
|
+
if (verbose) console.warn('[gjsify] --shebang skipped: no single outfile (use --outfile for GJS executables)');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = await readFile(outfile, 'utf-8');
|
|
140
|
+
if (content.startsWith('#!')) {
|
|
141
|
+
if (verbose) console.debug(`[gjsify] --shebang skipped: ${outfile} already starts with a shebang`);
|
|
142
|
+
} else {
|
|
143
|
+
await writeFile(outfile, GJS_SHEBANG + content);
|
|
144
|
+
}
|
|
145
|
+
await chmod(outfile, 0o755);
|
|
146
|
+
if (verbose) console.debug(`[gjsify] --shebang: wrote shebang + chmod 0o755 to ${outfile}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
/** Application mode */
|
|
127
150
|
async buildApp(app: App = 'gjs') {
|
|
128
151
|
|
|
@@ -172,6 +195,10 @@ export class BuildAction {
|
|
|
172
195
|
],
|
|
173
196
|
});
|
|
174
197
|
|
|
198
|
+
if (app === 'gjs' && this.configData.shebang) {
|
|
199
|
+
await this.applyShebang(esbuild?.outfile, verbose);
|
|
200
|
+
}
|
|
201
|
+
|
|
175
202
|
return [result];
|
|
176
203
|
}
|
|
177
204
|
|
|
@@ -192,6 +219,10 @@ export class BuildAction {
|
|
|
192
219
|
]
|
|
193
220
|
});
|
|
194
221
|
|
|
222
|
+
if (app === 'gjs' && this.configData.shebang) {
|
|
223
|
+
await this.applyShebang(esbuild?.outfile, verbose);
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
return [result];
|
|
196
227
|
}
|
|
197
228
|
|
package/src/commands/build.ts
CHANGED
|
@@ -94,6 +94,12 @@ export const buildCommand: Command<any, CliBuildOptions> = {
|
|
|
94
94
|
normalize: true,
|
|
95
95
|
default: 'auto'
|
|
96
96
|
})
|
|
97
|
+
.option('shebang', {
|
|
98
|
+
description: "Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output and mark it executable (chmod 755). Only applies to GJS app builds with a single --outfile.",
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
normalize: true,
|
|
101
|
+
default: false
|
|
102
|
+
})
|
|
97
103
|
},
|
|
98
104
|
handler: async (args) => {
|
|
99
105
|
const config = new Config();
|
package/src/commands/create.ts
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
1
|
-
import { createProject } from '@gjsify/create-app';
|
|
1
|
+
import { createProject, discoverTemplates } from '@gjsify/create-app';
|
|
2
|
+
import { promptTemplate } from '@gjsify/create-app/prompt';
|
|
2
3
|
import type { Command } from '../types/index.js';
|
|
3
4
|
|
|
4
5
|
interface CreateOptions {
|
|
5
6
|
'project-name': string;
|
|
7
|
+
template?: string;
|
|
8
|
+
force: boolean;
|
|
9
|
+
install: boolean;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export const createCommand: Command<any, CreateOptions> = {
|
|
9
13
|
command: 'create [project-name]',
|
|
10
14
|
description: 'Scaffold a new Gjsify project in a new directory.',
|
|
11
15
|
builder: (yargs) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const templates = discoverTemplates();
|
|
17
|
+
const templateChoices = templates.map((t) => t.name);
|
|
18
|
+
return yargs
|
|
19
|
+
.positional('project-name', {
|
|
20
|
+
describe: 'Name of the project directory to create',
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: 'my-gjs-app',
|
|
23
|
+
})
|
|
24
|
+
.option('template', {
|
|
25
|
+
alias: 't',
|
|
26
|
+
describe: 'Template to scaffold from',
|
|
27
|
+
type: 'string',
|
|
28
|
+
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
29
|
+
})
|
|
30
|
+
.option('force', {
|
|
31
|
+
alias: 'f',
|
|
32
|
+
describe: 'Scaffold into a non-empty directory',
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false,
|
|
35
|
+
})
|
|
36
|
+
.option('install', {
|
|
37
|
+
describe: 'Run npm install after scaffolding',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
default: false,
|
|
40
|
+
});
|
|
17
41
|
},
|
|
18
42
|
handler: async (args) => {
|
|
19
|
-
|
|
43
|
+
let template = args.template;
|
|
44
|
+
if (!template) {
|
|
45
|
+
const templates = discoverTemplates();
|
|
46
|
+
if (!process.stdin.isTTY) {
|
|
47
|
+
const list = templates.map((t) => t.name).join(', ');
|
|
48
|
+
console.error(
|
|
49
|
+
`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const picked = await promptTemplate(templates);
|
|
54
|
+
template = picked.name;
|
|
55
|
+
}
|
|
56
|
+
await createProject({
|
|
57
|
+
projectName: args['project-name'],
|
|
58
|
+
template,
|
|
59
|
+
force: args.force,
|
|
60
|
+
install: args.install,
|
|
61
|
+
});
|
|
20
62
|
},
|
|
21
63
|
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import type { Command } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
type GettextFormat = 'mo' | 'xml' | 'desktop' | 'json';
|
|
10
|
+
|
|
11
|
+
interface GettextOptions {
|
|
12
|
+
poDir: string;
|
|
13
|
+
outDir: string;
|
|
14
|
+
domain: string;
|
|
15
|
+
format?: GettextFormat;
|
|
16
|
+
metainfo?: string;
|
|
17
|
+
filename?: string;
|
|
18
|
+
removeXmlComments?: boolean;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function listLanguages(poDir: string): Promise<string[]> {
|
|
23
|
+
const entries = await readdir(poDir);
|
|
24
|
+
return entries
|
|
25
|
+
.filter((name) => name.endsWith('.po') && !name.startsWith('.'))
|
|
26
|
+
.map((name) => name.slice(0, -3))
|
|
27
|
+
.sort();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripXmlComments(source: string): string {
|
|
31
|
+
return source.replace(/<!--[\s\S]*?-->/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function ensureDir(path: string): Promise<void> {
|
|
35
|
+
await mkdir(path, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
39
|
+
try {
|
|
40
|
+
await stat(path);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function compileBulkXml(opts: {
|
|
48
|
+
poDir: string;
|
|
49
|
+
outDir: string;
|
|
50
|
+
domain: string;
|
|
51
|
+
template: string;
|
|
52
|
+
filename: string;
|
|
53
|
+
removeXmlComments: boolean;
|
|
54
|
+
verbose: boolean;
|
|
55
|
+
}): Promise<void> {
|
|
56
|
+
const outputFile = join(opts.outDir, opts.filename);
|
|
57
|
+
await ensureDir(opts.outDir);
|
|
58
|
+
|
|
59
|
+
const args = [
|
|
60
|
+
`--output-file=${outputFile}`,
|
|
61
|
+
'--xml',
|
|
62
|
+
`--template=${opts.template}`,
|
|
63
|
+
'-d',
|
|
64
|
+
opts.poDir,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
if (opts.verbose) {
|
|
68
|
+
console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await execFileAsync('msgfmt', args);
|
|
72
|
+
|
|
73
|
+
if (opts.removeXmlComments) {
|
|
74
|
+
const content = await readFile(outputFile, 'utf-8');
|
|
75
|
+
await writeFile(outputFile, stripXmlComments(content));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (opts.verbose) {
|
|
79
|
+
console.log(`[gjsify gettext] wrote ${outputFile}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function compilePerLanguage(opts: {
|
|
84
|
+
poDir: string;
|
|
85
|
+
outDir: string;
|
|
86
|
+
domain: string;
|
|
87
|
+
format: GettextFormat;
|
|
88
|
+
filename: string;
|
|
89
|
+
verbose: boolean;
|
|
90
|
+
}): Promise<void> {
|
|
91
|
+
const languages = await listLanguages(opts.poDir);
|
|
92
|
+
if (languages.length === 0) {
|
|
93
|
+
console.warn(`[gjsify gettext] no .po files found in ${opts.poDir}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const lang of languages) {
|
|
98
|
+
const poFile = join(opts.poDir, `${lang}.po`);
|
|
99
|
+
const langDir =
|
|
100
|
+
opts.format === 'mo'
|
|
101
|
+
? join(opts.outDir, lang, 'LC_MESSAGES')
|
|
102
|
+
: join(opts.outDir, lang);
|
|
103
|
+
await ensureDir(langDir);
|
|
104
|
+
const outputFile = join(langDir, opts.filename);
|
|
105
|
+
|
|
106
|
+
// msgfmt produces the binary .mo format by default — there is no
|
|
107
|
+
// `--mo` flag (only --xml, --desktop, --properties-output, ...).
|
|
108
|
+
const args = [`--output-file=${outputFile}`];
|
|
109
|
+
if (opts.format !== 'mo') {
|
|
110
|
+
args.push(`--${opts.format}`);
|
|
111
|
+
}
|
|
112
|
+
args.push(poFile);
|
|
113
|
+
|
|
114
|
+
if (opts.verbose) {
|
|
115
|
+
console.log(`[gjsify gettext] msgfmt ${args.join(' ')}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await execFileAsync('msgfmt', args);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (opts.verbose) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[gjsify gettext] compiled ${languages.length} language(s) into ${opts.outDir}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function defaultFilename(domain: string, format: GettextFormat, metainfoTemplate?: string): string {
|
|
129
|
+
switch (format) {
|
|
130
|
+
case 'mo':
|
|
131
|
+
return `${domain}.mo`;
|
|
132
|
+
case 'json':
|
|
133
|
+
return `${domain}.json`;
|
|
134
|
+
case 'desktop':
|
|
135
|
+
return `${domain}.desktop`;
|
|
136
|
+
case 'xml': {
|
|
137
|
+
// Mirror the template filename but without the trailing `.in` (convention for
|
|
138
|
+
// pre-processed metainfo templates: `org.foo.Bar.metainfo.xml.in`).
|
|
139
|
+
if (metainfoTemplate) {
|
|
140
|
+
const base = metainfoTemplate.replace(/\.in$/, '');
|
|
141
|
+
const slash = base.lastIndexOf('/');
|
|
142
|
+
return slash >= 0 ? base.slice(slash + 1) : base;
|
|
143
|
+
}
|
|
144
|
+
return `${domain}.xml`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const gettextCommand: Command<any, GettextOptions> = {
|
|
150
|
+
command: 'gettext <poDir> <outDir>',
|
|
151
|
+
description:
|
|
152
|
+
'Compile gettext .po files to .mo (per-language locale tree) or substitute a metainfo template via msgfmt --xml.',
|
|
153
|
+
builder: (yargs) => {
|
|
154
|
+
return yargs
|
|
155
|
+
.positional('poDir', {
|
|
156
|
+
description: 'Directory containing <lang>.po files',
|
|
157
|
+
type: 'string',
|
|
158
|
+
normalize: true,
|
|
159
|
+
demandOption: true,
|
|
160
|
+
})
|
|
161
|
+
.positional('outDir', {
|
|
162
|
+
description:
|
|
163
|
+
'Output directory (locale tree for --format=mo, plain dir for xml/desktop/json)',
|
|
164
|
+
type: 'string',
|
|
165
|
+
normalize: true,
|
|
166
|
+
demandOption: true,
|
|
167
|
+
})
|
|
168
|
+
.option('domain', {
|
|
169
|
+
description: 'Text domain / application ID (e.g. `org.pixelrpg.maker`)',
|
|
170
|
+
type: 'string',
|
|
171
|
+
normalize: true,
|
|
172
|
+
demandOption: true,
|
|
173
|
+
})
|
|
174
|
+
.option('format', {
|
|
175
|
+
description: 'Output format',
|
|
176
|
+
type: 'string',
|
|
177
|
+
choices: ['mo', 'xml', 'desktop', 'json'] as const,
|
|
178
|
+
default: 'mo' as const,
|
|
179
|
+
})
|
|
180
|
+
.option('metainfo', {
|
|
181
|
+
description:
|
|
182
|
+
'For --format=xml: path to the template (`.metainfo.xml.in`) used as msgfmt --template',
|
|
183
|
+
type: 'string',
|
|
184
|
+
normalize: true,
|
|
185
|
+
})
|
|
186
|
+
.option('filename', {
|
|
187
|
+
description: 'Override the output filename (defaults to <domain>.<ext>)',
|
|
188
|
+
type: 'string',
|
|
189
|
+
normalize: true,
|
|
190
|
+
})
|
|
191
|
+
.option('remove-xml-comments', {
|
|
192
|
+
description: 'For --format=xml: strip XML comments from the compiled output',
|
|
193
|
+
type: 'boolean',
|
|
194
|
+
default: true,
|
|
195
|
+
})
|
|
196
|
+
.option('verbose', {
|
|
197
|
+
description: 'Print each msgfmt invocation',
|
|
198
|
+
type: 'boolean',
|
|
199
|
+
default: false,
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
handler: async (args) => {
|
|
203
|
+
const poDir = resolve(args.poDir as string);
|
|
204
|
+
const outDir = resolve(args.outDir as string);
|
|
205
|
+
const domain = args.domain as string;
|
|
206
|
+
const format = (args.format as GettextFormat | undefined) ?? 'mo';
|
|
207
|
+
const metainfo = args.metainfo ? resolve(args.metainfo as string) : undefined;
|
|
208
|
+
const filename = args.filename ?? defaultFilename(domain, format, metainfo);
|
|
209
|
+
const verbose = !!args.verbose;
|
|
210
|
+
const removeXmlComments = !!args['remove-xml-comments'];
|
|
211
|
+
|
|
212
|
+
if (!(await fileExists(poDir))) {
|
|
213
|
+
console.error(`[gjsify gettext] PO directory does not exist: ${poDir}`);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (format === 'xml' && metainfo) {
|
|
220
|
+
await compileBulkXml({
|
|
221
|
+
poDir,
|
|
222
|
+
outDir,
|
|
223
|
+
domain,
|
|
224
|
+
template: metainfo,
|
|
225
|
+
filename,
|
|
226
|
+
removeXmlComments,
|
|
227
|
+
verbose,
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
if (format === 'xml') {
|
|
231
|
+
console.warn(
|
|
232
|
+
'[gjsify gettext] --format=xml without --metainfo: falling back to per-language XML files',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
await compilePerLanguage({
|
|
236
|
+
poDir,
|
|
237
|
+
outDir,
|
|
238
|
+
domain,
|
|
239
|
+
format,
|
|
240
|
+
filename,
|
|
241
|
+
verbose,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
} catch (err: any) {
|
|
245
|
+
if (err?.code === 'ENOENT') {
|
|
246
|
+
console.error(
|
|
247
|
+
'[gjsify gettext] msgfmt not found. Install it via your distro (package: gettext).',
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
if (err?.stderr) process.stderr.write(err.stderr);
|
|
251
|
+
console.error(
|
|
252
|
+
`[gjsify gettext] msgfmt failed${err?.code !== undefined ? ` (exit ${err.code})` : ''}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
process.exitCode = typeof err?.code === 'number' ? err.code : 1;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import type { Command } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
interface GResourceOptions {
|
|
10
|
+
xml: string;
|
|
11
|
+
sourcedir?: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive a default `.gresource` target path from the XML descriptor.
|
|
18
|
+
* `foo.gresource.xml` → `foo.gresource` in the same directory.
|
|
19
|
+
*/
|
|
20
|
+
function defaultTargetFor(xmlPath: string): string {
|
|
21
|
+
const ext = extname(xmlPath);
|
|
22
|
+
const stem = basename(xmlPath, ext);
|
|
23
|
+
return resolve(dirname(xmlPath), stem);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const gresourceCommand: Command<any, GResourceOptions> = {
|
|
27
|
+
command: 'gresource <xml>',
|
|
28
|
+
description:
|
|
29
|
+
'Compile a GResource XML descriptor into a binary .gresource bundle (wraps `glib-compile-resources`).',
|
|
30
|
+
builder: (yargs) => {
|
|
31
|
+
return yargs
|
|
32
|
+
.positional('xml', {
|
|
33
|
+
description: 'Path to the .gresource.xml descriptor',
|
|
34
|
+
type: 'string',
|
|
35
|
+
normalize: true,
|
|
36
|
+
demandOption: true,
|
|
37
|
+
})
|
|
38
|
+
.option('sourcedir', {
|
|
39
|
+
description: 'Directory containing the resource files referenced by <xml>',
|
|
40
|
+
type: 'string',
|
|
41
|
+
normalize: true,
|
|
42
|
+
})
|
|
43
|
+
.option('target', {
|
|
44
|
+
alias: 't',
|
|
45
|
+
description: 'Output .gresource file (default: <xml-without-.xml> next to <xml>)',
|
|
46
|
+
type: 'string',
|
|
47
|
+
normalize: true,
|
|
48
|
+
})
|
|
49
|
+
.option('verbose', {
|
|
50
|
+
description: 'Print the underlying glib-compile-resources invocation',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
handler: async (args) => {
|
|
56
|
+
const xmlPath = resolve(args.xml as string);
|
|
57
|
+
const target = args.target ? resolve(args.target as string) : defaultTargetFor(xmlPath);
|
|
58
|
+
const sourcedir = args.sourcedir
|
|
59
|
+
? resolve(args.sourcedir as string)
|
|
60
|
+
: dirname(xmlPath);
|
|
61
|
+
|
|
62
|
+
const cmdArgs = [
|
|
63
|
+
`--sourcedir=${sourcedir}`,
|
|
64
|
+
`--target=${target}`,
|
|
65
|
+
xmlPath,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
if (args.verbose) {
|
|
69
|
+
console.log(`[gjsify gresource] glib-compile-resources ${cmdArgs.join(' ')}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure the target directory exists — glib-compile-resources writes
|
|
73
|
+
// a temporary file next to the target and fails with ENOENT otherwise.
|
|
74
|
+
await mkdir(dirname(target), { recursive: true });
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { stdout, stderr } = await execFileAsync('glib-compile-resources', cmdArgs);
|
|
78
|
+
if (stdout) process.stdout.write(stdout);
|
|
79
|
+
if (stderr) process.stderr.write(stderr);
|
|
80
|
+
if (args.verbose) {
|
|
81
|
+
console.log(`[gjsify gresource] wrote ${target}`);
|
|
82
|
+
}
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
if (err?.code === 'ENOENT') {
|
|
85
|
+
console.error(
|
|
86
|
+
'[gjsify gresource] glib-compile-resources not found. Install it via your distro (package: glib2-devel / libglib2.0-dev).',
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
if (err?.stderr) process.stderr.write(err.stderr);
|
|
90
|
+
console.error(
|
|
91
|
+
`[gjsify gresource] glib-compile-resources failed${err?.code !== undefined ? ` (exit ${err.code})` : ''}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
process.exitCode = typeof err?.code === 'number' ? err.code : 1;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
package/src/commands/index.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -83,6 +83,7 @@ export class Config {
|
|
|
83
83
|
configData.exclude = cliArgs.exclude || [];
|
|
84
84
|
if (cliArgs.consoleShim !== undefined) configData.consoleShim = cliArgs.consoleShim;
|
|
85
85
|
if (cliArgs.globals !== undefined) configData.globals = cliArgs.globals;
|
|
86
|
+
if (cliArgs.shebang !== undefined) configData.shebang = cliArgs.shebang;
|
|
86
87
|
|
|
87
88
|
merge(configData.library ??= {}, pkg, configData.library);
|
|
88
89
|
merge(configData.typescript ??= {}, tsConfig, configData.typescript);
|
package/src/index.ts
CHANGED
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
import yargs from 'yargs'
|
|
3
3
|
import { hideBin } from 'yargs/helpers'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildCommand as build,
|
|
7
|
+
runCommand as run,
|
|
8
|
+
infoCommand as info,
|
|
9
|
+
checkCommand as check,
|
|
10
|
+
showcaseCommand as showcase,
|
|
11
|
+
createCommand as create,
|
|
12
|
+
gresourceCommand as gresource,
|
|
13
|
+
gettextCommand as gettext,
|
|
14
|
+
} from './commands/index.js'
|
|
6
15
|
import { APP_NAME } from './constants.js'
|
|
7
16
|
|
|
8
17
|
void yargs(hideBin(process.argv))
|
|
9
18
|
.scriptName(APP_NAME)
|
|
10
19
|
.strict()
|
|
11
|
-
// .usage(Config.usage)
|
|
12
20
|
.command(create.command, create.description, create.builder, create.handler)
|
|
13
21
|
.command(build.command, build.description, build.builder, build.handler)
|
|
14
22
|
.command(run.command, run.description, run.builder, run.handler)
|
|
15
23
|
.command(info.command, info.description, info.builder, info.handler)
|
|
16
24
|
.command(check.command, check.description, check.builder, check.handler)
|
|
17
25
|
.command(showcase.command, showcase.description, showcase.builder, showcase.handler)
|
|
26
|
+
.command(gresource.command, gresource.description, gresource.builder, gresource.handler)
|
|
27
|
+
.command(gettext.command, gettext.description, gettext.builder, gettext.handler)
|
|
18
28
|
.demandCommand(1)
|
|
19
29
|
.help().argv
|
|
@@ -55,4 +55,10 @@ export interface CliBuildOptions {
|
|
|
55
55
|
* bundle. Only applies to GJS app builds.
|
|
56
56
|
*/
|
|
57
57
|
globals?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Prepend a `#!/usr/bin/env -S gjs -m` shebang to the output file and mark
|
|
60
|
+
* it executable (chmod 755). Only applies to GJS app builds with a single
|
|
61
|
+
* `--outfile`. Default: false.
|
|
62
|
+
*/
|
|
63
|
+
shebang?: boolean;
|
|
58
64
|
}
|