create-gtkx 1.6.0 → 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 +47 -44
- 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 +274 -117
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/gtkx.config.ts.ejs +3 -13
- package/dist/templates/package.json.ejs +4 -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 +5 -7
- package/dist/templates.js.map +1 -1
- package/package.json +13 -9
- 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 +397 -145
- package/src/templates/gtkx.config.ts.ejs +3 -13
- package/src/templates/package.json.ejs +4 -1
- package/src/templates/src/app.tsx.ejs +27 -24
- package/src/templates.ts +6 -8
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,10 +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
|
|
43
|
-
const getDevCommand = (packageManager) => DEV_COMMAND[packageManager];
|
|
44
|
-
const getAddCommand = (packageManager) => ADD_COMMAND[packageManager];
|
|
45
|
-
const titleFromName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
44
|
+
const displayNameFromProjectName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
46
45
|
const gitConfigValue = (key) => {
|
|
47
46
|
const git = tryResolveExecutable("git");
|
|
48
47
|
if (git === undefined) {
|
|
@@ -74,6 +73,10 @@ const isDirEmpty = (dir) => {
|
|
|
74
73
|
const entries = readdirSync(dir);
|
|
75
74
|
return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
|
|
76
75
|
};
|
|
76
|
+
const shouldInitializeGit = (root) => {
|
|
77
|
+
const entry = lstatSync(root, { throwIfNoEntry: false });
|
|
78
|
+
return entry === undefined || (entry.isDirectory() && readdirSync(root).length === 0);
|
|
79
|
+
};
|
|
77
80
|
const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
78
81
|
const devDeps = [...DEV_DEPENDENCIES];
|
|
79
82
|
if (isTypescript) {
|
|
@@ -84,8 +87,9 @@ const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
|
84
87
|
}
|
|
85
88
|
return devDeps;
|
|
86
89
|
};
|
|
90
|
+
const isCancellation = (value) => p.isCancel(value);
|
|
87
91
|
const guardCancellation = (value) => {
|
|
88
|
-
if (
|
|
92
|
+
if (isCancellation(value)) {
|
|
89
93
|
p.cancel("Operation canceled");
|
|
90
94
|
throw new OperationCanceledError();
|
|
91
95
|
}
|
|
@@ -103,6 +107,13 @@ const validateProjectName = (value) => {
|
|
|
103
107
|
const validateApplicationIdFormat = (value) => isValidApplicationId(value) ? undefined : APPLICATION_ID_FORMAT_ERROR;
|
|
104
108
|
const validateApplicationIdInput = (value) => value ? validateApplicationIdFormat(value) : "Application ID is required";
|
|
105
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;
|
|
106
117
|
const fail = (message) => {
|
|
107
118
|
p.log.error(message);
|
|
108
119
|
throw new ScaffoldAbortedError(message);
|
|
@@ -116,14 +127,6 @@ const requestedPackageManager = (value) => {
|
|
|
116
127
|
}
|
|
117
128
|
return value;
|
|
118
129
|
};
|
|
119
|
-
const targetStats = (root) => {
|
|
120
|
-
try {
|
|
121
|
-
return statSync(root, { throwIfNoEntry: false });
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
130
|
const validateTargetDir = (target) => {
|
|
128
131
|
if (!target) {
|
|
129
132
|
return "Project directory is required";
|
|
@@ -145,29 +148,29 @@ const promptApplicationId = async (name) => {
|
|
|
145
148
|
validate: validateApplicationIdAnswer,
|
|
146
149
|
}));
|
|
147
150
|
};
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}));
|
|
154
159
|
};
|
|
155
|
-
const
|
|
160
|
+
const packageManagerOption = (manager, detected) => {
|
|
161
|
+
let hint;
|
|
156
162
|
if (detected === manager.value) {
|
|
157
|
-
|
|
163
|
+
hint = "detected";
|
|
158
164
|
}
|
|
159
|
-
if (manager.isRecommended) {
|
|
160
|
-
|
|
165
|
+
else if (manager.isRecommended) {
|
|
166
|
+
hint = "recommended";
|
|
161
167
|
}
|
|
162
|
-
return undefined;
|
|
163
|
-
};
|
|
164
|
-
const packageManagerOption = (manager, detected) => {
|
|
165
|
-
const hint = packageManagerHint(manager, detected);
|
|
166
168
|
return { value: manager.value, label: manager.label, ...(hint !== undefined && { hint }) };
|
|
167
169
|
};
|
|
168
170
|
const detectedPackageManager = async () => {
|
|
169
171
|
try {
|
|
170
|
-
|
|
172
|
+
const detected = await nypmDetectPackageManager(process.cwd(), { includeParentDirs: true });
|
|
173
|
+
return detected && isKnownPackageManager(detected.name) ? detected.name : undefined;
|
|
171
174
|
}
|
|
172
175
|
catch {
|
|
173
176
|
return undefined;
|
|
@@ -182,52 +185,64 @@ const promptPackageManager = async () => {
|
|
|
182
185
|
initialValue: initial,
|
|
183
186
|
}));
|
|
184
187
|
};
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}));
|
|
193
|
-
const emptyDir = (root) => {
|
|
194
|
-
for (const entry of readdirSync(root)) {
|
|
195
|
-
if (entry === ".git") {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
rmSync(join(root, entry), { recursive: true, force: true });
|
|
199
|
-
}
|
|
188
|
+
const isOptionEnabled = async (value, isInteractive, message) => value ??
|
|
189
|
+
(isInteractive
|
|
190
|
+
? guardCancellation(await p.confirm({ message, initialValue: true }))
|
|
191
|
+
: true);
|
|
192
|
+
const formatFileList = (heading, files) => {
|
|
193
|
+
const indentedFiles = files.map((file) => ` ${file}`).join("\n");
|
|
194
|
+
return `${heading}:\n${indentedFiles}`;
|
|
200
195
|
};
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
emptyDir(root);
|
|
196
|
+
const reportPlannedChanges = (changes) => {
|
|
197
|
+
if (changes.length > 0) {
|
|
198
|
+
p.log.info(formatFileList("Scaffold files to replace", changes));
|
|
205
199
|
}
|
|
206
200
|
};
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
|
|
201
|
+
const reportCompletedChanges = (changes) => {
|
|
202
|
+
if (changes.length > 0) {
|
|
203
|
+
p.log.success(formatFileList("Replaced scaffold files", changes));
|
|
210
204
|
}
|
|
211
|
-
|
|
212
|
-
|
|
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?`,
|
|
213
210
|
initialValue: false,
|
|
214
211
|
}));
|
|
212
|
+
if (!shouldReplace) {
|
|
213
|
+
fail(`Directory "${target}" is not empty`);
|
|
214
|
+
}
|
|
215
215
|
};
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
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;
|
|
220
226
|
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
227
|
+
if (options.shouldOverwrite === true) {
|
|
228
|
+
reportPlannedChanges(changes);
|
|
229
|
+
return;
|
|
223
230
|
}
|
|
224
|
-
|
|
225
|
-
|
|
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`);
|
|
226
237
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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;
|
|
229
244
|
}
|
|
230
|
-
|
|
245
|
+
await acceptNonEmptyDirectory(target, options, changes);
|
|
231
246
|
};
|
|
232
247
|
const resolveTarget = async (options) => {
|
|
233
248
|
if (options.name === undefined && options.isInteractive) {
|
|
@@ -243,6 +258,12 @@ const resolveApplicationId = async (options, name) => {
|
|
|
243
258
|
const error = validateApplicationIdInput(applicationId);
|
|
244
259
|
return error === undefined ? applicationId : fail(error);
|
|
245
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
|
+
};
|
|
246
267
|
const resolvePackageManager = async (requested, options) => {
|
|
247
268
|
if (requested !== undefined) {
|
|
248
269
|
return requested;
|
|
@@ -252,39 +273,30 @@ const resolvePackageManager = async (requested, options) => {
|
|
|
252
273
|
}
|
|
253
274
|
return (await detectedPackageManager()) ?? "pnpm";
|
|
254
275
|
};
|
|
255
|
-
const isTypescriptSelected = async (options) => {
|
|
256
|
-
if (options.isTypescript !== undefined) {
|
|
257
|
-
return options.isTypescript;
|
|
258
|
-
}
|
|
259
|
-
return options.isInteractive ? shouldUseTypeScript() : true;
|
|
260
|
-
};
|
|
261
|
-
const isTestingIncluded = async (options) => {
|
|
262
|
-
if (options.shouldIncludeTesting !== undefined) {
|
|
263
|
-
return options.shouldIncludeTesting;
|
|
264
|
-
}
|
|
265
|
-
return options.isInteractive ? shouldSetUpTesting() : true;
|
|
266
|
-
};
|
|
267
276
|
const resolveOptions = async (options) => {
|
|
268
277
|
const requested = requestedPackageManager(options.packageManager);
|
|
269
278
|
const target = await resolveTarget(options);
|
|
270
279
|
const name = deriveProjectName(target);
|
|
280
|
+
const displayName = await resolveDisplayName(options, name);
|
|
271
281
|
const applicationId = await resolveApplicationId(options, name);
|
|
272
282
|
const root = resolve(process.cwd(), target);
|
|
273
283
|
const isCurrentDirectory = root === process.cwd();
|
|
274
|
-
const
|
|
284
|
+
const shouldInitialize = shouldInitializeGit(root);
|
|
275
285
|
const packageManager = await resolvePackageManager(requested, options);
|
|
276
|
-
const isTypescript = await
|
|
277
|
-
const shouldIncludeTesting = await
|
|
286
|
+
const isTypescript = await isOptionEnabled(options.isTypescript, options.isInteractive, "Use TypeScript?");
|
|
287
|
+
const shouldIncludeTesting = await isOptionEnabled(options.shouldIncludeTesting, options.isInteractive, "Include testing setup (Vitest)?");
|
|
278
288
|
return {
|
|
279
289
|
target,
|
|
280
290
|
root,
|
|
281
291
|
isCurrentDirectory,
|
|
282
292
|
name,
|
|
293
|
+
displayName,
|
|
283
294
|
applicationId,
|
|
284
295
|
packageManager,
|
|
285
296
|
isTypescript,
|
|
286
297
|
shouldIncludeTesting,
|
|
287
|
-
|
|
298
|
+
shouldInitializeGit: shouldInitialize,
|
|
299
|
+
shouldInstallDependencies: options.shouldInstallDependencies !== false,
|
|
288
300
|
};
|
|
289
301
|
};
|
|
290
302
|
const isTemplateIncluded = (templateRelativePath, resolved) => {
|
|
@@ -296,37 +308,139 @@ const isTemplateIncluded = (templateRelativePath, resolved) => {
|
|
|
296
308
|
}
|
|
297
309
|
return true;
|
|
298
310
|
};
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
manifest.scripts.test = "vitest run";
|
|
305
|
-
});
|
|
311
|
+
const destinationPath = (template, resolved) => {
|
|
312
|
+
const scriptPath = resolved.isTypescript ? template : template.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
313
|
+
return scriptPath.endsWith(ICON_TEMPLATE_NAME)
|
|
314
|
+
? `${scriptPath.slice(0, -ICON_TEMPLATE_NAME.length)}${resolved.applicationId}.svg`
|
|
315
|
+
: scriptPath;
|
|
306
316
|
};
|
|
307
317
|
const templateContext = (resolved) => ({
|
|
308
318
|
name: resolved.name,
|
|
309
319
|
applicationId: resolved.applicationId,
|
|
310
|
-
|
|
320
|
+
displayName: resolved.displayName,
|
|
311
321
|
shouldIncludeTesting: resolved.shouldIncludeTesting,
|
|
312
322
|
isTypescript: resolved.isTypescript,
|
|
313
323
|
importExtension: resolved.isTypescript ? ".js" : ".jsx",
|
|
314
|
-
developerName: gitConfigValue("user.name") ??
|
|
324
|
+
developerName: gitConfigValue("user.name") ?? resolved.displayName,
|
|
315
325
|
developerEmail: gitConfigValue("user.email"),
|
|
316
326
|
});
|
|
317
|
-
const
|
|
318
|
-
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) => {
|
|
319
384
|
const context = templateContext(resolved);
|
|
385
|
+
const files = [];
|
|
320
386
|
for (const template of listTemplates()) {
|
|
321
|
-
if (
|
|
322
|
-
|
|
387
|
+
if (isTemplateIncluded(template, resolved)) {
|
|
388
|
+
files.push({
|
|
389
|
+
destination: join(root, destinationPath(template, resolved)),
|
|
390
|
+
contents: await renderFile(template, context),
|
|
391
|
+
});
|
|
323
392
|
}
|
|
324
|
-
const destination = join(root, toDestinationPath(template, isTypescript, applicationId));
|
|
325
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
326
|
-
writeFileSync(destination, await renderFile(template, context));
|
|
327
393
|
}
|
|
328
|
-
|
|
329
|
-
|
|
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);
|
|
439
|
+
}
|
|
440
|
+
if (resolved.shouldIncludeTesting) {
|
|
441
|
+
updateManifest(root, (manifest) => {
|
|
442
|
+
manifest.scripts.test = "vitest run";
|
|
443
|
+
});
|
|
330
444
|
}
|
|
331
445
|
};
|
|
332
446
|
const withoutInheritedScriptPolicy = async (run) => {
|
|
@@ -351,9 +465,26 @@ const installDependencies = async (options) => {
|
|
|
351
465
|
await addDependency(dependencies, { cwd, packageManager, dev: isDev, silent: true });
|
|
352
466
|
});
|
|
353
467
|
};
|
|
354
|
-
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
|
+
};
|
|
355
486
|
const formatRecovery = ({ resolved, devDependencies }) => {
|
|
356
|
-
const add =
|
|
487
|
+
const add = ADD_COMMAND[resolved.packageManager];
|
|
357
488
|
const steps = [
|
|
358
489
|
...(resolved.isCurrentDirectory ? [] : [`cd ${resolved.target}`]),
|
|
359
490
|
`${add} ${pin(DEPENDENCIES).join(" ")}`,
|
|
@@ -400,11 +531,27 @@ const installAllDependencies = async (options) => {
|
|
|
400
531
|
});
|
|
401
532
|
};
|
|
402
533
|
const writeInitialEnvModule = (root) => {
|
|
403
|
-
const
|
|
404
|
-
mkdirSync(
|
|
405
|
-
writeFileSync(
|
|
534
|
+
const path = envModulePath(root);
|
|
535
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
536
|
+
writeFileSync(path, `${GTKX_ENV_MODULE_HEADER}\n`);
|
|
406
537
|
};
|
|
407
|
-
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
|
+
}
|
|
408
555
|
const spinner = p.spinner();
|
|
409
556
|
spinner.start("Initializing git repository...");
|
|
410
557
|
try {
|
|
@@ -419,20 +566,24 @@ const initializeGitRepo = async (root) => {
|
|
|
419
566
|
}
|
|
420
567
|
};
|
|
421
568
|
const printNextSteps = (resolved) => {
|
|
422
|
-
const devCmd =
|
|
569
|
+
const devCmd = DEV_COMMAND[resolved.packageManager];
|
|
423
570
|
const cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
|
|
571
|
+
const installStep = resolved.shouldInstallDependencies ? "" : `${INSTALL_COMMAND[resolved.packageManager]}\n`;
|
|
424
572
|
const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
|
|
425
|
-
p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
|
|
573
|
+
p.note(`${cdStep}${installStep}${devCmd}${testingNote}`, "Next steps");
|
|
426
574
|
};
|
|
427
|
-
const createProjectStructure = async (root, resolved) => {
|
|
575
|
+
const createProjectStructure = async (root, resolved, plan) => {
|
|
428
576
|
await runWithSpinner({
|
|
429
577
|
pending: "Creating project structure...",
|
|
430
578
|
done: "Project structure created",
|
|
431
579
|
failed: "Failed to create the project structure",
|
|
432
|
-
run:
|
|
433
|
-
|
|
434
|
-
|
|
580
|
+
run: () => {
|
|
581
|
+
mkdirSync(root, { recursive: true });
|
|
582
|
+
scaffoldProject(root, resolved, plan);
|
|
435
583
|
writeBuildAllowance(root, resolved.packageManager);
|
|
584
|
+
if (shouldWriteEnvModule(resolved)) {
|
|
585
|
+
writeInitialEnvModule(root);
|
|
586
|
+
}
|
|
436
587
|
},
|
|
437
588
|
});
|
|
438
589
|
};
|
|
@@ -441,12 +592,18 @@ const scaffold = async (options = {}) => {
|
|
|
441
592
|
const resolved = await resolveOptions(options);
|
|
442
593
|
const { root } = resolved;
|
|
443
594
|
const devDeps = getDevDependencies(resolved);
|
|
444
|
-
|
|
445
|
-
await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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);
|
|
450
607
|
printNextSteps(resolved);
|
|
451
608
|
};
|
|
452
609
|
export { scaffold };
|