complete-cli 1.3.2 → 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.
- package/dist/commands/CheckCommand.js +15 -11
- package/dist/commands/MetadataCommand.js +4 -4
- package/dist/commands/PublishCommand.js +5 -5
- package/dist/commands/init/checkIfProjectPathExists.js +11 -5
- package/dist/commands/init/createProject.js +52 -32
- package/dist/commands/init/packageManager.js +15 -3
- package/dist/commands/init/vsCodeInit.js +6 -1
- package/dist/git.js +3 -3
- package/dist/prompt.js +1 -1
- package/file-templates/dynamic/.github/workflows/setup/action.yml +1 -1
- package/file-templates/dynamic/Node.gitignore +3 -1
- package/file-templates/dynamic/package.json +2 -6
- package/file-templates/static/eslint.config.mjs +2 -2
- package/file-templates/static/scripts/lint.ts +1 -1
- package/package.json +9 -12
- package/src/commands/CheckCommand.ts +18 -14
- package/src/commands/MetadataCommand.ts +4 -9
- package/src/commands/PublishCommand.ts +6 -6
- package/src/commands/init/checkIfProjectPathExists.ts +13 -9
- package/src/commands/init/createProject.ts +69 -36
- package/src/commands/init/packageManager.ts +22 -3
- package/src/commands/init/vsCodeInit.ts +9 -1
- package/src/git.ts +3 -3
- package/src/prompt.ts +1 -1
|
@@ -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 { $,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
113
|
-
const originalProjectFile = await
|
|
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
|
|
131
|
-
await
|
|
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
|
|
139
|
-
await
|
|
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
|
|
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,
|
|
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
|
|
21
|
+
const packageMetadataExists = await isFile(packageMetadataPath);
|
|
22
22
|
let packageMetadata;
|
|
23
23
|
if (packageMetadataExists) {
|
|
24
|
-
const packageMetadataContents = await
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
7
|
+
if (projectPath === CWD) {
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
await renameFileOrDirectory(cSpellConfigPath, correctCSpellConfigPath);
|
|
45
45
|
}
|
|
46
|
-
function copyTemplateDirectoryWithoutOverwriting(templateDirPath, projectPath) {
|
|
47
|
-
const fileNames = getFileNamesInDirectory(templateDirPath);
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
20
|
+
const hostsPathExists = await isFile(githubCLIHostsPath);
|
|
21
21
|
if (!hostsPathExists) {
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
|
-
const configYAMLRaw = await
|
|
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 "@
|
|
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() {
|
|
@@ -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
|
|
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",
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// @ts-check
|
|
5
5
|
|
|
6
6
|
import { completeConfigBase } from "eslint-config-complete";
|
|
7
|
-
import
|
|
7
|
+
import { defineConfig } from "eslint/config";
|
|
8
8
|
|
|
9
|
-
export default
|
|
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.
|
|
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
|
-
"@
|
|
38
|
-
"chalk": "5.
|
|
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": "
|
|
38
|
+
"complete-node": "9.3.0",
|
|
42
39
|
"klaw-sync": "7.0.0",
|
|
43
40
|
"yaml": "2.8.1"
|
|
44
41
|
},
|
|
45
42
|
"devDependencies": {
|
|
46
43
|
"@types/klaw-sync": "6.0.5",
|
|
47
|
-
"@types/node": "24.
|
|
48
|
-
"ts-loader": "9.5.
|
|
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.
|
|
52
|
-
"webpack": "5.101.
|
|
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
|
-
|
|
6
|
+
deleteFileOrDirectory,
|
|
7
7
|
fatalError,
|
|
8
8
|
isDirectory,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
207
|
-
const originalProjectFile = await
|
|
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
|
|
228
|
-
await
|
|
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
|
|
237
|
-
await
|
|
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
|
|
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
|
|
27
|
+
const packageMetadataExists = await isFile(packageMetadataPath);
|
|
33
28
|
|
|
34
29
|
let packageMetadata: Record<string, unknown>;
|
|
35
30
|
if (packageMetadataExists) {
|
|
36
|
-
const packageMetadataContents = await
|
|
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
|
|
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
|
-
|
|
11
|
+
isFile,
|
|
12
12
|
isGitRepository,
|
|
13
13
|
isGitRepositoryClean,
|
|
14
14
|
isLoggedInToNPM,
|
|
15
15
|
readFile,
|
|
16
16
|
updatePackageJSONDependencies,
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11
|
+
if (projectPath === CWD) {
|
|
15
12
|
return;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
24
|
+
const hostsPathExists = await isFile(githubCLIHostsPath);
|
|
25
25
|
if (!hostsPathExists) {
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const configYAMLRaw = await
|
|
29
|
+
const configYAMLRaw = await readFile(githubCLIHostsPath);
|
|
30
30
|
const configYAML = yaml.parse(configYAMLRaw) as GitHubCLIHostsYAML;
|
|
31
31
|
|
|
32
32
|
const githubCom = configYAML["github.com"];
|