create-lumen 1.0.0 → 1.1.0
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/README.md +113 -70
- package/bin/cli.js +9 -3
- package/package.json +2 -2
- package/src/aliases.js +7 -0
- package/src/cleanup.js +1 -1
- package/src/configure.js +21 -19
- package/src/confirm-empty-folder.js +1 -1
- package/src/css.js +1 -1
- package/src/dependencies.js +12 -2
- package/src/env.js +27 -0
- package/src/injector.js +103 -3
- package/src/main.js +30 -14
- package/src/prompts.js +69 -13
- package/src/readme.js +29 -5
- package/src/scaffold.js +1 -1
- package/templates/.env.example +4 -0
- package/templates/conditional/fetch/component-based/src/services/api.jsx +27 -0
- package/templates/conditional/fetch/component-based/src/services/api.ts +27 -0
- package/templates/conditional/fetch/component-based/src/services/index.js +1 -0
- package/templates/conditional/fetch/component-based/src/services/index.ts +1 -0
- package/templates/conditional/fetch/feature-based/src/lib/api.jsx +27 -0
- package/templates/conditional/fetch/feature-based/src/lib/api.ts +27 -0
- package/templates/conditional/fetch/feature-based/src/lib/index.js +1 -0
- package/templates/conditional/fetch/feature-based/src/lib/index.ts +1 -0
- package/templates/conditional/formatter/oxfmt/.oxfmtrc.json +7 -0
- package/templates/conditional/formatter/prettier/.prettierrc +7 -0
- package/templates/conditional/testing/jest/jest.config.js +1 -1
- package/templates/conditional/testing/jest/jest.config.ts +1 -1
package/README.md
CHANGED
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
# create-lumen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A scaffolder that generates production-ready React + Vite projects (feature-based or component-based).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
npm create lumen
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
## What it does
|
|
10
|
-
|
|
11
|
-
Creates a production-ready React + Vite project with:
|
|
5
|
+
Note: this repository is the CLI scaffolder itself, not a generated app. Generated projects live in the target folder you create.
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
- **TypeScript or JavaScript**
|
|
15
|
-
- **CSS framework** — Tailwind CSS, Bootstrap, or vanilla CSS
|
|
16
|
-
- **State management** — Zustand, Redux Toolkit, or none
|
|
17
|
-
- **React Router** — client-side routing (optional)
|
|
18
|
-
- **Testing** — Vitest or Jest (optional)
|
|
19
|
-
- **Linter** — ESLint (default) or Oxlint
|
|
20
|
-
- **Icon library** — Lucide React, Huge Icons, or none
|
|
21
|
-
- **Axios** — pre-configured HTTP client (optional)
|
|
22
|
-
- **README + LICENSE** — auto-generated (MIT)
|
|
23
|
-
- **Git init** — auto-initialize a git repo
|
|
24
|
-
- **Path alias** — `@/` imports configured out of the box
|
|
7
|
+
## Quick usage
|
|
25
8
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Quick start
|
|
29
|
-
|
|
30
|
-
### From npm (recommended)
|
|
9
|
+
From npm (recommended):
|
|
31
10
|
|
|
32
11
|
```bash
|
|
33
12
|
npm create lumen my-app
|
|
@@ -35,81 +14,145 @@ cd my-app
|
|
|
35
14
|
npm run dev
|
|
36
15
|
```
|
|
37
16
|
|
|
38
|
-
|
|
17
|
+
Non-interactive Quick Setup (TypeScript + Tailwind + Feature-based + Router + ESLint + Prettier + Vitest), no prompts:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create lumen my-app -y
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run the scaffolder locally (development / testing the generator):
|
|
39
24
|
|
|
40
25
|
```bash
|
|
41
|
-
|
|
42
|
-
cd create-lumen
|
|
26
|
+
node bin/cli.js # run interactively
|
|
43
27
|
npm install
|
|
44
|
-
npm link
|
|
45
|
-
|
|
46
|
-
cd my-app
|
|
47
|
-
npm run dev
|
|
28
|
+
npm link # install globally for testing
|
|
29
|
+
create-lumen my-app # after npm link
|
|
48
30
|
```
|
|
49
31
|
|
|
50
|
-
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Architecture choice: feature-based or component-based
|
|
35
|
+
- TypeScript or JavaScript output
|
|
36
|
+
- CSS frameworks: Tailwind, Bootstrap, or none
|
|
37
|
+
- Optional state management: Zustand or Redux Toolkit
|
|
38
|
+
- Optional router (React Router)
|
|
39
|
+
- Optional testing: Vitest or Jest
|
|
40
|
+
- Linting: ESLint (default) or Oxlint
|
|
41
|
+
- Code formatter: Prettier or Oxfmt (chosen after the linter; `eslint-config-prettier` is auto-wired for the ESLint + Prettier combo)
|
|
42
|
+
- Optional Axios setup and icon libraries
|
|
43
|
+
- Auto `git init`, generated `README` (with a project description reflecting the chosen tooling) and `LICENSE`
|
|
44
|
+
- `.env.example` scaffolded, and the generated `.gitignore` excludes env files (`.env`, `.env.*`) while keeping `.env.example`
|
|
45
|
+
- `@/` path alias configured (`tsconfig`/`jsconfig` + Vite)
|
|
51
46
|
|
|
52
|
-
|
|
47
|
+
Your selected options are cached and offered as defaults on subsequent runs.
|
|
53
48
|
|
|
54
|
-
##
|
|
49
|
+
## Developing the scaffolder
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
- Entry point: `bin/cli.js` → `src/main.js` (ESM-only)
|
|
52
|
+
- Templates: `templates/` (architectures + conditional overlays)
|
|
53
|
+
- Injector copies architecture, then overlays conditionals based on choices
|
|
54
|
+
- Config cache: `~/.lumen-config.json` stores previous choices
|
|
55
|
+
- This tool uses Node >= 18 and `execa` for subprocesses
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
Run locally (interactive):
|
|
59
58
|
|
|
59
|
+
```bash
|
|
60
|
+
node bin/cli.js
|
|
60
61
|
```
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
│ └── services/
|
|
68
|
-
├── layouts/
|
|
69
|
-
├── pages/
|
|
70
|
-
├── routes/
|
|
71
|
-
├── store/
|
|
72
|
-
├── hooks/
|
|
73
|
-
└── utils/
|
|
62
|
+
|
|
63
|
+
Install globally for testing:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm link
|
|
67
|
+
create-lumen my-app
|
|
74
68
|
```
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
## Testing the scaffolder
|
|
71
|
+
|
|
72
|
+
The CLI repo has no app-level test suite, but `npm test` runs the unit tests under `tests/unit/` plus the offline smoke tests under `tests/smoke/`, and two harnesses in `tests/e2e/` and `tests/smoke/` drive the real generator against generated scaffolds. See `docs/tests/README.md` (and `harness.md`) for details, scope, and what they verify.
|
|
77
73
|
|
|
78
74
|
```bash
|
|
79
|
-
npm
|
|
75
|
+
npm test # unit (tests/unit/) + offline smoke (tests/smoke/generate.test.mjs)
|
|
76
|
+
node --import ./register.js tests/smoke/install.mjs # real-install smoke (Quick Setup default; needs network)
|
|
77
|
+
node --import ./register.js tests/e2e/exhaustive.mjs # full option matrix (~11,664 combos, local-only; ~13 min). Use LIMIT=n.
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
## Project structure (generated app examples)
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
Feature-based (example):
|
|
85
83
|
|
|
86
84
|
```
|
|
87
85
|
src/
|
|
88
|
-
├──
|
|
89
|
-
│
|
|
90
|
-
│
|
|
91
|
-
│
|
|
92
|
-
|
|
93
|
-
├──
|
|
94
|
-
|
|
95
|
-
├──
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
├── app
|
|
87
|
+
│ ├── App.tsx
|
|
88
|
+
│ ├── providers
|
|
89
|
+
│ │ └── ThemeProvider.tsx
|
|
90
|
+
│ └── router.tsx
|
|
91
|
+
├── components
|
|
92
|
+
│ └── ui
|
|
93
|
+
├── config
|
|
94
|
+
│ └── constants.ts
|
|
95
|
+
├── features
|
|
96
|
+
│ └── logout
|
|
97
|
+
│ ├── components
|
|
98
|
+
│ ├── interfaces
|
|
99
|
+
│ ├── layouts
|
|
100
|
+
│ ├── pages
|
|
101
|
+
│ ├── services
|
|
102
|
+
│ └── store
|
|
103
|
+
├── hooks
|
|
104
|
+
│ ├── index.ts
|
|
105
|
+
│ └── useLocalStorage.ts
|
|
106
|
+
├── index.css
|
|
107
|
+
├── lib
|
|
108
|
+
├── main.tsx
|
|
109
|
+
├── stores
|
|
110
|
+
├── styles
|
|
111
|
+
│ └── globals.css
|
|
112
|
+
├── test
|
|
113
|
+
│ └── setup.ts
|
|
114
|
+
├── types
|
|
115
|
+
└── utils
|
|
98
116
|
```
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
Component-based (example):
|
|
101
119
|
|
|
102
|
-
All projects use the `@/` path alias:
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
import { Button } from "@/components/common/Button";
|
|
106
|
-
import { useAuth } from "@/hooks/useAuth";
|
|
107
120
|
```
|
|
121
|
+
src/
|
|
122
|
+
├── App.tsx
|
|
123
|
+
├── components
|
|
124
|
+
│ ├── common
|
|
125
|
+
│ ├── form
|
|
126
|
+
│ └── layout
|
|
127
|
+
├── config
|
|
128
|
+
│ ├── constants.ts
|
|
129
|
+
│ └── routes.ts
|
|
130
|
+
├── context
|
|
131
|
+
├── hooks
|
|
132
|
+
├── index.css
|
|
133
|
+
├── layouts
|
|
134
|
+
├── main.tsx
|
|
135
|
+
├── pages
|
|
136
|
+
├── routes
|
|
137
|
+
├── services
|
|
138
|
+
├── store
|
|
139
|
+
├── styles
|
|
140
|
+
├── test
|
|
141
|
+
└── utils
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
|
|
146
|
+
See `docs/ROADMAP.md` for planned features and upcoming releases.
|
|
108
147
|
|
|
109
148
|
## Requirements
|
|
110
149
|
|
|
111
150
|
- Node.js >= 18
|
|
112
151
|
|
|
152
|
+
## Acknowledgements
|
|
153
|
+
|
|
154
|
+
This project is based on [create-vrtw](https://github.com/Avijit07x/create-vrtw) by [@Avijit07x](https://github.com/Avijit07x). Thank you for the original work that inspired this scaffolder.
|
|
155
|
+
|
|
113
156
|
## License
|
|
114
157
|
|
|
115
158
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { main } from "../src/main.js";
|
|
4
3
|
import chalk from "chalk";
|
|
4
|
+
import "../register.js";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const quickSetup = args.includes("-y");
|
|
8
|
+
const projectName = args.find((a) => !a.startsWith("-"));
|
|
5
9
|
|
|
6
10
|
console.log(
|
|
7
11
|
chalk.bold.cyan("\n ✦ LUMEN") +
|
|
8
|
-
chalk.gray(" v1.
|
|
12
|
+
chalk.gray(" v1.1.0") +
|
|
9
13
|
chalk.gray(
|
|
10
14
|
"\n Scaffold a React + Vite project with architecture choice\n"
|
|
11
15
|
)
|
|
12
16
|
);
|
|
13
17
|
|
|
14
|
-
main(
|
|
18
|
+
const { main } = await import("@/main.js");
|
|
19
|
+
|
|
20
|
+
main({ quickSetup, projectName }).catch((e) => {
|
|
15
21
|
console.error(chalk.red("\nError:"), e.message || e);
|
|
16
22
|
process.exit(1);
|
|
17
23
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lumen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Scaffold a React + Vite project with architecture choice: feature-based or component-based.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "
|
|
17
|
+
"test": "node --import ./register.js --test \"tests/unit/*.test.mjs\" \"tests/smoke/*.test.mjs\""
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"scaffold",
|
package/src/aliases.js
ADDED
package/src/cleanup.js
CHANGED
package/src/configure.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { promises as fsp } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { readFileContent } from "
|
|
3
|
+
import { readFileContent } from "@/utils/fs.js";
|
|
4
|
+
import { getPathAliases, getViteAliases } from "@/aliases.js";
|
|
4
5
|
|
|
5
6
|
export async function configureProject(projectPath, language, cssFramework) {
|
|
6
7
|
const ext = language === "ts" ? "ts" : "js";
|
|
7
8
|
|
|
8
|
-
// Set up tsconfig/jsconfig with @/*
|
|
9
|
+
// Set up tsconfig/jsconfig with the @/* path alias
|
|
9
10
|
if (language === "ts") {
|
|
10
11
|
await configureTsconfig(projectPath);
|
|
11
12
|
} else {
|
|
12
13
|
await configureJsconfig(projectPath);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
// Add
|
|
16
|
+
// Add path aliases to vite.config for non-tailwind setups
|
|
16
17
|
if (cssFramework !== "tailwind") {
|
|
17
|
-
await
|
|
18
|
+
await addPathAliasesToViteConfig(projectPath, ext);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// Add scripts to package.json
|
|
@@ -22,6 +23,7 @@ export async function configureProject(projectPath, language, cssFramework) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function configureTsconfig(projectPath) {
|
|
26
|
+
const paths = getPathAliases();
|
|
25
27
|
// Create/update tsconfig.app.json with paths
|
|
26
28
|
const tsconfigAppPath = path.join(projectPath, "tsconfig.app.json");
|
|
27
29
|
try {
|
|
@@ -30,9 +32,7 @@ async function configureTsconfig(projectPath) {
|
|
|
30
32
|
|
|
31
33
|
if (!config.compilerOptions) config.compilerOptions = {};
|
|
32
34
|
config.compilerOptions.baseUrl = ".";
|
|
33
|
-
config.compilerOptions.paths =
|
|
34
|
-
"@/*": ["./src/*"],
|
|
35
|
-
};
|
|
35
|
+
config.compilerOptions.paths = paths;
|
|
36
36
|
|
|
37
37
|
await fsp.writeFile(
|
|
38
38
|
tsconfigAppPath,
|
|
@@ -59,9 +59,7 @@ async function configureTsconfig(projectPath) {
|
|
|
59
59
|
noUnusedParameters: true,
|
|
60
60
|
noFallthroughCasesInSwitch: true,
|
|
61
61
|
baseUrl: ".",
|
|
62
|
-
paths
|
|
63
|
-
"@/*": ["./src/*"],
|
|
64
|
-
},
|
|
62
|
+
paths,
|
|
65
63
|
},
|
|
66
64
|
include: ["src"],
|
|
67
65
|
};
|
|
@@ -90,16 +88,14 @@ async function configureJsconfig(projectPath) {
|
|
|
90
88
|
const config = {
|
|
91
89
|
compilerOptions: {
|
|
92
90
|
baseUrl: ".",
|
|
93
|
-
paths:
|
|
94
|
-
"@/*": ["./src/*"],
|
|
95
|
-
},
|
|
91
|
+
paths: getPathAliases(),
|
|
96
92
|
},
|
|
97
93
|
include: ["src"],
|
|
98
94
|
};
|
|
99
95
|
await fsp.writeFile(jsconfigPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
async function
|
|
98
|
+
async function addPathAliasesToViteConfig(projectPath, ext) {
|
|
103
99
|
const viteConfigPath = path.join(projectPath, `vite.config.${ext}`);
|
|
104
100
|
try {
|
|
105
101
|
let content = await readFileContent(viteConfigPath);
|
|
@@ -124,15 +120,21 @@ async function addTsconfigPathsToViteConfig(projectPath, ext) {
|
|
|
124
120
|
|
|
125
121
|
let result = lines.join("\n");
|
|
126
122
|
|
|
123
|
+
const aliasArray = getViteAliases()
|
|
124
|
+
.map(
|
|
125
|
+
(a) =>
|
|
126
|
+
`{ find: "${a.find}", replacement: ${a.replacement} }`
|
|
127
|
+
)
|
|
128
|
+
.join(",\n ");
|
|
129
|
+
|
|
130
|
+
const resolveBlock = `resolve: {\n tsconfigPaths: true,\n alias: [\n ${aliasArray}\n ],\n }`;
|
|
131
|
+
|
|
127
132
|
if (result.includes("resolve:")) {
|
|
128
|
-
result = result.replace(
|
|
129
|
-
/resolve:\s*\{/,
|
|
130
|
-
'resolve: {\n tsconfigPaths: true,\n alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],'
|
|
131
|
-
);
|
|
133
|
+
result = result.replace(/resolve:\s*\{/, resolveBlock);
|
|
132
134
|
} else {
|
|
133
135
|
result = result.replace(
|
|
134
136
|
/defineConfig\(\{/,
|
|
135
|
-
|
|
137
|
+
`defineConfig({\n ${resolveBlock},`
|
|
136
138
|
);
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isCancel, select } from "@clack/prompts";
|
|
2
2
|
import { access, mkdir, readdir, rm } from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { onCancel } from "
|
|
4
|
+
import { onCancel } from "@/prompts.js";
|
|
5
5
|
|
|
6
6
|
export async function confirmEmptyFolder(projectName) {
|
|
7
7
|
const cwd = process.cwd();
|
package/src/css.js
CHANGED
package/src/dependencies.js
CHANGED
|
@@ -55,8 +55,8 @@ export async function installAllDeps(responses, pkg, cwd) {
|
|
|
55
55
|
deps.push("@hugeicons/react", "@hugeicons/core-free-icons");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
if (responses.axios) {
|
|
58
|
+
// API Client
|
|
59
|
+
if (responses.apiClient === "axios") {
|
|
60
60
|
deps.push("axios");
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -89,6 +89,16 @@ export async function installAllDeps(responses, pkg, cwd) {
|
|
|
89
89
|
devDeps.push("oxlint");
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Formatter
|
|
93
|
+
if (responses.formatter === "prettier") {
|
|
94
|
+
devDeps.push("prettier");
|
|
95
|
+
if (responses.linter === "eslint") {
|
|
96
|
+
devDeps.push("eslint-config-prettier");
|
|
97
|
+
}
|
|
98
|
+
} else if (responses.formatter === "oxfmt") {
|
|
99
|
+
devDeps.push("oxfmt");
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
// Always re-install react + react-dom for consistency
|
|
93
103
|
deps.push("react", "react-dom");
|
|
94
104
|
|
package/src/env.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { promises as fsp } from "fs";
|
|
3
|
+
|
|
4
|
+
const ENV_GITIGNORE = `
|
|
5
|
+
# Environment variables
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!.env.example
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export async function copyEnvExample(projectPath, templatesDir) {
|
|
12
|
+
const src = path.join(templatesDir, ".env.example");
|
|
13
|
+
try {
|
|
14
|
+
await fsp.access(src);
|
|
15
|
+
await fsp.copyFile(src, path.join(projectPath, ".env.example"));
|
|
16
|
+
} catch {}
|
|
17
|
+
|
|
18
|
+
// Ensure the project gitignore excludes env files but keeps .env.example.
|
|
19
|
+
const gitignore = path.join(projectPath, ".gitignore");
|
|
20
|
+
try {
|
|
21
|
+
let content = await fsp.readFile(gitignore, "utf8");
|
|
22
|
+
if (!content.includes("!.env.example")) {
|
|
23
|
+
content = content.replace(/\s*$/, "\n") + ENV_GITIGNORE.trimEnd() + "\n";
|
|
24
|
+
await fsp.writeFile(gitignore, content, "utf8");
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
package/src/injector.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fsp } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { copyDirRecursive, writeFileRecursive } from "
|
|
3
|
+
import { copyDirRecursive, writeFileRecursive } from "@/utils/fs.js";
|
|
4
4
|
|
|
5
5
|
export async function injectArchitecture(projectPath, templatesDir, architecture, language) {
|
|
6
6
|
const ext = language === "ts" ? "tsx" : "jsx";
|
|
@@ -43,6 +43,89 @@ export async function injectArchitecture(projectPath, templatesDir, architecture
|
|
|
43
43
|
try { await fsp.rm(oppositeMain, { force: true }); } catch {}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export async function injectFormatter(projectPath, templatesDir, responses) {
|
|
47
|
+
if (!responses.formatter || responses.formatter === "none") return;
|
|
48
|
+
|
|
49
|
+
// Copy the formatter config file to the project root
|
|
50
|
+
const configFile =
|
|
51
|
+
responses.formatter === "prettier" ? ".prettierrc" : ".oxfmtrc.json";
|
|
52
|
+
const configSrc = path.join(
|
|
53
|
+
templatesDir,
|
|
54
|
+
"conditional",
|
|
55
|
+
"formatter",
|
|
56
|
+
responses.formatter,
|
|
57
|
+
configFile
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
await fsp.access(configSrc);
|
|
61
|
+
await fsp.copyFile(configSrc, path.join(projectPath, configFile));
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
// Append format script(s) to package.json
|
|
65
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
66
|
+
try {
|
|
67
|
+
const content = await fsp.readFile(pkgPath, "utf8");
|
|
68
|
+
const pkg = JSON.parse(content);
|
|
69
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
70
|
+
if (responses.formatter === "prettier") {
|
|
71
|
+
pkg.scripts.format = "prettier --write .";
|
|
72
|
+
pkg.scripts["format:check"] = "prettier --check .";
|
|
73
|
+
} else if (responses.formatter === "oxfmt") {
|
|
74
|
+
pkg.scripts.format = "oxfmt .";
|
|
75
|
+
}
|
|
76
|
+
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
// Wire eslint-config-prettier only for the eslint + prettier combination
|
|
80
|
+
if (responses.linter === "eslint" && responses.formatter === "prettier") {
|
|
81
|
+
await wireEslintPrettier(projectPath, responses.language);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function wireEslintPrettier(projectPath, language) {
|
|
86
|
+
const ext = language === "ts" ? "ts" : "js";
|
|
87
|
+
const eslintConfigPath = path.join(projectPath, `eslint.config.${ext}`);
|
|
88
|
+
try {
|
|
89
|
+
let content = await fsp.readFile(eslintConfigPath, "utf8");
|
|
90
|
+
|
|
91
|
+
// Avoid double-injection on re-runs
|
|
92
|
+
if (content.includes("eslint-config-prettier")) return;
|
|
93
|
+
|
|
94
|
+
// Add the import after the last import line
|
|
95
|
+
const lines = content.split("\n");
|
|
96
|
+
let lastImportIdx = -1;
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
if (lines[i].startsWith("import ")) lastImportIdx = i;
|
|
99
|
+
}
|
|
100
|
+
lines.splice(
|
|
101
|
+
lastImportIdx + 1,
|
|
102
|
+
0,
|
|
103
|
+
'import prettier from "eslint-config-prettier";'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
let result = lines.join("\n");
|
|
107
|
+
|
|
108
|
+
// Append ...prettier as the LAST element of the eslint config.
|
|
109
|
+
// The JS template exports an array (`export default [...]`); the TS
|
|
110
|
+
// template uses a function call (`export default tseslint.config(...)`).
|
|
111
|
+
const pushLast = (_, head, tail) =>
|
|
112
|
+
head.replace(/,\s*$/, "") + ",\n ...prettier," + tail;
|
|
113
|
+
if (result.includes("tseslint.config(")) {
|
|
114
|
+
result = result.replace(
|
|
115
|
+
/(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
|
|
116
|
+
pushLast
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
result = result.replace(
|
|
120
|
+
/(export default \[[\s\S]*?)(\n\s*\];)/,
|
|
121
|
+
pushLast
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await fsp.writeFile(eslintConfigPath, result, "utf8");
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
46
129
|
export async function injectConditionals(projectPath, templatesDir, responses, architecture, language) {
|
|
47
130
|
const ext = language === "ts" ? "tsx" : "jsx";
|
|
48
131
|
|
|
@@ -84,14 +167,21 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
|
|
|
84
167
|
);
|
|
85
168
|
}
|
|
86
169
|
|
|
87
|
-
// 4.
|
|
88
|
-
if (responses.axios) {
|
|
170
|
+
// 4. API Client
|
|
171
|
+
if (responses.apiClient === "axios") {
|
|
89
172
|
await injectOverlay(
|
|
90
173
|
projectPath,
|
|
91
174
|
templatesDir,
|
|
92
175
|
`conditional/axios/${architecture}`,
|
|
93
176
|
language
|
|
94
177
|
);
|
|
178
|
+
} else if (responses.apiClient === "fetch") {
|
|
179
|
+
await injectOverlay(
|
|
180
|
+
projectPath,
|
|
181
|
+
templatesDir,
|
|
182
|
+
`conditional/fetch/${architecture}`,
|
|
183
|
+
language
|
|
184
|
+
);
|
|
95
185
|
}
|
|
96
186
|
|
|
97
187
|
// 5. Testing
|
|
@@ -149,6 +239,16 @@ async function injectTesting(projectPath, templatesDir, framework, language, arc
|
|
|
149
239
|
await writeFileRecursive(setupDest, await fsp.readFile(setupSrc, "utf8"));
|
|
150
240
|
} catch {}
|
|
151
241
|
|
|
242
|
+
// Jest keeps its setup at the template root (jest.setup.ts/js), referenced by
|
|
243
|
+
// jest.config as <rootDir>/jest.setup.ts — copy it to the project root.
|
|
244
|
+
if (framework === "jest") {
|
|
245
|
+
const jestSetupSrc = path.join(testingDir, `jest.setup.${configExt}`);
|
|
246
|
+
try {
|
|
247
|
+
await fsp.access(jestSetupSrc);
|
|
248
|
+
await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${configExt}`));
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
|
|
152
252
|
// Copy test file
|
|
153
253
|
const testExt = language === "ts" ? "tsx" : "jsx";
|
|
154
254
|
const testSrc = path.join(testingDir, `src/__tests__/App.test.${testExt}`);
|
package/src/main.js
CHANGED
|
@@ -3,28 +3,30 @@ import chalk from "chalk";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
|
|
6
|
-
import { confirmEmptyFolder } from "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
6
|
+
import { confirmEmptyFolder } from "@/confirm-empty-folder.js";
|
|
7
|
+
import { copyEnvExample } from "@/env.js";
|
|
8
|
+
import { cleanupBoilerplate } from "@/cleanup.js";
|
|
9
|
+
import { configureProject } from "@/configure.js";
|
|
10
|
+
import { setupCssFramework } from "@/css.js";
|
|
11
|
+
import { injectArchitecture, injectConditionals, injectFormatter } from "@/injector.js";
|
|
12
|
+
import { getPkgManager } from "@/utils/pkg-manager.js";
|
|
13
|
+
import { handleDevServer } from "@/utils/dev-server.js";
|
|
14
|
+
import { getUserInputs, onCancel } from "@/prompts.js";
|
|
15
|
+
import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
|
|
16
|
+
import { installAllDeps } from "@/dependencies.js";
|
|
17
|
+
import { generateReadme } from "@/readme.js";
|
|
17
18
|
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = path.dirname(__filename);
|
|
20
21
|
const TEMPLATES_DIR = path.join(__dirname, "../templates");
|
|
21
22
|
const CURRENT_DIR = process.cwd();
|
|
22
23
|
|
|
23
|
-
export async function main() {
|
|
24
|
+
export async function main(options = {}) {
|
|
25
|
+
const { quickSetup = false, projectName: nameArg } = options;
|
|
24
26
|
const pkg = getPkgManager();
|
|
25
27
|
|
|
26
28
|
// 1. Project name
|
|
27
|
-
const cliArg = process.argv[2];
|
|
29
|
+
const cliArg = nameArg || process.argv[2];
|
|
28
30
|
let projectName;
|
|
29
31
|
|
|
30
32
|
if (cliArg && cliArg.trim()) {
|
|
@@ -45,7 +47,7 @@ export async function main() {
|
|
|
45
47
|
await confirmEmptyFolder(projectName);
|
|
46
48
|
|
|
47
49
|
// 3. Collect user preferences
|
|
48
|
-
const responses = await getUserInputs(projectName);
|
|
50
|
+
const responses = await getUserInputs(projectName, { quickSetup });
|
|
49
51
|
|
|
50
52
|
const projectPath = path.resolve(CURRENT_DIR, projectName);
|
|
51
53
|
|
|
@@ -119,6 +121,17 @@ export async function main() {
|
|
|
119
121
|
throw err;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// 8.5 Inject formatter (config file + scripts + eslint-config-prettier wiring)
|
|
125
|
+
const fmtSpin = spinner();
|
|
126
|
+
fmtSpin.start("Configuring code formatter...");
|
|
127
|
+
try {
|
|
128
|
+
await injectFormatter(projectPath, TEMPLATES_DIR, responses);
|
|
129
|
+
fmtSpin.stop(chalk.green("Formatter configured."));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
fmtSpin.stop(chalk.red("Failed to configure formatter."));
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
// 9. Install all additional dependencies in one batch
|
|
123
136
|
const extrasSpin = spinner();
|
|
124
137
|
extrasSpin.start("Installing additional dependencies...");
|
|
@@ -172,6 +185,9 @@ export async function main() {
|
|
|
172
185
|
// 12. Cleanup boilerplate
|
|
173
186
|
await cleanupBoilerplate(projectPath);
|
|
174
187
|
|
|
188
|
+
// 12.5 Copy .env.example into the project
|
|
189
|
+
await copyEnvExample(projectPath, TEMPLATES_DIR);
|
|
190
|
+
|
|
175
191
|
// 13. Generate README.md + LICENSE (if requested)
|
|
176
192
|
if (responses.readme) {
|
|
177
193
|
const readmeSpin = spinner();
|
package/src/prompts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cancel, confirm, isCancel, note, select, text } from "@clack/prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { configExists, loadConfig, saveConfig } from "
|
|
3
|
+
import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
|
|
4
4
|
|
|
5
5
|
export function onCancel() {
|
|
6
6
|
console.log(chalk.gray("\n\nOperation cancelled.\n"));
|
|
@@ -40,17 +40,39 @@ function formatConfig(responses) {
|
|
|
40
40
|
`• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`,
|
|
41
41
|
`• ${chalk.bold("State:")} ${chalk.blue(state)}`,
|
|
42
42
|
`• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
|
|
43
|
-
`• ${chalk.bold("
|
|
43
|
+
`• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
|
|
44
44
|
`• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
|
|
45
|
+
`• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
|
|
45
46
|
`• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
|
|
46
47
|
`• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`,
|
|
47
48
|
].join("\n");
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export async function getUserInputs(projectName) {
|
|
51
|
+
export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
51
52
|
let oldConfig = {};
|
|
52
53
|
let useOldConfig = false;
|
|
53
54
|
|
|
55
|
+
// Non-interactive quick setup: apply defaults without prompting.
|
|
56
|
+
if (quickSetup) {
|
|
57
|
+
const responses = {
|
|
58
|
+
projectName,
|
|
59
|
+
architecture: "feature-based",
|
|
60
|
+
language: "ts",
|
|
61
|
+
cssFramework: "tailwind",
|
|
62
|
+
testing: "vitest",
|
|
63
|
+
router: true,
|
|
64
|
+
stateManagement: "none",
|
|
65
|
+
iconLibrary: "none",
|
|
66
|
+
apiClient: "none",
|
|
67
|
+
linter: "eslint",
|
|
68
|
+
formatter: "prettier",
|
|
69
|
+
gitInit: true,
|
|
70
|
+
readme: true,
|
|
71
|
+
};
|
|
72
|
+
await saveConfig(responses);
|
|
73
|
+
return responses;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
if (await configExists()) {
|
|
55
77
|
oldConfig = await loadConfig();
|
|
56
78
|
oldConfig.projectName = projectName;
|
|
@@ -73,19 +95,25 @@ export async function getUserInputs(projectName) {
|
|
|
73
95
|
useOldConfig = useExisting;
|
|
74
96
|
|
|
75
97
|
if (useOldConfig) {
|
|
98
|
+
if (oldConfig.apiClient === undefined) {
|
|
99
|
+
oldConfig.apiClient = oldConfig.axios ? "axios" : "none";
|
|
100
|
+
}
|
|
101
|
+
if (oldConfig.formatter === undefined) {
|
|
102
|
+
oldConfig.formatter = "none";
|
|
103
|
+
}
|
|
76
104
|
return { ...oldConfig, projectName };
|
|
77
105
|
}
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
// Quick Setup
|
|
81
|
-
const
|
|
109
|
+
const quickSetupChoice = await confirm({
|
|
82
110
|
message:
|
|
83
111
|
"Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)",
|
|
84
112
|
initialValue: true,
|
|
85
113
|
});
|
|
86
|
-
if (isCancel(
|
|
114
|
+
if (isCancel(quickSetupChoice)) onCancel();
|
|
87
115
|
|
|
88
|
-
if (
|
|
116
|
+
if (quickSetupChoice) {
|
|
89
117
|
const responses = {
|
|
90
118
|
projectName,
|
|
91
119
|
architecture: "feature-based",
|
|
@@ -95,9 +123,10 @@ export async function getUserInputs(projectName) {
|
|
|
95
123
|
router: true,
|
|
96
124
|
stateManagement: "none",
|
|
97
125
|
iconLibrary: "none",
|
|
98
|
-
|
|
126
|
+
apiClient: "none",
|
|
99
127
|
linter: "eslint",
|
|
100
|
-
|
|
128
|
+
formatter: "prettier",
|
|
129
|
+
gitInit: true,
|
|
101
130
|
readme: true,
|
|
102
131
|
};
|
|
103
132
|
await saveConfig(responses);
|
|
@@ -183,11 +212,16 @@ export async function getUserInputs(projectName) {
|
|
|
183
212
|
});
|
|
184
213
|
if (isCancel(iconLibrary)) onCancel();
|
|
185
214
|
|
|
186
|
-
const
|
|
187
|
-
message: "
|
|
188
|
-
|
|
215
|
+
const apiClient = await select({
|
|
216
|
+
message: "Which API client do you want to use?",
|
|
217
|
+
options: [
|
|
218
|
+
{ label: "None", value: "none" },
|
|
219
|
+
{ label: "Axios", value: "axios" },
|
|
220
|
+
{ label: "Fetch (native)", value: "fetch" },
|
|
221
|
+
],
|
|
222
|
+
initialValue: "none",
|
|
189
223
|
});
|
|
190
|
-
if (isCancel(
|
|
224
|
+
if (isCancel(apiClient)) onCancel();
|
|
191
225
|
|
|
192
226
|
const gitInit = await confirm({
|
|
193
227
|
message: "Would you like to initialize a Git repository?",
|
|
@@ -206,6 +240,27 @@ export async function getUserInputs(projectName) {
|
|
|
206
240
|
});
|
|
207
241
|
if (isCancel(linter)) onCancel();
|
|
208
242
|
|
|
243
|
+
let formatter = "none";
|
|
244
|
+
if (linter !== "none") {
|
|
245
|
+
const formatterOptions =
|
|
246
|
+
linter === "eslint"
|
|
247
|
+
? [
|
|
248
|
+
{ label: "None", value: "none" },
|
|
249
|
+
{ label: "Prettier", value: "prettier" },
|
|
250
|
+
]
|
|
251
|
+
: [
|
|
252
|
+
{ label: "None", value: "none" },
|
|
253
|
+
{ label: "Oxfmt (Recommended)", value: "oxfmt" },
|
|
254
|
+
{ label: "Prettier", value: "prettier" },
|
|
255
|
+
];
|
|
256
|
+
formatter = await select({
|
|
257
|
+
message: "Which formatter do you want to use?",
|
|
258
|
+
options: formatterOptions,
|
|
259
|
+
initialValue: linter === "eslint" ? "prettier" : "oxfmt",
|
|
260
|
+
});
|
|
261
|
+
if (isCancel(formatter)) onCancel();
|
|
262
|
+
}
|
|
263
|
+
|
|
209
264
|
const readme = await confirm({
|
|
210
265
|
message: "Would you like to generate a README.md and LICENSE?",
|
|
211
266
|
initialValue: true,
|
|
@@ -221,8 +276,9 @@ export async function getUserInputs(projectName) {
|
|
|
221
276
|
router,
|
|
222
277
|
stateManagement,
|
|
223
278
|
iconLibrary,
|
|
224
|
-
|
|
279
|
+
apiClient,
|
|
225
280
|
linter,
|
|
281
|
+
formatter,
|
|
226
282
|
gitInit,
|
|
227
283
|
readme,
|
|
228
284
|
};
|
package/src/readme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileRecursive } from "
|
|
1
|
+
import { writeFileRecursive } from "@/utils/fs.js";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
function buildBuiltWith(responses) {
|
|
@@ -30,8 +30,10 @@ function buildBuiltWith(responses) {
|
|
|
30
30
|
items.push("[Huge Icons](https://hugeicons.com/)");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
if (responses.axios) {
|
|
33
|
+
if (responses.apiClient === "axios") {
|
|
34
34
|
items.push("[Axios](https://axios-http.com/)");
|
|
35
|
+
} else if (responses.apiClient === "fetch") {
|
|
36
|
+
items.push("Fetch API");
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
if (responses.testing === "vitest") {
|
|
@@ -46,6 +48,12 @@ function buildBuiltWith(responses) {
|
|
|
46
48
|
items.push("[Oxlint](https://oxc.rs/)");
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
if (responses.formatter === "prettier") {
|
|
52
|
+
items.push("[Prettier](https://prettier.io/)");
|
|
53
|
+
} else if (responses.formatter === "oxfmt") {
|
|
54
|
+
items.push("[Oxfmt](https://oxc.rs/)");
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
return items.map((item) => `- ${item}`).join("\n");
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -67,6 +75,10 @@ function buildScripts(responses) {
|
|
|
67
75
|
rows.push("| `npm run lint` | Run Oxlint |");
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
if (responses.formatter === "prettier" || responses.formatter === "oxfmt") {
|
|
79
|
+
rows.push("| `npm run format` | Format code |");
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
return rows.join("\n");
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -123,14 +135,26 @@ SOFTWARE.
|
|
|
123
135
|
`;
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
function buildDescription(projectName, responses) {
|
|
139
|
+
const tools = [];
|
|
140
|
+
if (responses.linter === "eslint") tools.push("ESLint");
|
|
141
|
+
else if (responses.linter === "oxlint") tools.push("Oxlint");
|
|
142
|
+
if (responses.formatter === "prettier") tools.push("Prettier");
|
|
143
|
+
else if (responses.formatter === "oxfmt") tools.push("Oxfmt");
|
|
144
|
+
|
|
145
|
+
let suffix = "";
|
|
146
|
+
if (tools.length > 0) {
|
|
147
|
+
suffix = `\n\nThis project is set up with ${tools.join(" + ")} for code quality.`;
|
|
148
|
+
}
|
|
149
|
+
return `# ${projectName}\n\nA React + Vite application built with ${responses.architecture}.${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
function buildReadme(projectName, responses) {
|
|
127
153
|
const builtWith = buildBuiltWith(responses);
|
|
128
154
|
const scripts = buildScripts(responses);
|
|
129
155
|
const structure = buildProjectStructure(responses.architecture);
|
|
130
156
|
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
<!-- TODO: Add a short description of your project -->
|
|
157
|
+
return `${buildDescription(projectName, responses)}
|
|
134
158
|
|
|
135
159
|
## Built With
|
|
136
160
|
|
package/src/scaffold.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { execa } from "execa";
|
|
3
|
-
import { isYarnV1 } from "
|
|
3
|
+
import { isYarnV1 } from "@/utils/yarn-v1.js";
|
|
4
4
|
|
|
5
5
|
export async function runViteCreate(projectPath, projectName, pkg, language) {
|
|
6
6
|
const useYarn = pkg === "yarn";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { API_BASE_URL } from "@/config/constants";
|
|
2
|
+
|
|
3
|
+
const apiClient = async (endpoint, options = {}) => {
|
|
4
|
+
const token = localStorage.getItem("access_token");
|
|
5
|
+
|
|
6
|
+
const headers = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
...options.headers,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (token) {
|
|
12
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
16
|
+
...options,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response.json();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default apiClient;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { API_BASE_URL } from "@/config/constants";
|
|
2
|
+
|
|
3
|
+
const apiClient = async (endpoint: string, options: RequestInit = {}) => {
|
|
4
|
+
const token = localStorage.getItem("access_token");
|
|
5
|
+
|
|
6
|
+
const headers: HeadersInit = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
...options.headers,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (token) {
|
|
12
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
16
|
+
...options,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response.json();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default apiClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as apiClient } from "./api";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as apiClient } from "./api";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { API_BASE_URL } from "@/config/constants";
|
|
2
|
+
|
|
3
|
+
const apiClient = async (endpoint, options = {}) => {
|
|
4
|
+
const token = localStorage.getItem("access_token");
|
|
5
|
+
|
|
6
|
+
const headers = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
...options.headers,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (token) {
|
|
12
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
16
|
+
...options,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response.json();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default apiClient;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { API_BASE_URL } from "@/config/constants";
|
|
2
|
+
|
|
3
|
+
const apiClient = async (endpoint: string, options: RequestInit = {}) => {
|
|
4
|
+
const token = localStorage.getItem("access_token");
|
|
5
|
+
|
|
6
|
+
const headers: HeadersInit = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
...options.headers,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (token) {
|
|
12
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
16
|
+
...options,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response.json();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default apiClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as apiClient } from "./api";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as apiClient } from "./api";
|