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.
Files changed (43) hide show
  1. package/README.md +8 -163
  2. package/dist/build-allowance.d.ts.map +1 -1
  3. package/dist/build-allowance.js +6 -5
  4. package/dist/build-allowance.js.map +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/command.d.ts +12 -3
  8. package/dist/command.d.ts.map +1 -1
  9. package/dist/command.js +111 -45
  10. package/dist/command.js.map +1 -1
  11. package/dist/manifest.d.ts.map +1 -1
  12. package/dist/manifest.js +2 -4
  13. package/dist/manifest.js.map +1 -1
  14. package/dist/package-managers.d.ts +3 -3
  15. package/dist/package-managers.d.ts.map +1 -1
  16. package/dist/package-managers.js +21 -3
  17. package/dist/package-managers.js.map +1 -1
  18. package/dist/scaffolder.d.ts +2 -0
  19. package/dist/scaffolder.d.ts.map +1 -1
  20. package/dist/scaffolder.js +200 -112
  21. package/dist/scaffolder.js.map +1 -1
  22. package/dist/templates/gtkx.config.ts.ejs +3 -4
  23. package/dist/templates/package.json.ejs +4 -1
  24. package/dist/templates/src/app.tsx.ejs +27 -24
  25. package/dist/templates.d.ts +1 -1
  26. package/dist/templates.d.ts.map +1 -1
  27. package/dist/templates.js.map +1 -1
  28. package/package.json +26 -12
  29. package/src/build-allowance.ts +6 -7
  30. package/src/cli.ts +1 -1
  31. package/src/command.ts +140 -57
  32. package/src/manifest.ts +2 -4
  33. package/src/package-managers.ts +21 -3
  34. package/src/scaffolder.ts +282 -153
  35. package/src/templates/gtkx.config.ts.ejs +3 -4
  36. package/src/templates/package.json.ejs +4 -1
  37. package/src/templates/src/app.tsx.ejs +27 -24
  38. package/src/templates.ts +1 -1
  39. package/dist/install-hints.d.ts +0 -3
  40. package/dist/install-hints.d.ts.map +0 -1
  41. package/dist/install-hints.js +0 -35
  42. package/dist/install-hints.js.map +0 -1
  43. package/src/install-hints.ts +0 -54
@@ -1 +1 @@
1
- {"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"AAsBA,KAAK,aAAa,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,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;CACzC,CAAC;AAkjBF,QAAA,MAAM,QAAQ,GAAU,UAAS,aAAkB,KAAG,OAAO,CAAC,IAAI,CAcjE,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
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"}
@@ -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 GTKX_ENV_MODULE_HEADER = `/**
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 ADD_COMMAND = Object.fromEntries(PACKAGE_MANAGERS.map((manager) => [manager.value, manager.addCommand]));
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 titleFromName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
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 (p.isCancel(value)) {
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(formatTargetDir(value ?? ""));
123
- const promptTarget = async () => formatTargetDir(guardCancellation(await p.text({
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
- (isInteractive
167
- ? guardCancellation(await p.confirm({ message, initialValue: true }))
168
- : true);
169
- const emptyDir = (root) => {
170
- for (const entry of readdirSync(root)) {
171
- if (entry === ".git") {
172
- continue;
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 shouldOverwriteDirectory = async (target, options) => {
178
- if (!options.isInteractive) {
179
- return options.shouldOverwrite === true;
184
+ const reportCompletedChanges = (changes) => {
185
+ if (changes.length > 0) {
186
+ p.log.success(formatFileList("Replaced scaffold files", changes));
180
187
  }
181
- return guardCancellation(await p.confirm({
182
- message: `Directory "${target}" is not empty. Overwrite its contents?`,
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 shouldEmptyTargetDirectory = async (root, target, options) => {
187
- const stats = statSync(root, { throwIfNoEntry: false });
188
- if (stats === undefined) {
189
- return false;
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 (!stats.isDirectory()) {
192
- return fail(`Target "${target}" is not a directory`);
210
+ if (options.shouldOverwrite === true) {
211
+ reportPlannedChanges(changes);
212
+ return;
193
213
  }
194
- if (isDirEmpty(root)) {
195
- return false;
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 (await shouldOverwriteDirectory(target, options)) {
198
- return true;
221
+ if (stats !== undefined && !stats.isDirectory()) {
222
+ fail(`Target "${target}" is not a directory`);
199
223
  }
200
- return fail(`Directory "${target}" is not empty`);
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 = formatTargetDir(options.name ?? "");
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 isCurrentDirectory = root === process.cwd();
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
- shouldEmptyTarget,
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
- title: titleFromName(resolved.name),
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") ?? titleFromName(resolved.name),
307
+ developerName: gitConfigValue("user.name") ?? resolved.displayName,
271
308
  developerEmail: gitConfigValue("user.email"),
272
309
  });
273
- const scaffoldProject = async (root, resolved) => {
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 (!isTemplateIncluded(template, resolved)) {
277
- continue;
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
- if (resolved.shouldIncludeTesting) {
284
- updateManifest(root, (manifest) => {
285
- manifest.scripts.test = "vitest run";
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 installDependencies = async (options) => {
306
- const { cwd, packageManager, dependencies, isDev } = options;
307
- await withoutInheritedScriptPolicy(async () => {
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
- p.log.info(formatRecovery(options));
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?.(error);
413
+ step.explain?.();
340
414
  throw new ScaffoldAbortedError(step.failed);
341
415
  }
342
416
  };
343
- const installAllDependencies = async (options) => {
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 installDependencies({ cwd: root, packageManager, dependencies: pin(DEPENDENCIES), isDev: false });
352
- await installDependencies({ cwd: root, packageManager, dependencies: pin(devDependencies), isDev: true });
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: (error) => {
355
- explainInstallFailure({ error, resolved, devDependencies });
430
+ explain: () => {
431
+ p.log.info(formatRecovery(resolved));
356
432
  },
357
433
  });
358
434
  };
359
- const writeInitialEnvModule = (root) => {
360
- const storeDir = join(root, "node_modules", ".gtkx");
361
- mkdirSync(storeDir, { recursive: true });
362
- writeFileSync(join(storeDir, "env.d.ts"), `${GTKX_ENV_MODULE_HEADER}\n`);
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 cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
467
+ const installStep = resolved.shouldInstallDependencies ? "" : `${INSTALL_COMMAND[resolved.packageManager]}\n`;
381
468
  const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
382
- p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
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: async () => {
476
+ run: () => {
390
477
  mkdirSync(root, { recursive: true });
391
- if (resolved.shouldEmptyTarget) {
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
- await createProjectStructure(root, resolved);
405
- await installAllDependencies({ resolved, devDependencies: devDeps });
406
- if (resolved.isTypescript) {
407
- writeInitialEnvModule(root);
408
- }
409
- await initializeGitRepo(root);
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 };