create-gtkx 1.0.0 → 1.2.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/README.md +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +3 -14
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +10 -19
- package/dist/command.js.map +1 -1
- package/dist/package-managers.d.ts +2 -1
- package/dist/package-managers.d.ts.map +1 -1
- package/dist/package-managers.js +1 -1
- package/dist/package-managers.js.map +1 -1
- package/dist/scaffolder.d.ts +2 -3
- package/dist/scaffolder.d.ts.map +1 -1
- package/dist/scaffolder.js +165 -96
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/.gitignore.ejs +1 -0
- package/dist/templates/data/icons/hicolor/scalable/apps/icon.svg.ejs +5 -0
- package/dist/templates/gtkx.config.ts.ejs +9 -0
- package/dist/templates/package.json.ejs +9 -1
- package/dist/templates/tsconfig.json.ejs +5 -1
- package/dist/templates.d.ts +2 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +2 -2
- package/dist/templates.js.map +1 -1
- package/package.json +9 -4
- package/src/cli.ts +2 -2
- package/src/command.ts +10 -23
- package/src/package-managers.ts +7 -1
- package/src/scaffolder.ts +220 -112
- package/src/templates/.gitignore.ejs +1 -0
- package/src/templates/data/icons/hicolor/scalable/apps/icon.svg.ejs +5 -0
- package/src/templates/gtkx.config.ts.ejs +9 -0
- package/src/templates/package.json.ejs +9 -1
- package/src/templates/tsconfig.json.ejs +5 -1
- package/src/templates.ts +4 -2
package/src/scaffolder.ts
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { isValidApplicationId } from "@gtkx/config/internal";
|
|
3
|
-
import { errorMessage,
|
|
4
|
-
import {
|
|
2
|
+
import { APPLICATION_ID_MAX_LENGTH, isValidApplicationId } from "@gtkx/config/internal";
|
|
3
|
+
import { errorMessage, tryResolveExecutable, upperFirst } from "@gtkx/utils";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync, readdirSync, rmSync, type Stats, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
7
|
import { addDependency, detectPackageManager as nypmDetectPackageManager } from "nypm";
|
|
7
8
|
import { x } from "tinyexec";
|
|
9
|
+
import packageManifest from "../package.json" with { type: "json" };
|
|
8
10
|
import { writeBuildAllowance } from "./build-allowance.js";
|
|
9
11
|
import { OperationCanceledError, ScaffoldAbortedError } from "./errors.js";
|
|
10
12
|
import { getInstallHint } from "./install-hints.js";
|
|
11
13
|
import { updateManifest } from "./manifest.js";
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
isKnownPackageManager,
|
|
16
|
+
PACKAGE_MANAGER_VALUES,
|
|
17
|
+
PACKAGE_MANAGERS,
|
|
18
|
+
type PackageManager,
|
|
19
|
+
} from "./package-managers.js";
|
|
13
20
|
import { isValidProjectName } from "./project-name.js";
|
|
14
21
|
import { listTemplates, renderFile, type TemplateContext } from "./templates.js";
|
|
15
22
|
|
|
16
23
|
type CreateOptions = {
|
|
17
24
|
name?: string | undefined;
|
|
18
25
|
applicationId?: string | undefined;
|
|
19
|
-
packageManager?:
|
|
26
|
+
packageManager?: string | undefined;
|
|
20
27
|
isTypescript?: boolean | undefined;
|
|
21
28
|
shouldIncludeTesting?: boolean | undefined;
|
|
22
29
|
isInteractive?: boolean | undefined;
|
|
@@ -25,11 +32,14 @@ type CreateOptions = {
|
|
|
25
32
|
|
|
26
33
|
type ResolvedOptions = {
|
|
27
34
|
target: string;
|
|
35
|
+
root: string;
|
|
36
|
+
isCurrentDirectory: boolean;
|
|
28
37
|
name: string;
|
|
29
38
|
applicationId: string;
|
|
30
39
|
packageManager: PackageManager;
|
|
31
40
|
isTypescript: boolean;
|
|
32
41
|
shouldIncludeTesting: boolean;
|
|
42
|
+
shouldEmptyTarget: boolean;
|
|
33
43
|
};
|
|
34
44
|
|
|
35
45
|
type InstallDependenciesOptions = {
|
|
@@ -40,20 +50,26 @@ type InstallDependenciesOptions = {
|
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
type InstallAllOptions = {
|
|
43
|
-
|
|
44
|
-
target: string;
|
|
45
|
-
packageManager: PackageManager;
|
|
53
|
+
resolved: ResolvedOptions;
|
|
46
54
|
devDependencies: string[];
|
|
47
55
|
};
|
|
48
56
|
|
|
49
57
|
type InstallFailureOptions = {
|
|
50
58
|
error: unknown;
|
|
51
|
-
|
|
52
|
-
packageManager: PackageManager;
|
|
59
|
+
resolved: ResolvedOptions;
|
|
53
60
|
devDependencies: string[];
|
|
54
61
|
};
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
type SpinnerStep = {
|
|
64
|
+
pending: string;
|
|
65
|
+
done: string;
|
|
66
|
+
failed: string;
|
|
67
|
+
run: () => Promise<void>;
|
|
68
|
+
explain?: ((error: unknown) => void) | undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const selfVersion = packageManifest.version;
|
|
72
|
+
const ICON_TEMPLATE_NAME = "icon.svg";
|
|
57
73
|
|
|
58
74
|
const GTKX_ENV_MODULE_HEADER = `/**
|
|
59
75
|
* Generated by \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
|
|
@@ -83,6 +99,12 @@ To run tests, you need a headless Wayland compositor installed:
|
|
|
83
99
|
Ubuntu: sudo apt install sway`;
|
|
84
100
|
|
|
85
101
|
const RECOVERY_HEADING = "Finish the setup by adding the dependencies again, which is safe to repeat:";
|
|
102
|
+
const APPLICATION_ID_FORMAT_ERROR = "Application ID must be reverse domain notation (e.g., com.example.myapp)";
|
|
103
|
+
const APPLICATION_ID_PREFIX = "com.";
|
|
104
|
+
const APPLICATION_ID_SUFFIX = ".app";
|
|
105
|
+
|
|
106
|
+
const APPLICATION_ID_SEGMENT_LIMIT = APPLICATION_ID_MAX_LENGTH - APPLICATION_ID_PREFIX.length -
|
|
107
|
+
APPLICATION_ID_SUFFIX.length;
|
|
86
108
|
|
|
87
109
|
const pinGtkxDependency = (name: string, version: string): string =>
|
|
88
110
|
name.startsWith("@gtkx/") ? `${name}@^${version}` : name;
|
|
@@ -90,7 +112,30 @@ const pinGtkxDependency = (name: string, version: string): string =>
|
|
|
90
112
|
const getDevCommand = (packageManager: PackageManager): string => DEV_COMMAND[packageManager];
|
|
91
113
|
const getAddCommand = (packageManager: PackageManager): string => ADD_COMMAND[packageManager];
|
|
92
114
|
const titleFromName = (name: string): string => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
93
|
-
|
|
115
|
+
|
|
116
|
+
const gitConfigValue = (key: string): string | null => {
|
|
117
|
+
const git = tryResolveExecutable("git");
|
|
118
|
+
|
|
119
|
+
if (git === undefined) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return execFileSync(git, ["config", "--get", key], { encoding: "utf8" }).trim() || null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const applicationIdSegment = (name: string): string => {
|
|
131
|
+
const compact = name.replaceAll("-", "");
|
|
132
|
+
const segment = /^[A-Za-z_]/.test(compact) ? compact : `_${compact}`;
|
|
133
|
+
|
|
134
|
+
return segment.slice(0, APPLICATION_ID_SEGMENT_LIMIT);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const suggestApplicationId = (name: string): string =>
|
|
138
|
+
`${APPLICATION_ID_PREFIX}${applicationIdSegment(name)}${APPLICATION_ID_SUFFIX}`;
|
|
94
139
|
|
|
95
140
|
const stripTrailingSlashes = (value: string): string => {
|
|
96
141
|
let end = value.length;
|
|
@@ -149,38 +194,60 @@ const validateProjectName = (value: string | undefined): string | undefined => {
|
|
|
149
194
|
return undefined;
|
|
150
195
|
};
|
|
151
196
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return "Application ID is required";
|
|
155
|
-
}
|
|
197
|
+
const validateApplicationIdFormat = (value: string): string | undefined =>
|
|
198
|
+
isValidApplicationId(value) ? undefined : APPLICATION_ID_FORMAT_ERROR;
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
200
|
+
const validateApplicationIdInput = (value: string | undefined): string | undefined =>
|
|
201
|
+
value ? validateApplicationIdFormat(value) : "Application ID is required";
|
|
160
202
|
|
|
161
|
-
|
|
162
|
-
|
|
203
|
+
const validateApplicationIdAnswer = (value: string | undefined): string | undefined =>
|
|
204
|
+
value ? validateApplicationIdFormat(value) : undefined;
|
|
163
205
|
|
|
164
206
|
const fail = (message: string): never => {
|
|
165
207
|
p.log.error(message);
|
|
166
208
|
throw new ScaffoldAbortedError(message);
|
|
167
209
|
};
|
|
168
210
|
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
211
|
+
const requestedPackageManager = (value: string | undefined): PackageManager | undefined => {
|
|
212
|
+
if (value === undefined) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!isKnownPackageManager(value)) {
|
|
217
|
+
return fail(`Unknown package manager "${value}". Expected one of: ${PACKAGE_MANAGER_VALUES.join(", ")}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return value;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const targetStats = (root: string): Stats | undefined => {
|
|
224
|
+
try {
|
|
225
|
+
return statSync(root, { throwIfNoEntry: false });
|
|
226
|
+
} catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const validateTargetDir = (target: string): string | undefined => {
|
|
232
|
+
if (!target) {
|
|
171
233
|
return "Project directory is required";
|
|
172
234
|
}
|
|
173
235
|
|
|
174
|
-
return validateProjectName(deriveProjectName(
|
|
236
|
+
return validateProjectName(deriveProjectName(target));
|
|
175
237
|
};
|
|
176
238
|
|
|
239
|
+
const validateTargetAnswer = (value: string | undefined): string | undefined =>
|
|
240
|
+
validateTargetDir(formatTargetDir(value ?? ""));
|
|
241
|
+
|
|
177
242
|
const promptTarget = async (): Promise<string> =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
formatTargetDir(
|
|
244
|
+
guardCancellation(
|
|
245
|
+
await p.text({
|
|
246
|
+
message: "Project directory",
|
|
247
|
+
placeholder: "my-app",
|
|
248
|
+
validate: validateTargetAnswer,
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
184
251
|
);
|
|
185
252
|
|
|
186
253
|
const promptApplicationId = async (name: string): Promise<string> => {
|
|
@@ -190,8 +257,8 @@ const promptApplicationId = async (name: string): Promise<string> => {
|
|
|
190
257
|
await p.text({
|
|
191
258
|
message: "Application ID",
|
|
192
259
|
placeholder: defaultApplicationId,
|
|
193
|
-
|
|
194
|
-
validate:
|
|
260
|
+
defaultValue: defaultApplicationId,
|
|
261
|
+
validate: validateApplicationIdAnswer,
|
|
195
262
|
}),
|
|
196
263
|
);
|
|
197
264
|
};
|
|
@@ -274,17 +341,11 @@ const emptyDir = (root: string): void => {
|
|
|
274
341
|
}
|
|
275
342
|
};
|
|
276
343
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (nameError) {
|
|
281
|
-
fail(nameError);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const applicationIdError = validateApplicationIdInput(applicationId);
|
|
344
|
+
const prepareTargetDirectory = (root: string, shouldEmptyTarget: boolean): void => {
|
|
345
|
+
mkdirSync(root, { recursive: true });
|
|
285
346
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
347
|
+
if (shouldEmptyTarget) {
|
|
348
|
+
emptyDir(root);
|
|
288
349
|
}
|
|
289
350
|
};
|
|
290
351
|
|
|
@@ -301,43 +362,54 @@ const shouldOverwriteDirectory = async (target: string, options: CreateOptions):
|
|
|
301
362
|
);
|
|
302
363
|
};
|
|
303
364
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
365
|
+
const shouldEmptyTargetDirectory = async (root: string, target: string, options: CreateOptions): Promise<boolean> => {
|
|
366
|
+
const stats = targetStats(root);
|
|
367
|
+
|
|
368
|
+
if (stats === undefined) {
|
|
369
|
+
return false;
|
|
307
370
|
}
|
|
308
371
|
|
|
309
|
-
if (
|
|
310
|
-
|
|
372
|
+
if (!stats.isDirectory()) {
|
|
373
|
+
return fail(`Target "${target}" is not a directory`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (isDirEmpty(root)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
311
379
|
|
|
312
|
-
|
|
380
|
+
if (await shouldOverwriteDirectory(target, options)) {
|
|
381
|
+
return true;
|
|
313
382
|
}
|
|
314
383
|
|
|
315
|
-
fail(`Directory "${target}" is not empty`);
|
|
384
|
+
return fail(`Directory "${target}" is not empty`);
|
|
316
385
|
};
|
|
317
386
|
|
|
318
387
|
const resolveTarget = async (options: CreateOptions): Promise<string> => {
|
|
319
|
-
if (options.name
|
|
320
|
-
return
|
|
388
|
+
if (options.name === undefined && options.isInteractive) {
|
|
389
|
+
return promptTarget();
|
|
321
390
|
}
|
|
322
391
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
392
|
+
const target = formatTargetDir(options.name ?? "");
|
|
393
|
+
const error = validateTargetDir(target);
|
|
326
394
|
|
|
327
|
-
return
|
|
395
|
+
return error === undefined ? target : fail(error);
|
|
328
396
|
};
|
|
329
397
|
|
|
330
398
|
const resolveApplicationId = async (options: CreateOptions, name: string): Promise<string> => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
399
|
+
const applicationId = options.applicationId ??
|
|
400
|
+
(options.isInteractive ? await promptApplicationId(name) : suggestApplicationId(name));
|
|
401
|
+
|
|
402
|
+
const error = validateApplicationIdInput(applicationId);
|
|
334
403
|
|
|
335
|
-
return
|
|
404
|
+
return error === undefined ? applicationId : fail(error);
|
|
336
405
|
};
|
|
337
406
|
|
|
338
|
-
const resolvePackageManager = async (
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
const resolvePackageManager = async (
|
|
408
|
+
requested: PackageManager | undefined,
|
|
409
|
+
options: CreateOptions,
|
|
410
|
+
): Promise<PackageManager> => {
|
|
411
|
+
if (requested !== undefined) {
|
|
412
|
+
return requested;
|
|
341
413
|
}
|
|
342
414
|
|
|
343
415
|
if (options.isInteractive) {
|
|
@@ -364,17 +436,28 @@ const isTestingIncluded = async (options: CreateOptions): Promise<boolean> => {
|
|
|
364
436
|
};
|
|
365
437
|
|
|
366
438
|
const resolveOptions = async (options: CreateOptions): Promise<ResolvedOptions> => {
|
|
439
|
+
const requested = requestedPackageManager(options.packageManager);
|
|
367
440
|
const target = await resolveTarget(options);
|
|
368
441
|
const name = deriveProjectName(target);
|
|
369
442
|
const applicationId = await resolveApplicationId(options, name);
|
|
370
|
-
validateResolvedOptions(name, applicationId);
|
|
371
443
|
const root = resolve(process.cwd(), target);
|
|
372
|
-
|
|
373
|
-
const
|
|
444
|
+
const isCurrentDirectory = root === process.cwd();
|
|
445
|
+
const shouldEmptyTarget = await shouldEmptyTargetDirectory(root, target, options);
|
|
446
|
+
const packageManager = await resolvePackageManager(requested, options);
|
|
374
447
|
const isTypescript = await isTypescriptSelected(options);
|
|
375
448
|
const shouldIncludeTesting = await isTestingIncluded(options);
|
|
376
449
|
|
|
377
|
-
return {
|
|
450
|
+
return {
|
|
451
|
+
target,
|
|
452
|
+
root,
|
|
453
|
+
isCurrentDirectory,
|
|
454
|
+
name,
|
|
455
|
+
applicationId,
|
|
456
|
+
packageManager,
|
|
457
|
+
isTypescript,
|
|
458
|
+
shouldIncludeTesting,
|
|
459
|
+
shouldEmptyTarget,
|
|
460
|
+
};
|
|
378
461
|
};
|
|
379
462
|
|
|
380
463
|
const isTemplateIncluded = (templateRelativePath: string, resolved: ResolvedOptions): boolean => {
|
|
@@ -389,35 +472,42 @@ const isTemplateIncluded = (templateRelativePath: string, resolved: ResolvedOpti
|
|
|
389
472
|
return true;
|
|
390
473
|
};
|
|
391
474
|
|
|
392
|
-
const
|
|
475
|
+
const toScriptExtension = (templateRelativePath: string, isTypescript: boolean): string =>
|
|
393
476
|
isTypescript ? templateRelativePath : templateRelativePath.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
394
477
|
|
|
478
|
+
const toIconPath = (path: string, applicationId: string): string =>
|
|
479
|
+
path.endsWith(ICON_TEMPLATE_NAME) ? `${path.slice(0, -ICON_TEMPLATE_NAME.length)}${applicationId}.svg` : path;
|
|
480
|
+
|
|
481
|
+
const toDestinationPath = (templateRelativePath: string, isTypescript: boolean, applicationId: string): string =>
|
|
482
|
+
toIconPath(toScriptExtension(templateRelativePath, isTypescript), applicationId);
|
|
483
|
+
|
|
395
484
|
const addTestScript = (root: string): void => {
|
|
396
485
|
updateManifest(root, (manifest) => {
|
|
397
486
|
(manifest.scripts as Record<string, string>).test = "vitest run";
|
|
398
487
|
});
|
|
399
488
|
};
|
|
400
489
|
|
|
401
|
-
const
|
|
402
|
-
|
|
490
|
+
const templateContext = (resolved: ResolvedOptions): TemplateContext => ({
|
|
491
|
+
name: resolved.name,
|
|
492
|
+
applicationId: resolved.applicationId,
|
|
493
|
+
title: titleFromName(resolved.name),
|
|
494
|
+
shouldIncludeTesting: resolved.shouldIncludeTesting,
|
|
495
|
+
isTypescript: resolved.isTypescript,
|
|
496
|
+
importExtension: resolved.isTypescript ? ".js" : ".jsx",
|
|
497
|
+
developerName: gitConfigValue("user.name") ?? titleFromName(resolved.name),
|
|
498
|
+
developerEmail: gitConfigValue("user.email"),
|
|
499
|
+
});
|
|
403
500
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
title: titleFromName(name),
|
|
408
|
-
shouldIncludeTesting,
|
|
409
|
-
isTypescript,
|
|
410
|
-
importExtension: isTypescript ? ".js" : ".jsx",
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
mkdirSync(root, { recursive: true });
|
|
501
|
+
const scaffoldProject = async (root: string, resolved: ResolvedOptions): Promise<void> => {
|
|
502
|
+
const { applicationId, isTypescript, shouldIncludeTesting } = resolved;
|
|
503
|
+
const context = templateContext(resolved);
|
|
414
504
|
|
|
415
505
|
for (const template of listTemplates()) {
|
|
416
506
|
if (!isTemplateIncluded(template, resolved)) {
|
|
417
507
|
continue;
|
|
418
508
|
}
|
|
419
509
|
|
|
420
|
-
const destination = join(root, toDestinationPath(template, isTypescript));
|
|
510
|
+
const destination = join(root, toDestinationPath(template, isTypescript, applicationId));
|
|
421
511
|
mkdirSync(dirname(destination), { recursive: true });
|
|
422
512
|
writeFileSync(destination, await renderFile(template, context));
|
|
423
513
|
}
|
|
@@ -455,11 +545,11 @@ const installDependencies = async (options: InstallDependenciesOptions): Promise
|
|
|
455
545
|
|
|
456
546
|
const pin = (names: string[]): string[] => names.map((dependency) => pinGtkxDependency(dependency, selfVersion));
|
|
457
547
|
|
|
458
|
-
const formatRecovery = ({
|
|
459
|
-
const add = getAddCommand(packageManager);
|
|
548
|
+
const formatRecovery = ({ resolved, devDependencies }: InstallFailureOptions): string => {
|
|
549
|
+
const add = getAddCommand(resolved.packageManager);
|
|
460
550
|
|
|
461
551
|
const steps = [
|
|
462
|
-
...(
|
|
552
|
+
...(resolved.isCurrentDirectory ? [] : [`cd ${resolved.target}`]),
|
|
463
553
|
`${add} ${pin(DEPENDENCIES).join(" ")}`,
|
|
464
554
|
`${add} -D ${pin(devDependencies).join(" ")}`,
|
|
465
555
|
];
|
|
@@ -469,10 +559,8 @@ const formatRecovery = ({ target, packageManager, devDependencies }: InstallFail
|
|
|
469
559
|
return `${RECOVERY_HEADING}\n${indented}`;
|
|
470
560
|
};
|
|
471
561
|
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
p.log.error(`Failed to install dependencies: ${message}`);
|
|
475
|
-
const hint = getInstallHint(message);
|
|
562
|
+
const explainInstallFailure = (options: InstallFailureOptions): void => {
|
|
563
|
+
const hint = getInstallHint(errorMessage(options.error));
|
|
476
564
|
|
|
477
565
|
if (hint !== undefined) {
|
|
478
566
|
p.log.warn(hint);
|
|
@@ -481,22 +569,39 @@ const reportInstallFailure = (options: InstallFailureOptions): void => {
|
|
|
481
569
|
p.log.info(formatRecovery(options));
|
|
482
570
|
};
|
|
483
571
|
|
|
484
|
-
const
|
|
485
|
-
const { root, target, packageManager, devDependencies } = options;
|
|
572
|
+
const runWithSpinner = async (step: SpinnerStep): Promise<void> => {
|
|
486
573
|
const spinner = p.spinner();
|
|
487
|
-
spinner.start(
|
|
574
|
+
spinner.start(step.pending);
|
|
488
575
|
|
|
489
576
|
try {
|
|
490
|
-
await
|
|
491
|
-
|
|
492
|
-
spinner.stop("Dependencies installed");
|
|
577
|
+
await step.run();
|
|
578
|
+
spinner.stop(step.done);
|
|
493
579
|
} catch (error) {
|
|
494
|
-
spinner.
|
|
495
|
-
|
|
496
|
-
|
|
580
|
+
spinner.error(step.failed);
|
|
581
|
+
p.log.error(errorMessage(error));
|
|
582
|
+
step.explain?.(error);
|
|
583
|
+
throw new ScaffoldAbortedError(step.failed);
|
|
497
584
|
}
|
|
498
585
|
};
|
|
499
586
|
|
|
587
|
+
const installAllDependencies = async (options: InstallAllOptions): Promise<void> => {
|
|
588
|
+
const { resolved, devDependencies } = options;
|
|
589
|
+
const { root, packageManager } = resolved;
|
|
590
|
+
|
|
591
|
+
await runWithSpinner({
|
|
592
|
+
pending: "Installing dependencies...",
|
|
593
|
+
done: "Dependencies installed",
|
|
594
|
+
failed: "Failed to install dependencies",
|
|
595
|
+
run: async () => {
|
|
596
|
+
await installDependencies({ cwd: root, packageManager, dependencies: pin(DEPENDENCIES), isDev: false });
|
|
597
|
+
await installDependencies({ cwd: root, packageManager, dependencies: pin(devDependencies), isDev: true });
|
|
598
|
+
},
|
|
599
|
+
explain: (error) => {
|
|
600
|
+
explainInstallFailure({ error, resolved, devDependencies });
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
|
|
500
605
|
const writeInitialEnvModule = (root: string): void => {
|
|
501
606
|
const storeDir = join(root, "node_modules", ".gtkx");
|
|
502
607
|
mkdirSync(storeDir, { recursive: true });
|
|
@@ -514,34 +619,37 @@ const initializeGitRepo = async (root: string): Promise<void> => {
|
|
|
514
619
|
await x("git", ["commit", "-m", "Initial commit"], opts);
|
|
515
620
|
spinner.stop("Git repository initialized");
|
|
516
621
|
} catch {
|
|
517
|
-
spinner.
|
|
622
|
+
spinner.error("Failed to initialize git repository");
|
|
518
623
|
}
|
|
519
624
|
};
|
|
520
625
|
|
|
521
626
|
const printNextSteps = (resolved: ResolvedOptions): void => {
|
|
522
627
|
const devCmd = getDevCommand(resolved.packageManager);
|
|
523
|
-
const cdStep = resolved.
|
|
628
|
+
const cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
|
|
524
629
|
const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
|
|
525
630
|
p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
|
|
526
631
|
};
|
|
527
632
|
|
|
633
|
+
const createProjectStructure = async (root: string, resolved: ResolvedOptions): Promise<void> => {
|
|
634
|
+
await runWithSpinner({
|
|
635
|
+
pending: "Creating project structure...",
|
|
636
|
+
done: "Project structure created",
|
|
637
|
+
failed: "Failed to create the project structure",
|
|
638
|
+
run: async () => {
|
|
639
|
+
prepareTargetDirectory(root, resolved.shouldEmptyTarget);
|
|
640
|
+
await scaffoldProject(root, resolved);
|
|
641
|
+
writeBuildAllowance(root, resolved.packageManager);
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
|
|
528
646
|
const scaffold = async (options: CreateOptions = {}): Promise<void> => {
|
|
529
647
|
p.intro("Create GTKX App");
|
|
530
648
|
const resolved = await resolveOptions(options);
|
|
531
|
-
const root =
|
|
649
|
+
const { root } = resolved;
|
|
532
650
|
const devDeps = getDevDependencies(resolved);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
await scaffoldProject(root, resolved);
|
|
536
|
-
writeBuildAllowance(root, resolved.packageManager);
|
|
537
|
-
projectSpinner.stop("Project structure created");
|
|
538
|
-
|
|
539
|
-
await installAllDependencies({
|
|
540
|
-
root,
|
|
541
|
-
target: resolved.target,
|
|
542
|
-
packageManager: resolved.packageManager,
|
|
543
|
-
devDependencies: devDeps,
|
|
544
|
-
});
|
|
651
|
+
await createProjectStructure(root, resolved);
|
|
652
|
+
await installAllDependencies({ resolved, devDependencies: devDeps });
|
|
545
653
|
|
|
546
654
|
if (resolved.isTypescript) {
|
|
547
655
|
writeInitialEnvModule(root);
|
|
@@ -551,4 +659,4 @@ const scaffold = async (options: CreateOptions = {}): Promise<void> => {
|
|
|
551
659
|
printNextSteps(resolved);
|
|
552
660
|
};
|
|
553
661
|
|
|
554
|
-
export { scaffold
|
|
662
|
+
export { scaffold };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<rect x="8" y="8" width="112" height="112" rx="24" fill="#3584e4"/>
|
|
4
|
+
<path d="m 40 66 l 16 16 l 32 -32" fill="none" stroke="#ffffff" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
|
@@ -3,4 +3,13 @@ import { defineConfig } from "@gtkx/config";
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
libraries: ["Gtk-4.0"],
|
|
5
5
|
applicationId: "<%= applicationId %>",
|
|
6
|
+
deploy: {
|
|
7
|
+
name: "<%= title %>",
|
|
8
|
+
summary: "A GTK4 application built with GTKX",
|
|
9
|
+
description: [
|
|
10
|
+
"<%= title %> is a GTK4 and Adwaita application built with GTKX, which renders native GObject "
|
|
11
|
+
+ "widgets from React. Replace this paragraph with a description of what your application does.",
|
|
12
|
+
],
|
|
13
|
+
categories: ["Utility"],
|
|
14
|
+
},
|
|
6
15
|
});
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
"name": "<%= name %>",
|
|
3
3
|
"version": "0.0.1",
|
|
4
4
|
"private": true,
|
|
5
|
+
"description": "A GTK4 application built with GTKX",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
<% if (developerEmail) { -%>
|
|
8
|
+
"author": <%- sourceStringLiteral(`${developerName} <${developerEmail}>`) %>,
|
|
9
|
+
<% } else { -%>
|
|
10
|
+
"author": <%- sourceStringLiteral(developerName) %>,
|
|
11
|
+
<% } -%>
|
|
5
12
|
"type": "module",
|
|
6
13
|
"imports": {
|
|
7
14
|
"#data/*": "./data/*"
|
|
@@ -13,6 +20,7 @@
|
|
|
13
20
|
<% if (isTypescript) { -%>
|
|
14
21
|
"typecheck": "gtkx codegen && tsc",
|
|
15
22
|
<% } -%>
|
|
16
|
-
"start": "node dist/bundle.
|
|
23
|
+
"start": "node dist/bundle.mjs",
|
|
24
|
+
"deploy": "gtkx deploy"
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"resolveJsonModule": true,
|
|
10
10
|
"lib": ["esnext"],
|
|
11
11
|
"types": ["node", "react"],
|
|
12
|
-
"noEmit": true
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"@gtkx/gi/*": ["./node_modules/.gtkx/gi/*/index.d.ts", "./node_modules/.gtkx/gi/*.d.ts"],
|
|
15
|
+
"@gtkx/jsx/*": ["./node_modules/.gtkx/jsx/*/index.d.ts", "./node_modules/.gtkx/jsx/*.d.ts"]
|
|
16
|
+
}
|
|
13
17
|
},
|
|
14
18
|
<% if (shouldIncludeTesting) { -%>
|
|
15
19
|
"include": ["src/**/*", "tests/**/*"]
|
package/src/templates.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sortStrings } from "@gtkx/utils";
|
|
1
|
+
import { sortStrings, sourceStringLiteral } from "@gtkx/utils";
|
|
2
2
|
import ejs from "ejs";
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -10,6 +10,8 @@ type TemplateContext = {
|
|
|
10
10
|
shouldIncludeTesting: boolean;
|
|
11
11
|
isTypescript: boolean;
|
|
12
12
|
importExtension: string;
|
|
13
|
+
developerName: string;
|
|
14
|
+
developerEmail: string | null;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
const getTemplatesDir = (): string => {
|
|
@@ -26,6 +28,6 @@ const listTemplates = (): string[] =>
|
|
|
26
28
|
);
|
|
27
29
|
|
|
28
30
|
const renderFile = async (templateName: string, context: TemplateContext): Promise<string> =>
|
|
29
|
-
ejs.renderFile(join(getTemplatesDir(), `${templateName}.ejs`), context);
|
|
31
|
+
ejs.renderFile(join(getTemplatesDir(), `${templateName}.ejs`), { ...context, sourceStringLiteral });
|
|
30
32
|
|
|
31
33
|
export { listTemplates, renderFile, type TemplateContext };
|