create-foliokit 0.0.0-stage โ 1.0.1
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/LICENSE +21 -0
- package/README.md +111 -2
- package/bin/index.js +6 -0
- package/package.json +50 -4
- package/src/cli.js +111 -0
- package/src/generator.js +133 -0
- package/src/prompts.js +44 -0
- package/src/utils/errors.js +7 -0
- package/src/utils/paths.js +12 -0
- package/src/utils/progress.js +5 -0
- package/src/utils/project-name.js +30 -0
- package/templates/default/data.ts +169 -0
- package/templates/default/index.html +23 -0
- package/templates/default/package-lock.json +1280 -0
- package/templates/default/package.json +28 -0
- package/templates/default/public/avatar-placeholder.png +0 -0
- package/templates/default/public/experience-logo-placeholder.png +0 -0
- package/templates/default/public/project-placeholder.png +0 -0
- package/templates/default/public/robots.txt +2 -0
- package/templates/default/scripts/generate-seo.mjs +34 -0
- package/templates/default/src/App.tsx +582 -0
- package/templates/default/src/BrandIcon.tsx +36 -0
- package/templates/default/src/ProjectMagazineGallery.css +90 -0
- package/templates/default/src/ProjectMagazineGallery.tsx +218 -0
- package/templates/default/src/main.tsx +28 -0
- package/templates/default/src/styles.css +501 -0
- package/templates/default/src/utils.ts +2 -0
- package/templates/default/src/vite-env.d.ts +1 -0
- package/templates/default/tests/portfolio.test.js +96 -0
- package/templates/default/tsconfig.app.json +21 -0
- package/templates/default/tsconfig.json +7 -0
- package/templates/default/tsconfig.node.json +16 -0
- package/templates/default/types.ts +73 -0
- package/templates/default/vite.config.ts +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saurabh Pandey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ๐ FolioKit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/create-foliokit)
|
|
4
|
+
[](package.json)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
FolioKit is a CLI for creating a customizable developer portfolio. It scaffolds a responsive **React 19**, **TypeScript**, and **Vite** website with a dark grid background, framed sections, social links, and an interactive project gallery.
|
|
8
|
+
|
|
9
|
+
Created and maintained by [Saurabh Pandey](https://github.com/saurabhhdev) ยท [GitHub repository](https://github.com/saurabhhdev/foliokit)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## โจ Make It Yours
|
|
14
|
+
|
|
15
|
+
Most portfolio content is edited in one file: `data.ts` in the generated project.
|
|
16
|
+
|
|
17
|
+
| What | Where |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Name, headline, bio, photo, location, time zone, resume | `data.ts` โ `profile` |
|
|
20
|
+
| Social profiles and links | `data.ts` โ `socialIcons` |
|
|
21
|
+
| Work history and organization logos | `data.ts` โ `experience` |
|
|
22
|
+
| Schools, programs, dates, locations | `data.ts` โ `education` |
|
|
23
|
+
| Skill names, icons, and categories | `data.ts` โ `skills` |
|
|
24
|
+
| Project details, screenshots, technologies, URLs | `data.ts` โ `projects` |
|
|
25
|
+
| Recommendations | `data.ts` โ `testimonials` |
|
|
26
|
+
| Contact email | `data.ts` โ `contact` |
|
|
27
|
+
| Browser and social preview metadata | `data.ts` โ `seo` |
|
|
28
|
+
|
|
29
|
+
Copy images into `public/` and use paths such as `'/my-photo.jpg'` in `data.ts`. The template includes avatar, project, and experience logo placeholders. Leave an optional section array empty (`[]`) to hide that section.
|
|
30
|
+
|
|
31
|
+
Interface labels are beside the components in `src/App.tsx` and `src/ProjectMagazineGallery.tsx`. Colors, grid, spacing, and responsive styles are in `src/styles.css` and `src/ProjectMagazineGallery.css`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ๐ Run Locally
|
|
36
|
+
|
|
37
|
+
You need Node.js **22.12.0 or newer** to run the CLI. No global installation is required.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx create-foliokit@latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Choose a project folder name and whether to install dependencies. Then:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd my-portfolio
|
|
47
|
+
npm run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Open the local address printed by Vite, usually `http://localhost:5173`. If you skipped dependency installation, run `npm install` in the project folder first.
|
|
51
|
+
|
|
52
|
+
To install the CLI globally instead:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install --global create-foliokit
|
|
56
|
+
foliokit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Available commands
|
|
60
|
+
|
|
61
|
+
| Command | Action |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `foliokit` | Start the setup wizard. |
|
|
64
|
+
| `foliokit create` | Start the wizard with the explicit `create` command. |
|
|
65
|
+
| `foliokit --help` | Show CLI help. |
|
|
66
|
+
| `foliokit --version` | Print the installed version. |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## ๐ฆ Build and Deploy
|
|
71
|
+
|
|
72
|
+
Run the generated portfolio's checks and create a production build:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm test
|
|
76
|
+
npm run build
|
|
77
|
+
npm run preview
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Vite writes the static site to `dist/`. Deploy it to a static host such as **Vercel** or **Netlify** with build command `npm run build` and output directory `dist`.
|
|
81
|
+
|
|
82
|
+
For canonical URLs and sitemap generation, set `SITE_URL` in the hosting environment to your HTTPS origin, for example `https://portfolio.example` (no path or query string). You can also set `seo.siteUrl` in `data.ts`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## ๐งฐ Stack and Project Files
|
|
87
|
+
|
|
88
|
+
The generated site uses React, TypeScript, Vite, Lucide icons, and CSS. The CLI package uses Commander, Inquirer, Chalk, and Ora.
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
my-portfolio/
|
|
92
|
+
โโโ data.ts # Edit your portfolio content
|
|
93
|
+
โโโ types.ts # Shared TypeScript types
|
|
94
|
+
โโโ public/ # Images and robots.txt
|
|
95
|
+
โโโ scripts/generate-seo.mjs
|
|
96
|
+
โโโ tests/portfolio.test.js
|
|
97
|
+
โโโ src/
|
|
98
|
+
โโโ App.tsx
|
|
99
|
+
โโโ BrandIcon.tsx
|
|
100
|
+
โโโ ProjectMagazineGallery.tsx
|
|
101
|
+
โโโ ProjectMagazineGallery.css
|
|
102
|
+
โโโ styles.css
|
|
103
|
+
โโโ main.tsx
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ๐ Contributing and License
|
|
109
|
+
|
|
110
|
+
For CLI development, run `npm install` and `npm test` at the repository root. To check the bundled website template, run `npm test` and `npm run build` inside `templates/default/`.
|
|
111
|
+
|
|
112
|
+
FolioKit is open source under the [MIT License](LICENSE).
|
package/bin/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-foliokit",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Generate customizable React and TypeScript developer portfolios",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/cli.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/cli.js",
|
|
9
|
+
"./generator": "./src/generator.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"author": "Saurabh Pandey",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test tests"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"foliokit": "./bin/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"src/",
|
|
22
|
+
"templates/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"portfolio",
|
|
31
|
+
"developer-portfolio",
|
|
32
|
+
"react",
|
|
33
|
+
"typescript",
|
|
34
|
+
"cli",
|
|
35
|
+
"generator"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/saurabhhdev/foliokit.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/saurabhhdev/foliokit#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/saurabhhdev/foliokit/issues"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"chalk": "^6.0.1",
|
|
48
|
+
"commander": "^15.0.0",
|
|
49
|
+
"inquirer": "^14.2.5",
|
|
50
|
+
"ora": "^9.4.1"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import inquirer from 'inquirer'
|
|
6
|
+
import { generatePortfolio } from './generator.js'
|
|
7
|
+
import { promptForProjectOptions } from './prompts.js'
|
|
8
|
+
import { getPackagePath } from './utils/paths.js'
|
|
9
|
+
import { createProgress } from './utils/progress.js'
|
|
10
|
+
|
|
11
|
+
const packageMetadata = JSON.parse(readFileSync(getPackagePath('package.json'), 'utf8'))
|
|
12
|
+
|
|
13
|
+
function showWelcome() {
|
|
14
|
+
console.log(chalk.bold.cyan('FOLIOKIT'))
|
|
15
|
+
console.log(chalk.dim('Developer Portfolio Generator'))
|
|
16
|
+
console.log()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function startSetup() {
|
|
20
|
+
showWelcome()
|
|
21
|
+
const options = await promptForProjectOptions()
|
|
22
|
+
|
|
23
|
+
if (!options) {
|
|
24
|
+
console.log(chalk.yellow('Setup cancelled.'))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let projectPath
|
|
29
|
+
try {
|
|
30
|
+
projectPath = await generatePortfolio(options)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!(error instanceof Error) || error.code !== 'EEXIST') throw error
|
|
33
|
+
let overwrite
|
|
34
|
+
try {
|
|
35
|
+
({ overwrite } = await inquirer.prompt([{
|
|
36
|
+
type: 'confirm',
|
|
37
|
+
name: 'overwrite',
|
|
38
|
+
message: `The folder ${options.projectName} already exists. Replace it?`,
|
|
39
|
+
default: false,
|
|
40
|
+
}]))
|
|
41
|
+
} catch (promptError) {
|
|
42
|
+
if (promptError instanceof Error && promptError.name === 'ExitPromptError') {
|
|
43
|
+
console.log(chalk.yellow('Setup cancelled. Existing project was left unchanged.'))
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
throw promptError
|
|
47
|
+
}
|
|
48
|
+
if (!overwrite) {
|
|
49
|
+
console.log(chalk.yellow('Existing project was left unchanged.'))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
projectPath = await generatePortfolio({ ...options, overwrite: true })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let dependenciesInstalled = false
|
|
56
|
+
if (options.installDependencies) {
|
|
57
|
+
const windows = process.platform === 'win32'
|
|
58
|
+
const install = spawn(windows ? (process.env.ComSpec || 'cmd.exe') : 'npm', windows ? ['/d', '/s', '/c', 'npm.cmd install'] : ['install'], {
|
|
59
|
+
cwd: projectPath,
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
shell: false,
|
|
62
|
+
})
|
|
63
|
+
const installation = createProgress('Installing dependencies...').start()
|
|
64
|
+
const result = await new Promise((resolve) => {
|
|
65
|
+
install.once('error', (error) => resolve({ code: 1, error }))
|
|
66
|
+
install.once('close', (code) => resolve({ code: code ?? 1 }))
|
|
67
|
+
})
|
|
68
|
+
dependenciesInstalled = result.code === 0
|
|
69
|
+
if (dependenciesInstalled) installation.succeed('Dependencies installed')
|
|
70
|
+
else installation.fail('Dependency installation failed')
|
|
71
|
+
if (!dependenciesInstalled) {
|
|
72
|
+
console.error(chalk.red('Dependency installation failed. Your generated project is still available.'))
|
|
73
|
+
if (result.error) console.error(chalk.dim(result.error.message))
|
|
74
|
+
console.error(`Run ${chalk.bold('npm install')} inside ${options.projectName} to try again.`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const configuration = { ...options, projectPath }
|
|
79
|
+
console.log()
|
|
80
|
+
console.log(chalk.green('โ Portfolio generated successfully!'))
|
|
81
|
+
console.log()
|
|
82
|
+
console.log(chalk.bold('Next steps:'))
|
|
83
|
+
console.log(` cd ${configuration.projectName}`)
|
|
84
|
+
if (!dependenciesInstalled) console.log(' npm install')
|
|
85
|
+
console.log(' npm run dev')
|
|
86
|
+
console.log()
|
|
87
|
+
console.log(`Customize your portfolio by editing ${chalk.bold('data.ts')}.`)
|
|
88
|
+
if (options.installDependencies && !dependenciesInstalled) process.exitCode = 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createCli() {
|
|
92
|
+
const program = new Command()
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.name('foliokit')
|
|
96
|
+
.description('Generate customizable developer portfolio websites')
|
|
97
|
+
.version(packageMetadata.version, '-v, --version')
|
|
98
|
+
.showHelpAfterError()
|
|
99
|
+
.action(startSetup)
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('create')
|
|
103
|
+
.description('Create a portfolio project')
|
|
104
|
+
.action(startSetup)
|
|
105
|
+
|
|
106
|
+
return program
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runCli(argv = process.argv) {
|
|
110
|
+
await createCli().parseAsync(argv)
|
|
111
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { copyFile, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { getDefaultTemplatePath } from './utils/paths.js'
|
|
4
|
+
import { validateProjectName } from './utils/project-name.js'
|
|
5
|
+
import { createProgress } from './utils/progress.js'
|
|
6
|
+
|
|
7
|
+
const excludedDirectories = new Set([
|
|
8
|
+
'.angular', '.cache', '.git', '.netlify', '.next', '.parcel-cache', '.svelte-kit',
|
|
9
|
+
'.turbo', '.vercel', '.vite', 'build', 'coverage', 'dist', 'node_modules', 'tmp', 'temp',
|
|
10
|
+
])
|
|
11
|
+
const excludedExtensions = new Set([
|
|
12
|
+
'.bak', '.log', '.orig', '.pem', '.p12', '.pfx', '.key', '.secret', '.token',
|
|
13
|
+
'.swp', '.swo', '.tsbuildinfo',
|
|
14
|
+
])
|
|
15
|
+
const excludedFiles = new Set([
|
|
16
|
+
'.ds_store', '.envrc', '.npmignore', '.npmrc', '.netrc', '.pypirc', 'credentials', 'credentials.json',
|
|
17
|
+
'id_ed25519', 'id_rsa', 'secrets.json', 'thumbs.db',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
function isWithin(parent, child) {
|
|
21
|
+
const path = relative(parent, child)
|
|
22
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldExclude(name, isDirectory) {
|
|
26
|
+
const lower = name.toLowerCase()
|
|
27
|
+
if (isDirectory && excludedDirectories.has(lower)) return true
|
|
28
|
+
if (excludedFiles.has(lower) || excludedExtensions.has(extname(lower))) return true
|
|
29
|
+
if (lower === '.env' || (lower.startsWith('.env.') && lower !== '.env.example')) return true
|
|
30
|
+
if (/\.(?:tmp|temp|swp|swo|orig|bak|log)$/.test(lower)) return true
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveProjectDestination(projectName, workingDirectory = process.cwd()) {
|
|
35
|
+
const validation = validateProjectName(projectName)
|
|
36
|
+
if (validation !== true) throw new Error(`Invalid project name: ${validation}`)
|
|
37
|
+
const base = resolve(workingDirectory)
|
|
38
|
+
const destination = resolve(base, projectName)
|
|
39
|
+
if (!isWithin(base, destination) || destination === base) {
|
|
40
|
+
throw new Error('The project destination must be a new child of the working directory.')
|
|
41
|
+
}
|
|
42
|
+
return destination
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function copyTree(source, destination) {
|
|
46
|
+
await mkdir(destination, { recursive: true })
|
|
47
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
48
|
+
if (shouldExclude(entry.name, entry.isDirectory())) continue
|
|
49
|
+
const from = join(source, entry.name)
|
|
50
|
+
const to = join(destination, entry.name)
|
|
51
|
+
if (entry.isSymbolicLink()) throw new Error(`Template contains an unsupported symbolic link: ${from}`)
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
await copyTree(from, to)
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
await copyFile(from, to)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function updatePackageNames(destination, projectName) {
|
|
61
|
+
const packagePath = join(destination, 'package.json')
|
|
62
|
+
const packageData = JSON.parse(await readFile(packagePath, 'utf8'))
|
|
63
|
+
packageData.name = projectName
|
|
64
|
+
await writeFile(packagePath, `${JSON.stringify(packageData, null, 2)}\n`)
|
|
65
|
+
|
|
66
|
+
const lockPath = join(destination, 'package-lock.json')
|
|
67
|
+
try {
|
|
68
|
+
const lock = JSON.parse(await readFile(lockPath, 'utf8'))
|
|
69
|
+
lock.name = projectName
|
|
70
|
+
if (lock.packages?.['']) lock.packages[''].name = projectName
|
|
71
|
+
await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 'ENOENT') throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getDefaultTemplateDirectory() {
|
|
78
|
+
return getDefaultTemplatePath()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function generatePortfolio(options, context = {}) {
|
|
82
|
+
const projectName = options?.projectName
|
|
83
|
+
const workingDirectory = resolve(context.workingDirectory ?? process.cwd())
|
|
84
|
+
const templatePath = resolve(context.templateDirectory ?? getDefaultTemplateDirectory())
|
|
85
|
+
|
|
86
|
+
const [workingStats, templateStats] = await Promise.all([lstat(workingDirectory), lstat(templatePath)])
|
|
87
|
+
if (!workingStats.isDirectory()) throw new Error(`Working directory is not a directory: ${workingDirectory}`)
|
|
88
|
+
if (!templateStats.isDirectory()) throw new Error(`Portfolio template directory was not found: ${templatePath}`)
|
|
89
|
+
const [realDestinationParent, realTemplate] = await Promise.all([realpath(workingDirectory), realpath(templatePath)])
|
|
90
|
+
const destination = resolveProjectDestination(projectName, realDestinationParent)
|
|
91
|
+
if (isWithin(realTemplate, destination) || isWithin(destination, realTemplate)) {
|
|
92
|
+
throw new Error('The project destination cannot overlap the bundled template directory.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const creating = createProgress('Creating project...').start()
|
|
96
|
+
try {
|
|
97
|
+
await mkdir(destination)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
creating.fail('Could not create project directory')
|
|
100
|
+
if (error.code === 'EEXIST' && options.overwrite === true) {
|
|
101
|
+
const existing = await lstat(destination)
|
|
102
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
103
|
+
throw new Error(`The destination exists and is not a regular directory: ${destination}`)
|
|
104
|
+
}
|
|
105
|
+
await rm(destination, { recursive: true })
|
|
106
|
+
await mkdir(destination)
|
|
107
|
+
} else if (error.code === 'EEXIST') {
|
|
108
|
+
const conflict = new Error(`The destination already exists: ${destination}`)
|
|
109
|
+
conflict.code = 'EEXIST'
|
|
110
|
+
conflict.projectPath = destination
|
|
111
|
+
throw conflict
|
|
112
|
+
} else {
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
creating.succeed('Project directory created')
|
|
117
|
+
|
|
118
|
+
const progress = createProgress('Copying portfolio template...').start()
|
|
119
|
+
let configuring
|
|
120
|
+
try {
|
|
121
|
+
await copyTree(templatePath, destination)
|
|
122
|
+
progress.succeed('Copying portfolio template')
|
|
123
|
+
configuring = createProgress('Configuring package.json...').start()
|
|
124
|
+
await updatePackageNames(destination, projectName)
|
|
125
|
+
configuring.succeed('Configuring package.json')
|
|
126
|
+
return destination
|
|
127
|
+
} catch (error) {
|
|
128
|
+
progress.fail('Could not create portfolio')
|
|
129
|
+
configuring?.fail('Could not configure project')
|
|
130
|
+
await rm(destination, { recursive: true, force: true })
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PROJECT_NAME,
|
|
4
|
+
normalizeProjectName,
|
|
5
|
+
validateProjectName,
|
|
6
|
+
} from './utils/project-name.js'
|
|
7
|
+
|
|
8
|
+
export function createProjectQuestions() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
type: 'input',
|
|
12
|
+
name: 'projectName',
|
|
13
|
+
message: 'What should your project be called?',
|
|
14
|
+
default: DEFAULT_PROJECT_NAME,
|
|
15
|
+
filter: normalizeProjectName,
|
|
16
|
+
validate: validateProjectName,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'confirm',
|
|
20
|
+
name: 'installDependencies',
|
|
21
|
+
message: 'Install dependencies?',
|
|
22
|
+
default: true,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function promptForProjectOptions(prompt = (questions) => inquirer.prompt(questions)) {
|
|
28
|
+
try {
|
|
29
|
+
const answers = await prompt(createProjectQuestions())
|
|
30
|
+
if (!answers) return null
|
|
31
|
+
|
|
32
|
+
const projectName = normalizeProjectName(answers.projectName)
|
|
33
|
+
const validation = validateProjectName(projectName)
|
|
34
|
+
if (validation !== true) throw new Error(validation)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
projectName,
|
|
38
|
+
installDependencies: Boolean(answers.installDependencies),
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof Error && error.name === 'ExitPromptError') return null
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
5
|
+
|
|
6
|
+
export function getPackagePath(...segments) {
|
|
7
|
+
return resolve(packageRoot, ...segments)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getDefaultTemplatePath() {
|
|
11
|
+
return getPackagePath('templates', 'default')
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const DEFAULT_PROJECT_NAME = 'my-portfolio'
|
|
2
|
+
|
|
3
|
+
const projectNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
4
|
+
const reservedNames = new Set([
|
|
5
|
+
'node_modules',
|
|
6
|
+
'con',
|
|
7
|
+
'prn',
|
|
8
|
+
'aux',
|
|
9
|
+
'nul',
|
|
10
|
+
...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
|
|
11
|
+
...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`),
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
export function normalizeProjectName(value) {
|
|
15
|
+
if (typeof value !== 'string') return DEFAULT_PROJECT_NAME
|
|
16
|
+
return value.trim() || DEFAULT_PROJECT_NAME
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateProjectName(value) {
|
|
20
|
+
const name = typeof value === 'string' ? value.trim() : ''
|
|
21
|
+
|
|
22
|
+
if (!name) return 'Enter a project name.'
|
|
23
|
+
if (name.length > 214) return 'Project names must be 214 characters or fewer.'
|
|
24
|
+
if (!projectNamePattern.test(name)) {
|
|
25
|
+
return 'Use lowercase letters and numbers separated by single hyphens.'
|
|
26
|
+
}
|
|
27
|
+
if (reservedNames.has(name)) return 'That name is reserved by the operating system or npm.'
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
}
|