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
17 changes: 2 additions & 15 deletions src/NewProject.res
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@ open Node

module P = ClackPrompts

let packageNameRegExp = /^[a-z0-9-]+$/

let validateProjectName = projectName =>
if projectName->String.trim->String.length === 0 {
Error("Project name must not be empty.")
} else if !(packageNameRegExp->RegExp.test(projectName)) {
Error("Project name may only contain lower case letters, numbers and hyphens.")
} else if Fs.existsSync(Path.join2(Process.cwd(), projectName)) {
Error(`The folder ${projectName} already exist in the current directory.`)
} else {
Ok()
}

let updatePackageJson = async (~projectName, ~versions) =>
await JsonUtils.updateJsonFile("package.json", json =>
switch json {
Expand Down Expand Up @@ -159,7 +146,7 @@ let createNewProject = async () => {
let projectName = switch commandLineArguments.projectName {
| Some(projectName) if useDefaultVersions =>
// Note this throws in the some case, which is why we cannot use Option.getOrThrow here.
switch validateProjectName(projectName) {
switch NewProjectValidation.validateProjectName(projectName) {
| Error(message) => JsError.throwWithMessage(message)
| Ok() => projectName
}
Expand All @@ -170,7 +157,7 @@ let createNewProject = async () => {
placeholder: "my-rescript-app",
?initialValue,
validate: projectName =>
switch validateProjectName(projectName) {
switch NewProjectValidation.validateProjectName(projectName) {
| Ok() => None
| Error(error) => Some(error)
},
Expand Down
30 changes: 30 additions & 0 deletions src/NewProjectValidation.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
open Node

let packageNameRegExp = /^[a-z0-9-]+$/
let devcontainerDirectoryName = ".devcontainer"

let containsOnlyDevcontainerDirectory = projectPath =>
try {
switch Fs.readdirSync(projectPath) {
| [entry] if entry === devcontainerDirectoryName =>
Path.join2(projectPath, devcontainerDirectoryName)->Fs.statSync->Fs.Stats.isDirectory
| _ => false
}
} catch {
| Exn.Error(_) => false
}

let validateProjectName = (~cwd=Process.cwd(), projectName) =>
if projectName->String.trim->String.length === 0 {
Error("Project name must not be empty.")
} else if !(packageNameRegExp->RegExp.test(projectName)) {
Error("Project name may only contain lower case letters, numbers and hyphens.")
} else {
let projectPath = Path.join2(cwd, projectName)

if Fs.existsSync(projectPath) && !(projectPath->containsOnlyDevcontainerDirectory) {
Error(`The folder ${projectName} already exist in the current directory.`)
} else {
Ok()
}
}
10 changes: 10 additions & 0 deletions src/bindings/Node.res
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
module Fs = {
@module("node:fs") external existsSync: string => bool = "existsSync"

@module("node:fs") external readdirSync: string => array<string> = "readdirSync"

@module("node:fs")
external readFileSync: (string, @as(json`"utf8"`) _) => string = "readFileSync"

type stats

@module("node:fs") external statSync: string => stats = "statSync"

module Stats = {
@send external isDirectory: stats => bool = "isDirectory"
}

module Promises = {
@module("node:fs") @scope("promises")
external readFile: (string, @as(json`"utf8"`) _) => promise<string> = "readFile"
Expand Down
65 changes: 65 additions & 0 deletions test/NewProjectValidationTest.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
open Node

let testRoot = Path.join2(Process.cwd(), ".tmp-new-project-validation-test")
let existingProjectMessage = "The folder my-app already exist in the current directory."

let cleanupTestRoot = async () =>
await Fs.Promises.rm(testRoot, ~options={recursive: true, force: true})

let resetTestRoot = async () => {
await cleanupTestRoot()
await Fs.Promises.mkdir(testRoot, ~options={recursive: true})
}

let assertValidationOk = result =>
switch result {
| Ok() => ()
| Error(message) => Assert.fail(`Expected project name to be valid, got: ${message}`)
}

let assertValidationError = (result, expectedMessage) =>
switch result {
| Error(message) => Assert.strictEqual(message, expectedMessage)
| Ok() => Assert.fail(`Expected validation error: ${expectedMessage}`)
}

let validateProjectName = projectName =>
NewProjectValidation.validateProjectName(~cwd=testRoot, projectName)

Test.describe("NewProjectValidation", () => {
Test.testAsync("allows an existing project directory with only .devcontainer", async () => {
await resetTestRoot()
await Fs.Promises.mkdir(
Path.join([testRoot, "my-app", ".devcontainer"]),
~options={
recursive: true,
},
)

validateProjectName("my-app")->assertValidationOk
await cleanupTestRoot()
})

Test.testAsync("rejects an existing project directory with additional files", async () => {
await resetTestRoot()
await Fs.Promises.mkdir(
Path.join([testRoot, "my-app", ".devcontainer"]),
~options={
recursive: true,
},
)
await Fs.Promises.writeFile(Path.join([testRoot, "my-app", "package.json"]), "{}")

validateProjectName("my-app")->assertValidationError(existingProjectMessage)
await cleanupTestRoot()
})

Test.testAsync("rejects an existing project directory with a .devcontainer file", async () => {
await resetTestRoot()
await Fs.Promises.mkdir(Path.join2(testRoot, "my-app"), ~options={recursive: true})
await Fs.Promises.writeFile(Path.join([testRoot, "my-app", ".devcontainer"]), "")

validateProjectName("my-app")->assertValidationError(existingProjectMessage)
await cleanupTestRoot()
})
})
Loading