create-gtkx 1.6.0 → 2.0.0-beta.2
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 +5 -5
- package/dist/command.d.ts +5 -1
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +6 -1
- package/dist/command.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -4
- package/dist/manifest.js.map +1 -1
- package/dist/scaffolder.d.ts +1 -0
- package/dist/scaffolder.d.ts.map +1 -1
- package/dist/scaffolder.js +171 -101
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/gtkx.config.ts.ejs +2 -12
- package/dist/templates/mise.toml.ejs +2 -0
- package/dist/templates/package.json.ejs +3 -0
- package/dist/templates/src/app.tsx.ejs +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +5 -7
- package/dist/templates.js.map +1 -1
- package/package.json +8 -6
- package/src/command.ts +7 -1
- package/src/manifest.ts +10 -4
- package/src/scaffolder.ts +230 -134
- package/src/templates/gtkx.config.ts.ejs +2 -12
- package/src/templates/mise.toml.ejs +2 -0
- package/src/templates/package.json.ejs +3 -0
- package/src/templates/src/app.tsx.ejs +1 -1
- package/src/templates.ts +6 -8
package/src/scaffolder.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import { APPLICATION_ID_MAX_LENGTH, isValidApplicationId } from "@gtkx/config/internal";
|
|
3
3
|
import { errorMessage, tryResolveExecutable, upperFirst } from "@gtkx/utils";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { mkdirSync, readdirSync, rmSync,
|
|
6
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { lstatSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { addDependency, detectPackageManager as nypmDetectPackageManager } from "nypm";
|
|
8
8
|
import { x } from "tinyexec";
|
|
9
9
|
import packageManifest from "../package.json" with { type: "json" };
|
|
@@ -23,6 +23,7 @@ import { listTemplates, renderFile, type TemplateContext } from "./templates.js"
|
|
|
23
23
|
type CreateOptions = {
|
|
24
24
|
name?: string | undefined;
|
|
25
25
|
applicationId?: string | undefined;
|
|
26
|
+
displayName?: string | undefined;
|
|
26
27
|
packageManager?: string | undefined;
|
|
27
28
|
isTypescript?: boolean | undefined;
|
|
28
29
|
shouldIncludeTesting?: boolean | undefined;
|
|
@@ -35,13 +36,16 @@ type ResolvedOptions = {
|
|
|
35
36
|
root: string;
|
|
36
37
|
isCurrentDirectory: boolean;
|
|
37
38
|
name: string;
|
|
39
|
+
displayName: string;
|
|
38
40
|
applicationId: string;
|
|
39
41
|
packageManager: PackageManager;
|
|
40
42
|
isTypescript: boolean;
|
|
41
43
|
shouldIncludeTesting: boolean;
|
|
42
|
-
|
|
44
|
+
shouldInitializeGit: boolean;
|
|
43
45
|
};
|
|
44
46
|
|
|
47
|
+
type ScaffoldFile = { destination: string; contents: string };
|
|
48
|
+
|
|
45
49
|
type InstallDependenciesOptions = {
|
|
46
50
|
cwd: string;
|
|
47
51
|
packageManager: PackageManager;
|
|
@@ -106,12 +110,8 @@ const APPLICATION_ID_SUFFIX = ".app";
|
|
|
106
110
|
const APPLICATION_ID_SEGMENT_LIMIT = APPLICATION_ID_MAX_LENGTH - APPLICATION_ID_PREFIX.length -
|
|
107
111
|
APPLICATION_ID_SUFFIX.length;
|
|
108
112
|
|
|
109
|
-
const
|
|
110
|
-
name.
|
|
111
|
-
|
|
112
|
-
const getDevCommand = (packageManager: PackageManager): string => DEV_COMMAND[packageManager];
|
|
113
|
-
const getAddCommand = (packageManager: PackageManager): string => ADD_COMMAND[packageManager];
|
|
114
|
-
const titleFromName = (name: string): string => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
113
|
+
const displayNameFromProjectName = (name: string): string =>
|
|
114
|
+
name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
115
115
|
|
|
116
116
|
const gitConfigValue = (key: string): string | null => {
|
|
117
117
|
const git = tryResolveExecutable("git");
|
|
@@ -147,7 +147,9 @@ const stripTrailingSlashes = (value: string): string => {
|
|
|
147
147
|
return value.slice(0, end);
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
-
const formatTargetDir = (target: string): string =>
|
|
150
|
+
const formatTargetDir = (target: string): string =>
|
|
151
|
+
stripTrailingSlashes(target.trim().replaceAll(/[<>:"\\|?*]/g, ""));
|
|
152
|
+
|
|
151
153
|
const deriveProjectName = (target: string): string => basename(resolve(process.cwd(), target));
|
|
152
154
|
|
|
153
155
|
const isDirEmpty = (dir: string): boolean => {
|
|
@@ -156,6 +158,12 @@ const isDirEmpty = (dir: string): boolean => {
|
|
|
156
158
|
return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
|
|
157
159
|
};
|
|
158
160
|
|
|
161
|
+
const shouldInitializeGit = (root: string): boolean => {
|
|
162
|
+
const entry = lstatSync(root, { throwIfNoEntry: false });
|
|
163
|
+
|
|
164
|
+
return entry === undefined || (entry.isDirectory() && readdirSync(root).length === 0);
|
|
165
|
+
};
|
|
166
|
+
|
|
159
167
|
const getDevDependencies = ({
|
|
160
168
|
isTypescript,
|
|
161
169
|
shouldIncludeTesting,
|
|
@@ -203,6 +211,17 @@ const validateApplicationIdInput = (value: string | undefined): string | undefin
|
|
|
203
211
|
const validateApplicationIdAnswer = (value: string | undefined): string | undefined =>
|
|
204
212
|
value ? validateApplicationIdFormat(value) : undefined;
|
|
205
213
|
|
|
214
|
+
const validateDisplayName = (value: string | undefined): string | undefined => {
|
|
215
|
+
if (value === undefined || value.trim().length === 0) {
|
|
216
|
+
return "Display name is required";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return value.includes("\n") || value.includes("\r") ? "Display name must be a single line" : undefined;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const validateDisplayNameAnswer = (value: string | undefined): string | undefined =>
|
|
223
|
+
value ? validateDisplayName(value) : undefined;
|
|
224
|
+
|
|
206
225
|
const fail = (message: string): never => {
|
|
207
226
|
p.log.error(message);
|
|
208
227
|
throw new ScaffoldAbortedError(message);
|
|
@@ -220,14 +239,6 @@ const requestedPackageManager = (value: string | undefined): PackageManager | un
|
|
|
220
239
|
return value;
|
|
221
240
|
};
|
|
222
241
|
|
|
223
|
-
const targetStats = (root: string): Stats | undefined => {
|
|
224
|
-
try {
|
|
225
|
-
return statSync(root, { throwIfNoEntry: false });
|
|
226
|
-
} catch {
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
242
|
const validateTargetDir = (target: string): string | undefined => {
|
|
232
243
|
if (!target) {
|
|
233
244
|
return "Project directory is required";
|
|
@@ -263,40 +274,36 @@ const promptApplicationId = async (name: string): Promise<string> => {
|
|
|
263
274
|
);
|
|
264
275
|
};
|
|
265
276
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
277
|
+
const promptDisplayName = async (name: string): Promise<string> => {
|
|
278
|
+
const defaultDisplayName = displayNameFromProjectName(name);
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
return guardCancellation(
|
|
281
|
+
await p.text({
|
|
282
|
+
message: "Display name",
|
|
283
|
+
placeholder: defaultDisplayName,
|
|
284
|
+
defaultValue: defaultDisplayName,
|
|
285
|
+
validate: validateDisplayNameAnswer,
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
274
288
|
};
|
|
275
289
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
detected: PackageManager | undefined,
|
|
279
|
-
): string | undefined => {
|
|
280
|
-
if (detected === manager.value) {
|
|
281
|
-
return "detected";
|
|
282
|
-
}
|
|
290
|
+
const packageManagerOption = (manager: (typeof PACKAGE_MANAGERS)[number], detected: PackageManager | undefined) => {
|
|
291
|
+
let hint: string | undefined;
|
|
283
292
|
|
|
284
|
-
if (manager.
|
|
285
|
-
|
|
293
|
+
if (detected === manager.value) {
|
|
294
|
+
hint = "detected";
|
|
295
|
+
} else if (manager.isRecommended) {
|
|
296
|
+
hint = "recommended";
|
|
286
297
|
}
|
|
287
298
|
|
|
288
|
-
return undefined;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const packageManagerOption = (manager: (typeof PACKAGE_MANAGERS)[number], detected: PackageManager | undefined) => {
|
|
292
|
-
const hint = packageManagerHint(manager, detected);
|
|
293
|
-
|
|
294
299
|
return { value: manager.value, label: manager.label, ...(hint !== undefined && { hint }) };
|
|
295
300
|
};
|
|
296
301
|
|
|
297
302
|
const detectedPackageManager = async (): Promise<PackageManager | undefined> => {
|
|
298
303
|
try {
|
|
299
|
-
|
|
304
|
+
const detected = await nypmDetectPackageManager(process.cwd(), { includeParentDirs: true });
|
|
305
|
+
|
|
306
|
+
return detected && isKnownPackageManager(detected.name) ? detected.name : undefined;
|
|
300
307
|
} catch {
|
|
301
308
|
return undefined;
|
|
302
309
|
}
|
|
@@ -315,39 +322,15 @@ const promptPackageManager = async (): Promise<PackageManager> => {
|
|
|
315
322
|
);
|
|
316
323
|
};
|
|
317
324
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
guardCancellation(
|
|
328
|
-
await p.confirm({
|
|
329
|
-
message: "Include testing setup (Vitest)?",
|
|
330
|
-
initialValue: true,
|
|
331
|
-
}),
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
const emptyDir = (root: string): void => {
|
|
335
|
-
for (const entry of readdirSync(root)) {
|
|
336
|
-
if (entry === ".git") {
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
rmSync(join(root, entry), { recursive: true, force: true });
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const prepareTargetDirectory = (root: string, shouldEmptyTarget: boolean): void => {
|
|
345
|
-
mkdirSync(root, { recursive: true });
|
|
346
|
-
|
|
347
|
-
if (shouldEmptyTarget) {
|
|
348
|
-
emptyDir(root);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
325
|
+
const isOptionEnabled = async (
|
|
326
|
+
value: boolean | undefined,
|
|
327
|
+
isInteractive: boolean | undefined,
|
|
328
|
+
message: string,
|
|
329
|
+
): Promise<boolean> =>
|
|
330
|
+
value ??
|
|
331
|
+
(isInteractive
|
|
332
|
+
? guardCancellation(await p.confirm({ message, initialValue: true }))
|
|
333
|
+
: true);
|
|
351
334
|
|
|
352
335
|
const shouldOverwriteDirectory = async (target: string, options: CreateOptions): Promise<boolean> => {
|
|
353
336
|
if (!options.isInteractive) {
|
|
@@ -356,17 +339,17 @@ const shouldOverwriteDirectory = async (target: string, options: CreateOptions):
|
|
|
356
339
|
|
|
357
340
|
return guardCancellation(
|
|
358
341
|
await p.confirm({
|
|
359
|
-
message: `Directory "${target}" is not empty.
|
|
342
|
+
message: `Directory "${target}" is not empty. Replace scaffold files?`,
|
|
360
343
|
initialValue: false,
|
|
361
344
|
}),
|
|
362
345
|
);
|
|
363
346
|
};
|
|
364
347
|
|
|
365
|
-
const
|
|
366
|
-
const stats =
|
|
348
|
+
const validateTargetAvailability = async (root: string, target: string, options: CreateOptions): Promise<void> => {
|
|
349
|
+
const stats = statSync(root, { throwIfNoEntry: false });
|
|
367
350
|
|
|
368
351
|
if (stats === undefined) {
|
|
369
|
-
return
|
|
352
|
+
return;
|
|
370
353
|
}
|
|
371
354
|
|
|
372
355
|
if (!stats.isDirectory()) {
|
|
@@ -374,14 +357,14 @@ const shouldEmptyTargetDirectory = async (root: string, target: string, options:
|
|
|
374
357
|
}
|
|
375
358
|
|
|
376
359
|
if (isDirEmpty(root)) {
|
|
377
|
-
return
|
|
360
|
+
return;
|
|
378
361
|
}
|
|
379
362
|
|
|
380
363
|
if (await shouldOverwriteDirectory(target, options)) {
|
|
381
|
-
return
|
|
364
|
+
return;
|
|
382
365
|
}
|
|
383
366
|
|
|
384
|
-
|
|
367
|
+
fail(`Directory "${target}" is not empty`);
|
|
385
368
|
};
|
|
386
369
|
|
|
387
370
|
const resolveTarget = async (options: CreateOptions): Promise<string> => {
|
|
@@ -404,6 +387,14 @@ const resolveApplicationId = async (options: CreateOptions, name: string): Promi
|
|
|
404
387
|
return error === undefined ? applicationId : fail(error);
|
|
405
388
|
};
|
|
406
389
|
|
|
390
|
+
const resolveDisplayName = async (options: CreateOptions, name: string): Promise<string> => {
|
|
391
|
+
const value = options.displayName ??
|
|
392
|
+
(options.isInteractive ? await promptDisplayName(name) : displayNameFromProjectName(name));
|
|
393
|
+
const error = validateDisplayName(value);
|
|
394
|
+
|
|
395
|
+
return error === undefined ? value.trim() : fail(error);
|
|
396
|
+
};
|
|
397
|
+
|
|
407
398
|
const resolvePackageManager = async (
|
|
408
399
|
requested: PackageManager | undefined,
|
|
409
400
|
options: CreateOptions,
|
|
@@ -419,44 +410,36 @@ const resolvePackageManager = async (
|
|
|
419
410
|
return (await detectedPackageManager()) ?? "pnpm";
|
|
420
411
|
};
|
|
421
412
|
|
|
422
|
-
const isTypescriptSelected = async (options: CreateOptions): Promise<boolean> => {
|
|
423
|
-
if (options.isTypescript !== undefined) {
|
|
424
|
-
return options.isTypescript;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return options.isInteractive ? shouldUseTypeScript() : true;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const isTestingIncluded = async (options: CreateOptions): Promise<boolean> => {
|
|
431
|
-
if (options.shouldIncludeTesting !== undefined) {
|
|
432
|
-
return options.shouldIncludeTesting;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return options.isInteractive ? shouldSetUpTesting() : true;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
413
|
const resolveOptions = async (options: CreateOptions): Promise<ResolvedOptions> => {
|
|
439
414
|
const requested = requestedPackageManager(options.packageManager);
|
|
440
415
|
const target = await resolveTarget(options);
|
|
441
416
|
const name = deriveProjectName(target);
|
|
417
|
+
const displayName = await resolveDisplayName(options, name);
|
|
442
418
|
const applicationId = await resolveApplicationId(options, name);
|
|
443
419
|
const root = resolve(process.cwd(), target);
|
|
444
420
|
const isCurrentDirectory = root === process.cwd();
|
|
445
|
-
const
|
|
421
|
+
const shouldInitialize = shouldInitializeGit(root);
|
|
422
|
+
await validateTargetAvailability(root, target, options);
|
|
446
423
|
const packageManager = await resolvePackageManager(requested, options);
|
|
447
|
-
const isTypescript = await
|
|
448
|
-
|
|
424
|
+
const isTypescript = await isOptionEnabled(options.isTypescript, options.isInteractive, "Use TypeScript?");
|
|
425
|
+
|
|
426
|
+
const shouldIncludeTesting = await isOptionEnabled(
|
|
427
|
+
options.shouldIncludeTesting,
|
|
428
|
+
options.isInteractive,
|
|
429
|
+
"Include testing setup (Vitest)?",
|
|
430
|
+
);
|
|
449
431
|
|
|
450
432
|
return {
|
|
451
433
|
target,
|
|
452
434
|
root,
|
|
453
435
|
isCurrentDirectory,
|
|
454
436
|
name,
|
|
437
|
+
displayName,
|
|
455
438
|
applicationId,
|
|
456
439
|
packageManager,
|
|
457
440
|
isTypescript,
|
|
458
441
|
shouldIncludeTesting,
|
|
459
|
-
|
|
442
|
+
shouldInitializeGit: shouldInitialize,
|
|
460
443
|
};
|
|
461
444
|
};
|
|
462
445
|
|
|
@@ -472,48 +455,140 @@ const isTemplateIncluded = (templateRelativePath: string, resolved: ResolvedOpti
|
|
|
472
455
|
return true;
|
|
473
456
|
};
|
|
474
457
|
|
|
475
|
-
const
|
|
476
|
-
isTypescript ?
|
|
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;
|
|
458
|
+
const destinationPath = (template: string, resolved: ResolvedOptions): string => {
|
|
459
|
+
const scriptPath = resolved.isTypescript ? template : template.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
480
460
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const addTestScript = (root: string): void => {
|
|
485
|
-
updateManifest(root, (manifest) => {
|
|
486
|
-
(manifest.scripts as Record<string, string>).test = "vitest run";
|
|
487
|
-
});
|
|
461
|
+
return scriptPath.endsWith(ICON_TEMPLATE_NAME)
|
|
462
|
+
? `${scriptPath.slice(0, -ICON_TEMPLATE_NAME.length)}${resolved.applicationId}.svg`
|
|
463
|
+
: scriptPath;
|
|
488
464
|
};
|
|
489
465
|
|
|
490
466
|
const templateContext = (resolved: ResolvedOptions): TemplateContext => ({
|
|
491
467
|
name: resolved.name,
|
|
492
468
|
applicationId: resolved.applicationId,
|
|
493
|
-
|
|
469
|
+
displayName: resolved.displayName,
|
|
494
470
|
shouldIncludeTesting: resolved.shouldIncludeTesting,
|
|
495
471
|
isTypescript: resolved.isTypescript,
|
|
496
472
|
importExtension: resolved.isTypescript ? ".js" : ".jsx",
|
|
497
|
-
developerName: gitConfigValue("user.name") ??
|
|
473
|
+
developerName: gitConfigValue("user.name") ?? resolved.displayName,
|
|
498
474
|
developerEmail: gitConfigValue("user.email"),
|
|
499
475
|
});
|
|
500
476
|
|
|
501
|
-
const
|
|
502
|
-
const
|
|
477
|
+
const replaceTemplateDestination = (destination: string): void => {
|
|
478
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
479
|
+
|
|
480
|
+
if (entry === undefined) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
485
|
+
fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
rmSync(destination, { force: true });
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const projectRelativeDestination = (root: string, destination: string): string => {
|
|
492
|
+
const relativePath = relative(root, destination);
|
|
493
|
+
const isOutsideProject = relativePath.length === 0 || relativePath.startsWith(`..${sep}`) ||
|
|
494
|
+
isAbsolute(relativePath);
|
|
495
|
+
|
|
496
|
+
if (isOutsideProject) {
|
|
497
|
+
fail(`Scaffold destination is outside the project: ${destination}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return relativePath;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const assertRootIsNotSymbolicLink = (root: string): void => {
|
|
504
|
+
const rootEntry = lstatSync(root, { throwIfNoEntry: false });
|
|
505
|
+
|
|
506
|
+
if (rootEntry?.isSymbolicLink()) {
|
|
507
|
+
fail(`Cannot scaffold through a symbolic link: ${root}`);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const assertSafeAncestor = (ancestor: string): void => {
|
|
512
|
+
const entry = lstatSync(ancestor, { throwIfNoEntry: false });
|
|
513
|
+
|
|
514
|
+
if (entry?.isSymbolicLink()) {
|
|
515
|
+
fail(`Cannot scaffold through a symbolic link: ${ancestor}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (entry !== undefined && !entry.isDirectory()) {
|
|
519
|
+
fail(`Cannot scaffold through a non-directory path: ${ancestor}`);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const assertSafeAncestors = (root: string, relativePath: string): void => {
|
|
524
|
+
let ancestor = root;
|
|
525
|
+
const segments = relativePath.split(sep);
|
|
526
|
+
const ancestorSegments = segments.slice(0, -1);
|
|
527
|
+
|
|
528
|
+
for (const segment of ancestorSegments) {
|
|
529
|
+
ancestor = join(ancestor, segment);
|
|
530
|
+
assertSafeAncestor(ancestor);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const assertReplaceableDestination = (destination: string): void => {
|
|
535
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
536
|
+
const isReplaceable = entry === undefined || entry.isFile() || entry.isSymbolicLink();
|
|
537
|
+
|
|
538
|
+
if (!isReplaceable) {
|
|
539
|
+
fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const assertSafeDestination = (root: string, destination: string): void => {
|
|
544
|
+
const relativePath = projectRelativeDestination(root, destination);
|
|
545
|
+
assertRootIsNotSymbolicLink(root);
|
|
546
|
+
assertSafeAncestors(root, relativePath);
|
|
547
|
+
assertReplaceableDestination(destination);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const plannedScaffoldFiles = async (root: string, resolved: ResolvedOptions): Promise<ScaffoldFile[]> => {
|
|
503
551
|
const context = templateContext(resolved);
|
|
552
|
+
const files: ScaffoldFile[] = [];
|
|
504
553
|
|
|
505
554
|
for (const template of listTemplates()) {
|
|
506
|
-
if (
|
|
507
|
-
|
|
555
|
+
if (isTemplateIncluded(template, resolved)) {
|
|
556
|
+
files.push({
|
|
557
|
+
destination: join(root, destinationPath(template, resolved)),
|
|
558
|
+
contents: await renderFile(template, context),
|
|
559
|
+
});
|
|
508
560
|
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return files;
|
|
564
|
+
};
|
|
509
565
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
566
|
+
const auxiliaryDestinations = (root: string, resolved: ResolvedOptions): string[] => [
|
|
567
|
+
...(resolved.packageManager === "pnpm" ? [join(root, "pnpm-workspace.yaml")] : []),
|
|
568
|
+
...(resolved.isTypescript ? [join(root, "node_modules", ".gtkx", "env.d.ts")] : []),
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
const scaffoldProject = async (root: string, resolved: ResolvedOptions): Promise<void> => {
|
|
572
|
+
const files = await plannedScaffoldFiles(root, resolved);
|
|
573
|
+
const destinations = [...files.map((file) => file.destination), ...auxiliaryDestinations(root, resolved)];
|
|
574
|
+
|
|
575
|
+
for (const destination of destinations) {
|
|
576
|
+
assertSafeDestination(root, destination);
|
|
513
577
|
}
|
|
514
578
|
|
|
515
|
-
|
|
516
|
-
|
|
579
|
+
for (const destination of destinations) {
|
|
580
|
+
replaceTemplateDestination(destination);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (const file of files) {
|
|
584
|
+
mkdirSync(dirname(file.destination), { recursive: true });
|
|
585
|
+
writeFileSync(file.destination, file.contents);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (resolved.shouldIncludeTesting) {
|
|
589
|
+
updateManifest(root, (manifest) => {
|
|
590
|
+
(manifest.scripts as Record<string, string>).test = "vitest run";
|
|
591
|
+
});
|
|
517
592
|
}
|
|
518
593
|
};
|
|
519
594
|
|
|
@@ -543,10 +618,11 @@ const installDependencies = async (options: InstallDependenciesOptions): Promise
|
|
|
543
618
|
});
|
|
544
619
|
};
|
|
545
620
|
|
|
546
|
-
const pin = (names: string[]): string[] =>
|
|
621
|
+
const pin = (names: string[]): string[] =>
|
|
622
|
+
names.map((dependency) => dependency.startsWith("@gtkx/") ? `${dependency}@^${selfVersion}` : dependency);
|
|
547
623
|
|
|
548
624
|
const formatRecovery = ({ resolved, devDependencies }: InstallFailureOptions): string => {
|
|
549
|
-
const add =
|
|
625
|
+
const add = ADD_COMMAND[resolved.packageManager];
|
|
550
626
|
|
|
551
627
|
const steps = [
|
|
552
628
|
...(resolved.isCurrentDirectory ? [] : [`cd ${resolved.target}`]),
|
|
@@ -608,7 +684,27 @@ const writeInitialEnvModule = (root: string): void => {
|
|
|
608
684
|
writeFileSync(join(storeDir, "env.d.ts"), `${GTKX_ENV_MODULE_HEADER}\n`);
|
|
609
685
|
};
|
|
610
686
|
|
|
611
|
-
const
|
|
687
|
+
const isInsideGitRepository = (root: string): boolean => {
|
|
688
|
+
const git = tryResolveExecutable("git");
|
|
689
|
+
|
|
690
|
+
if (git === undefined) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
execFileSync(git, ["-C", root, "rev-parse", "--show-toplevel"], { stdio: "ignore" });
|
|
696
|
+
|
|
697
|
+
return true;
|
|
698
|
+
} catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const initializeGitRepo = async (root: string, shouldInitialize: boolean): Promise<void> => {
|
|
704
|
+
if (!shouldInitialize || isInsideGitRepository(root)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
612
708
|
const spinner = p.spinner();
|
|
613
709
|
spinner.start("Initializing git repository...");
|
|
614
710
|
|
|
@@ -624,7 +720,7 @@ const initializeGitRepo = async (root: string): Promise<void> => {
|
|
|
624
720
|
};
|
|
625
721
|
|
|
626
722
|
const printNextSteps = (resolved: ResolvedOptions): void => {
|
|
627
|
-
const devCmd =
|
|
723
|
+
const devCmd = DEV_COMMAND[resolved.packageManager];
|
|
628
724
|
const cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
|
|
629
725
|
const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
|
|
630
726
|
p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
|
|
@@ -636,9 +732,13 @@ const createProjectStructure = async (root: string, resolved: ResolvedOptions):
|
|
|
636
732
|
done: "Project structure created",
|
|
637
733
|
failed: "Failed to create the project structure",
|
|
638
734
|
run: async () => {
|
|
639
|
-
|
|
735
|
+
mkdirSync(root, { recursive: true });
|
|
640
736
|
await scaffoldProject(root, resolved);
|
|
641
737
|
writeBuildAllowance(root, resolved.packageManager);
|
|
738
|
+
|
|
739
|
+
if (resolved.isTypescript) {
|
|
740
|
+
writeInitialEnvModule(root);
|
|
741
|
+
}
|
|
642
742
|
},
|
|
643
743
|
});
|
|
644
744
|
};
|
|
@@ -651,11 +751,7 @@ const scaffold = async (options: CreateOptions = {}): Promise<void> => {
|
|
|
651
751
|
await createProjectStructure(root, resolved);
|
|
652
752
|
await installAllDependencies({ resolved, devDependencies: devDeps });
|
|
653
753
|
|
|
654
|
-
|
|
655
|
-
writeInitialEnvModule(root);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
await initializeGitRepo(root);
|
|
754
|
+
await initializeGitRepo(root, resolved.shouldInitializeGit);
|
|
659
755
|
printNextSteps(resolved);
|
|
660
756
|
};
|
|
661
757
|
|
|
@@ -3,21 +3,11 @@ import { defineConfig } from "@gtkx/config";
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
applicationId: "<%= applicationId %>",
|
|
5
5
|
applicationIcon: "data/icons",
|
|
6
|
-
future: {
|
|
7
|
-
v2ByteArrays: true,
|
|
8
|
-
v2ValueReturns: true,
|
|
9
|
-
v2FinishResults: true,
|
|
10
|
-
v2InoutReturns: true,
|
|
11
|
-
v2ResourceImports: true,
|
|
12
|
-
v2DefaultLibraries: true,
|
|
13
|
-
v2TreeShaking: true,
|
|
14
|
-
},
|
|
15
6
|
deploy: {
|
|
16
|
-
name:
|
|
7
|
+
name: <%- sourceStringLiteral(displayName) %>,
|
|
17
8
|
summary: "A GTK4 application built with GTKX",
|
|
18
9
|
description: [
|
|
19
|
-
|
|
20
|
-
+ "widgets from React. Replace this paragraph with a description of what your application does.",
|
|
10
|
+
<%- sourceStringLiteral(`${displayName} is a GTK4 and Adwaita application built with GTKX, which renders native GObject widgets from React. Replace this paragraph with a description of what your application does.`) %>,
|
|
21
11
|
],
|
|
22
12
|
categories: ["Utility"],
|
|
23
13
|
},
|
package/src/templates.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sortStrings, sourceStringLiteral } from "@gtkx/utils";
|
|
1
|
+
import { sortStrings, sourceStringLiteral, toPosixPath } from "@gtkx/utils";
|
|
2
2
|
import ejs from "ejs";
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
type TemplateContext = {
|
|
7
7
|
name: string;
|
|
8
8
|
applicationId: string;
|
|
9
|
-
|
|
9
|
+
displayName: string;
|
|
10
10
|
shouldIncludeTesting: boolean;
|
|
11
11
|
isTypescript: boolean;
|
|
12
12
|
importExtension: string;
|
|
@@ -14,20 +14,18 @@ type TemplateContext = {
|
|
|
14
14
|
developerEmail: string | null;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
return join(import.meta.dirname, "templates");
|
|
19
|
-
};
|
|
17
|
+
const TEMPLATES_DIR = join(import.meta.dirname, "templates");
|
|
20
18
|
|
|
21
19
|
const listTemplates = (): string[] =>
|
|
22
20
|
sortStrings(
|
|
23
|
-
readdirSync(
|
|
21
|
+
readdirSync(TEMPLATES_DIR, { recursive: true, withFileTypes: true })
|
|
24
22
|
.filter((entry) => entry.isFile())
|
|
25
23
|
.map((entry) => join(entry.parentPath, entry.name))
|
|
26
|
-
.map((absolute) => absolute.slice(
|
|
24
|
+
.map((absolute) => toPosixPath(absolute.slice(TEMPLATES_DIR.length + 1)))
|
|
27
25
|
.map((relative) => relative.replace(/\.ejs$/, "")),
|
|
28
26
|
);
|
|
29
27
|
|
|
30
28
|
const renderFile = async (templateName: string, context: TemplateContext): Promise<string> =>
|
|
31
|
-
ejs.renderFile(join(
|
|
29
|
+
ejs.renderFile(join(TEMPLATES_DIR, `${templateName}.ejs`), { ...context, sourceStringLiteral });
|
|
32
30
|
|
|
33
31
|
export { listTemplates, renderFile, type TemplateContext };
|