create-reactaform-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # create-reactaform-app
2
+
3
+ Scaffold a minimal Vite (default) or Next.js React app with ReactaForm preconfigured. Paste a form definition and see results instantly.
4
+
5
+ ## Quick start
6
+ Create a new app from the CLI (JavaScript):
7
+
8
+ ```bash
9
+ npx create-reactaform-app my-app
10
+ ```
11
+
12
+ TypeScript project:
13
+
14
+ ```bash
15
+ npx create-reactaform-app my-app --ts
16
+ ```
17
+
18
+ Create a Next.js app (App Router):
19
+
20
+ ```bash
21
+ npx create-reactaform-app my-next-app --next
22
+ ```
23
+
24
+ Use an example overlay (overwrites the starter `formDefinition`):
25
+
26
+ ```bash
27
+ npx create-reactaform-app my-app --ts --example contact-form
28
+ ```
29
+
30
+ After creating the project:
31
+
32
+ ```bash
33
+ cd my-app
34
+ npm install
35
+ npm run dev
36
+ ```
37
+
38
+ Open the URL shown by the dev server (usually http://localhost:5173 or http://localhost:3000).
39
+
40
+ ## What you get
41
+
42
+ - Vite + React app or Next.js App Router scaffolded
43
+ - `reactaform` installed and wired into the starter app (`src/App.jsx` or `app/page`)
44
+ - A starter `formDefinition` file you can edit to update the UI instantly
45
+
46
+ ## Project layout (Vite)
47
+
48
+ my-app/
49
+ - src/
50
+ - App.jsx # ReactaForm renderer
51
+ - formDefinition.js
52
+ - main.jsx
53
+ - index.html
54
+ - package.json
55
+ - vite.config.js
56
+
57
+ ## Project layout (Next.js App Router)
58
+
59
+ my-next-app/
60
+ - app/
61
+ - page.jsx # imports `./formDefinition`
62
+ - formDefinition.js
63
+ - package.json
64
+
65
+ ## How it works
66
+
67
+ 1. CLI scaffolds a base project (Vite or Next) using the official initializers.
68
+ 2. The CLI copies small template files into the generated project (App wiring, styles).
69
+ 3. The CLI creates a starter `formDefinition.js` (or `.ts` when `--ts`) inside the app so `ReactaForm` has data to render.
70
+ 4. If `--example <name>` is provided, the example files are overlaid into the project (JS examples are converted to TypeScript when `--ts`).
71
+
72
+ ## Starter `formDefinition` example
73
+
74
+ ```js
75
+ // src/formDefinition.js
76
+ export const formDefinition = {
77
+ name: 'contactForm',
78
+ displayName: 'Contact Form',
79
+ version: '1.0.0',
80
+ properties: [
81
+ { name: 'fullName', displayName: 'Full Name', type: 'text', defaultValue: '', required: true },
82
+ { name: 'email', displayName: 'Email', type: 'email', defaultValue: '', required: true }
83
+ ]
84
+ };
85
+ ```
86
+
87
+ ## CLI options
88
+
89
+ - `--ts` Create a TypeScript project
90
+ - `--next` Create a Next.js App Router project
91
+ - `--install` Run `npm install` after scaffolding
92
+ - `--example` Overlay an example (e.g. `--example contact-form`)
93
+
94
+ ## Testing the CLI locally
95
+
96
+ Pack and test without publishing:
97
+
98
+ ```bash
99
+ npm pack
100
+ npx ./create-reactaform-app-0.1.0.tgz my-app-test --ts --example contact-form
101
+ ```
102
+
103
+ Or link locally for iterative testing:
104
+
105
+ ```bash
106
+ npm link
107
+ create-reactaform-app my-app-test --ts --example contact-form
108
+ ```
109
+
110
+ ## Publishing notes
111
+
112
+ - Ensure `bin/cli.js` is executable and has the Node shebang (`#!/usr/bin/env node`).
113
+ - Add `repository`, `author`, `files`, and `publishConfig` to `package.json` before publishing.
114
+
115
+ ## Requirements
116
+
117
+ - Node.js >= 18
118
+
119
+ ## Contributing
120
+
121
+ Contributions welcome: open an issue or PR with improvements, examples, or bugfixes.
122
+
123
+ ## License
124
+
125
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { createApp } from '../src/createApp.js';
3
+
4
+ const args = process.argv.slice(2);
5
+ const name = args.find(arg => !arg.startsWith('-'));
6
+
7
+ if (!name) {
8
+ console.error('❌ Please provide a project name.');
9
+ console.log('Usage: npx create-reactaform-app my-app');
10
+ process.exit(1);
11
+ }
12
+
13
+ const options = {
14
+ ts: args.includes('--ts'),
15
+ next: args.includes('--next'),
16
+ install: args.includes('--install'),
17
+ };
18
+
19
+ // parse --example value (support `--example name` and `--example=name`)
20
+ const exampleArg = args.find(a => a === '--example' || a.startsWith('--example='));
21
+ if (exampleArg) {
22
+ if (exampleArg.includes('=')) {
23
+ options.example = exampleArg.split('=')[1];
24
+ } else {
25
+ const idx = args.indexOf('--example');
26
+ if (idx >= 0 && idx < args.length - 1 && !args[idx + 1].startsWith('-')) {
27
+ options.example = args[idx + 1];
28
+ }
29
+ }
30
+ }
31
+
32
+ createApp(name, options).catch(err => {
33
+ console.error('❌ Failed to create app');
34
+ console.error(err);
35
+ process.exit(1);
36
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-reactaform-app",
3
+ "version": "0.1.0",
4
+ "description": "Create a Vite/Next.js + React app with ReactaForm preconfigured",
5
+ "bin": {
6
+ "create-reactaform-app": "./bin/cli.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "keywords": ["vite", "react", "ReactaForm", "cli"],
13
+ "author": "yanggmtl <yanggmtl@gmail.com>",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/yanggmtl/create-reactaform-app.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/yanggmtl/create-reactaform-app/issues"
20
+ },
21
+ "homepage": "https://github.com/yanggmtl/create-reactaform-app#readme",
22
+ "files": [
23
+ "bin/",
24
+ "src/",
25
+ "README.md"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "test": "echo \"No tests specified\" && exit 0",
32
+ "lint": "echo \"No lint configured\" && exit 0"
33
+ },
34
+ "license": "MIT"
35
+ }
@@ -0,0 +1,269 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { exec } from "./utils/exec.js";
4
+ import { copyDir } from "./utils/fs.js";
5
+ import fs from "node:fs/promises";
6
+ import * as _logModule from "./utils/log.js";
7
+ const log = _logModule.log ??
8
+ _logModule.default ?? {
9
+ info: (msg) => console.log(msg),
10
+ success: (msg) => console.log(msg),
11
+ error: (msg) => console.error(msg),
12
+ };
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ export async function createApp(appName, options) {
17
+ // `scaffoldTemplate` is the template name passed to `create-vite` (use Vite's
18
+ // official React templates: `react` / `react-ts`).
19
+ // `template` is the local folder name under `src/templates` that we'll copy
20
+ // into the generated project (we keep `vite` / `vite-ts` here).
21
+ const scaffoldTemplate = options.next
22
+ ? options.ts
23
+ ? "next-ts"
24
+ : "next"
25
+ : options.ts
26
+ ? "react-ts"
27
+ : "react";
28
+ const template = options.next
29
+ ? options.ts
30
+ ? "next-ts"
31
+ : "next"
32
+ : options.ts
33
+ ? "vite-ts"
34
+ : "vite";
35
+
36
+ const creatingLabel = options.next ? "Next.js" : "ReactaForm";
37
+ log.info(`Creating ${creatingLabel} app: ${appName}`);
38
+
39
+ // 1. Scaffold Vite/Next.js (try primary command, then a fallback for environments
40
+ // where `npm create` may fail)
41
+ // Run scaffold commands with CI=true to force non-interactive defaults
42
+ const spawnEnv = { ...process.env, CI: "true" };
43
+
44
+ const extraFlags = ["--yes"];
45
+
46
+ if (options.next) {
47
+ // Scaffold Next.js app (try npm create, fallback to npx)
48
+ const nextArgsBase = ["create", "next-app@latest", appName];
49
+ // add flags after a -- so they're passed to the initializer
50
+ const nextArgsTS = options.ts
51
+ ? ["--", "--ts", "--use-npm"]
52
+ : ["--", "--use-npm"];
53
+ try {
54
+ await exec("npm", [...nextArgsBase, ...nextArgsTS, ...extraFlags], {
55
+ env: spawnEnv,
56
+ capture: true,
57
+ });
58
+ } catch (err) {
59
+ try {
60
+ await exec(
61
+ "npx",
62
+ ["create-next-app@latest", appName, ...nextArgsTS, ...extraFlags],
63
+ { env: spawnEnv, capture: true },
64
+ );
65
+ } catch (err2) {
66
+ // give up and rethrow
67
+ throw err2;
68
+ }
69
+ }
70
+ } else {
71
+ // Vite React scaffolding (existing behavior)
72
+ try {
73
+ await exec(
74
+ "npm",
75
+ [
76
+ "create",
77
+ "vite@latest",
78
+ appName,
79
+ "--",
80
+ "--template",
81
+ scaffoldTemplate,
82
+ ...extraFlags,
83
+ ],
84
+ { env: spawnEnv, capture: true },
85
+ );
86
+ } catch (err) {
87
+ // Fallback: try `npm init` or `npx create-vite` style invocation
88
+ try {
89
+ await exec(
90
+ "npm",
91
+ [
92
+ "init",
93
+ "vite@latest",
94
+ appName,
95
+ "--",
96
+ "--template",
97
+ scaffoldTemplate,
98
+ ...extraFlags,
99
+ ],
100
+ { env: spawnEnv, capture: true },
101
+ );
102
+ } catch (err2) {
103
+ // final fallback: npx
104
+ await exec(
105
+ "npx",
106
+ [
107
+ "create-vite@latest",
108
+ appName,
109
+ "--",
110
+ "--template",
111
+ scaffoldTemplate,
112
+ ...extraFlags,
113
+ ],
114
+ { env: spawnEnv, capture: true },
115
+ );
116
+ }
117
+ }
118
+ }
119
+
120
+ const appDir = path.resolve(process.cwd(), appName);
121
+
122
+ // 2. Install ReactaForm
123
+ await exec("npm", ["install", "reactaform"], { cwd: appDir });
124
+
125
+ // 3. Copy templates (for Next copy to project root, for React copy to /src)
126
+ const templateDir = path.join(__dirname, "templates", template);
127
+ const targetDir = options.next ? appDir : path.join(appDir, "src");
128
+
129
+ await copyDir(templateDir, targetDir);
130
+
131
+ // For Next apps we write `formDefinition.*` inside `app/`.
132
+ if (options.next) {
133
+ try {
134
+ const rootJs = path.join(appDir, "formDefinition.js");
135
+ const rootTs = path.join(appDir, "formDefinition.ts");
136
+ await fs.unlink(rootJs).catch(() => {});
137
+ await fs.unlink(rootTs).catch(() => {});
138
+ } catch (e) {
139
+ // ignore
140
+ }
141
+ }
142
+
143
+ // Copy shared formDefinition into the new project. For Next apps place it
144
+ // inside the `app/` folder so pages can import `./formDefinition`.
145
+ // Determine where to write app-local files (Next -> app/, React -> src)
146
+ const writeDir = options.next ? path.join(appDir, "app") : targetDir;
147
+ await fs.mkdir(writeDir, { recursive: true });
148
+
149
+ // If an `--example` was requested, copy example files first so they
150
+ // overlay template defaults. Examples live under
151
+ // `src/templates/examples/<example>/js` (we only keep JS examples).
152
+ if (options.example) {
153
+ try {
154
+ const exampleDirJs = path.join(
155
+ __dirname,
156
+ "templates",
157
+ "examples",
158
+ options.example,
159
+ "js",
160
+ );
161
+ await copyDir(exampleDirJs, writeDir);
162
+ } catch (e) {
163
+ // ignore if example folder doesn't exist
164
+ }
165
+ }
166
+
167
+ // Choose source formDefinition: prefer example's JS file when present,
168
+ // otherwise fall back to the shared common file. Then write it as .js or
169
+ // convert to .ts if requested.
170
+ try {
171
+ const commonFormPath = path.join(
172
+ __dirname,
173
+ "templates",
174
+ "common",
175
+ "formDefinition.js",
176
+ );
177
+ const exampleFormPath = options.example
178
+ ? path.join(
179
+ __dirname,
180
+ "templates",
181
+ "examples",
182
+ options.example,
183
+ "js",
184
+ "formDefinition.js",
185
+ )
186
+ : null;
187
+
188
+ let sourcePath = commonFormPath;
189
+ if (exampleFormPath) {
190
+ try {
191
+ await fs.access(exampleFormPath);
192
+ sourcePath = exampleFormPath;
193
+ } catch (e) {
194
+ // example form not present, keep common
195
+ }
196
+ }
197
+
198
+ let formData = await fs.readFile(sourcePath, "utf8");
199
+ const outName = options.ts ? "formDefinition.ts" : "formDefinition.js";
200
+ if (options.ts) {
201
+ formData = formData.replace(
202
+ /export const formDefinition\s*=\s*/m,
203
+ "export const formDefinition: Record<string, unknown> = ",
204
+ );
205
+ }
206
+
207
+ await fs.writeFile(path.join(writeDir, outName), formData, "utf8");
208
+
209
+ // If we wrote a TS file for a previously-copied example, remove the JS
210
+ // variant so project has the correct extension.
211
+ if (options.ts) {
212
+ try {
213
+ await fs.unlink(path.join(writeDir, "formDefinition.js"));
214
+ } catch (e) {
215
+ // ignore if not present
216
+ }
217
+ }
218
+ } catch (e) {
219
+ // fail silently if no source formDefinition found
220
+ }
221
+
222
+ // If user requested a JS Next app (no --ts), remove any TypeScript files
223
+ // that might exist in the generated project (safety cleanup).
224
+ if (options.next && !options.ts) {
225
+ async function removeTsFiles(dir) {
226
+ let entries;
227
+ try {
228
+ entries = await fs.readdir(dir, { withFileTypes: true });
229
+ } catch (err) {
230
+ return;
231
+ }
232
+
233
+ for (const entry of entries) {
234
+ const entryPath = path.join(dir, entry.name);
235
+ if (entry.isDirectory()) {
236
+ await removeTsFiles(entryPath);
237
+ } else {
238
+ const ext = path.extname(entry.name).toLowerCase();
239
+ if (
240
+ ext === ".ts" ||
241
+ ext === ".tsx" ||
242
+ entry.name === "tsconfig.json" ||
243
+ entry.name === "next-env.d.ts"
244
+ ) {
245
+ try {
246
+ await fs.unlink(entryPath);
247
+ } catch (e) {
248
+ // ignore errors
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ await removeTsFiles(appDir);
256
+ }
257
+
258
+ // 4. Optional full install
259
+ if (options.install) {
260
+ await exec("npm", ["install"], { cwd: appDir });
261
+ }
262
+
263
+ log.success("Done!");
264
+ log.info(`Next steps:
265
+ cd ${appName}
266
+ npm install
267
+ npm run dev
268
+ `);
269
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared example ReactaForm definition.
3
+ */
4
+ export const formDefinition = {
5
+ name: 'contactForm',
6
+ displayName: 'Contact Form',
7
+ version: '1.0.0',
8
+ properties: [
9
+ {
10
+ name: 'fullName',
11
+ displayName: 'Full Name',
12
+ type: 'text',
13
+ defaultValue: '',
14
+ required: true,
15
+ },
16
+ {
17
+ name: 'email',
18
+ displayName: 'Email',
19
+ type: 'email',
20
+ defaultValue: '',
21
+ required: true,
22
+ },
23
+ ],
24
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Contact form example (JavaScript)
3
+ */
4
+ export const formDefinition = {
5
+ name: 'contactForm',
6
+ displayName: 'Contact Form (Example)',
7
+ version: '1.0.0',
8
+ properties: [
9
+ {
10
+ name: 'fullName',
11
+ displayName: 'Full Name',
12
+ type: 'text',
13
+ defaultValue: '',
14
+ required: true,
15
+ },
16
+ {
17
+ name: 'email',
18
+ displayName: 'Email',
19
+ type: 'email',
20
+ defaultValue: '',
21
+ required: true,
22
+ },
23
+ {
24
+ name: 'message',
25
+ displayName: 'Message',
26
+ type: 'multiline',
27
+ defaultValue: '',
28
+ required: false,
29
+ },
30
+ ],
31
+ };
@@ -0,0 +1,7 @@
1
+ html, body {
2
+ padding: 0;
3
+ margin: 0;
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
5
+ }
6
+
7
+ * { box-sizing: border-box; }
@@ -0,0 +1,13 @@
1
+ import './globals.css';
2
+
3
+ export const metadata = {
4
+ title: 'ReactaForm Next App',
5
+ };
6
+
7
+ export default function RootLayout({ children }) {
8
+ return (
9
+ <html lang="en">
10
+ <body>{children}</body>
11
+ </html>
12
+ );
13
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { ReactaForm } from 'reactaform';
3
+ import { formDefinition } from './formDefinition';
4
+
5
+ export default function Page() {
6
+ return (
7
+ <div style={{ padding: 24, maxWidth: 600 , flex: '1', margin: '0 auto'}}>
8
+ <ReactaForm definitionData={formDefinition} />
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,7 @@
1
+ html, body {
2
+ padding: 0;
3
+ margin: 0;
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
5
+ }
6
+
7
+ * { box-sizing: border-box; }
@@ -0,0 +1,13 @@
1
+ import './globals.css';
2
+
3
+ export const metadata = {
4
+ title: 'ReactaForm Next App',
5
+ };
6
+
7
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
8
+ return (
9
+ <html lang="en">
10
+ <body>{children}</body>
11
+ </html>
12
+ );
13
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { ReactaForm } from 'reactaform';
3
+ import { formDefinition } from './formDefinition';
4
+
5
+ export default function Page() {
6
+ return (
7
+ <div style={{ padding: 24, maxWidth: 600 , flex: '1', margin: '0 auto'}}>
8
+ <ReactaForm definitionData={formDefinition} />
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,13 @@
1
+ import { ReactaForm } from 'reactaform';
2
+ import { formDefinition } from './formDefinition';
3
+ import './App.css';
4
+
5
+ export default function App() {
6
+ return (
7
+ <div className="App" style={{ padding: 24, maxWidth: 600 }}>
8
+ <ReactaForm
9
+ definitionData={formDefinition}
10
+ />
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import { ReactaForm } from 'reactaform';
2
+ import { formDefinition } from './formDefinition';
3
+ import './App.css';
4
+
5
+ export default function App() {
6
+ return (
7
+ <div className="App" style={{ padding: 24, maxWidth: 600 }}>
8
+ <ReactaForm
9
+ definitionData={formDefinition}
10
+ />
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ function isPromptLine(line) {
4
+ if (!line) return true;
5
+ // Filter box-drawing lines and create-vite prompt markers
6
+ if (/^[\s│└┌┐├┤┬┴─]+$/.test(line)) return true;
7
+ if (line.includes('◇')) return true;
8
+ if (/Use rolldown-vite/i.test(line)) return true;
9
+ if (/Install with npm and start now/i.test(line)) return true;
10
+ return false;
11
+ }
12
+
13
+ export function exec(command, args, options = {}) {
14
+ const capture = Boolean(options.capture);
15
+ const spawnOpts = {
16
+ shell: process.platform === 'win32',
17
+ ...options,
18
+ };
19
+
20
+ // When capturing, do not set stdio: 'inherit' so we can filter output
21
+ if (!capture) spawnOpts.stdio = 'inherit';
22
+
23
+ return new Promise((resolve, reject) => {
24
+ const child = spawn(command, args, spawnOpts);
25
+
26
+ if (capture) {
27
+ if (child.stdout) {
28
+ let stdoutBuf = '';
29
+ child.stdout.on('data', chunk => {
30
+ stdoutBuf += chunk.toString();
31
+ const parts = stdoutBuf.split(/\r?\n/);
32
+ stdoutBuf = parts.pop();
33
+ for (const line of parts) {
34
+ if (!isPromptLine(line)) process.stdout.write(line + '\n');
35
+ }
36
+ });
37
+ child.stdout.on('end', () => {
38
+ if (stdoutBuf && !isPromptLine(stdoutBuf)) process.stdout.write(stdoutBuf + '\n');
39
+ });
40
+ }
41
+
42
+ if (child.stderr) {
43
+ let stderrBuf = '';
44
+ child.stderr.on('data', chunk => {
45
+ stderrBuf += chunk.toString();
46
+ const parts = stderrBuf.split(/\r?\n/);
47
+ stderrBuf = parts.pop();
48
+ for (const line of parts) {
49
+ if (!isPromptLine(line)) process.stderr.write(line + '\n');
50
+ }
51
+ });
52
+ child.stderr.on('end', () => {
53
+ if (stderrBuf && !isPromptLine(stderrBuf)) process.stderr.write(stderrBuf + '\n');
54
+ });
55
+ }
56
+ }
57
+
58
+ child.on('close', code => {
59
+ if (code !== 0) {
60
+ reject(new Error(`${command} exited with code ${code}`));
61
+ } else {
62
+ resolve();
63
+ }
64
+ });
65
+ });
66
+ }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ export async function copyDir(src, dest) {
5
+ await fs.mkdir(dest, { recursive: true });
6
+ const entries = await fs.readdir(src, { withFileTypes: true });
7
+
8
+ for (const entry of entries) {
9
+ const srcPath = path.join(src, entry.name);
10
+ const destPath = path.join(dest, entry.name);
11
+
12
+ if (entry.isDirectory()) {
13
+ await copyDir(srcPath, destPath);
14
+ } else {
15
+ await fs.copyFile(srcPath, destPath);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,5 @@
1
+ export const log = {
2
+ info: msg => console.log(`ℹ️ ${msg}`),
3
+ success: msg => console.log(`✅ ${msg}`),
4
+ error: msg => console.error(`❌ ${msg}`),
5
+ };