create-gtkx 2.0.0-beta.1 → 2.0.0-beta.11
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 +8 -163
- package/dist/build-allowance.d.ts.map +1 -1
- package/dist/build-allowance.js +6 -5
- package/dist/build-allowance.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/command.d.ts +12 -3
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +111 -45
- package/dist/command.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +2 -4
- package/dist/manifest.js.map +1 -1
- package/dist/package-managers.d.ts +3 -3
- package/dist/package-managers.d.ts.map +1 -1
- package/dist/package-managers.js +21 -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 +200 -112
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/gtkx.config.ts.ejs +3 -4
- 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.map +1 -1
- package/package.json +26 -12
- package/src/build-allowance.ts +6 -7
- package/src/cli.ts +1 -1
- package/src/command.ts +140 -57
- package/src/manifest.ts +2 -4
- package/src/package-managers.ts +21 -3
- package/src/scaffolder.ts +282 -153
- package/src/templates/gtkx.config.ts.ejs +3 -4
- package/src/templates/package.json.ejs +4 -1
- package/src/templates/src/app.tsx.ejs +27 -24
- package/src/templates.ts +1 -1
- package/dist/install-hints.d.ts +0 -3
- package/dist/install-hints.d.ts.map +0 -1
- package/dist/install-hints.js +0 -35
- package/dist/install-hints.js.map +0 -1
- package/src/install-hints.ts +0 -54
package/dist/scaffolder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"AAqBA,KAAK,aAAa,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,yBAAyB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnD,CAAC;AA8qBF,QAAA,MAAM,QAAQ,GAAU,UAAS,aAAkB,KAAG,OAAO,CAAC,IAAI,CAkBjE,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/scaffolder.js
CHANGED
|
@@ -2,29 +2,26 @@ 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, 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" };
|
|
10
10
|
import { writeBuildAllowance } from "./build-allowance.js";
|
|
11
11
|
import { OperationCanceledError, ScaffoldAbortedError } from "./errors.js";
|
|
12
|
-
import { getInstallHint } from "./install-hints.js";
|
|
13
12
|
import { updateManifest } from "./manifest.js";
|
|
14
13
|
import { isKnownPackageManager, PACKAGE_MANAGER_VALUES, PACKAGE_MANAGERS, } from "./package-managers.js";
|
|
15
14
|
import { isValidProjectName } from "./project-name.js";
|
|
16
15
|
import { listTemplates, renderFile } from "./templates.js";
|
|
17
16
|
const selfVersion = packageManifest.version;
|
|
18
17
|
const ICON_TEMPLATE_NAME = "icon.svg";
|
|
19
|
-
const
|
|
20
|
-
* Generated by \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
|
|
21
|
-
*/`;
|
|
18
|
+
const TYPESCRIPT_VERSION = "^6.0.3";
|
|
22
19
|
const DEPENDENCIES = ["@gtkx/cairo", "@gtkx/css", "@gtkx/runtime", "@gtkx/react", "react"];
|
|
23
20
|
const DEV_DEPENDENCIES = ["@gtkx/cli", "@gtkx/config", "@gtkx/mcp", "vite"];
|
|
24
21
|
const TYPESCRIPT_DEV_DEPENDENCIES = ["@types/node", "@types/react", "typescript"];
|
|
25
22
|
const TESTING_DEV_DEPENDENCIES = ["@gtkx/testing", "vitest"];
|
|
26
23
|
const DEV_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.devCommand]));
|
|
27
|
-
const
|
|
24
|
+
const INSTALL_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.installCommand]));
|
|
28
25
|
const INHERITED_SCRIPT_POLICY_VARS = ["npm_config_allow_scripts", "npm_config_allow-scripts"];
|
|
29
26
|
const TESTING_TEMPLATES = new Set(["vitest.config.ts", "tests/app.test.tsx"]);
|
|
30
27
|
const TYPESCRIPT_TEMPLATES = new Set(["tsconfig.json", "src/gtkx-env.d.ts"]);
|
|
@@ -33,13 +30,12 @@ const HEADLESS_COMPOSITOR_NOTE = `
|
|
|
33
30
|
To run tests, you need a headless Wayland compositor installed:
|
|
34
31
|
Fedora: sudo dnf install sway
|
|
35
32
|
Ubuntu: sudo apt install sway`;
|
|
36
|
-
const RECOVERY_HEADING = "Finish the setup by adding the dependencies again, which is safe to repeat:";
|
|
37
33
|
const APPLICATION_ID_FORMAT_ERROR = "Application ID must be reverse domain notation (e.g., com.example.myapp)";
|
|
38
34
|
const APPLICATION_ID_PREFIX = "com.";
|
|
39
35
|
const APPLICATION_ID_SUFFIX = ".app";
|
|
40
36
|
const APPLICATION_ID_SEGMENT_LIMIT = APPLICATION_ID_MAX_LENGTH - APPLICATION_ID_PREFIX.length -
|
|
41
37
|
APPLICATION_ID_SUFFIX.length;
|
|
42
|
-
const
|
|
38
|
+
const displayNameFromProjectName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
|
|
43
39
|
const gitConfigValue = (key) => {
|
|
44
40
|
const git = tryResolveExecutable("git");
|
|
45
41
|
if (git === undefined) {
|
|
@@ -58,19 +54,15 @@ const applicationIdSegment = (name) => {
|
|
|
58
54
|
return segment.slice(0, APPLICATION_ID_SEGMENT_LIMIT);
|
|
59
55
|
};
|
|
60
56
|
const suggestApplicationId = (name) => `${APPLICATION_ID_PREFIX}${applicationIdSegment(name)}${APPLICATION_ID_SUFFIX}`;
|
|
61
|
-
const stripTrailingSlashes = (value) => {
|
|
62
|
-
let end = value.length;
|
|
63
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
64
|
-
end--;
|
|
65
|
-
}
|
|
66
|
-
return value.slice(0, end);
|
|
67
|
-
};
|
|
68
|
-
const formatTargetDir = (target) => stripTrailingSlashes(target.trim().replaceAll(/[<>:"\\|?*]/g, ""));
|
|
69
57
|
const deriveProjectName = (target) => basename(resolve(process.cwd(), target));
|
|
70
58
|
const isDirEmpty = (dir) => {
|
|
71
59
|
const entries = readdirSync(dir);
|
|
72
60
|
return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
|
|
73
61
|
};
|
|
62
|
+
const shouldInitializeGit = (root) => {
|
|
63
|
+
const entry = lstatSync(root, { throwIfNoEntry: false });
|
|
64
|
+
return entry === undefined || (entry.isDirectory() && readdirSync(root).length === 0);
|
|
65
|
+
};
|
|
74
66
|
const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
75
67
|
const devDeps = [...DEV_DEPENDENCIES];
|
|
76
68
|
if (isTypescript) {
|
|
@@ -81,8 +73,9 @@ const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
|
|
|
81
73
|
}
|
|
82
74
|
return devDeps;
|
|
83
75
|
};
|
|
76
|
+
const isCancellation = (value) => p.isCancel(value);
|
|
84
77
|
const guardCancellation = (value) => {
|
|
85
|
-
if (
|
|
78
|
+
if (isCancellation(value)) {
|
|
86
79
|
p.cancel("Operation canceled");
|
|
87
80
|
throw new OperationCanceledError();
|
|
88
81
|
}
|
|
@@ -100,6 +93,13 @@ const validateProjectName = (value) => {
|
|
|
100
93
|
const validateApplicationIdFormat = (value) => isValidApplicationId(value) ? undefined : APPLICATION_ID_FORMAT_ERROR;
|
|
101
94
|
const validateApplicationIdInput = (value) => value ? validateApplicationIdFormat(value) : "Application ID is required";
|
|
102
95
|
const validateApplicationIdAnswer = (value) => value ? validateApplicationIdFormat(value) : undefined;
|
|
96
|
+
const validateDisplayName = (value) => {
|
|
97
|
+
if (value === undefined || value.trim().length === 0) {
|
|
98
|
+
return "Display name is required";
|
|
99
|
+
}
|
|
100
|
+
return value.includes("\n") || value.includes("\r") ? "Display name must be a single line" : undefined;
|
|
101
|
+
};
|
|
102
|
+
const validateDisplayNameAnswer = (value) => value ? validateDisplayName(value) : undefined;
|
|
103
103
|
const fail = (message) => {
|
|
104
104
|
p.log.error(message);
|
|
105
105
|
throw new ScaffoldAbortedError(message);
|
|
@@ -119,12 +119,12 @@ const validateTargetDir = (target) => {
|
|
|
119
119
|
}
|
|
120
120
|
return validateProjectName(deriveProjectName(target));
|
|
121
121
|
};
|
|
122
|
-
const validateTargetAnswer = (value) => validateTargetDir(
|
|
123
|
-
const promptTarget = async () =>
|
|
122
|
+
const validateTargetAnswer = (value) => validateTargetDir(value ?? "");
|
|
123
|
+
const promptTarget = async () => guardCancellation(await p.text({
|
|
124
124
|
message: "Project directory",
|
|
125
125
|
placeholder: "my-app",
|
|
126
126
|
validate: validateTargetAnswer,
|
|
127
|
-
}))
|
|
127
|
+
}));
|
|
128
128
|
const promptApplicationId = async (name) => {
|
|
129
129
|
const defaultApplicationId = suggestApplicationId(name);
|
|
130
130
|
return guardCancellation(await p.text({
|
|
@@ -134,6 +134,15 @@ const promptApplicationId = async (name) => {
|
|
|
134
134
|
validate: validateApplicationIdAnswer,
|
|
135
135
|
}));
|
|
136
136
|
};
|
|
137
|
+
const promptDisplayName = async (name) => {
|
|
138
|
+
const defaultDisplayName = displayNameFromProjectName(name);
|
|
139
|
+
return guardCancellation(await p.text({
|
|
140
|
+
message: "Display name",
|
|
141
|
+
placeholder: defaultDisplayName,
|
|
142
|
+
defaultValue: defaultDisplayName,
|
|
143
|
+
validate: validateDisplayNameAnswer,
|
|
144
|
+
}));
|
|
145
|
+
};
|
|
137
146
|
const packageManagerOption = (manager, detected) => {
|
|
138
147
|
let hint;
|
|
139
148
|
if (detected === manager.value) {
|
|
@@ -162,48 +171,69 @@ const promptPackageManager = async () => {
|
|
|
162
171
|
initialValue: initial,
|
|
163
172
|
}));
|
|
164
173
|
};
|
|
165
|
-
const isOptionEnabled = async (value, isInteractive, message) => value ??
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
rmSync(join(root, entry), { recursive: true, force: true });
|
|
174
|
+
const isOptionEnabled = async (value, isInteractive, message) => value ?? (!isInteractive || guardCancellation(await p.confirm({ message, initialValue: true })));
|
|
175
|
+
const formatFileList = (heading, files) => {
|
|
176
|
+
const indentedFiles = files.map((file) => ` ${file}`).join("\n");
|
|
177
|
+
return `${heading}:\n${indentedFiles}`;
|
|
178
|
+
};
|
|
179
|
+
const reportPlannedChanges = (changes) => {
|
|
180
|
+
if (changes.length > 0) {
|
|
181
|
+
p.log.info(formatFileList("Scaffold files to replace", changes));
|
|
175
182
|
}
|
|
176
183
|
};
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
184
|
+
const reportCompletedChanges = (changes) => {
|
|
185
|
+
if (changes.length > 0) {
|
|
186
|
+
p.log.success(formatFileList("Replaced scaffold files", changes));
|
|
180
187
|
}
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
};
|
|
189
|
+
const confirmOverwrite = async (target, changes) => {
|
|
190
|
+
reportPlannedChanges(changes);
|
|
191
|
+
const shouldReplace = guardCancellation(await p.confirm({
|
|
192
|
+
message: `Directory "${target}" is not empty. Replace scaffold files?`,
|
|
183
193
|
initialValue: false,
|
|
184
194
|
}));
|
|
195
|
+
if (!shouldReplace) {
|
|
196
|
+
fail(`Directory "${target}" is not empty`);
|
|
197
|
+
}
|
|
185
198
|
};
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
const refuseNonEmptyDirectory = (target, changes) => {
|
|
200
|
+
const heading = `Directory "${target}" is not empty. Pass --overwrite to`;
|
|
201
|
+
return fail(changes.length === 0
|
|
202
|
+
? `${heading} scaffold into it`
|
|
203
|
+
: formatFileList(`${heading} replace these scaffold files`, changes));
|
|
204
|
+
};
|
|
205
|
+
const acceptNonEmptyDirectory = async (target, options, changes) => {
|
|
206
|
+
if (options.isInteractive) {
|
|
207
|
+
await confirmOverwrite(target, changes);
|
|
208
|
+
return;
|
|
190
209
|
}
|
|
191
|
-
if (
|
|
192
|
-
|
|
210
|
+
if (options.shouldOverwrite === true) {
|
|
211
|
+
reportPlannedChanges(changes);
|
|
212
|
+
return;
|
|
193
213
|
}
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
refuseNonEmptyDirectory(target, changes);
|
|
215
|
+
};
|
|
216
|
+
const assertTargetIsDirectory = (root, target) => {
|
|
217
|
+
const stats = lstatSync(root, { throwIfNoEntry: false });
|
|
218
|
+
if (stats?.isSymbolicLink()) {
|
|
219
|
+
fail(`Cannot scaffold through a symbolic link: ${root}`);
|
|
196
220
|
}
|
|
197
|
-
if (
|
|
198
|
-
|
|
221
|
+
if (stats !== undefined && !stats.isDirectory()) {
|
|
222
|
+
fail(`Target "${target}" is not a directory`);
|
|
199
223
|
}
|
|
200
|
-
|
|
224
|
+
};
|
|
225
|
+
const validateTargetAvailability = async (root, target, options, changes) => {
|
|
226
|
+
const stats = statSync(root, { throwIfNoEntry: false });
|
|
227
|
+
if (stats === undefined || isDirEmpty(root)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
await acceptNonEmptyDirectory(target, options, changes);
|
|
201
231
|
};
|
|
202
232
|
const resolveTarget = async (options) => {
|
|
203
233
|
if (options.name === undefined && options.isInteractive) {
|
|
204
234
|
return promptTarget();
|
|
205
235
|
}
|
|
206
|
-
const target =
|
|
236
|
+
const target = options.name ?? "";
|
|
207
237
|
const error = validateTargetDir(target);
|
|
208
238
|
return error === undefined ? target : fail(error);
|
|
209
239
|
};
|
|
@@ -213,6 +243,12 @@ const resolveApplicationId = async (options, name) => {
|
|
|
213
243
|
const error = validateApplicationIdInput(applicationId);
|
|
214
244
|
return error === undefined ? applicationId : fail(error);
|
|
215
245
|
};
|
|
246
|
+
const resolveDisplayName = async (options, name) => {
|
|
247
|
+
const value = options.displayName ??
|
|
248
|
+
(options.isInteractive ? await promptDisplayName(name) : displayNameFromProjectName(name));
|
|
249
|
+
const error = validateDisplayName(value);
|
|
250
|
+
return error === undefined ? value.trim() : fail(error);
|
|
251
|
+
};
|
|
216
252
|
const resolvePackageManager = async (requested, options) => {
|
|
217
253
|
if (requested !== undefined) {
|
|
218
254
|
return requested;
|
|
@@ -226,23 +262,24 @@ const resolveOptions = async (options) => {
|
|
|
226
262
|
const requested = requestedPackageManager(options.packageManager);
|
|
227
263
|
const target = await resolveTarget(options);
|
|
228
264
|
const name = deriveProjectName(target);
|
|
265
|
+
const displayName = await resolveDisplayName(options, name);
|
|
229
266
|
const applicationId = await resolveApplicationId(options, name);
|
|
230
267
|
const root = resolve(process.cwd(), target);
|
|
231
|
-
const
|
|
232
|
-
const shouldEmptyTarget = await shouldEmptyTargetDirectory(root, target, options);
|
|
268
|
+
const shouldInitialize = shouldInitializeGit(root);
|
|
233
269
|
const packageManager = await resolvePackageManager(requested, options);
|
|
234
270
|
const isTypescript = await isOptionEnabled(options.isTypescript, options.isInteractive, "Use TypeScript?");
|
|
235
271
|
const shouldIncludeTesting = await isOptionEnabled(options.shouldIncludeTesting, options.isInteractive, "Include testing setup (Vitest)?");
|
|
236
272
|
return {
|
|
237
273
|
target,
|
|
238
274
|
root,
|
|
239
|
-
isCurrentDirectory,
|
|
240
275
|
name,
|
|
276
|
+
displayName,
|
|
241
277
|
applicationId,
|
|
242
278
|
packageManager,
|
|
243
279
|
isTypescript,
|
|
244
280
|
shouldIncludeTesting,
|
|
245
|
-
|
|
281
|
+
shouldInitializeGit: shouldInitialize,
|
|
282
|
+
shouldInstallDependencies: options.shouldInstallDependencies !== false,
|
|
246
283
|
};
|
|
247
284
|
};
|
|
248
285
|
const isTemplateIncluded = (templateRelativePath, resolved) => {
|
|
@@ -263,27 +300,74 @@ const destinationPath = (template, resolved) => {
|
|
|
263
300
|
const templateContext = (resolved) => ({
|
|
264
301
|
name: resolved.name,
|
|
265
302
|
applicationId: resolved.applicationId,
|
|
266
|
-
|
|
303
|
+
displayName: resolved.displayName,
|
|
267
304
|
shouldIncludeTesting: resolved.shouldIncludeTesting,
|
|
268
305
|
isTypescript: resolved.isTypescript,
|
|
269
306
|
importExtension: resolved.isTypescript ? ".js" : ".jsx",
|
|
270
|
-
developerName: gitConfigValue("user.name") ??
|
|
307
|
+
developerName: gitConfigValue("user.name") ?? resolved.displayName,
|
|
271
308
|
developerEmail: gitConfigValue("user.email"),
|
|
272
309
|
});
|
|
273
|
-
const
|
|
310
|
+
const assertSafeAncestor = (ancestor) => {
|
|
311
|
+
const entry = lstatSync(ancestor, { throwIfNoEntry: false });
|
|
312
|
+
if (entry?.isSymbolicLink()) {
|
|
313
|
+
fail(`Cannot scaffold through a symbolic link: ${ancestor}`);
|
|
314
|
+
}
|
|
315
|
+
if (entry !== undefined && !entry.isDirectory()) {
|
|
316
|
+
fail(`Cannot scaffold through a non-directory path: ${ancestor}`);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const assertSafeAncestors = (root, relativePath) => {
|
|
320
|
+
let ancestor = root;
|
|
321
|
+
const segments = relativePath.split(sep);
|
|
322
|
+
const ancestorSegments = segments.slice(0, -1);
|
|
323
|
+
for (const segment of ancestorSegments) {
|
|
324
|
+
ancestor = join(ancestor, segment);
|
|
325
|
+
assertSafeAncestor(ancestor);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const hasReplaceableDestination = (destination) => {
|
|
329
|
+
const entry = lstatSync(destination, { throwIfNoEntry: false });
|
|
330
|
+
const isReplaceable = entry === undefined || entry.isFile() || entry.isSymbolicLink();
|
|
331
|
+
if (!isReplaceable) {
|
|
332
|
+
fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
|
|
333
|
+
}
|
|
334
|
+
return entry !== undefined;
|
|
335
|
+
};
|
|
336
|
+
const hasExistingDestination = (root, destination) => {
|
|
337
|
+
assertSafeAncestors(root, relative(root, destination));
|
|
338
|
+
return hasReplaceableDestination(destination);
|
|
339
|
+
};
|
|
340
|
+
const plannedScaffoldFiles = async (root, resolved) => {
|
|
274
341
|
const context = templateContext(resolved);
|
|
342
|
+
const files = [];
|
|
275
343
|
for (const template of listTemplates()) {
|
|
276
|
-
if (
|
|
277
|
-
|
|
344
|
+
if (isTemplateIncluded(template, resolved)) {
|
|
345
|
+
files.push({
|
|
346
|
+
destination: join(root, destinationPath(template, resolved)),
|
|
347
|
+
contents: await renderFile(template, context),
|
|
348
|
+
});
|
|
278
349
|
}
|
|
279
|
-
const destination = join(root, destinationPath(template, resolved));
|
|
280
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
281
|
-
writeFileSync(destination, await renderFile(template, context));
|
|
282
350
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
351
|
+
return files;
|
|
352
|
+
};
|
|
353
|
+
const auxiliaryDestinations = (root, resolved) => resolved.packageManager === "pnpm" ? [join(root, "pnpm-workspace.yaml")] : [];
|
|
354
|
+
const planScaffoldProject = async (root, resolved) => {
|
|
355
|
+
const files = await plannedScaffoldFiles(root, resolved);
|
|
356
|
+
const destinations = [...files.map((file) => file.destination), ...auxiliaryDestinations(root, resolved)];
|
|
357
|
+
const replaceDestinations = destinations.filter((destination) => hasExistingDestination(root, destination));
|
|
358
|
+
return {
|
|
359
|
+
files,
|
|
360
|
+
replaceDestinations,
|
|
361
|
+
changes: replaceDestinations.map((destination) => relative(root, destination)),
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
const scaffoldProject = (plan) => {
|
|
365
|
+
for (const destination of plan.replaceDestinations) {
|
|
366
|
+
rmSync(destination);
|
|
367
|
+
}
|
|
368
|
+
for (const file of plan.files) {
|
|
369
|
+
mkdirSync(dirname(file.destination), { recursive: true });
|
|
370
|
+
writeFileSync(file.destination, file.contents);
|
|
287
371
|
}
|
|
288
372
|
};
|
|
289
373
|
const withoutInheritedScriptPolicy = async (run) => {
|
|
@@ -302,30 +386,20 @@ const withoutInheritedScriptPolicy = async (run) => {
|
|
|
302
386
|
}
|
|
303
387
|
}
|
|
304
388
|
};
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
await addDependency(dependencies, { cwd, packageManager, dev: isDev, silent: true });
|
|
309
|
-
});
|
|
310
|
-
};
|
|
311
|
-
const pin = (names) => names.map((dependency) => dependency.startsWith("@gtkx/") ? `${dependency}@^${selfVersion}` : dependency);
|
|
312
|
-
const formatRecovery = ({ resolved, devDependencies }) => {
|
|
313
|
-
const add = ADD_COMMAND[resolved.packageManager];
|
|
314
|
-
const steps = [
|
|
315
|
-
...(resolved.isCurrentDirectory ? [] : [`cd ${resolved.target}`]),
|
|
316
|
-
`${add} ${pin(DEPENDENCIES).join(" ")}`,
|
|
317
|
-
`${add} -D ${pin(devDependencies).join(" ")}`,
|
|
318
|
-
];
|
|
319
|
-
const indented = steps.map((step) => ` ${step}`).join("\n");
|
|
320
|
-
return `${RECOVERY_HEADING}\n${indented}`;
|
|
321
|
-
};
|
|
322
|
-
const explainInstallFailure = (options) => {
|
|
323
|
-
const hint = getInstallHint(errorMessage(options.error));
|
|
324
|
-
if (hint !== undefined) {
|
|
325
|
-
p.log.warn(hint);
|
|
389
|
+
const dependencyVersions = (names) => Object.fromEntries(names.map((dependency) => {
|
|
390
|
+
if (dependency.startsWith("@gtkx/")) {
|
|
391
|
+
return [dependency, `^${selfVersion}`];
|
|
326
392
|
}
|
|
327
|
-
|
|
393
|
+
return [dependency, dependency === "typescript" ? TYPESCRIPT_VERSION : "latest"];
|
|
394
|
+
}));
|
|
395
|
+
const dependencyArguments = (names) => Object.entries(dependencyVersions(names)).map(([name, version]) => version === "latest" ? name : `${name}@${version}`);
|
|
396
|
+
const writeDependencies = (root, devDependencies) => {
|
|
397
|
+
updateManifest(root, (manifest) => {
|
|
398
|
+
manifest.dependencies = dependencyVersions(DEPENDENCIES);
|
|
399
|
+
manifest.devDependencies = dependencyVersions(devDependencies);
|
|
400
|
+
});
|
|
328
401
|
};
|
|
402
|
+
const formatRecovery = (resolved) => `To finish setup, run ${INSTALL_COMMAND[resolved.packageManager]} in ${resolved.root}.`;
|
|
329
403
|
const runWithSpinner = async (step) => {
|
|
330
404
|
const spinner = p.spinner();
|
|
331
405
|
spinner.start(step.pending);
|
|
@@ -336,32 +410,45 @@ const runWithSpinner = async (step) => {
|
|
|
336
410
|
catch (error) {
|
|
337
411
|
spinner.error(step.failed);
|
|
338
412
|
p.log.error(errorMessage(error));
|
|
339
|
-
step.explain?.(
|
|
413
|
+
step.explain?.();
|
|
340
414
|
throw new ScaffoldAbortedError(step.failed);
|
|
341
415
|
}
|
|
342
416
|
};
|
|
343
|
-
const installAllDependencies = async (
|
|
344
|
-
const { resolved, devDependencies } = options;
|
|
417
|
+
const installAllDependencies = async (resolved, devDependencies) => {
|
|
345
418
|
const { root, packageManager } = resolved;
|
|
346
419
|
await runWithSpinner({
|
|
347
420
|
pending: "Installing dependencies...",
|
|
348
421
|
done: "Dependencies installed",
|
|
349
422
|
failed: "Failed to install dependencies",
|
|
350
423
|
run: async () => {
|
|
351
|
-
await
|
|
352
|
-
|
|
424
|
+
await withoutInheritedScriptPolicy(async () => {
|
|
425
|
+
const options = { cwd: root, packageManager, silent: true };
|
|
426
|
+
await addDependency(dependencyArguments(DEPENDENCIES), options);
|
|
427
|
+
await addDependency(dependencyArguments(devDependencies), { ...options, dev: true });
|
|
428
|
+
});
|
|
353
429
|
},
|
|
354
|
-
explain: (
|
|
355
|
-
|
|
430
|
+
explain: () => {
|
|
431
|
+
p.log.info(formatRecovery(resolved));
|
|
356
432
|
},
|
|
357
433
|
});
|
|
358
434
|
};
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
435
|
+
const isInsideGitRepository = (root) => {
|
|
436
|
+
const git = tryResolveExecutable("git");
|
|
437
|
+
if (git === undefined) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
execFileSync(git, ["-C", root, "rev-parse", "--show-toplevel"], { stdio: "ignore" });
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
363
447
|
};
|
|
364
|
-
const initializeGitRepo = async (root) => {
|
|
448
|
+
const initializeGitRepo = async (root, shouldInitialize) => {
|
|
449
|
+
if (!shouldInitialize || isInsideGitRepository(root)) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
365
452
|
const spinner = p.spinner();
|
|
366
453
|
spinner.start("Initializing git repository...");
|
|
367
454
|
try {
|
|
@@ -377,21 +464,18 @@ const initializeGitRepo = async (root) => {
|
|
|
377
464
|
};
|
|
378
465
|
const printNextSteps = (resolved) => {
|
|
379
466
|
const devCmd = DEV_COMMAND[resolved.packageManager];
|
|
380
|
-
const
|
|
467
|
+
const installStep = resolved.shouldInstallDependencies ? "" : `${INSTALL_COMMAND[resolved.packageManager]}\n`;
|
|
381
468
|
const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
|
|
382
|
-
p.note(`${
|
|
469
|
+
p.note(`${installStep}${devCmd}${testingNote}`, `Run in ${resolved.root}`);
|
|
383
470
|
};
|
|
384
|
-
const createProjectStructure = async (root, resolved) => {
|
|
471
|
+
const createProjectStructure = async (root, resolved, plan) => {
|
|
385
472
|
await runWithSpinner({
|
|
386
473
|
pending: "Creating project structure...",
|
|
387
474
|
done: "Project structure created",
|
|
388
475
|
failed: "Failed to create the project structure",
|
|
389
|
-
run:
|
|
476
|
+
run: () => {
|
|
390
477
|
mkdirSync(root, { recursive: true });
|
|
391
|
-
|
|
392
|
-
emptyDir(root);
|
|
393
|
-
}
|
|
394
|
-
await scaffoldProject(root, resolved);
|
|
478
|
+
scaffoldProject(plan);
|
|
395
479
|
writeBuildAllowance(root, resolved.packageManager);
|
|
396
480
|
},
|
|
397
481
|
});
|
|
@@ -401,12 +485,16 @@ const scaffold = async (options = {}) => {
|
|
|
401
485
|
const resolved = await resolveOptions(options);
|
|
402
486
|
const { root } = resolved;
|
|
403
487
|
const devDeps = getDevDependencies(resolved);
|
|
404
|
-
|
|
405
|
-
await
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
488
|
+
assertTargetIsDirectory(root, resolved.target);
|
|
489
|
+
const plan = await planScaffoldProject(root, resolved);
|
|
490
|
+
await validateTargetAvailability(root, resolved.target, options, plan.changes);
|
|
491
|
+
await createProjectStructure(root, resolved, plan);
|
|
492
|
+
writeDependencies(root, devDeps);
|
|
493
|
+
reportCompletedChanges(plan.changes);
|
|
494
|
+
if (resolved.shouldInstallDependencies) {
|
|
495
|
+
await installAllDependencies(resolved, devDeps);
|
|
496
|
+
}
|
|
497
|
+
await initializeGitRepo(root, resolved.shouldInitializeGit);
|
|
410
498
|
printNextSteps(resolved);
|
|
411
499
|
};
|
|
412
500
|
export { scaffold };
|