create-gtkx 2.0.0-beta.1 → 2.0.0-beta.10
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 +43 -40
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +9 -1
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +72 -4
- 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/package-managers.d.ts +3 -0
- package/dist/package-managers.d.ts.map +1 -1
- package/dist/package-managers.js +24 -3
- package/dist/package-managers.js.map +1 -1
- package/dist/scaffolder.d.ts +2 -0
- package/dist/scaffolder.d.ts.map +1 -1
- package/dist/scaffolder.js +251 -54
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/gtkx.config.ts.ejs +3 -4
- package/dist/templates/package.json.ejs +1 -1
- package/dist/templates/src/app.tsx.ejs +27 -24
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js.map +1 -1
- package/package.json +10 -8
- package/src/cli.ts +1 -1
- package/src/command.ts +88 -4
- package/src/manifest.ts +10 -4
- package/src/package-managers.ts +24 -3
- package/src/scaffolder.ts +361 -56
- package/src/templates/gtkx.config.ts.ejs +3 -4
- package/src/templates/package.json.ejs +1 -1
- package/src/templates/src/app.tsx.ejs +27 -24
- package/src/templates.ts +1 -1
package/dist/scaffolder.js
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, statSync, writeFileSync } from "node:fs";
|
|
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" };
|
|
@@ -16,6 +16,7 @@ import { isValidProjectName } from "./project-name.js";
|
|
|
16
16
|
import { listTemplates, renderFile } from "./templates.js";
|
|
17
17
|
const selfVersion = packageManifest.version;
|
|
18
18
|
const ICON_TEMPLATE_NAME = "icon.svg";
|
|
19
|
+
const TYPESCRIPT_VERSION = "^6.0.3";
|
|
19
20
|
const GTKX_ENV_MODULE_HEADER = `/**
|
|
20
21
|
* Generated by \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
|
|
21
22
|
*/`;
|
|
@@ -25,6 +26,7 @@ const TYPESCRIPT_DEV_DEPENDENCIES = ["@types/node", "@types/react", "typescript"
|
|
|
25
26
|
const TESTING_DEV_DEPENDENCIES = ["@gtkx/testing", "vitest"];
|
|
26
27
|
const DEV_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.devCommand]));
|
|
27
28
|
const ADD_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.addCommand]));
|
|
29
|
+
const INSTALL_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.installCommand]));
|
|
28
30
|
const INHERITED_SCRIPT_POLICY_VARS = ["npm_config_allow_scripts", "npm_config_allow-scripts"];
|
|
29
31
|
const TESTING_TEMPLATES = new Set(["vitest.config.ts", "tests/app.test.tsx"]);
|
|
30
32
|
const TYPESCRIPT_TEMPLATES = new Set(["tsconfig.json", "src/gtkx-env.d.ts"]);
|
|
@@ -39,7 +41,7 @@ const APPLICATION_ID_PREFIX = "com.";
|
|
|
39
41
|
const APPLICATION_ID_SUFFIX = ".app";
|
|
40
42
|
const APPLICATION_ID_SEGMENT_LIMIT = APPLICATION_ID_MAX_LENGTH - APPLICATION_ID_PREFIX.length -
|
|
41
43
|
APPLICATION_ID_SUFFIX.length;
|
|
42
|
-
const
|
|
44
|
+
const displayNameFromProjectName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
43
45
|
const gitConfigValue = (key) => {
|
|
44
46
|
const git = tryResolveExecutable("git");
|
|
45
47
|
if (git === undefined) {
|
|
@@ -71,6 +73,10 @@ const isDirEmpty = (dir) => {
|
|
|
71
73
|
const entries = readdirSync(dir);
|
|
72
74
|
return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
|
|
73
75
|
};
|
|
76
|
+
const shouldInitializeGit = (root) => {
|
|
77
|
+
const entry = lstatSync(root, { throwIfNoEntry: false });
|
|
78
|
+
return entry === undefined || (entry.isDirectory() && readdirSync(root).length === 0);
|
|
79
|
+
};
|
|
74
80
|
const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
75
81
|
const devDeps = [...DEV_DEPENDENCIES];
|
|
76
82
|
if (isTypescript) {
|
|
@@ -81,8 +87,9 @@ const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
|
81
87
|
}
|
|
82
88
|
return devDeps;
|
|
83
89
|
};
|
|
90
|
+
const isCancellation = (value) => p.isCancel(value);
|
|
84
91
|
const guardCancellation = (value) => {
|
|
85
|
-
if (
|
|
92
|
+
if (isCancellation(value)) {
|
|
86
93
|
p.cancel("Operation canceled");
|
|
87
94
|
throw new OperationCanceledError();
|
|
88
95
|
}
|
|
@@ -100,6 +107,13 @@ const validateProjectName = (value) => {
|
|
|
100
107
|
const validateApplicationIdFormat = (value) => isValidApplicationId(value) ? undefined : APPLICATION_ID_FORMAT_ERROR;
|
|
101
108
|
const validateApplicationIdInput = (value) => value ? validateApplicationIdFormat(value) : "Application ID is required";
|
|
102
109
|
const validateApplicationIdAnswer = (value) => value ? validateApplicationIdFormat(value) : undefined;
|
|
110
|
+
const validateDisplayName = (value) => {
|
|
111
|
+
if (value === undefined || value.trim().length === 0) {
|
|
112
|
+
return "Display name is required";
|
|
113
|
+
}
|
|
114
|
+
return value.includes("\n") || value.includes("\r") ? "Display name must be a single line" : undefined;
|
|
115
|
+
};
|
|
116
|
+
const validateDisplayNameAnswer = (value) => value ? validateDisplayName(value) : undefined;
|
|
103
117
|
const fail = (message) => {
|
|
104
118
|
p.log.error(message);
|
|
105
119
|
throw new ScaffoldAbortedError(message);
|
|
@@ -134,6 +148,15 @@ const promptApplicationId = async (name) => {
|
|
|
134
148
|
validate: validateApplicationIdAnswer,
|
|
135
149
|
}));
|
|
136
150
|
};
|
|
151
|
+
const promptDisplayName = async (name) => {
|
|
152
|
+
const defaultDisplayName = displayNameFromProjectName(name);
|
|
153
|
+
return guardCancellation(await p.text({
|
|
154
|
+
message: "Display name",
|
|
155
|
+
placeholder: defaultDisplayName,
|
|
156
|
+
defaultValue: defaultDisplayName,
|
|
157
|
+
validate: validateDisplayNameAnswer,
|
|
158
|
+
}));
|
|
159
|
+
};
|
|
137
160
|
const packageManagerOption = (manager, detected) => {
|
|
138
161
|
let hint;
|
|
139
162
|
if (detected === manager.value) {
|
|
@@ -166,38 +189,60 @@ const isOptionEnabled = async (value, isInteractive, message) => value ??
|
|
|
166
189
|
(isInteractive
|
|
167
190
|
? guardCancellation(await p.confirm({ message, initialValue: true }))
|
|
168
191
|
: true);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
const formatFileList = (heading, files) => {
|
|
193
|
+
const indentedFiles = files.map((file) => ` ${file}`).join("\n");
|
|
194
|
+
return `${heading}:\n${indentedFiles}`;
|
|
195
|
+
};
|
|
196
|
+
const reportPlannedChanges = (changes) => {
|
|
197
|
+
if (changes.length > 0) {
|
|
198
|
+
p.log.info(formatFileList("Scaffold files to replace", changes));
|
|
175
199
|
}
|
|
176
200
|
};
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
201
|
+
const reportCompletedChanges = (changes) => {
|
|
202
|
+
if (changes.length > 0) {
|
|
203
|
+
p.log.success(formatFileList("Replaced scaffold files", changes));
|
|
180
204
|
}
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
};
|
|
206
|
+
const confirmOverwrite = async (target, changes) => {
|
|
207
|
+
reportPlannedChanges(changes);
|
|
208
|
+
const shouldReplace = guardCancellation(await p.confirm({
|
|
209
|
+
message: `Directory "${target}" is not empty. Replace scaffold files?`,
|
|
183
210
|
initialValue: false,
|
|
184
211
|
}));
|
|
212
|
+
if (!shouldReplace) {
|
|
213
|
+
fail(`Directory "${target}" is not empty`);
|
|
214
|
+
}
|
|
185
215
|
};
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
const refuseNonEmptyDirectory = (target, changes) => {
|
|
217
|
+
const heading = `Directory "${target}" is not empty. Pass --overwrite to`;
|
|
218
|
+
return fail(changes.length === 0
|
|
219
|
+
? `${heading} scaffold into it`
|
|
220
|
+
: formatFileList(`${heading} replace these scaffold files`, changes));
|
|
221
|
+
};
|
|
222
|
+
const acceptNonEmptyDirectory = async (target, options, changes) => {
|
|
223
|
+
if (options.isInteractive) {
|
|
224
|
+
await confirmOverwrite(target, changes);
|
|
225
|
+
return;
|
|
190
226
|
}
|
|
191
|
-
if (
|
|
192
|
-
|
|
227
|
+
if (options.shouldOverwrite === true) {
|
|
228
|
+
reportPlannedChanges(changes);
|
|
229
|
+
return;
|
|
193
230
|
}
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
refuseNonEmptyDirectory(target, changes);
|
|
232
|
+
};
|
|
233
|
+
const assertTargetIsDirectory = (root, target) => {
|
|
234
|
+
const stats = statSync(root, { throwIfNoEntry: false });
|
|
235
|
+
if (stats !== undefined && !stats.isDirectory()) {
|
|
236
|
+
fail(`Target "${target}" is not a directory`);
|
|
196
237
|
}
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
};
|
|
239
|
+
const validateTargetAvailability = async (root, target, options, changes) => {
|
|
240
|
+
const stats = statSync(root, { throwIfNoEntry: false });
|
|
241
|
+
assertTargetIsDirectory(root, target);
|
|
242
|
+
if (stats === undefined || isDirEmpty(root)) {
|
|
243
|
+
return;
|
|
199
244
|
}
|
|
200
|
-
|
|
245
|
+
await acceptNonEmptyDirectory(target, options, changes);
|
|
201
246
|
};
|
|
202
247
|
const resolveTarget = async (options) => {
|
|
203
248
|
if (options.name === undefined && options.isInteractive) {
|
|
@@ -213,6 +258,12 @@ const resolveApplicationId = async (options, name) => {
|
|
|
213
258
|
const error = validateApplicationIdInput(applicationId);
|
|
214
259
|
return error === undefined ? applicationId : fail(error);
|
|
215
260
|
};
|
|
261
|
+
const resolveDisplayName = async (options, name) => {
|
|
262
|
+
const value = options.displayName ??
|
|
263
|
+
(options.isInteractive ? await promptDisplayName(name) : displayNameFromProjectName(name));
|
|
264
|
+
const error = validateDisplayName(value);
|
|
265
|
+
return error === undefined ? value.trim() : fail(error);
|
|
266
|
+
};
|
|
216
267
|
const resolvePackageManager = async (requested, options) => {
|
|
217
268
|
if (requested !== undefined) {
|
|
218
269
|
return requested;
|
|
@@ -226,10 +277,11 @@ const resolveOptions = async (options) => {
|
|
|
226
277
|
const requested = requestedPackageManager(options.packageManager);
|
|
227
278
|
const target = await resolveTarget(options);
|
|
228
279
|
const name = deriveProjectName(target);
|
|
280
|
+
const displayName = await resolveDisplayName(options, name);
|
|
229
281
|
const applicationId = await resolveApplicationId(options, name);
|
|
230
282
|
const root = resolve(process.cwd(), target);
|
|
231
283
|
const isCurrentDirectory = root === process.cwd();
|
|
232
|
-
const
|
|
284
|
+
const shouldInitialize = shouldInitializeGit(root);
|
|
233
285
|
const packageManager = await resolvePackageManager(requested, options);
|
|
234
286
|
const isTypescript = await isOptionEnabled(options.isTypescript, options.isInteractive, "Use TypeScript?");
|
|
235
287
|
const shouldIncludeTesting = await isOptionEnabled(options.shouldIncludeTesting, options.isInteractive, "Include testing setup (Vitest)?");
|
|
@@ -238,11 +290,13 @@ const resolveOptions = async (options) => {
|
|
|
238
290
|
root,
|
|
239
291
|
isCurrentDirectory,
|
|
240
292
|
name,
|
|
293
|
+
displayName,
|
|
241
294
|
applicationId,
|
|
242
295
|
packageManager,
|
|
243
296
|
isTypescript,
|
|
244
297
|
shouldIncludeTesting,
|
|
245
|
-
|
|
298
|
+
shouldInitializeGit: shouldInitialize,
|
|
299
|
+
shouldInstallDependencies: options.shouldInstallDependencies !== false,
|
|
246
300
|
};
|
|
247
301
|
};
|
|
248
302
|
const isTemplateIncluded = (templateRelativePath, resolved) => {
|
|
@@ -263,22 +317,125 @@ const destinationPath = (template, resolved) => {
|
|
|
263
317
|
const templateContext = (resolved) => ({
|
|
264
318
|
name: resolved.name,
|
|
265
319
|
applicationId: resolved.applicationId,
|
|
266
|
-
|
|
320
|
+
displayName: resolved.displayName,
|
|
267
321
|
shouldIncludeTesting: resolved.shouldIncludeTesting,
|
|
268
322
|
isTypescript: resolved.isTypescript,
|
|
269
323
|
importExtension: resolved.isTypescript ? ".js" : ".jsx",
|
|
270
|
-
developerName: gitConfigValue("user.name") ??
|
|
324
|
+
developerName: gitConfigValue("user.name") ?? resolved.displayName,
|
|
271
325
|
developerEmail: gitConfigValue("user.email"),
|
|
272
326
|
});
|
|
273
|
-
const
|
|
327
|
+
const replaceTemplateDestination = (destination) => {
|
|
328
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
329
|
+
if (entry === undefined) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
333
|
+
fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
|
|
334
|
+
}
|
|
335
|
+
rmSync(destination, { force: true });
|
|
336
|
+
};
|
|
337
|
+
const projectRelativeDestination = (root, destination) => {
|
|
338
|
+
const relativePath = relative(root, destination);
|
|
339
|
+
const isOutsideProject = relativePath.length === 0 || relativePath.startsWith(`..${sep}`) ||
|
|
340
|
+
isAbsolute(relativePath);
|
|
341
|
+
if (isOutsideProject) {
|
|
342
|
+
fail(`Scaffold destination is outside the project: ${destination}`);
|
|
343
|
+
}
|
|
344
|
+
return relativePath;
|
|
345
|
+
};
|
|
346
|
+
const assertRootIsNotSymbolicLink = (root) => {
|
|
347
|
+
const rootEntry = lstatSync(root, { throwIfNoEntry: false });
|
|
348
|
+
if (rootEntry?.isSymbolicLink()) {
|
|
349
|
+
fail(`Cannot scaffold through a symbolic link: ${root}`);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const assertSafeAncestor = (ancestor) => {
|
|
353
|
+
const entry = lstatSync(ancestor, { throwIfNoEntry: false });
|
|
354
|
+
if (entry?.isSymbolicLink()) {
|
|
355
|
+
fail(`Cannot scaffold through a symbolic link: ${ancestor}`);
|
|
356
|
+
}
|
|
357
|
+
if (entry !== undefined && !entry.isDirectory()) {
|
|
358
|
+
fail(`Cannot scaffold through a non-directory path: ${ancestor}`);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const assertSafeAncestors = (root, relativePath) => {
|
|
362
|
+
let ancestor = root;
|
|
363
|
+
const segments = relativePath.split(sep);
|
|
364
|
+
const ancestorSegments = segments.slice(0, -1);
|
|
365
|
+
for (const segment of ancestorSegments) {
|
|
366
|
+
ancestor = join(ancestor, segment);
|
|
367
|
+
assertSafeAncestor(ancestor);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const assertReplaceableDestination = (destination) => {
|
|
371
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
372
|
+
const isReplaceable = entry === undefined || entry.isFile() || entry.isSymbolicLink();
|
|
373
|
+
if (!isReplaceable) {
|
|
374
|
+
fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const assertSafeDestination = (root, destination) => {
|
|
378
|
+
const relativePath = projectRelativeDestination(root, destination);
|
|
379
|
+
assertRootIsNotSymbolicLink(root);
|
|
380
|
+
assertSafeAncestors(root, relativePath);
|
|
381
|
+
assertReplaceableDestination(destination);
|
|
382
|
+
};
|
|
383
|
+
const plannedScaffoldFiles = async (root, resolved) => {
|
|
274
384
|
const context = templateContext(resolved);
|
|
385
|
+
const files = [];
|
|
275
386
|
for (const template of listTemplates()) {
|
|
276
|
-
if (
|
|
277
|
-
|
|
387
|
+
if (isTemplateIncluded(template, resolved)) {
|
|
388
|
+
files.push({
|
|
389
|
+
destination: join(root, destinationPath(template, resolved)),
|
|
390
|
+
contents: await renderFile(template, context),
|
|
391
|
+
});
|
|
278
392
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
393
|
+
}
|
|
394
|
+
return files;
|
|
395
|
+
};
|
|
396
|
+
const auxiliaryDestinations = (root, resolved) => resolved.packageManager === "pnpm" ? [join(root, "pnpm-workspace.yaml")] : [];
|
|
397
|
+
const envModulePath = (root) => join(root, "node_modules", ".gtkx", "env.d.ts");
|
|
398
|
+
const shouldWriteEnvModule = (resolved) => resolved.isTypescript && resolved.shouldInstallDependencies;
|
|
399
|
+
const generatedDestinations = (root, resolved) => shouldWriteEnvModule(resolved) ? [envModulePath(root)] : [];
|
|
400
|
+
const isPresent = (path) => lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
401
|
+
const classifyScaffoldChange = (root, destination) => {
|
|
402
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
403
|
+
if (entry === undefined) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
return { destination, relativePath: projectRelativeDestination(root, destination) };
|
|
407
|
+
};
|
|
408
|
+
const addScaffoldChange = (plan, change) => {
|
|
409
|
+
if (change === undefined) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
plan.replaceDestinations.push(change.destination);
|
|
413
|
+
plan.changes.push(change.relativePath);
|
|
414
|
+
};
|
|
415
|
+
const planScaffoldProject = async (root, resolved) => {
|
|
416
|
+
const files = await plannedScaffoldFiles(root, resolved);
|
|
417
|
+
const reported = [...files.map((file) => file.destination), ...auxiliaryDestinations(root, resolved)];
|
|
418
|
+
const generated = generatedDestinations(root, resolved);
|
|
419
|
+
for (const destination of [...reported, ...generated]) {
|
|
420
|
+
assertSafeDestination(root, destination);
|
|
421
|
+
}
|
|
422
|
+
const plan = {
|
|
423
|
+
files,
|
|
424
|
+
replaceDestinations: generated.filter((destination) => isPresent(destination)),
|
|
425
|
+
changes: [],
|
|
426
|
+
};
|
|
427
|
+
for (const destination of reported) {
|
|
428
|
+
addScaffoldChange(plan, classifyScaffoldChange(root, destination));
|
|
429
|
+
}
|
|
430
|
+
return plan;
|
|
431
|
+
};
|
|
432
|
+
const scaffoldProject = (root, resolved, plan) => {
|
|
433
|
+
for (const destination of plan.replaceDestinations) {
|
|
434
|
+
replaceTemplateDestination(destination);
|
|
435
|
+
}
|
|
436
|
+
for (const file of plan.files) {
|
|
437
|
+
mkdirSync(dirname(file.destination), { recursive: true });
|
|
438
|
+
writeFileSync(file.destination, file.contents);
|
|
282
439
|
}
|
|
283
440
|
if (resolved.shouldIncludeTesting) {
|
|
284
441
|
updateManifest(root, (manifest) => {
|
|
@@ -308,7 +465,24 @@ const installDependencies = async (options) => {
|
|
|
308
465
|
await addDependency(dependencies, { cwd, packageManager, dev: isDev, silent: true });
|
|
309
466
|
});
|
|
310
467
|
};
|
|
311
|
-
const pin = (names) => names.map((dependency) =>
|
|
468
|
+
const pin = (names) => names.map((dependency) => {
|
|
469
|
+
if (dependency.startsWith("@gtkx/")) {
|
|
470
|
+
return `${dependency}@^${selfVersion}`;
|
|
471
|
+
}
|
|
472
|
+
return dependency === "typescript" ? `${dependency}@${TYPESCRIPT_VERSION}` : dependency;
|
|
473
|
+
});
|
|
474
|
+
const dependencyVersions = (names) => Object.fromEntries(names.map((dependency) => {
|
|
475
|
+
if (dependency.startsWith("@gtkx/")) {
|
|
476
|
+
return [dependency, `^${selfVersion}`];
|
|
477
|
+
}
|
|
478
|
+
return [dependency, dependency === "typescript" ? TYPESCRIPT_VERSION : "latest"];
|
|
479
|
+
}));
|
|
480
|
+
const writeDependencies = (root, devDependencies) => {
|
|
481
|
+
updateManifest(root, (manifest) => {
|
|
482
|
+
manifest.dependencies = dependencyVersions(DEPENDENCIES);
|
|
483
|
+
manifest.devDependencies = dependencyVersions(devDependencies);
|
|
484
|
+
});
|
|
485
|
+
};
|
|
312
486
|
const formatRecovery = ({ resolved, devDependencies }) => {
|
|
313
487
|
const add = ADD_COMMAND[resolved.packageManager];
|
|
314
488
|
const steps = [
|
|
@@ -357,11 +531,27 @@ const installAllDependencies = async (options) => {
|
|
|
357
531
|
});
|
|
358
532
|
};
|
|
359
533
|
const writeInitialEnvModule = (root) => {
|
|
360
|
-
const
|
|
361
|
-
mkdirSync(
|
|
362
|
-
writeFileSync(
|
|
534
|
+
const path = envModulePath(root);
|
|
535
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
536
|
+
writeFileSync(path, `${GTKX_ENV_MODULE_HEADER}\n`);
|
|
363
537
|
};
|
|
364
|
-
const
|
|
538
|
+
const isInsideGitRepository = (root) => {
|
|
539
|
+
const git = tryResolveExecutable("git");
|
|
540
|
+
if (git === undefined) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
execFileSync(git, ["-C", root, "rev-parse", "--show-toplevel"], { stdio: "ignore" });
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const initializeGitRepo = async (root, shouldInitialize) => {
|
|
552
|
+
if (!shouldInitialize || isInsideGitRepository(root)) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
365
555
|
const spinner = p.spinner();
|
|
366
556
|
spinner.start("Initializing git repository...");
|
|
367
557
|
try {
|
|
@@ -378,21 +568,22 @@ const initializeGitRepo = async (root) => {
|
|
|
378
568
|
const printNextSteps = (resolved) => {
|
|
379
569
|
const devCmd = DEV_COMMAND[resolved.packageManager];
|
|
380
570
|
const cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
|
|
571
|
+
const installStep = resolved.shouldInstallDependencies ? "" : `${INSTALL_COMMAND[resolved.packageManager]}\n`;
|
|
381
572
|
const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
|
|
382
|
-
p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
|
|
573
|
+
p.note(`${cdStep}${installStep}${devCmd}${testingNote}`, "Next steps");
|
|
383
574
|
};
|
|
384
|
-
const createProjectStructure = async (root, resolved) => {
|
|
575
|
+
const createProjectStructure = async (root, resolved, plan) => {
|
|
385
576
|
await runWithSpinner({
|
|
386
577
|
pending: "Creating project structure...",
|
|
387
578
|
done: "Project structure created",
|
|
388
579
|
failed: "Failed to create the project structure",
|
|
389
|
-
run:
|
|
580
|
+
run: () => {
|
|
390
581
|
mkdirSync(root, { recursive: true });
|
|
391
|
-
|
|
392
|
-
emptyDir(root);
|
|
393
|
-
}
|
|
394
|
-
await scaffoldProject(root, resolved);
|
|
582
|
+
scaffoldProject(root, resolved, plan);
|
|
395
583
|
writeBuildAllowance(root, resolved.packageManager);
|
|
584
|
+
if (shouldWriteEnvModule(resolved)) {
|
|
585
|
+
writeInitialEnvModule(root);
|
|
586
|
+
}
|
|
396
587
|
},
|
|
397
588
|
});
|
|
398
589
|
};
|
|
@@ -401,12 +592,18 @@ const scaffold = async (options = {}) => {
|
|
|
401
592
|
const resolved = await resolveOptions(options);
|
|
402
593
|
const { root } = resolved;
|
|
403
594
|
const devDeps = getDevDependencies(resolved);
|
|
404
|
-
|
|
405
|
-
await
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
595
|
+
assertTargetIsDirectory(root, resolved.target);
|
|
596
|
+
const plan = await planScaffoldProject(root, resolved);
|
|
597
|
+
await validateTargetAvailability(root, resolved.target, options, plan.changes);
|
|
598
|
+
await createProjectStructure(root, resolved, plan);
|
|
599
|
+
reportCompletedChanges(plan.changes);
|
|
600
|
+
if (resolved.shouldInstallDependencies) {
|
|
601
|
+
await installAllDependencies({ resolved, devDependencies: devDeps });
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
writeDependencies(root, devDeps);
|
|
605
|
+
}
|
|
606
|
+
await initializeGitRepo(root, resolved.shouldInitializeGit);
|
|
410
607
|
printNextSteps(resolved);
|
|
411
608
|
};
|
|
412
609
|
export { scaffold };
|