Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ In just a few short steps we will set up a project containing a Hatchify fronten
1. Ensure you’re using [node 18 and npm 9 or above](https://nodejs.org/en/download)

```bash
node -v
npm -v
echo Node: $(node -v) && echo npm: $(npm -v)
```

2. Create a new project:
Expand Down
28 changes: 14 additions & 14 deletions packages/create/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@ import { blue, green, yellow } from "kolorist"
import type { Database, Backend, Frontend } from "./types"

export const BACKENDS: Record<string, Backend> = {
EXPRESS: {
name: "express",
display: "Express",
color: yellow,
dependencies: ["express", "@hatchifyjs/express"],
devDependencies: [],
},
KOA: {
name: "koa",
display: "Koa",
color: green,
dependencies: ["koa", "@hatchifyjs/koa"],
devDependencies: ["@types/koa", "koa-connect"],
},
EXPRESS: {
name: "express",
display: "Express",
color: yellow,
dependencies: ["express", "@hatchifyjs/express"],
devDependencies: [],
},
}

export const DATABASES: Record<string, Database> = {
POSTGRES: {
name: "postgres",
display: "Postgres",
color: yellow,
dependencies: ["pg", "dotenv"],
devDependencies: ["@types/pg"],
},
SQLITE: {
name: "sqlite",
display: "SQLite",
color: blue,
dependencies: ["sqlite3"],
devDependencies: [],
},
POSTGRES: {
name: "postgres",
display: "Postgres",
color: yellow,
dependencies: ["pg", "dotenv"],
devDependencies: ["@types/pg"],
},
}

export const FRONTENDS: Record<string, Frontend> = {
Expand Down
25 changes: 14 additions & 11 deletions packages/create/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import { fileURLToPath } from "node:url"
import minimist from "minimist"
import prompts from "prompts"
import { red, reset } from "kolorist"
import { exec } from "child_process"
import {
copyDir,
emptyDir,
emptyDir, execCommandExitOnError,
formatTargetDir,
isEmpty,
isValidPackageName,
pkgFromUserAgent,
pkgFromUserAgent, readFileWithRetries,
replaceStringInFile,
runCommand,
toValidPackageName,
} from "./util"
import { DATABASES, BACKENDS, FRONTENDS } from "./constants"
import { BACKENDS, DATABASES, FRONTENDS } from "./constants"
import type { Database } from "./types"

// Avoids autoconversion to number of the project name by defining that the args
Expand Down Expand Up @@ -202,6 +203,8 @@ async function init() {
databaseHost,
)}:${databasePort}/${encodeURIComponent(databaseName)}`)

targetDir = packageName || targetDir;

const root = path.join(cwd, targetDir)

if (overwrite) {
Expand All @@ -215,15 +218,15 @@ async function init() {

console.log(`\nScaffolding project in ${root}...`)

runCommand(
`npm create vite@latest ${targetDir} -- --template react-ts`,
cwd,
true,
)
execCommandExitOnError(`npm create vite@latest "${targetDir}" -- --template react-ts`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would adding await here save the need to retry reading?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to code with a then or a delay, but the core reason for retry is to avoid any I/O delays with the OS that could happen when the node asks for a file but the OS has not fully registered filesystem changes. For example, if you are working within a virtual or symlinked folder, there can be a delay between a written file and when you can read it, and we want to handle edge cases to make the program more robust.


const templatePackage = JSON.parse(
await fs.promises.readFile(path.join(root, "package.json"), "utf-8"),
)
let templatePackage;
try {
templatePackage = JSON.parse(await readFileWithRetries(path.join(root, "package.json")));
} catch (e) {
console.error(e)
process.exit(1)
}

const backendTemplateDir = path.resolve(
fileURLToPath(import.meta.url),
Expand Down
29 changes: 29 additions & 0 deletions packages/create/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs"
import path from "node:path"
import spawn from "cross-spawn"
import { exec } from "child_process"

const renameFiles: Record<string, string> = {
_gitignore: ".gitignore",
Expand All @@ -20,6 +21,34 @@ export async function replaceStringInFile(
)
}

export async function readFileWithRetries(filePath: string, retries: number = 5, delay: number = 1000): Promise<string> {
for (let attempt = 1; attempt <= retries; attempt++) {
if (fs.existsSync(filePath)) {
return await fs.promises.readFile(filePath, "utf-8");
}
await new Promise(resolve => setTimeout(resolve, attempt * delay));
}
throw new Error(`${filePath} could not be found after ${retries} attempts`);
}

export function execCommandExitOnError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the existing runCommand that is using spawn.sync and the cross-spawn import then?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did the exec since it spawns a new process, as there might be a bug with spawn.sync because it tries to reuse the same shell. It is the reason I believe 'npm create vite@latest' was failing, but is more of a hunch than 100% confirmed.

I haven't tested on Windows, so likely the better approach would be to see if cross-spawn is needed for Windows support and then decide if the project can just use the simpler standard library function or if the third-party library is needed, which in that case, the execCommandExitOnError should be rewritten to use cross-spawn for compatibility.

command: string,
args: string[] = [],
): void {
exec(`${command} ${args.join(" ")}`,
(error, stdout, stderr) => {
if (error) {
console.error(`${command} error: ${error.message}`)
process.exit(1)
}
if (stderr) {
console.error(`${command} stderr: ${stderr}`)
process.exit(1)
}
},
)
}

export function runCommand(
fullCommand: string,
cwd: string,
Expand Down