create-satset-react 0.0.1-beta.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/.gitkeep +0 -0
- package/README.md +5 -0
- package/dist/index.js +538 -0
- package/package.json +21 -0
- package/src/assets/favicon.ts +69 -0
- package/src/assets/index.ts +5 -0
- package/src/assets/metadata.ts +77 -0
- package/src/assets/robots.ts +95 -0
- package/src/assets/sitemap.ts +93 -0
- package/src/create-app.ts +430 -0
- package/src/index.ts +18 -0
- package/tsconfig.json +23 -0
package/.gitkeep
ADDED
|
File without changes
|
package/README.md
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/create-app.ts
|
|
27
|
+
var import_fs3 = __toESM(require("fs"));
|
|
28
|
+
var import_path3 = __toESM(require("path"));
|
|
29
|
+
var import_child_process = require("child_process");
|
|
30
|
+
var import_readline = __toESM(require("readline"));
|
|
31
|
+
|
|
32
|
+
// src/assets/favicon.ts
|
|
33
|
+
var import_fs = __toESM(require("fs"));
|
|
34
|
+
var import_path = __toESM(require("path"));
|
|
35
|
+
var import_https = __toESM(require("https"));
|
|
36
|
+
async function generateFavicon(root) {
|
|
37
|
+
const publicPath = import_path.default.join(root, "public");
|
|
38
|
+
const faviconPath = import_path.default.join(publicPath, "favicon.png");
|
|
39
|
+
if (import_fs.default.existsSync(faviconPath)) {
|
|
40
|
+
console.log("\u2705 Favicon already exists, skipping...");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log("\u{1F3A8} Generating favicon...");
|
|
44
|
+
const faviconUrl = "https://raw.githubusercontent.com/IndokuDev/IndokuDev-all-Logo/refs/heads/main/satset.png";
|
|
45
|
+
try {
|
|
46
|
+
await downloadImage(faviconUrl, faviconPath);
|
|
47
|
+
console.log(`\u2705 Favicon generated`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("\u274C Failed to generate favicon:", error);
|
|
50
|
+
createDefaultFavicon(faviconPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function downloadImage(url, dest) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const file = import_fs.default.createWriteStream(dest);
|
|
56
|
+
import_https.default.get(url, (response) => {
|
|
57
|
+
if (response.statusCode !== 200) {
|
|
58
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
response.pipe(file);
|
|
62
|
+
file.on("finish", () => {
|
|
63
|
+
file.close();
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
}).on("error", (err) => {
|
|
67
|
+
import_fs.default.unlink(dest, () => {
|
|
68
|
+
});
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function createDefaultFavicon(dest) {
|
|
74
|
+
const svg = `
|
|
75
|
+
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
76
|
+
<defs>
|
|
77
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
78
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
79
|
+
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
80
|
+
</linearGradient>
|
|
81
|
+
</defs>
|
|
82
|
+
<rect width="256" height="256" fill="url(#grad)" rx="32"/>
|
|
83
|
+
<text x="128" y="180" font-family="Arial, sans-serif" font-size="140" font-weight="bold" fill="white" text-anchor="middle">S</text>
|
|
84
|
+
</svg>
|
|
85
|
+
`.trim();
|
|
86
|
+
import_fs.default.writeFileSync(dest.replace(".png", ".svg"), svg);
|
|
87
|
+
console.log("\u2705 Default SVG favicon created");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/assets/robots.ts
|
|
91
|
+
var import_fs2 = __toESM(require("fs"));
|
|
92
|
+
var import_path2 = __toESM(require("path"));
|
|
93
|
+
function generateRobotsTxt(options = {}) {
|
|
94
|
+
const {
|
|
95
|
+
allow = ["/"],
|
|
96
|
+
disallow = [],
|
|
97
|
+
sitemap,
|
|
98
|
+
crawlDelay,
|
|
99
|
+
userAgent = "*"
|
|
100
|
+
} = options;
|
|
101
|
+
let content = `User-agent: ${userAgent}
|
|
102
|
+
`;
|
|
103
|
+
allow.forEach((path4) => {
|
|
104
|
+
content += `Allow: ${path4}
|
|
105
|
+
`;
|
|
106
|
+
});
|
|
107
|
+
disallow.forEach((path4) => {
|
|
108
|
+
content += `Disallow: ${path4}
|
|
109
|
+
`;
|
|
110
|
+
});
|
|
111
|
+
if (crawlDelay) {
|
|
112
|
+
content += `Crawl-delay: ${crawlDelay}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
if (sitemap) {
|
|
116
|
+
content += `
|
|
117
|
+
Sitemap: ${sitemap}
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
return content;
|
|
121
|
+
}
|
|
122
|
+
function saveRobotsTxt(root, robots) {
|
|
123
|
+
const publicPath = import_path2.default.join(root, "public");
|
|
124
|
+
const robotsPath = import_path2.default.join(publicPath, "robots.txt");
|
|
125
|
+
import_fs2.default.mkdirSync(publicPath, { recursive: true });
|
|
126
|
+
import_fs2.default.writeFileSync(robotsPath, robots);
|
|
127
|
+
console.log("\u2705 Robots.txt generated: public/robots.txt");
|
|
128
|
+
}
|
|
129
|
+
async function generateAndSaveRobots(root, baseUrl, options = {}) {
|
|
130
|
+
const publicPath = import_path2.default.join(root, "public");
|
|
131
|
+
const robotsPath = import_path2.default.join(publicPath, "robots.txt");
|
|
132
|
+
if (import_fs2.default.existsSync(robotsPath)) {
|
|
133
|
+
console.log("\u2139\uFE0F Robots.txt exists \u2014 overwriting with generated content...");
|
|
134
|
+
}
|
|
135
|
+
let url = baseUrl || "http://localhost:3000";
|
|
136
|
+
const pkgPath = import_path2.default.join(root, "package.json");
|
|
137
|
+
if (import_fs2.default.existsSync(pkgPath)) {
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(import_fs2.default.readFileSync(pkgPath, "utf-8"));
|
|
140
|
+
if (pkg.homepage) {
|
|
141
|
+
url = pkg.homepage;
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (process.env.SATSET_PUBLIC_URL) {
|
|
147
|
+
url = process.env.SATSET_PUBLIC_URL;
|
|
148
|
+
}
|
|
149
|
+
const robots = generateRobotsTxt({
|
|
150
|
+
...options,
|
|
151
|
+
sitemap: `${url}/sitemap.xml`
|
|
152
|
+
});
|
|
153
|
+
saveRobotsTxt(root, robots);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/create-app.ts
|
|
157
|
+
async function createApp(projectName) {
|
|
158
|
+
console.log("\u{1F680} Create Satset App\n");
|
|
159
|
+
const config = {
|
|
160
|
+
name: projectName,
|
|
161
|
+
typescript: true,
|
|
162
|
+
packageManager: "npm",
|
|
163
|
+
template: "default",
|
|
164
|
+
git: true
|
|
165
|
+
};
|
|
166
|
+
const useTypeScript = await prompt("Would you like to use TypeScript?", "Y/n");
|
|
167
|
+
config.typescript = useTypeScript.toLowerCase() !== "n";
|
|
168
|
+
const template = await prompt("Which template would you like to use?", "1) Default 2) Minimal 3) Fullstack");
|
|
169
|
+
if (template === "2") config.template = "minimal";
|
|
170
|
+
else if (template === "3") config.template = "fullstack";
|
|
171
|
+
const pkgMgr = await prompt("Which package manager?", "1) npm 2) yarn 3) pnpm");
|
|
172
|
+
if (pkgMgr === "2") config.packageManager = "yarn";
|
|
173
|
+
else if (pkgMgr === "3") config.packageManager = "pnpm";
|
|
174
|
+
const useGit = await prompt("Initialize a git repository?", "Y/n");
|
|
175
|
+
config.git = useGit.toLowerCase() !== "n";
|
|
176
|
+
console.log("\n\u{1F4E6} Creating project...\n");
|
|
177
|
+
const projectPath = import_path3.default.join(process.cwd(), projectName);
|
|
178
|
+
if (import_fs3.default.existsSync(projectPath)) {
|
|
179
|
+
console.error(`\u274C Directory ${projectName} already exists`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
await createProjectStructure(projectPath, config);
|
|
183
|
+
if (config.git) {
|
|
184
|
+
try {
|
|
185
|
+
(0, import_child_process.execSync)("git init", { cwd: projectPath, stdio: "ignore" });
|
|
186
|
+
console.log("\u2705 Git initialized");
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.log("\u26A0\uFE0F Git initialization failed (optional)");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
console.log("\n\u{1F4E6} Installing dependencies...\n");
|
|
192
|
+
try {
|
|
193
|
+
const installCmd = {
|
|
194
|
+
npm: "npm install",
|
|
195
|
+
yarn: "yarn",
|
|
196
|
+
pnpm: "pnpm install"
|
|
197
|
+
}[config.packageManager];
|
|
198
|
+
(0, import_child_process.execSync)(installCmd, { cwd: projectPath, stdio: "inherit" });
|
|
199
|
+
console.log("\n\u2705 Dependencies installed");
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.log("\n\u26A0\uFE0F Failed to install dependencies. Please run manually.");
|
|
202
|
+
}
|
|
203
|
+
console.log("\n\u2705 Project created successfully!\n");
|
|
204
|
+
console.log("Next steps:");
|
|
205
|
+
console.log(` cd ${projectName}`);
|
|
206
|
+
console.log(` ${config.packageManager} run dev`);
|
|
207
|
+
console.log("");
|
|
208
|
+
}
|
|
209
|
+
function prompt(question, hint) {
|
|
210
|
+
const rl = import_readline.default.createInterface({
|
|
211
|
+
input: process.stdin,
|
|
212
|
+
output: process.stdout
|
|
213
|
+
});
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
const promptText = hint ? `${question} (${hint}): ` : `${question}: `;
|
|
216
|
+
rl.question(promptText, (answer) => {
|
|
217
|
+
rl.close();
|
|
218
|
+
resolve(answer.trim() || "");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async function createProjectStructure(projectPath, config) {
|
|
223
|
+
const ext = config.typescript ? "tsx" : "jsx";
|
|
224
|
+
const configExt = config.typescript ? "ts" : "js";
|
|
225
|
+
const dirs = [
|
|
226
|
+
"src/app",
|
|
227
|
+
"src/app/api",
|
|
228
|
+
"src/components",
|
|
229
|
+
"src/styles",
|
|
230
|
+
"public"
|
|
231
|
+
];
|
|
232
|
+
if (config.template === "fullstack") {
|
|
233
|
+
dirs.push("src/lib", "src/utils");
|
|
234
|
+
}
|
|
235
|
+
dirs.forEach((dir) => {
|
|
236
|
+
import_fs3.default.mkdirSync(import_path3.default.join(projectPath, dir), { recursive: true });
|
|
237
|
+
});
|
|
238
|
+
createPackageJson(projectPath, config);
|
|
239
|
+
createSatsetConfig(projectPath, configExt);
|
|
240
|
+
if (config.typescript) {
|
|
241
|
+
createTsConfig(projectPath);
|
|
242
|
+
}
|
|
243
|
+
createLayoutFile(projectPath, ext, config);
|
|
244
|
+
createPageFile(projectPath, ext, config);
|
|
245
|
+
createApiRoute(projectPath, configExt);
|
|
246
|
+
createGlobalCSS(projectPath);
|
|
247
|
+
createGitignore(projectPath);
|
|
248
|
+
createReadme(projectPath, config);
|
|
249
|
+
createEnvExample(projectPath);
|
|
250
|
+
console.log("\n\u{1F3A8} Generating assets...");
|
|
251
|
+
try {
|
|
252
|
+
await generateFavicon(projectPath);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.log("\u26A0\uFE0F Favicon generation skipped");
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await generateAndSaveRobots(projectPath);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.log("\u26A0\uFE0F Robots.txt generation skipped");
|
|
260
|
+
}
|
|
261
|
+
console.log("\u2705 Assets generated");
|
|
262
|
+
}
|
|
263
|
+
function createPackageJson(projectPath, config) {
|
|
264
|
+
const packageJson = {
|
|
265
|
+
name: config.name,
|
|
266
|
+
version: "0.1.0",
|
|
267
|
+
private: true,
|
|
268
|
+
scripts: {
|
|
269
|
+
dev: "satset dev",
|
|
270
|
+
build: "satset build",
|
|
271
|
+
start: "satset start",
|
|
272
|
+
lint: config.typescript ? "tsc --noEmit" : 'echo "No linting configured"'
|
|
273
|
+
},
|
|
274
|
+
dependencies: {
|
|
275
|
+
"@satset/core": "^0.0.1",
|
|
276
|
+
react: "^18.2.0",
|
|
277
|
+
"react-dom": "^18.2.0"
|
|
278
|
+
},
|
|
279
|
+
devDependencies: config.typescript ? {
|
|
280
|
+
"@types/react": "^18.2.0",
|
|
281
|
+
"@types/react-dom": "^18.2.0",
|
|
282
|
+
"@types/node": "^20.0.0",
|
|
283
|
+
typescript: "^5.3.0"
|
|
284
|
+
} : {}
|
|
285
|
+
};
|
|
286
|
+
import_fs3.default.writeFileSync(
|
|
287
|
+
import_path3.default.join(projectPath, "package.json"),
|
|
288
|
+
JSON.stringify(packageJson, null, 2)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
function createSatsetConfig(projectPath, ext) {
|
|
292
|
+
const config = `module.exports = {
|
|
293
|
+
port: 3000,
|
|
294
|
+
host: 'localhost',
|
|
295
|
+
favicon: '/favicon.png',
|
|
296
|
+
};
|
|
297
|
+
`;
|
|
298
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, `satset.config.${ext}`), config);
|
|
299
|
+
}
|
|
300
|
+
function createTsConfig(projectPath) {
|
|
301
|
+
const tsConfig = {
|
|
302
|
+
compilerOptions: {
|
|
303
|
+
target: "ES2020",
|
|
304
|
+
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
305
|
+
jsx: "react-jsx",
|
|
306
|
+
module: "ESNext",
|
|
307
|
+
moduleResolution: "bundler",
|
|
308
|
+
resolveJsonModule: true,
|
|
309
|
+
allowJs: true,
|
|
310
|
+
strict: true,
|
|
311
|
+
esModuleInterop: true,
|
|
312
|
+
skipLibCheck: true,
|
|
313
|
+
forceConsistentCasingInFileNames: true,
|
|
314
|
+
paths: {
|
|
315
|
+
"@/*": ["./src/*"]
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
include: ["src"],
|
|
319
|
+
exclude: ["node_modules", "dist", ".satset"]
|
|
320
|
+
};
|
|
321
|
+
import_fs3.default.writeFileSync(
|
|
322
|
+
import_path3.default.join(projectPath, "tsconfig.json"),
|
|
323
|
+
JSON.stringify(tsConfig, null, 2)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
function createLayoutFile(projectPath, ext, config) {
|
|
327
|
+
const layout = `import React from 'react';
|
|
328
|
+
import './globals.css';
|
|
329
|
+
|
|
330
|
+
export default function RootLayout({ children }${config.typescript ? ": { children: React.ReactNode }" : ""}) {
|
|
331
|
+
return (
|
|
332
|
+
<html lang="en">
|
|
333
|
+
<head>
|
|
334
|
+
<meta charSet="UTF-8" />
|
|
335
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
336
|
+
<title>${config.name}</title>
|
|
337
|
+
</head>
|
|
338
|
+
<body>
|
|
339
|
+
{children}
|
|
340
|
+
</body>
|
|
341
|
+
</html>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, `src/app/layout.${ext}`), layout);
|
|
346
|
+
}
|
|
347
|
+
function createPageFile(projectPath, ext, config) {
|
|
348
|
+
const templates = {
|
|
349
|
+
default: `import React from 'react';
|
|
350
|
+
import { Link } from '@satset/core';
|
|
351
|
+
|
|
352
|
+
export const metadata = { title: "Welcome to ${config.name}", description: "Built with Satset.js - The fastest React framework" };
|
|
353
|
+
|
|
354
|
+
export default function HomePage() {
|
|
355
|
+
return (
|
|
356
|
+
<>
|
|
357
|
+
<div style={{ padding: '3rem', fontFamily: 'system-ui', maxWidth: '900px', margin: '0 auto' }}>
|
|
358
|
+
<h1 style={{ fontSize: '3rem', marginBottom: '1rem', background: 'linear-gradient(to right, #3b82f6, #8b5cf6)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
|
|
359
|
+
Welcome to Satset.js! \u{1F680}
|
|
360
|
+
</h1>
|
|
361
|
+
<p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '2rem' }}>
|
|
362
|
+
The fastest way to build fullstack React apps.
|
|
363
|
+
</p>
|
|
364
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
365
|
+
<Link href="/about" style={{ padding: '0.75rem 1.5rem', background: '#3b82f6', color: 'white', borderRadius: '8px', textDecoration: 'none' }}>
|
|
366
|
+
Get Started
|
|
367
|
+
</Link>
|
|
368
|
+
<a href="https://github.com/satset/satset" style={{ padding: '0.75rem 1.5rem', border: '1px solid #e2e8f0', borderRadius: '8px', textDecoration: 'none', color: '#64748b' }}>
|
|
369
|
+
GitHub
|
|
370
|
+
</a>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
`,
|
|
377
|
+
minimal: `import React from 'react';
|
|
378
|
+
|
|
379
|
+
export default function HomePage() {
|
|
380
|
+
return (
|
|
381
|
+
<div>
|
|
382
|
+
<h1>Hello Satset!</h1>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
`,
|
|
387
|
+
fullstack: `import React from 'react';
|
|
388
|
+
export const metadata = { title: "${config.name}" };
|
|
389
|
+
|
|
390
|
+
export default function HomePage() {
|
|
391
|
+
const [data, setData] = React.useState${config.typescript ? "<any>" : ""}(null);
|
|
392
|
+
|
|
393
|
+
React.useEffect(() => {
|
|
394
|
+
fetch('/api/hello')
|
|
395
|
+
.then(res => res.json())
|
|
396
|
+
.then(setData);
|
|
397
|
+
}, []);
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
<div style={{ padding: '2rem' }}>
|
|
402
|
+
<h1>Fullstack Satset App</h1>
|
|
403
|
+
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
|
404
|
+
</div>
|
|
405
|
+
</>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
`
|
|
409
|
+
};
|
|
410
|
+
import_fs3.default.writeFileSync(
|
|
411
|
+
import_path3.default.join(projectPath, `src/app/page.${ext}`),
|
|
412
|
+
templates[config.template]
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function createApiRoute(projectPath, ext) {
|
|
416
|
+
const apiRoute = `import { SatsetResponse } from '@satset/core';
|
|
417
|
+
|
|
418
|
+
export async function GET() {
|
|
419
|
+
return SatsetResponse.json({
|
|
420
|
+
message: 'Hello from Satset API!',
|
|
421
|
+
timestamp: new Date().toISOString(),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
`;
|
|
425
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, `src/app/api/hello.${ext}`), apiRoute);
|
|
426
|
+
}
|
|
427
|
+
function createGlobalCSS(projectPath) {
|
|
428
|
+
const css = `* {
|
|
429
|
+
margin: 0;
|
|
430
|
+
padding: 0;
|
|
431
|
+
box-sizing: border-box;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
body {
|
|
435
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
436
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
437
|
+
sans-serif;
|
|
438
|
+
-webkit-font-smoothing: antialiased;
|
|
439
|
+
-moz-osx-font-smoothing: grayscale;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
code {
|
|
443
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
444
|
+
monospace;
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, "src/app/globals.css"), css);
|
|
448
|
+
}
|
|
449
|
+
function createGitignore(projectPath) {
|
|
450
|
+
const gitignore = `# dependencies
|
|
451
|
+
node_modules
|
|
452
|
+
.pnp
|
|
453
|
+
.pnp.js
|
|
454
|
+
|
|
455
|
+
# testing
|
|
456
|
+
coverage
|
|
457
|
+
|
|
458
|
+
# production
|
|
459
|
+
dist
|
|
460
|
+
.satset
|
|
461
|
+
|
|
462
|
+
# misc
|
|
463
|
+
.DS_Store
|
|
464
|
+
*.pem
|
|
465
|
+
|
|
466
|
+
# debug
|
|
467
|
+
npm-debug.log*
|
|
468
|
+
yarn-debug.log*
|
|
469
|
+
yarn-error.log*
|
|
470
|
+
|
|
471
|
+
# local env files
|
|
472
|
+
.env*.local
|
|
473
|
+
.env
|
|
474
|
+
|
|
475
|
+
# vercel
|
|
476
|
+
.vercel
|
|
477
|
+
|
|
478
|
+
# typescript
|
|
479
|
+
*.tsbuildinfo
|
|
480
|
+
next-env.d.ts
|
|
481
|
+
`;
|
|
482
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, ".gitignore"), gitignore);
|
|
483
|
+
}
|
|
484
|
+
function createEnvExample(projectPath) {
|
|
485
|
+
const envExample = `# Satset Environment Variables
|
|
486
|
+
# Copy this file to .env.local and fill in your values
|
|
487
|
+
|
|
488
|
+
# SATSET_PUBLIC_URL=https://yoursite.com
|
|
489
|
+
# SATSET_PORT=3000
|
|
490
|
+
# SATSET_HOST=localhost
|
|
491
|
+
`;
|
|
492
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, ".env.example"), envExample);
|
|
493
|
+
}
|
|
494
|
+
function createReadme(projectPath, config) {
|
|
495
|
+
const readme = `# ${config.name}
|
|
496
|
+
|
|
497
|
+
This is a [Satset.js](https://satset.dev) project created with \`create-satset-app\`.
|
|
498
|
+
|
|
499
|
+
## Getting Started
|
|
500
|
+
|
|
501
|
+
First, run the development server:
|
|
502
|
+
|
|
503
|
+
\`\`\`bash
|
|
504
|
+
${config.packageManager} run dev
|
|
505
|
+
\`\`\`
|
|
506
|
+
|
|
507
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
508
|
+
|
|
509
|
+
## Learn More
|
|
510
|
+
|
|
511
|
+
To learn more about Satset.js, check out the following resources:
|
|
512
|
+
|
|
513
|
+
- [Satset.js Documentation](https://satset.dev/docs)
|
|
514
|
+
- [Learn Satset.js](https://satset.dev/learn)
|
|
515
|
+
- [GitHub Repository](https://github.com/satset/satset)
|
|
516
|
+
|
|
517
|
+
## Deploy
|
|
518
|
+
|
|
519
|
+
Deploy your Satset.js app to Vercel, Netlify, or any Node.js hosting platform.
|
|
520
|
+
|
|
521
|
+
Check out the [deployment documentation](https://satset.dev/docs/deployment) for more details.
|
|
522
|
+
`;
|
|
523
|
+
import_fs3.default.writeFileSync(import_path3.default.join(projectPath, "README.md"), readme);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/index.ts
|
|
527
|
+
var args = process.argv.slice(2);
|
|
528
|
+
var command = args[0];
|
|
529
|
+
async function main() {
|
|
530
|
+
if (command === "create-satset-react" || !command) {
|
|
531
|
+
await createApp(command);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
main().catch((error) => {
|
|
536
|
+
console.error("\u274C Error:", error.message);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-satset-react",
|
|
3
|
+
"version": "0.0.1-beta.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "CLI tool to create a new Satset React project",
|
|
6
|
+
"author": "IndokuDev",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-satset-react": "bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs --out-dir dist",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"create-satset-react": "workspace:*"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsup": "^8.5.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
|
|
5
|
+
export async function generateFavicon(root: string): Promise<void> {
|
|
6
|
+
const publicPath = path.join(root, 'public');
|
|
7
|
+
const faviconPath = path.join(publicPath, 'favicon.png');
|
|
8
|
+
|
|
9
|
+
// Skip if favicon already exists
|
|
10
|
+
if (fs.existsSync(faviconPath)) {
|
|
11
|
+
console.log('✅ Favicon already exists, skipping...');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('🎨 Generating favicon...');
|
|
16
|
+
|
|
17
|
+
// Get username from package.json or use default
|
|
18
|
+
const faviconUrl = "https://raw.githubusercontent.com/IndokuDev/IndokuDev-all-Logo/refs/heads/main/satset.png"
|
|
19
|
+
try {
|
|
20
|
+
await downloadImage(faviconUrl, faviconPath);
|
|
21
|
+
console.log(`✅ Favicon generated`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('❌ Failed to generate favicon:', error);
|
|
24
|
+
// Create default SVG favicon
|
|
25
|
+
createDefaultFavicon(faviconPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function downloadImage(url: string, dest: string): Promise<void> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const file = fs.createWriteStream(dest);
|
|
32
|
+
|
|
33
|
+
https.get(url, (response) => {
|
|
34
|
+
if (response.statusCode !== 200) {
|
|
35
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
response.pipe(file);
|
|
40
|
+
|
|
41
|
+
file.on('finish', () => {
|
|
42
|
+
file.close();
|
|
43
|
+
resolve();
|
|
44
|
+
});
|
|
45
|
+
}).on('error', (err) => {
|
|
46
|
+
fs.unlink(dest, () => {});
|
|
47
|
+
reject(err);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createDefaultFavicon(dest: string) {
|
|
53
|
+
// Create a simple SVG favicon
|
|
54
|
+
const svg = `
|
|
55
|
+
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
|
56
|
+
<defs>
|
|
57
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
58
|
+
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
|
|
59
|
+
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
|
60
|
+
</linearGradient>
|
|
61
|
+
</defs>
|
|
62
|
+
<rect width="256" height="256" fill="url(#grad)" rx="32"/>
|
|
63
|
+
<text x="128" y="180" font-family="Arial, sans-serif" font-size="140" font-weight="bold" fill="white" text-anchor="middle">S</text>
|
|
64
|
+
</svg>
|
|
65
|
+
`.trim();
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(dest.replace('.png', '.svg'), svg);
|
|
68
|
+
console.log('✅ Default SVG favicon created');
|
|
69
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { generateFavicon } from './favicon';
|
|
2
|
+
export { generateSitemap, saveSitemap, generateAndSaveSitemap } from './sitemap';
|
|
3
|
+
export type { SitemapOptions } from './sitemap';
|
|
4
|
+
export { generateRobotsTxt, saveRobotsTxt, generateAndSaveRobots } from './robots';
|
|
5
|
+
export type { RobotsOptions } from './robots';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface OpenGraph {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
image?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TwitterCard {
|
|
10
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
11
|
+
site?: string;
|
|
12
|
+
creator?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Metadata {
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
keywords?: string;
|
|
19
|
+
canonical?: string;
|
|
20
|
+
openGraph?: OpenGraph;
|
|
21
|
+
twitter?: TwitterCard;
|
|
22
|
+
robots?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderMetaTags(meta: Metadata | null | undefined): string {
|
|
26
|
+
if (!meta) return '';
|
|
27
|
+
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
|
|
30
|
+
if (meta.title) {
|
|
31
|
+
parts.push(`<title>${escapeHtml(meta.title)}</title>`);
|
|
32
|
+
parts.push(`<meta name="title" content="${escapeHtml(meta.title)}" />`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (meta.description) {
|
|
36
|
+
parts.push(`<meta name="description" content="${escapeHtml(meta.description)}" />`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (meta.keywords) {
|
|
40
|
+
parts.push(`<meta name="keywords" content="${escapeHtml(meta.keywords)}" />`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (meta.canonical) {
|
|
44
|
+
parts.push(`<link rel="canonical" href="${escapeHtml(meta.canonical)}" />`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (meta.robots) {
|
|
48
|
+
parts.push(`<meta name="robots" content="${escapeHtml(meta.robots)}" />`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (meta.openGraph) {
|
|
52
|
+
const og = meta.openGraph;
|
|
53
|
+
if (og.title) parts.push(`<meta property="og:title" content="${escapeHtml(og.title)}" />`);
|
|
54
|
+
if (og.description) parts.push(`<meta property="og:description" content="${escapeHtml(og.description)}" />`);
|
|
55
|
+
if (og.url) parts.push(`<meta property="og:url" content="${escapeHtml(og.url)}" />`);
|
|
56
|
+
if (og.image) parts.push(`<meta property="og:image" content="${escapeHtml(og.image)}" />`);
|
|
57
|
+
parts.push(`<meta property="og:type" content="${escapeHtml(og.type || 'website')}" />`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (meta.twitter) {
|
|
61
|
+
const t = meta.twitter;
|
|
62
|
+
if (t.card) parts.push(`<meta name="twitter:card" content="${escapeHtml(t.card)}" />`);
|
|
63
|
+
if (t.site) parts.push(`<meta name="twitter:site" content="${escapeHtml(t.site)}" />`);
|
|
64
|
+
if (t.creator) parts.push(`<meta name="twitter:creator" content="${escapeHtml(t.creator)}" />`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return parts.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function escapeHtml(s: string) {
|
|
71
|
+
return s
|
|
72
|
+
.replace(/&/g, '&')
|
|
73
|
+
.replace(/</g, '<')
|
|
74
|
+
.replace(/>/g, '>')
|
|
75
|
+
.replace(/"/g, '"')
|
|
76
|
+
.replace(/'/g, ''');
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface RobotsOptions {
|
|
5
|
+
allow?: string[];
|
|
6
|
+
disallow?: string[];
|
|
7
|
+
sitemap?: string;
|
|
8
|
+
crawlDelay?: number;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function generateRobotsTxt(options: RobotsOptions = {}): string {
|
|
13
|
+
const {
|
|
14
|
+
allow = ['/'],
|
|
15
|
+
disallow = [],
|
|
16
|
+
sitemap,
|
|
17
|
+
crawlDelay,
|
|
18
|
+
userAgent = '*',
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
let content = `User-agent: ${userAgent}\n`;
|
|
22
|
+
|
|
23
|
+
// Add allowed paths
|
|
24
|
+
allow.forEach(path => {
|
|
25
|
+
content += `Allow: ${path}\n`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Add disallowed paths
|
|
29
|
+
disallow.forEach(path => {
|
|
30
|
+
content += `Disallow: ${path}\n`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Add crawl delay
|
|
34
|
+
if (crawlDelay) {
|
|
35
|
+
content += `Crawl-delay: ${crawlDelay}\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add sitemap
|
|
39
|
+
if (sitemap) {
|
|
40
|
+
content += `\nSitemap: ${sitemap}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function saveRobotsTxt(root: string, robots: string): void {
|
|
47
|
+
const publicPath = path.join(root, 'public');
|
|
48
|
+
const robotsPath = path.join(publicPath, 'robots.txt');
|
|
49
|
+
|
|
50
|
+
fs.mkdirSync(publicPath, { recursive: true });
|
|
51
|
+
fs.writeFileSync(robotsPath, robots);
|
|
52
|
+
|
|
53
|
+
console.log('✅ Robots.txt generated: public/robots.txt');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function generateAndSaveRobots(
|
|
57
|
+
root: string,
|
|
58
|
+
baseUrl?: string,
|
|
59
|
+
options: RobotsOptions = {}
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const publicPath = path.join(root, 'public');
|
|
62
|
+
const robotsPath = path.join(publicPath, 'robots.txt');
|
|
63
|
+
|
|
64
|
+
// Always generate (and overwrite) robots.txt. In both dev and build we should write
|
|
65
|
+
// the current generated file so changes to routes or config are reflected immediately.
|
|
66
|
+
if (fs.existsSync(robotsPath)) {
|
|
67
|
+
console.log('ℹ️ Robots.txt exists — overwriting with generated content...');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get base URL
|
|
71
|
+
let url = baseUrl || 'http://localhost:3000';
|
|
72
|
+
|
|
73
|
+
const pkgPath = path.join(root, 'package.json');
|
|
74
|
+
if (fs.existsSync(pkgPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
77
|
+
if (pkg.homepage) {
|
|
78
|
+
url = pkg.homepage;
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.env.SATSET_PUBLIC_URL) {
|
|
86
|
+
url = process.env.SATSET_PUBLIC_URL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const robots = generateRobotsTxt({
|
|
90
|
+
...options,
|
|
91
|
+
sitemap: `${url}/sitemap.xml`,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
saveRobotsTxt(root, robots);
|
|
95
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Route } from '../router/file-system';
|
|
4
|
+
|
|
5
|
+
export interface SitemapOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
routes: Route[];
|
|
8
|
+
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
9
|
+
priority?: number;
|
|
10
|
+
lastmod?: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function generateSitemap(options: SitemapOptions): string {
|
|
14
|
+
const {
|
|
15
|
+
baseUrl,
|
|
16
|
+
routes,
|
|
17
|
+
changefreq = 'weekly',
|
|
18
|
+
priority = 0.7,
|
|
19
|
+
lastmod = new Date(),
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
const urls = routes
|
|
23
|
+
.filter(route => !route.dynamic) // Skip dynamic routes
|
|
24
|
+
.map(route => {
|
|
25
|
+
const loc = `${baseUrl}${route.path}`;
|
|
26
|
+
const lastmodStr = lastmod.toISOString().split('T')[0];
|
|
27
|
+
|
|
28
|
+
return ` <url>
|
|
29
|
+
<loc>${escapeXml(loc)}</loc>
|
|
30
|
+
<lastmod>${lastmodStr}</lastmod>
|
|
31
|
+
<changefreq>${changefreq}</changefreq>
|
|
32
|
+
<priority>${priority}</priority>
|
|
33
|
+
</url>`;
|
|
34
|
+
})
|
|
35
|
+
.join('\n');
|
|
36
|
+
|
|
37
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
38
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
39
|
+
${urls}
|
|
40
|
+
</urlset>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function saveSitemap(root: string, sitemap: string): void {
|
|
44
|
+
const publicPath = path.join(root, 'public');
|
|
45
|
+
const sitemapPath = path.join(publicPath, 'sitemap.xml');
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(publicPath, { recursive: true });
|
|
48
|
+
fs.writeFileSync(sitemapPath, sitemap);
|
|
49
|
+
|
|
50
|
+
console.log('✅ Sitemap generated: public/sitemap.xml');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function generateAndSaveSitemap(
|
|
54
|
+
root: string,
|
|
55
|
+
routes: Route[],
|
|
56
|
+
baseUrl?: string
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
// Get base URL from package.json or env
|
|
59
|
+
let url = baseUrl || 'http://localhost:3000';
|
|
60
|
+
|
|
61
|
+
const pkgPath = path.join(root, 'package.json');
|
|
62
|
+
if (fs.existsSync(pkgPath)) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
65
|
+
if (pkg.homepage) {
|
|
66
|
+
url = pkg.homepage;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for env var
|
|
74
|
+
if (process.env.SATSET_PUBLIC_URL) {
|
|
75
|
+
url = process.env.SATSET_PUBLIC_URL;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sitemap = generateSitemap({
|
|
79
|
+
baseUrl: url,
|
|
80
|
+
routes,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
saveSitemap(root, sitemap);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function escapeXml(text: string): string {
|
|
87
|
+
return text
|
|
88
|
+
.replace(/&/g, '&')
|
|
89
|
+
.replace(/</g, '<')
|
|
90
|
+
.replace(/>/g, '>')
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/'/g, ''');
|
|
93
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { generateFavicon, generateAndSaveRobots } from './assets';
|
|
7
|
+
|
|
8
|
+
interface ProjectConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
typescript: boolean;
|
|
11
|
+
packageManager: 'npm' | 'yarn' | 'pnpm';
|
|
12
|
+
template: 'default' | 'minimal' | 'fullstack';
|
|
13
|
+
git: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createApp(projectName: string) {
|
|
17
|
+
console.log('🚀 Create Satset App\n');
|
|
18
|
+
|
|
19
|
+
const config: ProjectConfig = {
|
|
20
|
+
name: projectName,
|
|
21
|
+
typescript: true,
|
|
22
|
+
packageManager: 'npm',
|
|
23
|
+
template: 'default',
|
|
24
|
+
git: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Interactive prompts
|
|
28
|
+
const useTypeScript = await prompt('Would you like to use TypeScript?', 'Y/n');
|
|
29
|
+
config.typescript = useTypeScript.toLowerCase() !== 'n';
|
|
30
|
+
|
|
31
|
+
const template = await prompt('Which template would you like to use?', '1) Default 2) Minimal 3) Fullstack');
|
|
32
|
+
if (template === '2') config.template = 'minimal';
|
|
33
|
+
else if (template === '3') config.template = 'fullstack';
|
|
34
|
+
|
|
35
|
+
const pkgMgr = await prompt('Which package manager?', '1) npm 2) yarn 3) pnpm');
|
|
36
|
+
if (pkgMgr === '2') config.packageManager = 'yarn';
|
|
37
|
+
else if (pkgMgr === '3') config.packageManager = 'pnpm';
|
|
38
|
+
|
|
39
|
+
const useGit = await prompt('Initialize a git repository?', 'Y/n');
|
|
40
|
+
config.git = useGit.toLowerCase() !== 'n';
|
|
41
|
+
|
|
42
|
+
console.log('\n📦 Creating project...\n');
|
|
43
|
+
|
|
44
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
45
|
+
|
|
46
|
+
// Check if directory exists
|
|
47
|
+
if (fs.existsSync(projectPath)) {
|
|
48
|
+
console.error(`❌ Directory ${projectName} already exists`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create project structure
|
|
53
|
+
await createProjectStructure(projectPath, config);
|
|
54
|
+
|
|
55
|
+
// Initialize git
|
|
56
|
+
if (config.git) {
|
|
57
|
+
try {
|
|
58
|
+
execSync('git init', { cwd: projectPath, stdio: 'ignore' });
|
|
59
|
+
console.log('✅ Git initialized');
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.log('⚠️ Git initialization failed (optional)');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Install dependencies
|
|
66
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
67
|
+
try {
|
|
68
|
+
const installCmd = {
|
|
69
|
+
npm: 'npm install',
|
|
70
|
+
yarn: 'yarn',
|
|
71
|
+
pnpm: 'pnpm install',
|
|
72
|
+
}[config.packageManager];
|
|
73
|
+
|
|
74
|
+
execSync(installCmd, { cwd: projectPath, stdio: 'inherit' });
|
|
75
|
+
console.log('\n✅ Dependencies installed');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log('\n⚠️ Failed to install dependencies. Please run manually.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('\n✅ Project created successfully!\n');
|
|
81
|
+
console.log('Next steps:');
|
|
82
|
+
console.log(` cd ${projectName}`);
|
|
83
|
+
console.log(` ${config.packageManager} run dev`);
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function prompt(question: string, hint?: string): Promise<string> {
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const promptText = hint ? `${question} (${hint}): ` : `${question}: `;
|
|
95
|
+
rl.question(promptText, (answer) => {
|
|
96
|
+
rl.close();
|
|
97
|
+
resolve(answer.trim() || '');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function createProjectStructure(projectPath: string, config: ProjectConfig) {
|
|
103
|
+
const ext = config.typescript ? 'tsx' : 'jsx';
|
|
104
|
+
const configExt = config.typescript ? 'ts' : 'js';
|
|
105
|
+
|
|
106
|
+
// Create directories
|
|
107
|
+
const dirs = [
|
|
108
|
+
'src/app',
|
|
109
|
+
'src/app/api',
|
|
110
|
+
'src/components',
|
|
111
|
+
'src/styles',
|
|
112
|
+
'public',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
if (config.template === 'fullstack') {
|
|
116
|
+
dirs.push('src/lib', 'src/utils');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dirs.forEach(dir => {
|
|
120
|
+
fs.mkdirSync(path.join(projectPath, dir), { recursive: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Create files based on template
|
|
124
|
+
createPackageJson(projectPath, config);
|
|
125
|
+
createSatsetConfig(projectPath, configExt);
|
|
126
|
+
|
|
127
|
+
if (config.typescript) {
|
|
128
|
+
createTsConfig(projectPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
createLayoutFile(projectPath, ext, config);
|
|
132
|
+
createPageFile(projectPath, ext, config);
|
|
133
|
+
createApiRoute(projectPath, configExt);
|
|
134
|
+
createGlobalCSS(projectPath);
|
|
135
|
+
createGitignore(projectPath);
|
|
136
|
+
createReadme(projectPath, config);
|
|
137
|
+
createEnvExample(projectPath);
|
|
138
|
+
|
|
139
|
+
// Generate assets
|
|
140
|
+
console.log('\n🎨 Generating assets...');
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await generateFavicon(projectPath);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.log('⚠️ Favicon generation skipped');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await generateAndSaveRobots(projectPath);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.log('⚠️ Robots.txt generation skipped');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log('✅ Assets generated');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createPackageJson(projectPath: string, config: ProjectConfig) {
|
|
158
|
+
const packageJson = {
|
|
159
|
+
name: config.name,
|
|
160
|
+
version: '0.1.0',
|
|
161
|
+
private: true,
|
|
162
|
+
scripts: {
|
|
163
|
+
dev: 'satset dev',
|
|
164
|
+
build: 'satset build',
|
|
165
|
+
start: 'satset start',
|
|
166
|
+
lint: config.typescript ? 'tsc --noEmit' : 'echo "No linting configured"',
|
|
167
|
+
},
|
|
168
|
+
dependencies: {
|
|
169
|
+
'@satset/core': '^0.0.1',
|
|
170
|
+
react: '^18.2.0',
|
|
171
|
+
'react-dom': '^18.2.0',
|
|
172
|
+
},
|
|
173
|
+
devDependencies: config.typescript ? {
|
|
174
|
+
'@types/react': '^18.2.0',
|
|
175
|
+
'@types/react-dom': '^18.2.0',
|
|
176
|
+
'@types/node': '^20.0.0',
|
|
177
|
+
typescript: '^5.3.0',
|
|
178
|
+
} : {},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
path.join(projectPath, 'package.json'),
|
|
183
|
+
JSON.stringify(packageJson, null, 2)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createSatsetConfig(projectPath: string, ext: string) {
|
|
188
|
+
const config = `module.exports = {
|
|
189
|
+
port: 3000,
|
|
190
|
+
host: 'localhost',
|
|
191
|
+
favicon: '/favicon.png',
|
|
192
|
+
};
|
|
193
|
+
`;
|
|
194
|
+
fs.writeFileSync(path.join(projectPath, `satset.config.${ext}`), config);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createTsConfig(projectPath: string) {
|
|
198
|
+
const tsConfig = {
|
|
199
|
+
compilerOptions: {
|
|
200
|
+
target: 'ES2020',
|
|
201
|
+
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
202
|
+
jsx: 'react-jsx',
|
|
203
|
+
module: 'ESNext',
|
|
204
|
+
moduleResolution: 'bundler',
|
|
205
|
+
resolveJsonModule: true,
|
|
206
|
+
allowJs: true,
|
|
207
|
+
strict: true,
|
|
208
|
+
esModuleInterop: true,
|
|
209
|
+
skipLibCheck: true,
|
|
210
|
+
forceConsistentCasingInFileNames: true,
|
|
211
|
+
paths: {
|
|
212
|
+
'@/*': ['./src/*'],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
include: ['src'],
|
|
216
|
+
exclude: ['node_modules', 'dist', '.satset'],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
fs.writeFileSync(
|
|
220
|
+
path.join(projectPath, 'tsconfig.json'),
|
|
221
|
+
JSON.stringify(tsConfig, null, 2)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function createLayoutFile(projectPath: string, ext: string, config: ProjectConfig) {
|
|
226
|
+
const layout = `import React from 'react';
|
|
227
|
+
import './globals.css';
|
|
228
|
+
|
|
229
|
+
export default function RootLayout({ children }${config.typescript ? ': { children: React.ReactNode }' : ''}) {
|
|
230
|
+
return (
|
|
231
|
+
<html lang="en">
|
|
232
|
+
<head>
|
|
233
|
+
<meta charSet="UTF-8" />
|
|
234
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
235
|
+
<title>${config.name}</title>
|
|
236
|
+
</head>
|
|
237
|
+
<body>
|
|
238
|
+
{children}
|
|
239
|
+
</body>
|
|
240
|
+
</html>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
fs.writeFileSync(path.join(projectPath, `src/app/layout.${ext}`), layout);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createPageFile(projectPath: string, ext: string, config: ProjectConfig) {
|
|
248
|
+
const templates = {
|
|
249
|
+
default: `import React from 'react';
|
|
250
|
+
import { Link } from '@satset/core';
|
|
251
|
+
|
|
252
|
+
export const metadata = { title: "Welcome to ${config.name}", description: "Built with Satset.js - The fastest React framework" };
|
|
253
|
+
|
|
254
|
+
export default function HomePage() {
|
|
255
|
+
return (
|
|
256
|
+
<>
|
|
257
|
+
<div style={{ padding: '3rem', fontFamily: 'system-ui', maxWidth: '900px', margin: '0 auto' }}>
|
|
258
|
+
<h1 style={{ fontSize: '3rem', marginBottom: '1rem', background: 'linear-gradient(to right, #3b82f6, #8b5cf6)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>
|
|
259
|
+
Welcome to Satset.js! 🚀
|
|
260
|
+
</h1>
|
|
261
|
+
<p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '2rem' }}>
|
|
262
|
+
The fastest way to build fullstack React apps.
|
|
263
|
+
</p>
|
|
264
|
+
<div style={{ display: 'flex', gap: '1rem' }}>
|
|
265
|
+
<Link href="/about" style={{ padding: '0.75rem 1.5rem', background: '#3b82f6', color: 'white', borderRadius: '8px', textDecoration: 'none' }}>
|
|
266
|
+
Get Started
|
|
267
|
+
</Link>
|
|
268
|
+
<a href="https://github.com/satset/satset" style={{ padding: '0.75rem 1.5rem', border: '1px solid #e2e8f0', borderRadius: '8px', textDecoration: 'none', color: '#64748b' }}>
|
|
269
|
+
GitHub
|
|
270
|
+
</a>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
`,
|
|
277
|
+
minimal: `import React from 'react';
|
|
278
|
+
|
|
279
|
+
export default function HomePage() {
|
|
280
|
+
return (
|
|
281
|
+
<div>
|
|
282
|
+
<h1>Hello Satset!</h1>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
`,
|
|
287
|
+
fullstack: `import React from 'react';
|
|
288
|
+
export const metadata = { title: "${config.name}" };
|
|
289
|
+
|
|
290
|
+
export default function HomePage() {
|
|
291
|
+
const [data, setData] = React.useState${config.typescript ? '<any>' : ''}(null);
|
|
292
|
+
|
|
293
|
+
React.useEffect(() => {
|
|
294
|
+
fetch('/api/hello')
|
|
295
|
+
.then(res => res.json())
|
|
296
|
+
.then(setData);
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<>
|
|
301
|
+
<div style={{ padding: '2rem' }}>
|
|
302
|
+
<h1>Fullstack Satset App</h1>
|
|
303
|
+
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
|
304
|
+
</div>
|
|
305
|
+
</>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
fs.writeFileSync(
|
|
312
|
+
path.join(projectPath, `src/app/page.${ext}`),
|
|
313
|
+
templates[config.template]
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function createApiRoute(projectPath: string, ext: string) {
|
|
318
|
+
const apiRoute = `import { SatsetResponse } from '@satset/core';
|
|
319
|
+
|
|
320
|
+
export async function GET() {
|
|
321
|
+
return SatsetResponse.json({
|
|
322
|
+
message: 'Hello from Satset API!',
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
fs.writeFileSync(path.join(projectPath, `src/app/api/hello.${ext}`), apiRoute);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function createGlobalCSS(projectPath: string) {
|
|
331
|
+
const css = `* {
|
|
332
|
+
margin: 0;
|
|
333
|
+
padding: 0;
|
|
334
|
+
box-sizing: border-box;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
body {
|
|
338
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
339
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
340
|
+
sans-serif;
|
|
341
|
+
-webkit-font-smoothing: antialiased;
|
|
342
|
+
-moz-osx-font-smoothing: grayscale;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
code {
|
|
346
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
347
|
+
monospace;
|
|
348
|
+
}
|
|
349
|
+
`;
|
|
350
|
+
fs.writeFileSync(path.join(projectPath, 'src/app/globals.css'), css);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function createGitignore(projectPath: string) {
|
|
354
|
+
const gitignore = `# dependencies
|
|
355
|
+
node_modules
|
|
356
|
+
.pnp
|
|
357
|
+
.pnp.js
|
|
358
|
+
|
|
359
|
+
# testing
|
|
360
|
+
coverage
|
|
361
|
+
|
|
362
|
+
# production
|
|
363
|
+
dist
|
|
364
|
+
.satset
|
|
365
|
+
|
|
366
|
+
# misc
|
|
367
|
+
.DS_Store
|
|
368
|
+
*.pem
|
|
369
|
+
|
|
370
|
+
# debug
|
|
371
|
+
npm-debug.log*
|
|
372
|
+
yarn-debug.log*
|
|
373
|
+
yarn-error.log*
|
|
374
|
+
|
|
375
|
+
# local env files
|
|
376
|
+
.env*.local
|
|
377
|
+
.env
|
|
378
|
+
|
|
379
|
+
# vercel
|
|
380
|
+
.vercel
|
|
381
|
+
|
|
382
|
+
# typescript
|
|
383
|
+
*.tsbuildinfo
|
|
384
|
+
next-env.d.ts
|
|
385
|
+
`;
|
|
386
|
+
fs.writeFileSync(path.join(projectPath, '.gitignore'), gitignore);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createEnvExample(projectPath: string) {
|
|
390
|
+
const envExample = `# Satset Environment Variables
|
|
391
|
+
# Copy this file to .env.local and fill in your values
|
|
392
|
+
|
|
393
|
+
# SATSET_PUBLIC_URL=https://yoursite.com
|
|
394
|
+
# SATSET_PORT=3000
|
|
395
|
+
# SATSET_HOST=localhost
|
|
396
|
+
`;
|
|
397
|
+
fs.writeFileSync(path.join(projectPath, '.env.example'), envExample);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createReadme(projectPath: string, config: ProjectConfig) {
|
|
401
|
+
const readme = `# ${config.name}
|
|
402
|
+
|
|
403
|
+
This is a [Satset.js](https://satset.dev) project created with \`create-satset-app\`.
|
|
404
|
+
|
|
405
|
+
## Getting Started
|
|
406
|
+
|
|
407
|
+
First, run the development server:
|
|
408
|
+
|
|
409
|
+
\`\`\`bash
|
|
410
|
+
${config.packageManager} run dev
|
|
411
|
+
\`\`\`
|
|
412
|
+
|
|
413
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
414
|
+
|
|
415
|
+
## Learn More
|
|
416
|
+
|
|
417
|
+
To learn more about Satset.js, check out the following resources:
|
|
418
|
+
|
|
419
|
+
- [Satset.js Documentation](https://satset.dev/docs)
|
|
420
|
+
- [Learn Satset.js](https://satset.dev/learn)
|
|
421
|
+
- [GitHub Repository](https://github.com/satset/satset)
|
|
422
|
+
|
|
423
|
+
## Deploy
|
|
424
|
+
|
|
425
|
+
Deploy your Satset.js app to Vercel, Netlify, or any Node.js hosting platform.
|
|
426
|
+
|
|
427
|
+
Check out the [deployment documentation](https://satset.dev/docs/deployment) for more details.
|
|
428
|
+
`;
|
|
429
|
+
fs.writeFileSync(path.join(projectPath, 'README.md'), readme);
|
|
430
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createApp } from './create-app';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
// npm create-satset-react my-app
|
|
10
|
+
if (command === 'create-satset-react' || !command) {
|
|
11
|
+
await createApp(command);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
console.error('❌ Error:', error.message);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext", // Ganti ini! Biar TS dukung exports di package.json
|
|
5
|
+
"moduleResolution": "NodeNext", // Ganti ini! Biar sinkron sama NodeNext
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"isolatedModules": true // Tambahin ini biar aman buat Next.js/Esbuild
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
23
|
+
}
|