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 ADDED
File without changes
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # create-satset-app
2
+
3
+ Scaffold package to implement the `create-satset-app` CLI.
4
+
5
+ This workspace is intentionally minimal — add generator logic (prompting, templating, installers) under `src/` and publish the CLI in `bin/` when ready.
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, '&amp;')
73
+ .replace(/</g, '&lt;')
74
+ .replace(/>/g, '&gt;')
75
+ .replace(/"/g, '&quot;')
76
+ .replace(/'/g, '&#39;');
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, '&amp;')
89
+ .replace(/</g, '&lt;')
90
+ .replace(/>/g, '&gt;')
91
+ .replace(/"/g, '&quot;')
92
+ .replace(/'/g, '&apos;');
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
+ }