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/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, packageVersion, upperFirst } from "@gtkx/utils";
4
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
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 { isKnownPackageManager, PACKAGE_MANAGERS, type PackageManager } from "./package-managers.js";
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?: PackageManager | undefined;
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
- root: string;
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
- target: string;
52
- packageManager: PackageManager;
59
+ resolved: ResolvedOptions;
53
60
  devDependencies: string[];
54
61
  };
55
62
 
56
- const selfVersion = packageVersion(import.meta.url, "../package.json");
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
- const suggestApplicationId = (name: string): string => `com.${name.replaceAll("-", "")}.app`;
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 validateApplicationIdInput = (value: string | undefined): string | undefined => {
153
- if (!value) {
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
- if (!isValidApplicationId(value)) {
158
- return "Application ID must be reverse domain notation (e.g., com.example.myapp)";
159
- }
200
+ const validateApplicationIdInput = (value: string | undefined): string | undefined =>
201
+ value ? validateApplicationIdFormat(value) : "Application ID is required";
160
202
 
161
- return undefined;
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 validateTarget = (value: string | undefined): string | undefined => {
170
- if (!value) {
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(formatTargetDir(value)));
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
- guardCancellation(
179
- await p.text({
180
- message: "Project directory",
181
- placeholder: "my-app",
182
- validate: validateTarget,
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
- initialValue: defaultApplicationId,
194
- validate: validateApplicationIdInput,
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 validateResolvedOptions = (name: string, applicationId: string): void => {
278
- const nameError = validateProjectName(name);
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 (applicationIdError) {
287
- fail(applicationIdError);
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 handleTargetDirectory = async (root: string, target: string, options: CreateOptions): Promise<void> => {
305
- if (!existsSync(root) || isDirEmpty(root)) {
306
- return;
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 (await shouldOverwriteDirectory(target, options)) {
310
- emptyDir(root);
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
- return;
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 !== undefined) {
320
- return formatTargetDir(options.name);
388
+ if (options.name === undefined && options.isInteractive) {
389
+ return promptTarget();
321
390
  }
322
391
 
323
- if (!options.isInteractive) {
324
- return fail("Project directory is required");
325
- }
392
+ const target = formatTargetDir(options.name ?? "");
393
+ const error = validateTargetDir(target);
326
394
 
327
- return formatTargetDir(await promptTarget());
395
+ return error === undefined ? target : fail(error);
328
396
  };
329
397
 
330
398
  const resolveApplicationId = async (options: CreateOptions, name: string): Promise<string> => {
331
- if (options.applicationId !== undefined) {
332
- return options.applicationId;
333
- }
399
+ const applicationId = options.applicationId ??
400
+ (options.isInteractive ? await promptApplicationId(name) : suggestApplicationId(name));
401
+
402
+ const error = validateApplicationIdInput(applicationId);
334
403
 
335
- return options.isInteractive ? promptApplicationId(name) : suggestApplicationId(name);
404
+ return error === undefined ? applicationId : fail(error);
336
405
  };
337
406
 
338
- const resolvePackageManager = async (options: CreateOptions): Promise<PackageManager> => {
339
- if (options.packageManager !== undefined) {
340
- return options.packageManager;
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
- await handleTargetDirectory(root, target, options);
373
- const packageManager = await resolvePackageManager(options);
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 { target, name, applicationId, packageManager, isTypescript, shouldIncludeTesting };
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 toDestinationPath = (templateRelativePath: string, isTypescript: boolean): string =>
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 scaffoldProject = async (root: string, resolved: ResolvedOptions): Promise<void> => {
402
- const { name, applicationId, isTypescript, shouldIncludeTesting } = resolved;
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
- const context: TemplateContext = {
405
- name,
406
- applicationId,
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 = ({ target, packageManager, devDependencies }: InstallFailureOptions): string => {
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
- ...(target === "." ? [] : [`cd ${target}`]),
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 reportInstallFailure = (options: InstallFailureOptions): void => {
473
- const message = errorMessage(options.error);
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 installAllDependencies = async (options: InstallAllOptions): Promise<void> => {
485
- const { root, target, packageManager, devDependencies } = options;
572
+ const runWithSpinner = async (step: SpinnerStep): Promise<void> => {
486
573
  const spinner = p.spinner();
487
- spinner.start("Installing dependencies...");
574
+ spinner.start(step.pending);
488
575
 
489
576
  try {
490
- await installDependencies({ cwd: root, packageManager, dependencies: pin(DEPENDENCIES), isDev: false });
491
- await installDependencies({ cwd: root, packageManager, dependencies: pin(devDependencies), isDev: true });
492
- spinner.stop("Dependencies installed");
577
+ await step.run();
578
+ spinner.stop(step.done);
493
579
  } catch (error) {
494
- spinner.stop("Failed to install dependencies");
495
- reportInstallFailure({ error, target, packageManager, devDependencies });
496
- throw new ScaffoldAbortedError("Failed to install dependencies");
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.stop("Failed to initialize git repository");
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.target === "." ? "" : `cd ${resolved.target}\n`;
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 = resolve(process.cwd(), resolved.target);
649
+ const { root } = resolved;
532
650
  const devDeps = getDevDependencies(resolved);
533
- const projectSpinner = p.spinner();
534
- projectSpinner.start("Creating project structure...");
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, type CreateOptions };
662
+ export { scaffold };
@@ -1,3 +1,4 @@
1
1
  node_modules/
2
2
  dist/
3
+ build/
3
4
  *.log
@@ -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.js"
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 };