complete-cli 1.3.1 → 1.3.3

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.
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { Command, Option } from "clipanion";
3
3
  import { ReadonlySet } from "complete-common";
4
- import { $, deleteFileOrDirectoryAsync, fatalError, isDirectory, isFileAsync, readFileAsync, writeFileAsync, } from "complete-node";
4
+ import { $, deleteFileOrDirectory, fatalError, isDirectory, isFile, readFile, writeFile, } from "complete-node";
5
5
  import klawSync from "klaw-sync";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
@@ -42,9 +42,13 @@ export class CheckCommand extends Command {
42
42
  /** @returns Whether the directory was valid. */
43
43
  async function checkTemplateDirectory(templateDirectory, ignoreFileNamesSet, verbose) {
44
44
  let oneOrMoreErrors = false;
45
- for (const klawItem of klawSync(templateDirectory)) {
45
+ // We use `klawSync` instead of `klaw` so that the output will be deterministic.
46
+ const klawItems = klawSync(templateDirectory);
47
+ for (const klawItem of klawItems) {
46
48
  const templateFilePath = klawItem.path;
47
- if (isDirectory(templateFilePath)) {
49
+ // eslint-disable-next-line no-await-in-loop
50
+ const templateExists = await isDirectory(templateFilePath);
51
+ if (templateExists) {
48
52
  continue;
49
53
  }
50
54
  const originalFileName = path.basename(templateFilePath);
@@ -95,7 +99,7 @@ async function checkDynamicFiles(ignoreFileNamesSet, verbose) {
95
99
  }
96
100
  /** @returns Whether the project file is valid in reference to the template file. */
97
101
  async function compareTextFiles(projectFilePath, templateFilePath, verbose) {
98
- const fileExists = await isFileAsync(projectFilePath);
102
+ const fileExists = await isFile(projectFilePath);
99
103
  if (!fileExists) {
100
104
  console.log(`Failed to find the following file: ${projectFilePath}`);
101
105
  printTemplateLocation(templateFilePath);
@@ -109,8 +113,8 @@ async function compareTextFiles(projectFilePath, templateFilePath, verbose) {
109
113
  console.log(`The contents of the following file do not match: ${chalk.red(projectFilePath)}`);
110
114
  printTemplateLocation(templateFilePath);
111
115
  if (verbose) {
112
- const originalTemplateFile = await readFileAsync(templateFilePath);
113
- const originalProjectFile = await readFileAsync(projectFilePath);
116
+ const originalTemplateFile = await readFile(templateFilePath);
117
+ const originalProjectFile = await readFile(projectFilePath);
114
118
  console.log("--- Original template file: ---\n");
115
119
  console.log(originalTemplateFile);
116
120
  console.log();
@@ -127,21 +131,21 @@ async function compareTextFiles(projectFilePath, templateFilePath, verbose) {
127
131
  const tempDir = os.tmpdir();
128
132
  const tempProjectFilePath = path.join(tempDir, "tempProjectFile.txt");
129
133
  const tempTemplateFilePath = path.join(tempDir, "tempTemplateFile.txt");
130
- await writeFileAsync(tempProjectFilePath, projectFileObject.text);
131
- await writeFileAsync(tempTemplateFilePath, templateFileObject.text);
134
+ await writeFile(tempProjectFilePath, projectFileObject.text);
135
+ await writeFile(tempTemplateFilePath, templateFileObject.text);
132
136
  try {
133
137
  await $ `diff ${tempProjectFilePath} ${tempTemplateFilePath} --ignore-blank-lines`;
134
138
  }
135
139
  catch {
136
140
  // `diff` will exit with a non-zero code if the files are different, which is expected.
137
141
  }
138
- await deleteFileOrDirectoryAsync(tempProjectFilePath);
139
- await deleteFileOrDirectoryAsync(tempTemplateFilePath);
142
+ await deleteFileOrDirectory(tempProjectFilePath);
143
+ await deleteFileOrDirectory(tempTemplateFilePath);
140
144
  return false;
141
145
  }
142
146
  async function getTruncatedFileText(filePath, ignoreLines, linesBeforeIgnore) {
143
147
  const fileName = path.basename(filePath);
144
- const fileContents = await readFileAsync(filePath);
148
+ const fileContents = await readFile(filePath);
145
149
  return getTruncatedText(fileName, fileContents, ignoreLines, linesBeforeIgnore);
146
150
  }
147
151
  function printTemplateLocation(templateFilePath) {
@@ -1,6 +1,6 @@
1
1
  import { Command, Option } from "clipanion";
2
2
  import { assertObject, isObject } from "complete-common";
3
- import { getFilePath, isFileAsync, readFileAsync, writeFileAsync, } from "complete-node";
3
+ import { getFilePath, isFile, readFile, writeFile } from "complete-node";
4
4
  import path from "node:path";
5
5
  export class MetadataCommand extends Command {
6
6
  static paths = [["metadata"], ["m"]];
@@ -18,10 +18,10 @@ export class MetadataCommand extends Command {
18
18
  const packageJSONPath = await getFilePath("package.json", undefined);
19
19
  const packageRoot = path.dirname(packageJSONPath);
20
20
  const packageMetadataPath = path.join(packageRoot, "package-metadata.json");
21
- const packageMetadataExists = await isFileAsync(packageMetadataPath);
21
+ const packageMetadataExists = await isFile(packageMetadataPath);
22
22
  let packageMetadata;
23
23
  if (packageMetadataExists) {
24
- const packageMetadataContents = await readFileAsync(packageMetadataPath);
24
+ const packageMetadataContents = await readFile(packageMetadataPath);
25
25
  const packageMetadataUnknown = JSON.parse(packageMetadataContents);
26
26
  assertObject(packageMetadataUnknown, `Failed to parse the metadata file at: ${packageMetadataPath}`);
27
27
  packageMetadata = packageMetadataUnknown;
@@ -43,7 +43,7 @@ export class MetadataCommand extends Command {
43
43
  "lock-reason": this.reason ?? "",
44
44
  };
45
45
  const packageMetadataJSON = JSON.stringify(packageMetadata, undefined, 2);
46
- await writeFileAsync(packageMetadataPath, packageMetadataJSON);
46
+ await writeFile(packageMetadataPath, packageMetadataJSON);
47
47
  const verb = packageMetadataExists ? "modified" : "created";
48
48
  console.log(`Successfully ${verb}: ${packageMetadataPath}`);
49
49
  }
@@ -1,6 +1,6 @@
1
1
  import { Command, Option } from "clipanion";
2
2
  import { isSemanticVersion } from "complete-common";
3
- import { $, fatalError, getPackageJSONFieldsMandatory, getPackageManagerInstallCommand, getPackageManagerLockFileName, getPackageManagersForProject, isFileAsync, isGitRepository, isGitRepositoryClean, isLoggedInToNPM, readFile, updatePackageJSONDependencies, writeFileAsync, } from "complete-node";
3
+ import { $, fatalError, getPackageJSONFieldsMandatory, getPackageManagerInstallCommand, getPackageManagerLockFileName, getPackageManagersForProject, isFile, isGitRepository, isGitRepositoryClean, isLoggedInToNPM, readFile, updatePackageJSONDependencies, writeFile, } from "complete-node";
4
4
  import path from "node:path";
5
5
  import { CWD, DEFAULT_PACKAGE_MANAGER } from "../constants.js";
6
6
  export class PublishCommand extends Command {
@@ -36,7 +36,7 @@ async function validate() {
36
36
  if (!isRepositoryClean) {
37
37
  fatalError("Failed to publish since the Git repository was dirty. Before publishing, you must push any current changes to git. (Version commits should not contain any code changes.)");
38
38
  }
39
- const packageJSONExists = await isFileAsync("package.json");
39
+ const packageJSONExists = await isFile("package.json");
40
40
  if (!packageJSONExists) {
41
41
  fatalError('Failed to find the "package.json" file in the current working directory.');
42
42
  }
@@ -118,15 +118,15 @@ async function incrementVersion(versionBumpType) {
118
118
  }
119
119
  async function unsetDevelopmentConstants() {
120
120
  const constantsTSPath = path.join(CWD, "src", "constants.ts");
121
- const constantsTSExists = await isFileAsync(constantsTSPath);
121
+ const constantsTSExists = await isFile(constantsTSPath);
122
122
  if (!constantsTSExists) {
123
123
  return;
124
124
  }
125
- const constantsTS = readFile(constantsTSPath);
125
+ const constantsTS = await readFile(constantsTSPath);
126
126
  const newConstantsTS = constantsTS
127
127
  .replace("const IS_DEV = true", "const IS_DEV = false")
128
128
  .replace("const DEBUG = true", "const DEBUG = false");
129
- await writeFileAsync(constantsTSPath, newConstantsTS);
129
+ await writeFile(constantsTSPath, newConstantsTS);
130
130
  }
131
131
  async function tryRunNPMScript(scriptName) {
132
132
  console.log(`Running: ${scriptName}`);
@@ -1,14 +1,20 @@
1
1
  import chalk from "chalk";
2
- import { deleteFileOrDirectory, fileOrDirectoryExists, isDirectory, } from "complete-node";
2
+ import { deleteFileOrDirectory, isDirectory, isFile } from "complete-node";
3
3
  import { CWD } from "../../constants.js";
4
4
  import { getInputYesNo, promptEnd, promptLog } from "../../prompt.js";
5
+ /** @throws If the project path is not a file or a directory. */
5
6
  export async function checkIfProjectPathExists(projectPath, yes) {
6
- if (projectPath === CWD || !fileOrDirectoryExists(projectPath)) {
7
+ if (projectPath === CWD) {
7
8
  return;
8
9
  }
9
- const fileType = isDirectory(projectPath) ? "directory" : "file";
10
+ const file = await isFile(projectPath);
11
+ const directory = await isDirectory(projectPath);
12
+ if (!file && !directory) {
13
+ throw new Error(`Failed to detect if the path was a file or a directory: ${projectPath}`);
14
+ }
15
+ const fileType = file ? "file" : "directory";
10
16
  if (yes) {
11
- deleteFileOrDirectory(projectPath);
17
+ await deleteFileOrDirectory(projectPath);
12
18
  promptLog(`Deleted ${fileType}: ${chalk.green(projectPath)}`);
13
19
  return;
14
20
  }
@@ -17,5 +23,5 @@ export async function checkIfProjectPathExists(projectPath, yes) {
17
23
  if (!shouldDelete) {
18
24
  promptEnd("Ok then. Goodbye.");
19
25
  }
20
- deleteFileOrDirectory(projectPath);
26
+ await deleteFileOrDirectory(projectPath);
21
27
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { assertObject, repeat } from "complete-common";
3
- import { $q, copyFileOrDirectory, formatWithPrettier, getFileNamesInDirectory, getPackageJSON, getPackageManagerInstallCICommand, getPackageManagerInstallCommand, isFile, makeDirectory, PackageManager, readFile, renameFile, updatePackageJSONDependencies, writeFile, writeFileAsync, } from "complete-node";
3
+ import { $q, copyFileOrDirectory, formatWithPrettier, getFileNamesInDirectory, getPackageJSON, getPackageManagerInstallCICommand, getPackageManagerInstallCommand, isFile, makeDirectory, PackageManager, readFile, renameFileOrDirectory, updatePackageJSONDependencies, writeFile, } from "complete-node";
4
4
  import path from "node:path";
5
5
  import { ACTION_YML, ACTION_YML_TEMPLATE_PATH, TEMPLATES_DYNAMIC_DIR, TEMPLATES_STATIC_DIR, } from "../../constants.js";
6
6
  import { initGitRepository } from "../../git.js";
@@ -8,11 +8,11 @@ import { promptError, promptLog, promptSpinnerStart } from "../../prompt.js";
8
8
  import { LOCKED_DEPENDENCIES } from "./lockedDependencies.js";
9
9
  export async function createProject(projectName, authorName, projectPath, createNewDir, gitRemoteURL, skipInstall, packageManager) {
10
10
  if (createNewDir) {
11
- makeDirectory(projectPath);
11
+ await makeDirectory(projectPath);
12
12
  }
13
- copyStaticFiles(projectPath);
14
- copyDynamicFiles(projectName, authorName, projectPath, packageManager);
15
- copyPackageManagerSpecificFiles(projectPath, packageManager);
13
+ await copyStaticFiles(projectPath);
14
+ await copyDynamicFiles(projectName, authorName, projectPath, packageManager);
15
+ await copyPackageManagerSpecificFiles(projectPath, packageManager);
16
16
  // There is no package manager lock files yet, so we have to pass "false" to this function.
17
17
  const updated = await updatePackageJSONDependencies(projectPath, false, true);
18
18
  if (!updated) {
@@ -30,49 +30,50 @@ export async function createProject(projectName, authorName, projectPath, create
30
30
  promptLog(`Successfully created project: ${chalk.green(projectName)}`);
31
31
  }
32
32
  /** Copy static files, like "eslint.config.mjs", "tsconfig.json", etc. */
33
- function copyStaticFiles(projectPath) {
34
- copyTemplateDirectoryWithoutOverwriting(TEMPLATES_STATIC_DIR, projectPath);
33
+ async function copyStaticFiles(projectPath) {
34
+ await copyTemplateDirectoryWithoutOverwriting(TEMPLATES_STATIC_DIR, projectPath);
35
35
  // Rename "_gitattributes" to ".gitattributes". (If it is kept as ".gitattributes", then it won't
36
36
  // be committed to git.)
37
37
  const gitAttributesPath = path.join(projectPath, "_gitattributes");
38
38
  const correctGitAttributesPath = path.join(projectPath, ".gitattributes");
39
- renameFile(gitAttributesPath, correctGitAttributesPath);
39
+ await renameFileOrDirectory(gitAttributesPath, correctGitAttributesPath);
40
40
  // Rename "_cspell.config.jsonc" to "cspell.config.jsonc". (If it is kept as
41
41
  // "cspell.config.jsonc", then local spell checking will fail.)
42
42
  const cSpellConfigPath = path.join(projectPath, "_cspell.config.jsonc");
43
43
  const correctCSpellConfigPath = path.join(projectPath, "cspell.config.jsonc");
44
- renameFile(cSpellConfigPath, correctCSpellConfigPath);
44
+ await renameFileOrDirectory(cSpellConfigPath, correctCSpellConfigPath);
45
45
  }
46
- function copyTemplateDirectoryWithoutOverwriting(templateDirPath, projectPath) {
47
- const fileNames = getFileNamesInDirectory(templateDirPath);
48
- for (const fileName of fileNames) {
46
+ async function copyTemplateDirectoryWithoutOverwriting(templateDirPath, projectPath) {
47
+ const fileNames = await getFileNamesInDirectory(templateDirPath);
48
+ await Promise.all(fileNames.map(async (fileName) => {
49
49
  const templateFilePath = path.join(templateDirPath, fileName);
50
50
  const destinationFilePath = path.join(projectPath, fileName);
51
- if (!isFile(destinationFilePath)) {
52
- copyFileOrDirectory(templateFilePath, destinationFilePath);
51
+ const file = await isFile(destinationFilePath);
52
+ if (!file) {
53
+ await copyFileOrDirectory(templateFilePath, destinationFilePath);
53
54
  }
54
- }
55
+ }));
55
56
  }
56
57
  /** Copy files that need to have text replaced inside of them. */
57
- function copyDynamicFiles(projectName, authorName, projectPath, packageManager) {
58
+ async function copyDynamicFiles(projectName, authorName, projectPath, packageManager) {
58
59
  // `.github/workflows/setup/action.yml`
59
60
  {
60
61
  const fileName = ACTION_YML;
61
62
  const templatePath = ACTION_YML_TEMPLATE_PATH;
62
- const template = readFile(templatePath);
63
+ const template = await readFile(templatePath);
63
64
  const installCommand = getPackageManagerInstallCICommand(packageManager);
64
65
  const actionYML = template
65
66
  .replaceAll("PACKAGE_MANAGER_NAME", packageManager)
66
67
  .replaceAll("PACKAGE_MANAGER_INSTALL_COMMAND", installCommand);
67
68
  const setupPath = path.join(projectPath, ".github", "workflows", "setup");
68
- makeDirectory(setupPath);
69
+ await makeDirectory(setupPath);
69
70
  const destinationPath = path.join(setupPath, fileName);
70
- writeFile(destinationPath, actionYML);
71
+ await writeFile(destinationPath, actionYML);
71
72
  }
72
73
  // `.gitignore`
73
74
  {
74
75
  const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "_gitignore");
75
- const template = readFile(templatePath);
76
+ const template = await readFile(templatePath);
76
77
  // Prepend a header with the project name.
77
78
  let separatorLine = "# ";
78
79
  repeat(projectName.length, () => {
@@ -81,27 +82,27 @@ function copyDynamicFiles(projectName, authorName, projectPath, packageManager)
81
82
  separatorLine += "\n";
82
83
  const gitIgnoreHeader = `${separatorLine}# ${projectName}\n${separatorLine}\n`;
83
84
  const nodeGitIgnorePath = path.join(TEMPLATES_DYNAMIC_DIR, "Node.gitignore");
84
- const nodeGitIgnore = readFile(nodeGitIgnorePath);
85
+ const nodeGitIgnore = await readFile(nodeGitIgnorePath);
85
86
  // eslint-disable-next-line prefer-template
86
87
  const gitignore = gitIgnoreHeader + template + "\n" + nodeGitIgnore;
87
88
  // We need to replace the underscore with a period.
88
89
  const destinationPath = path.join(projectPath, ".gitignore");
89
- writeFile(destinationPath, gitignore);
90
+ await writeFile(destinationPath, gitignore);
90
91
  }
91
92
  // `package.json`
92
93
  {
93
94
  const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "package.json");
94
- const template = readFile(templatePath);
95
+ const template = await readFile(templatePath);
95
96
  const packageJSON = template
96
97
  .replaceAll("project-name", projectName)
97
98
  .replaceAll("author-name", authorName ?? "unknown");
98
99
  const destinationPath = path.join(projectPath, "package.json");
99
- writeFile(destinationPath, packageJSON);
100
+ await writeFile(destinationPath, packageJSON);
100
101
  }
101
102
  // `README.md`
102
103
  {
103
104
  const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "README.md");
104
- const template = readFile(templatePath);
105
+ const template = await readFile(templatePath);
105
106
  // "PROJECT-NAME" must be hyphenated, as using an underscore will break Prettier for some
106
107
  // reason.
107
108
  const command = getPackageManagerInstallCICommand(packageManager);
@@ -109,15 +110,15 @@ function copyDynamicFiles(projectName, authorName, projectPath, packageManager)
109
110
  .replaceAll("PROJECT-NAME", projectName)
110
111
  .replaceAll("PACKAGE-MANAGER-INSTALL-COMMAND", command);
111
112
  const destinationPath = path.join(projectPath, "README.md");
112
- writeFile(destinationPath, readmeMD);
113
+ await writeFile(destinationPath, readmeMD);
113
114
  }
114
115
  }
115
- function copyPackageManagerSpecificFiles(projectPath, packageManager) {
116
+ async function copyPackageManagerSpecificFiles(projectPath, packageManager) {
116
117
  switch (packageManager) {
117
118
  case PackageManager.npm: {
118
119
  const npmrc = "save-exact=true\n";
119
120
  const npmrcPath = path.join(projectPath, ".npmrc");
120
- writeFile(npmrcPath, npmrc);
121
+ await writeFile(npmrcPath, npmrc);
121
122
  break;
122
123
  }
123
124
  // `pnpm` requires the `shamefully-hoist` option to be enabled for "complete-lint" to work
@@ -125,7 +126,7 @@ function copyPackageManagerSpecificFiles(projectPath, packageManager) {
125
126
  case PackageManager.pnpm: {
126
127
  const npmrc = "save-exact=true\nshamefully-hoist=true\n";
127
128
  const npmrcPath = path.join(projectPath, ".npmrc");
128
- writeFile(npmrcPath, npmrc);
129
+ await writeFile(npmrcPath, npmrc);
129
130
  break;
130
131
  }
131
132
  case PackageManager.yarn: {
@@ -134,7 +135,26 @@ function copyPackageManagerSpecificFiles(projectPath, packageManager) {
134
135
  case PackageManager.bun: {
135
136
  const bunfig = "[install]\nexact = true\n";
136
137
  const bunfigPath = path.join(projectPath, "bunfig.toml");
137
- writeFile(bunfigPath, bunfig);
138
+ await writeFile(bunfigPath, bunfig);
139
+ // Additionally, we assume that if they are using the Bun package manager, they also want to
140
+ // use the Bun runtime. First, replace "complete-tsconfig/tsconfig.node.json" with
141
+ // "complete-tsconfig/tsconfig.bun.json".
142
+ const tsConfigJSONPath = path.join(projectPath, "tsconfig.json");
143
+ const tsConfigJSONScriptsPath = path.join(projectPath, "scripts", "tsconfig.json");
144
+ const filePathsToReplaceNodeWithBun = [
145
+ tsConfigJSONPath,
146
+ tsConfigJSONScriptsPath,
147
+ ];
148
+ await Promise.all(filePathsToReplaceNodeWithBun.map(async (filePath) => {
149
+ const fileContents = await readFile(filePath);
150
+ const newFileContents = fileContents.replaceAll("node", "bun");
151
+ await writeFile(filePath, newFileContents);
152
+ }));
153
+ // Second, replace "tsx" with "bun run".
154
+ const packageJSONPath = path.join(projectPath, "package.json");
155
+ const fileContents = await readFile(packageJSONPath);
156
+ const newFileContents = fileContents.replaceAll("tsx", "bun run");
157
+ await writeFile(packageJSONPath, newFileContents);
138
158
  break;
139
159
  }
140
160
  }
@@ -149,7 +169,7 @@ async function revertVersionsInPackageJSON(projectPath) {
149
169
  }
150
170
  const packageJSONText = JSON.stringify(packageJSON);
151
171
  await formatWithPrettier(packageJSONText, "json", projectPath);
152
- await writeFileAsync(packageJSONPath, packageJSONText);
172
+ await writeFile(packageJSONPath, packageJSONText);
153
173
  }
154
174
  async function createPackageMetadataJSON(projectPath) {
155
175
  const packageMetadata = {
@@ -164,7 +184,7 @@ async function createPackageMetadataJSON(projectPath) {
164
184
  const packageMetadataText = JSON.stringify(packageMetadata);
165
185
  await formatWithPrettier(packageMetadataText, "json", projectPath);
166
186
  const packageMetadataPath = path.join(projectPath, "package-metadata.json");
167
- await writeFileAsync(packageMetadataPath, packageMetadataText);
187
+ await writeFile(packageMetadataPath, packageMetadataText);
168
188
  }
169
189
  async function installNodeModules(projectPath, skipInstall, packageManager) {
170
190
  if (skipInstall) {
@@ -1,16 +1,28 @@
1
1
  import chalk from "chalk";
2
- import { getEnumValues } from "complete-common";
3
- import { commandExists, PackageManager } from "complete-node";
2
+ import { assertDefined, getEnumValues } from "complete-common";
3
+ import { commandExists, getJavaScriptRuntime, JavaScriptRuntime, PackageManager, } from "complete-node";
4
4
  import { DEFAULT_PACKAGE_MANAGER } from "../../constants.js";
5
5
  import { promptError } from "../../prompt.js";
6
6
  const PACKAGE_MANAGERS = getEnumValues(PackageManager);
7
7
  export async function getPackageManagerUsedForNewProject(options) {
8
+ // If the package manager was explicitly specified in the options, use that.
8
9
  const packageManagerFromOptions = await getPackageManagerFromOptions(options);
9
- return packageManagerFromOptions ?? DEFAULT_PACKAGE_MANAGER;
10
+ if (packageManagerFromOptions !== undefined) {
11
+ return packageManagerFromOptions;
12
+ }
13
+ // If `bun` or `bunx` was used to launch this program, assume that they also want to use the Bun
14
+ // package manager.
15
+ const javaScriptRuntime = getJavaScriptRuntime();
16
+ assertDefined(javaScriptRuntime, "Failed to get the JavaScript runtime.");
17
+ if (javaScriptRuntime === JavaScriptRuntime.bun) {
18
+ return PackageManager.bun;
19
+ }
20
+ return DEFAULT_PACKAGE_MANAGER;
10
21
  }
11
22
  async function getPackageManagerFromOptions(options) {
12
23
  for (const packageManager of PACKAGE_MANAGERS) {
13
24
  if (options[packageManager]) {
25
+ // Only one package manager flag will be specified at a time.
14
26
  // eslint-disable-next-line no-await-in-loop
15
27
  const exists = await commandExists(packageManager);
16
28
  if (!exists) {
@@ -1,3 +1,4 @@
1
+ import { assertObject } from "complete-common";
1
2
  import { $, $q, commandExists, getJSONC, isFile } from "complete-node";
2
3
  import path from "node:path";
3
4
  import { getInputYesNo, promptError, promptLog } from "../../prompt.js";
@@ -18,6 +19,8 @@ export async function vsCodeInit(projectPath, vscode, yes) {
18
19
  }
19
20
  async function getVSCodeCommand() {
20
21
  for (const command of VS_CODE_COMMANDS) {
22
+ // We want to only check for one command at a time, since it is unlikely that the special VSCode
23
+ // commands will exist.
21
24
  // eslint-disable-next-line no-await-in-loop
22
25
  const exists = await commandExists(command);
23
26
  if (exists) {
@@ -38,10 +41,12 @@ async function installVSCodeExtensions(projectPath, vsCodeCommand) {
38
41
  }
39
42
  async function getExtensionsFromJSON(projectPath) {
40
43
  const extensionsJSONPath = path.join(projectPath, ".vscode", "extensions.json");
41
- if (!isFile(extensionsJSONPath)) {
44
+ const extensionsJSONExists = await isFile(extensionsJSONPath);
45
+ if (!extensionsJSONExists) {
42
46
  return [];
43
47
  }
44
48
  const extensionsJSON = await getJSONC(extensionsJSONPath);
49
+ assertObject(extensionsJSON, `The "${extensionsJSONPath}" file is not an object.`);
45
50
  const { recommendations } = extensionsJSON;
46
51
  if (!Array.isArray(recommendations)) {
47
52
  promptError('The "recommendations" field in the "extensions.json" file is not an array.');
package/dist/git.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { $q, commandExists, isFileAsync, readFileAsync } from "complete-node";
2
+ import { $q, commandExists, isFile, readFile } from "complete-node";
3
3
  import path from "node:path";
4
4
  import yaml from "yaml";
5
5
  import { HOME_DIR, PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
@@ -17,11 +17,11 @@ export async function getGitHubUsername() {
17
17
  if (githubCLIHostsPath === undefined) {
18
18
  return undefined;
19
19
  }
20
- const hostsPathExists = await isFileAsync(githubCLIHostsPath);
20
+ const hostsPathExists = await isFile(githubCLIHostsPath);
21
21
  if (!hostsPathExists) {
22
22
  return undefined;
23
23
  }
24
- const configYAMLRaw = await readFileAsync(githubCLIHostsPath);
24
+ const configYAMLRaw = await readFile(githubCLIHostsPath);
25
25
  const configYAML = yaml.parse(configYAMLRaw);
26
26
  const githubCom = configYAML["github.com"];
27
27
  if (githubCom === undefined) {
package/dist/prompt.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Both the Inquirer.js library and the Prompts library have a bug where text is duplicated in a Git
2
2
  // Bash terminal. Thus, we revert to using the simpler Prompt library.
3
- import { cancel, confirm, intro, isCancel, log, outro, spinner, text, } from "@zamiell/clack-prompts";
3
+ import { cancel, confirm, intro, isCancel, log, outro, spinner, text, } from "@clack/prompts";
4
4
  import chalk from "chalk";
5
5
  import { PROJECT_NAME } from "./constants.js";
6
6
  export function promptStart() {
@@ -3,7 +3,7 @@ runs:
3
3
 
4
4
  steps:
5
5
  - name: Setup Node.js
6
- uses: actions/setup-node@v4
6
+ uses: actions/setup-node@v5
7
7
  with:
8
8
  node-version: lts/*
9
9
  cache: PACKAGE_MANAGER_NAME
@@ -81,6 +81,7 @@ out
81
81
  # Nuxt.js build / generate output
82
82
  .nuxt
83
83
  dist
84
+ .output
84
85
 
85
86
  # Gatsby files
86
87
  .cache/
@@ -134,6 +135,7 @@ dist
134
135
  !.yarn/sdks
135
136
  !.yarn/versions
136
137
 
137
- # Vite logs files
138
+ # Vite files
138
139
  vite.config.js.timestamp-*
139
140
  vite.config.ts.timestamp-*
141
+ .vite/
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "project-name",
3
3
  "version": "0.0.0",
4
- "description": "",
5
- "keywords": [],
4
+ "description": "A TypeScript project.",
6
5
  "homepage": "https://github.com/author-name/project-name",
7
6
  "bugs": {
8
7
  "url": "https://github.com/author-name/project-name/issues"
@@ -15,10 +14,7 @@
15
14
  "author": "author-name",
16
15
  "type": "module",
17
16
  "files": [
18
- "dist",
19
- "LICENSE",
20
- "package.json",
21
- "README.md"
17
+ "dist"
22
18
  ],
23
19
  "scripts": {
24
20
  "build": "tsx ./scripts/build.ts",
@@ -50,8 +50,8 @@
50
50
  "typescript.suggest.completeFunctionCalls": true,
51
51
 
52
52
  // By default, VSCode will prefer non-relative paths for deeply nested files.
53
- "javascript.preferences.importModuleSpecifier": "relative",
54
- "typescript.preferences.importModuleSpecifier": "relative",
53
+ "javascript.preferences.importModuleSpecifier": "project-relative",
54
+ "typescript.preferences.importModuleSpecifier": "project-relative",
55
55
 
56
56
  // By default, VSCode will not add `import type` automatically.
57
57
  "typescript.preferences.preferTypeOnlyAutoImports": true,
@@ -4,9 +4,9 @@
4
4
  // @ts-check
5
5
 
6
6
  import { completeConfigBase } from "eslint-config-complete";
7
- import tseslint from "typescript-eslint";
7
+ import { defineConfig } from "eslint/config";
8
8
 
9
- export default tseslint.config(
9
+ export default defineConfig(
10
10
  // https://github.com/complete-ts/complete/blob/main/packages/eslint-config-complete/src/base.js
11
11
  ...completeConfigBase,
12
12
 
@@ -23,7 +23,7 @@ await lintScript(async () => {
23
23
 
24
24
  // Use CSpell to spell check every file.
25
25
  // - "--no-progress" and "--no-summary" make it only output errors.
26
- $`cspell --no-progress --no-summary .`,
26
+ $`cspell --no-progress --no-summary`,
27
27
 
28
28
  // Check for unused words in the CSpell configuration file.
29
29
  $`cspell-check-unused-words`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "complete-cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "A command line tool for bootstrapping TypeScript projects.",
5
5
  "keywords": [
6
6
  "typescript"
@@ -22,10 +22,7 @@
22
22
  "files": [
23
23
  "dist",
24
24
  "file-templates",
25
- "src",
26
- "LICENSE",
27
- "package.json",
28
- "README.md"
25
+ "src"
29
26
  ],
30
27
  "scripts": {
31
28
  "build": "tsx ./scripts/build.ts",
@@ -34,22 +31,22 @@
34
31
  "test": "tsx --test"
35
32
  },
36
33
  "dependencies": {
37
- "@zamiell/clack-prompts": "0.10.2",
38
- "chalk": "5.5.0",
34
+ "@clack/prompts": "0.11.0",
35
+ "chalk": "5.6.0",
39
36
  "clipanion": "4.0.0-rc.4",
40
37
  "complete-common": "2.5.0",
41
- "complete-node": "7.4.4",
38
+ "complete-node": "9.3.0",
42
39
  "klaw-sync": "7.0.0",
43
- "yaml": "2.8.0"
40
+ "yaml": "2.8.1"
44
41
  },
45
42
  "devDependencies": {
46
43
  "@types/klaw-sync": "6.0.5",
47
- "@types/node": "24.2.0",
48
- "ts-loader": "9.5.2",
44
+ "@types/node": "24.3.1",
45
+ "ts-loader": "9.5.4",
49
46
  "tsconfig-paths-webpack-plugin": "4.2.0",
50
47
  "typescript": "5.9.2",
51
- "typescript-eslint": "8.39.0",
52
- "webpack": "5.101.0",
48
+ "typescript-eslint": "8.42.0",
49
+ "webpack": "5.101.3",
53
50
  "webpack-cli": "6.0.1",
54
51
  "webpack-shebang-plugin": "1.1.8"
55
52
  },
@@ -3,12 +3,12 @@ import { Command, Option } from "clipanion";
3
3
  import { ReadonlySet } from "complete-common";
4
4
  import {
5
5
  $,
6
- deleteFileOrDirectoryAsync,
6
+ deleteFileOrDirectory,
7
7
  fatalError,
8
8
  isDirectory,
9
- isFileAsync,
10
- readFileAsync,
11
- writeFileAsync,
9
+ isFile,
10
+ readFile,
11
+ writeFile,
12
12
  } from "complete-node";
13
13
  import klawSync from "klaw-sync";
14
14
  import os from "node:os";
@@ -80,10 +80,14 @@ async function checkTemplateDirectory(
80
80
  ): Promise<boolean> {
81
81
  let oneOrMoreErrors = false;
82
82
 
83
- for (const klawItem of klawSync(templateDirectory)) {
83
+ // We use `klawSync` instead of `klaw` so that the output will be deterministic.
84
+ const klawItems = klawSync(templateDirectory);
85
+ for (const klawItem of klawItems) {
84
86
  const templateFilePath = klawItem.path;
85
87
 
86
- if (isDirectory(templateFilePath)) {
88
+ // eslint-disable-next-line no-await-in-loop
89
+ const templateExists = await isDirectory(templateFilePath);
90
+ if (templateExists) {
87
91
  continue;
88
92
  }
89
93
 
@@ -171,7 +175,7 @@ async function compareTextFiles(
171
175
  templateFilePath: string,
172
176
  verbose: boolean,
173
177
  ): Promise<boolean> {
174
- const fileExists = await isFileAsync(projectFilePath);
178
+ const fileExists = await isFile(projectFilePath);
175
179
  if (!fileExists) {
176
180
  console.log(`Failed to find the following file: ${projectFilePath}`);
177
181
  printTemplateLocation(templateFilePath);
@@ -203,8 +207,8 @@ async function compareTextFiles(
203
207
  printTemplateLocation(templateFilePath);
204
208
 
205
209
  if (verbose) {
206
- const originalTemplateFile = await readFileAsync(templateFilePath);
207
- const originalProjectFile = await readFileAsync(projectFilePath);
210
+ const originalTemplateFile = await readFile(templateFilePath);
211
+ const originalProjectFile = await readFile(projectFilePath);
208
212
 
209
213
  console.log("--- Original template file: ---\n");
210
214
  console.log(originalTemplateFile);
@@ -224,8 +228,8 @@ async function compareTextFiles(
224
228
  const tempProjectFilePath = path.join(tempDir, "tempProjectFile.txt");
225
229
  const tempTemplateFilePath = path.join(tempDir, "tempTemplateFile.txt");
226
230
 
227
- await writeFileAsync(tempProjectFilePath, projectFileObject.text);
228
- await writeFileAsync(tempTemplateFilePath, templateFileObject.text);
231
+ await writeFile(tempProjectFilePath, projectFileObject.text);
232
+ await writeFile(tempTemplateFilePath, templateFileObject.text);
229
233
 
230
234
  try {
231
235
  await $`diff ${tempProjectFilePath} ${tempTemplateFilePath} --ignore-blank-lines`;
@@ -233,8 +237,8 @@ async function compareTextFiles(
233
237
  // `diff` will exit with a non-zero code if the files are different, which is expected.
234
238
  }
235
239
 
236
- await deleteFileOrDirectoryAsync(tempProjectFilePath);
237
- await deleteFileOrDirectoryAsync(tempTemplateFilePath);
240
+ await deleteFileOrDirectory(tempProjectFilePath);
241
+ await deleteFileOrDirectory(tempTemplateFilePath);
238
242
 
239
243
  return false;
240
244
  }
@@ -245,7 +249,7 @@ async function getTruncatedFileText(
245
249
  linesBeforeIgnore: ReadonlySet<string>,
246
250
  ) {
247
251
  const fileName = path.basename(filePath);
248
- const fileContents = await readFileAsync(filePath);
252
+ const fileContents = await readFile(filePath);
249
253
 
250
254
  return getTruncatedText(
251
255
  fileName,
@@ -1,11 +1,6 @@
1
1
  import { Command, Option } from "clipanion";
2
2
  import { assertObject, isObject } from "complete-common";
3
- import {
4
- getFilePath,
5
- isFileAsync,
6
- readFileAsync,
7
- writeFileAsync,
8
- } from "complete-node";
3
+ import { getFilePath, isFile, readFile, writeFile } from "complete-node";
9
4
  import path from "node:path";
10
5
 
11
6
  export class MetadataCommand extends Command {
@@ -29,11 +24,11 @@ export class MetadataCommand extends Command {
29
24
  const packageJSONPath = await getFilePath("package.json", undefined);
30
25
  const packageRoot = path.dirname(packageJSONPath);
31
26
  const packageMetadataPath = path.join(packageRoot, "package-metadata.json");
32
- const packageMetadataExists = await isFileAsync(packageMetadataPath);
27
+ const packageMetadataExists = await isFile(packageMetadataPath);
33
28
 
34
29
  let packageMetadata: Record<string, unknown>;
35
30
  if (packageMetadataExists) {
36
- const packageMetadataContents = await readFileAsync(packageMetadataPath);
31
+ const packageMetadataContents = await readFile(packageMetadataPath);
37
32
  const packageMetadataUnknown = JSON.parse(
38
33
  packageMetadataContents,
39
34
  ) as unknown;
@@ -61,7 +56,7 @@ export class MetadataCommand extends Command {
61
56
  };
62
57
 
63
58
  const packageMetadataJSON = JSON.stringify(packageMetadata, undefined, 2);
64
- await writeFileAsync(packageMetadataPath, packageMetadataJSON);
59
+ await writeFile(packageMetadataPath, packageMetadataJSON);
65
60
 
66
61
  const verb = packageMetadataExists ? "modified" : "created";
67
62
  console.log(`Successfully ${verb}: ${packageMetadataPath}`);
@@ -8,13 +8,13 @@ import {
8
8
  getPackageManagerInstallCommand,
9
9
  getPackageManagerLockFileName,
10
10
  getPackageManagersForProject,
11
- isFileAsync,
11
+ isFile,
12
12
  isGitRepository,
13
13
  isGitRepositoryClean,
14
14
  isLoggedInToNPM,
15
15
  readFile,
16
16
  updatePackageJSONDependencies,
17
- writeFileAsync,
17
+ writeFile,
18
18
  } from "complete-node";
19
19
  import path from "node:path";
20
20
  import { CWD, DEFAULT_PACKAGE_MANAGER } from "../constants.js";
@@ -70,7 +70,7 @@ async function validate() {
70
70
  );
71
71
  }
72
72
 
73
- const packageJSONExists = await isFileAsync("package.json");
73
+ const packageJSONExists = await isFile("package.json");
74
74
  if (!packageJSONExists) {
75
75
  fatalError(
76
76
  'Failed to find the "package.json" file in the current working directory.',
@@ -190,16 +190,16 @@ async function incrementVersion(versionBumpType: string) {
190
190
 
191
191
  async function unsetDevelopmentConstants() {
192
192
  const constantsTSPath = path.join(CWD, "src", "constants.ts");
193
- const constantsTSExists = await isFileAsync(constantsTSPath);
193
+ const constantsTSExists = await isFile(constantsTSPath);
194
194
  if (!constantsTSExists) {
195
195
  return;
196
196
  }
197
197
 
198
- const constantsTS = readFile(constantsTSPath);
198
+ const constantsTS = await readFile(constantsTSPath);
199
199
  const newConstantsTS = constantsTS
200
200
  .replace("const IS_DEV = true", "const IS_DEV = false")
201
201
  .replace("const DEBUG = true", "const DEBUG = false");
202
- await writeFileAsync(constantsTSPath, newConstantsTS);
202
+ await writeFile(constantsTSPath, newConstantsTS);
203
203
  }
204
204
 
205
205
  async function tryRunNPMScript(scriptName: string) {
@@ -1,24 +1,28 @@
1
1
  import chalk from "chalk";
2
- import {
3
- deleteFileOrDirectory,
4
- fileOrDirectoryExists,
5
- isDirectory,
6
- } from "complete-node";
2
+ import { deleteFileOrDirectory, isDirectory, isFile } from "complete-node";
7
3
  import { CWD } from "../../constants.js";
8
4
  import { getInputYesNo, promptEnd, promptLog } from "../../prompt.js";
9
5
 
6
+ /** @throws If the project path is not a file or a directory. */
10
7
  export async function checkIfProjectPathExists(
11
8
  projectPath: string,
12
9
  yes: boolean,
13
10
  ): Promise<void> {
14
- if (projectPath === CWD || !fileOrDirectoryExists(projectPath)) {
11
+ if (projectPath === CWD) {
15
12
  return;
16
13
  }
17
14
 
18
- const fileType = isDirectory(projectPath) ? "directory" : "file";
15
+ const file = await isFile(projectPath);
16
+ const directory = await isDirectory(projectPath);
17
+ if (!file && !directory) {
18
+ throw new Error(
19
+ `Failed to detect if the path was a file or a directory: ${projectPath}`,
20
+ );
21
+ }
22
+ const fileType = file ? "file" : "directory";
19
23
 
20
24
  if (yes) {
21
- deleteFileOrDirectory(projectPath);
25
+ await deleteFileOrDirectory(projectPath);
22
26
  promptLog(`Deleted ${fileType}: ${chalk.green(projectPath)}`);
23
27
  return;
24
28
  }
@@ -32,5 +36,5 @@ export async function checkIfProjectPathExists(
32
36
  promptEnd("Ok then. Goodbye.");
33
37
  }
34
38
 
35
- deleteFileOrDirectory(projectPath);
39
+ await deleteFileOrDirectory(projectPath);
36
40
  }
@@ -12,10 +12,9 @@ import {
12
12
  makeDirectory,
13
13
  PackageManager,
14
14
  readFile,
15
- renameFile,
15
+ renameFileOrDirectory,
16
16
  updatePackageJSONDependencies,
17
17
  writeFile,
18
- writeFileAsync,
19
18
  } from "complete-node";
20
19
  import path from "node:path";
21
20
  import {
@@ -38,12 +37,12 @@ export async function createProject(
38
37
  packageManager: PackageManager,
39
38
  ): Promise<void> {
40
39
  if (createNewDir) {
41
- makeDirectory(projectPath);
40
+ await makeDirectory(projectPath);
42
41
  }
43
42
 
44
- copyStaticFiles(projectPath);
45
- copyDynamicFiles(projectName, authorName, projectPath, packageManager);
46
- copyPackageManagerSpecificFiles(projectPath, packageManager);
43
+ await copyStaticFiles(projectPath);
44
+ await copyDynamicFiles(projectName, authorName, projectPath, packageManager);
45
+ await copyPackageManagerSpecificFiles(projectPath, packageManager);
47
46
 
48
47
  // There is no package manager lock files yet, so we have to pass "false" to this function.
49
48
  const updated = await updatePackageJSONDependencies(projectPath, false, true);
@@ -69,38 +68,44 @@ export async function createProject(
69
68
  }
70
69
 
71
70
  /** Copy static files, like "eslint.config.mjs", "tsconfig.json", etc. */
72
- function copyStaticFiles(projectPath: string) {
73
- copyTemplateDirectoryWithoutOverwriting(TEMPLATES_STATIC_DIR, projectPath);
71
+ async function copyStaticFiles(projectPath: string) {
72
+ await copyTemplateDirectoryWithoutOverwriting(
73
+ TEMPLATES_STATIC_DIR,
74
+ projectPath,
75
+ );
74
76
 
75
77
  // Rename "_gitattributes" to ".gitattributes". (If it is kept as ".gitattributes", then it won't
76
78
  // be committed to git.)
77
79
  const gitAttributesPath = path.join(projectPath, "_gitattributes");
78
80
  const correctGitAttributesPath = path.join(projectPath, ".gitattributes");
79
- renameFile(gitAttributesPath, correctGitAttributesPath);
81
+ await renameFileOrDirectory(gitAttributesPath, correctGitAttributesPath);
80
82
 
81
83
  // Rename "_cspell.config.jsonc" to "cspell.config.jsonc". (If it is kept as
82
84
  // "cspell.config.jsonc", then local spell checking will fail.)
83
85
  const cSpellConfigPath = path.join(projectPath, "_cspell.config.jsonc");
84
86
  const correctCSpellConfigPath = path.join(projectPath, "cspell.config.jsonc");
85
- renameFile(cSpellConfigPath, correctCSpellConfigPath);
87
+ await renameFileOrDirectory(cSpellConfigPath, correctCSpellConfigPath);
86
88
  }
87
89
 
88
- function copyTemplateDirectoryWithoutOverwriting(
90
+ async function copyTemplateDirectoryWithoutOverwriting(
89
91
  templateDirPath: string,
90
92
  projectPath: string,
91
93
  ) {
92
- const fileNames = getFileNamesInDirectory(templateDirPath);
93
- for (const fileName of fileNames) {
94
- const templateFilePath = path.join(templateDirPath, fileName);
95
- const destinationFilePath = path.join(projectPath, fileName);
96
- if (!isFile(destinationFilePath)) {
97
- copyFileOrDirectory(templateFilePath, destinationFilePath);
98
- }
99
- }
94
+ const fileNames = await getFileNamesInDirectory(templateDirPath);
95
+ await Promise.all(
96
+ fileNames.map(async (fileName) => {
97
+ const templateFilePath = path.join(templateDirPath, fileName);
98
+ const destinationFilePath = path.join(projectPath, fileName);
99
+ const file = await isFile(destinationFilePath);
100
+ if (!file) {
101
+ await copyFileOrDirectory(templateFilePath, destinationFilePath);
102
+ }
103
+ }),
104
+ );
100
105
  }
101
106
 
102
107
  /** Copy files that need to have text replaced inside of them. */
103
- function copyDynamicFiles(
108
+ async function copyDynamicFiles(
104
109
  projectName: string,
105
110
  authorName: string | undefined,
106
111
  projectPath: string,
@@ -110,7 +115,7 @@ function copyDynamicFiles(
110
115
  {
111
116
  const fileName = ACTION_YML;
112
117
  const templatePath = ACTION_YML_TEMPLATE_PATH;
113
- const template = readFile(templatePath);
118
+ const template = await readFile(templatePath);
114
119
 
115
120
  const installCommand = getPackageManagerInstallCICommand(packageManager);
116
121
  const actionYML = template
@@ -118,9 +123,9 @@ function copyDynamicFiles(
118
123
  .replaceAll("PACKAGE_MANAGER_INSTALL_COMMAND", installCommand);
119
124
 
120
125
  const setupPath = path.join(projectPath, ".github", "workflows", "setup");
121
- makeDirectory(setupPath);
126
+ await makeDirectory(setupPath);
122
127
  const destinationPath = path.join(setupPath, fileName);
123
- writeFile(destinationPath, actionYML);
128
+ await writeFile(destinationPath, actionYML);
124
129
  }
125
130
 
126
131
  // `.gitignore`
@@ -129,7 +134,7 @@ function copyDynamicFiles(
129
134
  TEMPLATES_DYNAMIC_DIR,
130
135
  "_gitignore", // Not named ".gitignore" to prevent npm from deleting it.
131
136
  );
132
- const template = readFile(templatePath);
137
+ const template = await readFile(templatePath);
133
138
 
134
139
  // Prepend a header with the project name.
135
140
  let separatorLine = "# ";
@@ -142,33 +147,33 @@ function copyDynamicFiles(
142
147
  TEMPLATES_DYNAMIC_DIR,
143
148
  "Node.gitignore",
144
149
  );
145
- const nodeGitIgnore = readFile(nodeGitIgnorePath);
150
+ const nodeGitIgnore = await readFile(nodeGitIgnorePath);
146
151
 
147
152
  // eslint-disable-next-line prefer-template
148
153
  const gitignore = gitIgnoreHeader + template + "\n" + nodeGitIgnore;
149
154
 
150
155
  // We need to replace the underscore with a period.
151
156
  const destinationPath = path.join(projectPath, ".gitignore");
152
- writeFile(destinationPath, gitignore);
157
+ await writeFile(destinationPath, gitignore);
153
158
  }
154
159
 
155
160
  // `package.json`
156
161
  {
157
162
  const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "package.json");
158
- const template = readFile(templatePath);
163
+ const template = await readFile(templatePath);
159
164
 
160
165
  const packageJSON = template
161
166
  .replaceAll("project-name", projectName)
162
167
  .replaceAll("author-name", authorName ?? "unknown");
163
168
 
164
169
  const destinationPath = path.join(projectPath, "package.json");
165
- writeFile(destinationPath, packageJSON);
170
+ await writeFile(destinationPath, packageJSON);
166
171
  }
167
172
 
168
173
  // `README.md`
169
174
  {
170
175
  const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "README.md");
171
- const template = readFile(templatePath);
176
+ const template = await readFile(templatePath);
172
177
 
173
178
  // "PROJECT-NAME" must be hyphenated, as using an underscore will break Prettier for some
174
179
  // reason.
@@ -177,11 +182,11 @@ function copyDynamicFiles(
177
182
  .replaceAll("PROJECT-NAME", projectName)
178
183
  .replaceAll("PACKAGE-MANAGER-INSTALL-COMMAND", command);
179
184
  const destinationPath = path.join(projectPath, "README.md");
180
- writeFile(destinationPath, readmeMD);
185
+ await writeFile(destinationPath, readmeMD);
181
186
  }
182
187
  }
183
188
 
184
- function copyPackageManagerSpecificFiles(
189
+ async function copyPackageManagerSpecificFiles(
185
190
  projectPath: string,
186
191
  packageManager: PackageManager,
187
192
  ) {
@@ -189,7 +194,7 @@ function copyPackageManagerSpecificFiles(
189
194
  case PackageManager.npm: {
190
195
  const npmrc = "save-exact=true\n";
191
196
  const npmrcPath = path.join(projectPath, ".npmrc");
192
- writeFile(npmrcPath, npmrc);
197
+ await writeFile(npmrcPath, npmrc);
193
198
  break;
194
199
  }
195
200
 
@@ -198,7 +203,7 @@ function copyPackageManagerSpecificFiles(
198
203
  case PackageManager.pnpm: {
199
204
  const npmrc = "save-exact=true\nshamefully-hoist=true\n";
200
205
  const npmrcPath = path.join(projectPath, ".npmrc");
201
- writeFile(npmrcPath, npmrc);
206
+ await writeFile(npmrcPath, npmrc);
202
207
  break;
203
208
  }
204
209
 
@@ -209,7 +214,35 @@ function copyPackageManagerSpecificFiles(
209
214
  case PackageManager.bun: {
210
215
  const bunfig = "[install]\nexact = true\n";
211
216
  const bunfigPath = path.join(projectPath, "bunfig.toml");
212
- writeFile(bunfigPath, bunfig);
217
+ await writeFile(bunfigPath, bunfig);
218
+
219
+ // Additionally, we assume that if they are using the Bun package manager, they also want to
220
+ // use the Bun runtime. First, replace "complete-tsconfig/tsconfig.node.json" with
221
+ // "complete-tsconfig/tsconfig.bun.json".
222
+ const tsConfigJSONPath = path.join(projectPath, "tsconfig.json");
223
+ const tsConfigJSONScriptsPath = path.join(
224
+ projectPath,
225
+ "scripts",
226
+ "tsconfig.json",
227
+ );
228
+ const filePathsToReplaceNodeWithBun = [
229
+ tsConfigJSONPath,
230
+ tsConfigJSONScriptsPath,
231
+ ];
232
+ await Promise.all(
233
+ filePathsToReplaceNodeWithBun.map(async (filePath) => {
234
+ const fileContents = await readFile(filePath);
235
+ const newFileContents = fileContents.replaceAll("node", "bun");
236
+ await writeFile(filePath, newFileContents);
237
+ }),
238
+ );
239
+
240
+ // Second, replace "tsx" with "bun run".
241
+ const packageJSONPath = path.join(projectPath, "package.json");
242
+ const fileContents = await readFile(packageJSONPath);
243
+ const newFileContents = fileContents.replaceAll("tsx", "bun run");
244
+ await writeFile(packageJSONPath, newFileContents);
245
+
213
246
  break;
214
247
  }
215
248
  }
@@ -228,7 +261,7 @@ async function revertVersionsInPackageJSON(projectPath: string) {
228
261
  }
229
262
  const packageJSONText = JSON.stringify(packageJSON);
230
263
  await formatWithPrettier(packageJSONText, "json", projectPath);
231
- await writeFileAsync(packageJSONPath, packageJSONText);
264
+ await writeFile(packageJSONPath, packageJSONText);
232
265
  }
233
266
 
234
267
  async function createPackageMetadataJSON(projectPath: string) {
@@ -244,7 +277,7 @@ async function createPackageMetadataJSON(projectPath: string) {
244
277
  const packageMetadataText = JSON.stringify(packageMetadata);
245
278
  await formatWithPrettier(packageMetadataText, "json", projectPath);
246
279
  const packageMetadataPath = path.join(projectPath, "package-metadata.json");
247
- await writeFileAsync(packageMetadataPath, packageMetadataText);
280
+ await writeFile(packageMetadataPath, packageMetadataText);
248
281
  }
249
282
 
250
283
  async function installNodeModules(
@@ -1,7 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import type { ReadonlyRecord } from "complete-common";
3
- import { getEnumValues } from "complete-common";
4
- import { commandExists, PackageManager } from "complete-node";
3
+ import { assertDefined, getEnumValues } from "complete-common";
4
+ import {
5
+ commandExists,
6
+ getJavaScriptRuntime,
7
+ JavaScriptRuntime,
8
+ PackageManager,
9
+ } from "complete-node";
5
10
  import { DEFAULT_PACKAGE_MANAGER } from "../../constants.js";
6
11
  import { promptError } from "../../prompt.js";
7
12
 
@@ -10,8 +15,21 @@ const PACKAGE_MANAGERS = getEnumValues(PackageManager);
10
15
  export async function getPackageManagerUsedForNewProject(
11
16
  options: ReadonlyRecord<PackageManager, boolean>,
12
17
  ): Promise<PackageManager> {
18
+ // If the package manager was explicitly specified in the options, use that.
13
19
  const packageManagerFromOptions = await getPackageManagerFromOptions(options);
14
- return packageManagerFromOptions ?? DEFAULT_PACKAGE_MANAGER;
20
+ if (packageManagerFromOptions !== undefined) {
21
+ return packageManagerFromOptions;
22
+ }
23
+
24
+ // If `bun` or `bunx` was used to launch this program, assume that they also want to use the Bun
25
+ // package manager.
26
+ const javaScriptRuntime = getJavaScriptRuntime();
27
+ assertDefined(javaScriptRuntime, "Failed to get the JavaScript runtime.");
28
+ if (javaScriptRuntime === JavaScriptRuntime.bun) {
29
+ return PackageManager.bun;
30
+ }
31
+
32
+ return DEFAULT_PACKAGE_MANAGER;
15
33
  }
16
34
 
17
35
  async function getPackageManagerFromOptions(
@@ -19,6 +37,7 @@ async function getPackageManagerFromOptions(
19
37
  ) {
20
38
  for (const packageManager of PACKAGE_MANAGERS) {
21
39
  if (options[packageManager]) {
40
+ // Only one package manager flag will be specified at a time.
22
41
  // eslint-disable-next-line no-await-in-loop
23
42
  const exists = await commandExists(packageManager);
24
43
  if (!exists) {
@@ -1,3 +1,4 @@
1
+ import { assertObject } from "complete-common";
1
2
  import { $, $q, commandExists, getJSONC, isFile } from "complete-node";
2
3
  import path from "node:path";
3
4
  import { getInputYesNo, promptError, promptLog } from "../../prompt.js";
@@ -28,6 +29,8 @@ export async function vsCodeInit(
28
29
 
29
30
  async function getVSCodeCommand(): Promise<string | undefined> {
30
31
  for (const command of VS_CODE_COMMANDS) {
32
+ // We want to only check for one command at a time, since it is unlikely that the special VSCode
33
+ // commands will exist.
31
34
  // eslint-disable-next-line no-await-in-loop
32
35
  const exists = await commandExists(command);
33
36
  if (exists) {
@@ -67,11 +70,16 @@ async function getExtensionsFromJSON(
67
70
  "extensions.json",
68
71
  );
69
72
 
70
- if (!isFile(extensionsJSONPath)) {
73
+ const extensionsJSONExists = await isFile(extensionsJSONPath);
74
+ if (!extensionsJSONExists) {
71
75
  return [];
72
76
  }
73
77
 
74
78
  const extensionsJSON = await getJSONC(extensionsJSONPath);
79
+ assertObject(
80
+ extensionsJSON,
81
+ `The "${extensionsJSONPath}" file is not an object.`,
82
+ );
75
83
 
76
84
  const { recommendations } = extensionsJSON;
77
85
  if (!Array.isArray(recommendations)) {
package/src/git.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { $q, commandExists, isFileAsync, readFileAsync } from "complete-node";
2
+ import { $q, commandExists, isFile, readFile } from "complete-node";
3
3
  import path from "node:path";
4
4
  import yaml from "yaml";
5
5
  import { HOME_DIR, PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
@@ -21,12 +21,12 @@ export async function getGitHubUsername(): Promise<string | undefined> {
21
21
  return undefined;
22
22
  }
23
23
 
24
- const hostsPathExists = await isFileAsync(githubCLIHostsPath);
24
+ const hostsPathExists = await isFile(githubCLIHostsPath);
25
25
  if (!hostsPathExists) {
26
26
  return undefined;
27
27
  }
28
28
 
29
- const configYAMLRaw = await readFileAsync(githubCLIHostsPath);
29
+ const configYAMLRaw = await readFile(githubCLIHostsPath);
30
30
  const configYAML = yaml.parse(configYAMLRaw) as GitHubCLIHostsYAML;
31
31
 
32
32
  const githubCom = configYAML["github.com"];
package/src/prompt.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  outro,
11
11
  spinner,
12
12
  text,
13
- } from "@zamiell/clack-prompts";
13
+ } from "@clack/prompts";
14
14
  import chalk from "chalk";
15
15
  import { PROJECT_NAME } from "./constants.js";
16
16