@fireberry/cli 0.2.3-beta.0 ā 0.2.4-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/CLAUDE.md +40 -2
- package/dist/bin/fireberry.js +9 -0
- package/dist/commands/create-component.d.ts +6 -0
- package/dist/commands/create-component.js +211 -0
- package/package.json +1 -1
- package/src/bin/fireberry.ts +10 -0
- package/src/commands/create-component.ts +292 -0
- package/src/templates/App-other.jsx +28 -0
- package/src/templates/App-record.jsx +64 -0
package/CLAUDE.md
CHANGED
|
@@ -58,6 +58,7 @@ Each CLI command is in [src/commands/](src/commands/):
|
|
|
58
58
|
|
|
59
59
|
- **init**: Stores Fireberry API token in local config using `env-paths`
|
|
60
60
|
- **create**: Creates new Fireberry app from templates with generated UUIDs
|
|
61
|
+
- **create-component**: Scaffolds new Vite React component, installs Fireberry packages, builds it, and adds to manifest
|
|
61
62
|
- **push**: Validates manifest, zips components, uploads to Fireberry API
|
|
62
63
|
- **install**: Installs app on user's Fireberry account
|
|
63
64
|
- **delete**: Deletes app from Fireberry platform (requires confirmation)
|
|
@@ -117,6 +118,42 @@ Templates use mustache-style placeholders (`{{appName}}`, `{{appId}}`, `{{compon
|
|
|
117
118
|
|
|
118
119
|
- **manifest.yml**: Default manifest with single component
|
|
119
120
|
- **index.html**: Basic HTML template
|
|
121
|
+
- **App-record.jsx**: React component template for record-type components
|
|
122
|
+
- **App-other.jsx**: React component template for global-menu and side-menu components
|
|
123
|
+
|
|
124
|
+
### Create Component Command
|
|
125
|
+
|
|
126
|
+
The `create-component` command ([src/commands/create-component.ts](src/commands/create-component.ts)) scaffolds a new React component within an existing Fireberry app:
|
|
127
|
+
|
|
128
|
+
**Usage**: `fireberry create-component [name] [--type <type>]`
|
|
129
|
+
|
|
130
|
+
**Process**:
|
|
131
|
+
|
|
132
|
+
1. Validates component name doesn't already exist in manifest.yml
|
|
133
|
+
2. Prompts for component type if not provided (record, global-menu, side-menu)
|
|
134
|
+
3. Prompts for type-specific settings:
|
|
135
|
+
- **record**: objectType (number), height (S/M/L), sets default iconName and iconColor
|
|
136
|
+
- **global-menu**: displayName, sets default iconName
|
|
137
|
+
- **side-menu**: width (S/M/L), sets default iconName
|
|
138
|
+
4. Creates Vite React app in `static/<componentName>` using `npm create vite@latest`
|
|
139
|
+
5. Installs standard dependencies with `npm install`
|
|
140
|
+
6. Installs Fireberry packages: `@fireberry/ds` and `@fireberry/sdk`
|
|
141
|
+
7. Copies appropriate App.jsx template based on component type
|
|
142
|
+
8. Builds the component with `npm run build`
|
|
143
|
+
9. Generates UUID for component
|
|
144
|
+
10. Adds component entry to manifest.yml with path `static/<componentName>/dist`
|
|
145
|
+
11. Displays success message with component details and next steps
|
|
146
|
+
|
|
147
|
+
**Requirements**:
|
|
148
|
+
|
|
149
|
+
- Must be run from directory containing `manifest.yml`
|
|
150
|
+
- Component name must be unique within the app
|
|
151
|
+
|
|
152
|
+
**Output**:
|
|
153
|
+
|
|
154
|
+
- Creates component directory at `static/<componentName>/`
|
|
155
|
+
- Updates manifest.yml with new component entry
|
|
156
|
+
- Built component ready at `static/<componentName>/dist/`
|
|
120
157
|
|
|
121
158
|
## Key Patterns
|
|
122
159
|
|
|
@@ -144,13 +181,14 @@ Component paths in manifest.yml are relative to the current working directory, n
|
|
|
144
181
|
|
|
145
182
|
## Important Notes
|
|
146
183
|
|
|
147
|
-
- **Manifest Required**: `push`, `install`, and `
|
|
148
|
-
- **Token Required**: Most commands require prior `init` to store API token
|
|
184
|
+
- **Manifest Required**: `push`, `install`, `delete`, and `create-component` commands must be run from a directory containing `manifest.yml`
|
|
185
|
+
- **Token Required**: Most commands (`push`, `install`, `delete`) require prior `init` to store API token
|
|
149
186
|
- **Component IDs**: Must be unique UUIDs within a manifest
|
|
150
187
|
- **Component Settings Validation**: Each component type has required settings that are validated during `push`:
|
|
151
188
|
- `record`: Must have `iconName`, `iconColor`, and `objectType`
|
|
152
189
|
- `global-menu`: Must have `displayName`
|
|
153
190
|
- `side-menu`: Must have `iconName` and `width` (S/M/L)
|
|
191
|
+
- **Component Creation**: `create-component` automatically scaffolds a Vite React app, installs `@fireberry/ds` and `@fireberry/sdk`, builds it, and adds it to the manifest
|
|
154
192
|
- **Build Zipping**: Single files are wrapped in a directory before tar.gz creation
|
|
155
193
|
- **Template Location**: Templates are resolved from `src/templates/` at compile time, copied to `dist/templates/`
|
|
156
194
|
- **Delete Safety**: Delete command requires user confirmation before executing (cannot be undone)
|
package/dist/bin/fireberry.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runPush } from "../commands/push.js";
|
|
|
8
8
|
import { runInstall } from "../commands/install.js";
|
|
9
9
|
import { runDelete } from "../commands/delete.js";
|
|
10
10
|
import { runDebug } from "../commands/debug.js";
|
|
11
|
+
import { runCreateComponent } from "../commands/create-component.js";
|
|
11
12
|
const program = new Command();
|
|
12
13
|
program
|
|
13
14
|
.name("fireberry")
|
|
@@ -28,6 +29,14 @@ program
|
|
|
28
29
|
const name = nameArgs ? nameArgs.join("-") : undefined;
|
|
29
30
|
await runCreate({ name });
|
|
30
31
|
});
|
|
32
|
+
program
|
|
33
|
+
.command("create-component")
|
|
34
|
+
.argument("[name]", "Component name")
|
|
35
|
+
.argument("[type]", "Component type (record, global-menu, side-menu)")
|
|
36
|
+
.description("Add a new component to the existing app manifest")
|
|
37
|
+
.action(async (name, type) => {
|
|
38
|
+
await runCreateComponent({ name, type });
|
|
39
|
+
});
|
|
31
40
|
program
|
|
32
41
|
.command("push")
|
|
33
42
|
.description("Push app to Fireberry")
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import yaml from "js-yaml";
|
|
10
|
+
import { getManifest } from "../utils/components.utils.js";
|
|
11
|
+
import { COMPONENT_TYPE } from "../constants/component-types.js";
|
|
12
|
+
import { HEIGHT_OPTIONS } from "../constants/height-options.js";
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const VALID_COMPONENT_TYPES = Object.values(COMPONENT_TYPE);
|
|
16
|
+
function validateComponentType(type) {
|
|
17
|
+
if (!VALID_COMPONENT_TYPES.includes(type)) {
|
|
18
|
+
throw new Error(`Invalid component type: "${type}". Valid types are: ${VALID_COMPONENT_TYPES.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
return type;
|
|
21
|
+
}
|
|
22
|
+
async function promptForSettings(type) {
|
|
23
|
+
switch (type) {
|
|
24
|
+
case COMPONENT_TYPE.RECORD: {
|
|
25
|
+
const answers = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: "number",
|
|
28
|
+
name: "objectType",
|
|
29
|
+
message: "Object type:",
|
|
30
|
+
default: 0,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "list",
|
|
34
|
+
name: "height",
|
|
35
|
+
message: "Component height:",
|
|
36
|
+
choices: HEIGHT_OPTIONS,
|
|
37
|
+
default: "M",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
return {
|
|
41
|
+
...answers,
|
|
42
|
+
iconName: "related-single",
|
|
43
|
+
iconColor: "#7aae7f",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
case COMPONENT_TYPE.GLOBAL_MENU: {
|
|
47
|
+
const answers = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: "input",
|
|
50
|
+
name: "displayName",
|
|
51
|
+
message: "Display name:",
|
|
52
|
+
default: "Global Menu",
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
return {
|
|
56
|
+
...answers,
|
|
57
|
+
iconName: "related-single",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
case COMPONENT_TYPE.SIDE_MENU: {
|
|
61
|
+
const answers = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: "list",
|
|
64
|
+
name: "width",
|
|
65
|
+
message: "Component width:",
|
|
66
|
+
choices: ["S", "M", "L"],
|
|
67
|
+
default: "M",
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
return {
|
|
71
|
+
...answers,
|
|
72
|
+
iconName: "related-single",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function runCreateComponent({ type, name, }) {
|
|
80
|
+
let componentName = name;
|
|
81
|
+
let componentType = type;
|
|
82
|
+
const manifestPath = path.join(process.cwd(), "manifest.yml");
|
|
83
|
+
const manifest = await getManifest();
|
|
84
|
+
const components = manifest.components;
|
|
85
|
+
const existingComponent = components?.find((comp) => comp.title === componentName);
|
|
86
|
+
if (existingComponent) {
|
|
87
|
+
throw new Error(`Component with name "${componentName}" already exists in manifest.yml`);
|
|
88
|
+
}
|
|
89
|
+
if (!componentName || !componentType) {
|
|
90
|
+
const questions = [];
|
|
91
|
+
if (!componentName) {
|
|
92
|
+
questions.push({
|
|
93
|
+
type: "input",
|
|
94
|
+
name: "name",
|
|
95
|
+
message: "Component name:",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (!componentType) {
|
|
99
|
+
questions.push({
|
|
100
|
+
type: "list",
|
|
101
|
+
name: "type",
|
|
102
|
+
message: "Component type:",
|
|
103
|
+
choices: VALID_COMPONENT_TYPES,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const answers = await inquirer.prompt(questions);
|
|
107
|
+
if (!componentName) {
|
|
108
|
+
componentName = (answers.name || "").trim();
|
|
109
|
+
}
|
|
110
|
+
if (!componentType) {
|
|
111
|
+
componentType = answers.type;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!componentName) {
|
|
115
|
+
throw new Error("Missing component name.");
|
|
116
|
+
}
|
|
117
|
+
if (!componentType) {
|
|
118
|
+
throw new Error("Missing component type.");
|
|
119
|
+
}
|
|
120
|
+
const validatedType = validateComponentType(componentType);
|
|
121
|
+
const spinner = ora();
|
|
122
|
+
try {
|
|
123
|
+
const componentSettings = await promptForSettings(validatedType);
|
|
124
|
+
spinner.text = chalk.cyan(`Creating Vite React app for "${chalk.cyan(componentName)}"...`);
|
|
125
|
+
spinner.start();
|
|
126
|
+
const componentDir = path.join(process.cwd(), "static", componentName);
|
|
127
|
+
await fs.ensureDir(componentDir);
|
|
128
|
+
// Create Vite app with React template
|
|
129
|
+
spinner.text = `Running npm create vite@latest...`;
|
|
130
|
+
const viteResult = spawnSync(`npm create vite@latest ${componentName} -- --template react --no-interactive`, {
|
|
131
|
+
cwd: path.join(process.cwd(), "static"),
|
|
132
|
+
stdio: "inherit",
|
|
133
|
+
shell: true,
|
|
134
|
+
});
|
|
135
|
+
if (viteResult.error || viteResult.status !== 0) {
|
|
136
|
+
throw new Error(`Failed to create Vite app: ${viteResult.error?.message || `Exit code ${viteResult.status}`}`);
|
|
137
|
+
}
|
|
138
|
+
spinner.text = `Installing dependencies...`;
|
|
139
|
+
const installResult = spawnSync("npm install", {
|
|
140
|
+
cwd: componentDir,
|
|
141
|
+
stdio: "inherit",
|
|
142
|
+
shell: true,
|
|
143
|
+
});
|
|
144
|
+
if (installResult.error || installResult.status !== 0) {
|
|
145
|
+
throw new Error("Failed to install dependencies");
|
|
146
|
+
}
|
|
147
|
+
spinner.text = `Installing Fireberry packages...`;
|
|
148
|
+
const fireberryResult = spawnSync("npm install @fireberry/ds @fireberry/sdk", {
|
|
149
|
+
cwd: componentDir,
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
shell: true,
|
|
152
|
+
});
|
|
153
|
+
if (fireberryResult.error || fireberryResult.status !== 0) {
|
|
154
|
+
throw new Error("Failed to install Fireberry packages");
|
|
155
|
+
}
|
|
156
|
+
spinner.text = `Configuring component...`;
|
|
157
|
+
const templatesDir = path.join(__dirname, "..", "..", "src", "templates");
|
|
158
|
+
// Choose the right App.jsx template based on component type
|
|
159
|
+
const appTemplateFile = validatedType === COMPONENT_TYPE.RECORD
|
|
160
|
+
? "App-record.jsx"
|
|
161
|
+
: "App-other.jsx";
|
|
162
|
+
const appTemplate = await fs.readFile(path.join(templatesDir, appTemplateFile), "utf-8");
|
|
163
|
+
await fs.writeFile(path.join(componentDir, "src", "App.jsx"), appTemplate);
|
|
164
|
+
// Build the component
|
|
165
|
+
spinner.text = `Building component...`;
|
|
166
|
+
const buildResult = spawnSync("npm run build", {
|
|
167
|
+
cwd: componentDir,
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
shell: true,
|
|
170
|
+
});
|
|
171
|
+
if (buildResult.error || buildResult.status !== 0) {
|
|
172
|
+
throw new Error("Failed to build component");
|
|
173
|
+
}
|
|
174
|
+
spinner.text = `Adding component to manifest...`;
|
|
175
|
+
const componentId = uuidv4();
|
|
176
|
+
const newComponent = {
|
|
177
|
+
type: validatedType,
|
|
178
|
+
title: componentName,
|
|
179
|
+
id: componentId,
|
|
180
|
+
path: `static/${componentName}/dist`,
|
|
181
|
+
settings: componentSettings,
|
|
182
|
+
};
|
|
183
|
+
if (!manifest.components) {
|
|
184
|
+
manifest.components = [];
|
|
185
|
+
}
|
|
186
|
+
manifest.components.push(newComponent);
|
|
187
|
+
const updatedYaml = yaml.dump(manifest, {
|
|
188
|
+
indent: 2,
|
|
189
|
+
lineWidth: -1,
|
|
190
|
+
noRefs: true,
|
|
191
|
+
});
|
|
192
|
+
await fs.writeFile(manifestPath, updatedYaml, "utf-8");
|
|
193
|
+
spinner.succeed(`Successfully created component "${chalk.cyan(componentName)}"!`);
|
|
194
|
+
console.log(chalk.gray(`Component ID: ${componentId}`));
|
|
195
|
+
console.log(chalk.gray(`Type: ${validatedType}`));
|
|
196
|
+
console.log(chalk.gray(`Path: static/${componentName}/dist`));
|
|
197
|
+
console.log(chalk.green("\nš Your component is ready!"));
|
|
198
|
+
console.log(chalk.white(` cd static/${componentName}`));
|
|
199
|
+
console.log(chalk.white(` npm run dev # Start development server`));
|
|
200
|
+
console.log(chalk.white(` npm run build # Build for production`));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof Error) {
|
|
204
|
+
spinner.fail(chalk.red(`Failed to add component: ${error.message}`));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
spinner.fail(chalk.red(`Failed to add component "${chalk.cyan(componentName || "unknown")}"`));
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/package.json
CHANGED
package/src/bin/fireberry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { runPush } from "../commands/push.js";
|
|
|
8
8
|
import { runInstall } from "../commands/install.js";
|
|
9
9
|
import { runDelete } from "../commands/delete.js";
|
|
10
10
|
import { runDebug } from "../commands/debug.js";
|
|
11
|
+
import { runCreateComponent } from "../commands/create-component.js";
|
|
11
12
|
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
@@ -33,6 +34,15 @@ program
|
|
|
33
34
|
await runCreate({ name });
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
program
|
|
38
|
+
.command("create-component")
|
|
39
|
+
.argument("[name]", "Component name")
|
|
40
|
+
.argument("[type]", "Component type (record, global-menu, side-menu)")
|
|
41
|
+
.description("Add a new component to the existing app manifest")
|
|
42
|
+
.action(async (name, type) => {
|
|
43
|
+
await runCreateComponent({ name, type });
|
|
44
|
+
});
|
|
45
|
+
|
|
36
46
|
program
|
|
37
47
|
.command("push")
|
|
38
48
|
.description("Push app to Fireberry")
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import inquirer, { QuestionCollection } from "inquirer";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import yaml from "js-yaml";
|
|
10
|
+
import { getManifest } from "../utils/components.utils.js";
|
|
11
|
+
import { COMPONENT_TYPE, ComponentType } from "../constants/component-types.js";
|
|
12
|
+
|
|
13
|
+
import { HEIGHT_OPTIONS } from "../constants/height-options.js";
|
|
14
|
+
import { UntypedManifestComponent } from "../api/types.js";
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
interface CreateComponentOptions {
|
|
19
|
+
name?: string;
|
|
20
|
+
type?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const VALID_COMPONENT_TYPES = Object.values(COMPONENT_TYPE);
|
|
24
|
+
|
|
25
|
+
function validateComponentType(type: string): ComponentType {
|
|
26
|
+
if (!VALID_COMPONENT_TYPES.includes(type as ComponentType)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid component type: "${type}". Valid types are: ${VALID_COMPONENT_TYPES.join(
|
|
29
|
+
", "
|
|
30
|
+
)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return type as ComponentType;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function promptForSettings(
|
|
37
|
+
type: ComponentType
|
|
38
|
+
): Promise<Record<string, unknown>> {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case COMPONENT_TYPE.RECORD: {
|
|
41
|
+
const answers = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: "number",
|
|
44
|
+
name: "objectType",
|
|
45
|
+
message: "Object type:",
|
|
46
|
+
default: 0,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "list",
|
|
50
|
+
name: "height",
|
|
51
|
+
message: "Component height:",
|
|
52
|
+
choices: HEIGHT_OPTIONS,
|
|
53
|
+
default: "M",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
return {
|
|
57
|
+
...answers,
|
|
58
|
+
iconName: "related-single",
|
|
59
|
+
iconColor: "#7aae7f",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case COMPONENT_TYPE.GLOBAL_MENU: {
|
|
63
|
+
const answers = await inquirer.prompt([
|
|
64
|
+
{
|
|
65
|
+
type: "input",
|
|
66
|
+
name: "displayName",
|
|
67
|
+
message: "Display name:",
|
|
68
|
+
default: "Global Menu",
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
...answers,
|
|
73
|
+
iconName: "related-single",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case COMPONENT_TYPE.SIDE_MENU: {
|
|
77
|
+
const answers = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: "list",
|
|
80
|
+
name: "width",
|
|
81
|
+
message: "Component width:",
|
|
82
|
+
choices: ["S", "M", "L"],
|
|
83
|
+
default: "M",
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
...answers,
|
|
88
|
+
iconName: "related-single",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runCreateComponent({
|
|
97
|
+
type,
|
|
98
|
+
name,
|
|
99
|
+
}: CreateComponentOptions): Promise<void> {
|
|
100
|
+
let componentName = name;
|
|
101
|
+
let componentType = type;
|
|
102
|
+
|
|
103
|
+
const manifestPath = path.join(process.cwd(), "manifest.yml");
|
|
104
|
+
const manifest = await getManifest();
|
|
105
|
+
|
|
106
|
+
const components = manifest.components as unknown as
|
|
107
|
+
| UntypedManifestComponent[]
|
|
108
|
+
| undefined;
|
|
109
|
+
|
|
110
|
+
const existingComponent = components?.find(
|
|
111
|
+
(comp) => comp.title === componentName
|
|
112
|
+
);
|
|
113
|
+
if (existingComponent) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Component with name "${componentName}" already exists in manifest.yml`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!componentName || !componentType) {
|
|
120
|
+
const questions: QuestionCollection<{ name: string; type: string }>[] = [];
|
|
121
|
+
|
|
122
|
+
if (!componentName) {
|
|
123
|
+
questions.push({
|
|
124
|
+
type: "input",
|
|
125
|
+
name: "name",
|
|
126
|
+
message: "Component name:",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!componentType) {
|
|
131
|
+
questions.push({
|
|
132
|
+
type: "list",
|
|
133
|
+
name: "type",
|
|
134
|
+
message: "Component type:",
|
|
135
|
+
choices: VALID_COMPONENT_TYPES,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const answers = await inquirer.prompt(questions);
|
|
140
|
+
|
|
141
|
+
if (!componentName) {
|
|
142
|
+
componentName = (answers.name || "").trim();
|
|
143
|
+
}
|
|
144
|
+
if (!componentType) {
|
|
145
|
+
componentType = answers.type;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!componentName) {
|
|
150
|
+
throw new Error("Missing component name.");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!componentType) {
|
|
154
|
+
throw new Error("Missing component type.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const validatedType = validateComponentType(componentType);
|
|
158
|
+
|
|
159
|
+
const spinner = ora();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const componentSettings = await promptForSettings(validatedType);
|
|
163
|
+
|
|
164
|
+
spinner.text = chalk.cyan(
|
|
165
|
+
`Creating Vite React app for "${chalk.cyan(componentName)}"...`
|
|
166
|
+
);
|
|
167
|
+
spinner.start();
|
|
168
|
+
|
|
169
|
+
const componentDir = path.join(process.cwd(), "static", componentName);
|
|
170
|
+
await fs.ensureDir(componentDir);
|
|
171
|
+
|
|
172
|
+
// Create Vite app with React template
|
|
173
|
+
spinner.text = `Running npm create vite@latest...`;
|
|
174
|
+
const viteResult = spawnSync(
|
|
175
|
+
`npm create vite@latest ${componentName} -- --template react --no-interactive`,
|
|
176
|
+
{
|
|
177
|
+
cwd: path.join(process.cwd(), "static"),
|
|
178
|
+
stdio: "inherit",
|
|
179
|
+
shell: true,
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (viteResult.error || viteResult.status !== 0) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Failed to create Vite app: ${
|
|
186
|
+
viteResult.error?.message || `Exit code ${viteResult.status}`
|
|
187
|
+
}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
spinner.text = `Installing dependencies...`;
|
|
192
|
+
const installResult = spawnSync("npm install", {
|
|
193
|
+
cwd: componentDir,
|
|
194
|
+
stdio: "inherit",
|
|
195
|
+
shell: true,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (installResult.error || installResult.status !== 0) {
|
|
199
|
+
throw new Error("Failed to install dependencies");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
spinner.text = `Installing Fireberry packages...`;
|
|
203
|
+
const fireberryResult = spawnSync(
|
|
204
|
+
"npm install @fireberry/ds @fireberry/sdk",
|
|
205
|
+
{
|
|
206
|
+
cwd: componentDir,
|
|
207
|
+
stdio: "inherit",
|
|
208
|
+
shell: true,
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (fireberryResult.error || fireberryResult.status !== 0) {
|
|
213
|
+
throw new Error("Failed to install Fireberry packages");
|
|
214
|
+
}
|
|
215
|
+
spinner.text = `Configuring component...`;
|
|
216
|
+
|
|
217
|
+
const templatesDir = path.join(__dirname, "..", "..", "src", "templates");
|
|
218
|
+
|
|
219
|
+
// Choose the right App.jsx template based on component type
|
|
220
|
+
const appTemplateFile =
|
|
221
|
+
validatedType === COMPONENT_TYPE.RECORD
|
|
222
|
+
? "App-record.jsx"
|
|
223
|
+
: "App-other.jsx";
|
|
224
|
+
const appTemplate = await fs.readFile(
|
|
225
|
+
path.join(templatesDir, appTemplateFile),
|
|
226
|
+
"utf-8"
|
|
227
|
+
);
|
|
228
|
+
await fs.writeFile(path.join(componentDir, "src", "App.jsx"), appTemplate);
|
|
229
|
+
|
|
230
|
+
// Build the component
|
|
231
|
+
spinner.text = `Building component...`;
|
|
232
|
+
const buildResult = spawnSync("npm run build", {
|
|
233
|
+
cwd: componentDir,
|
|
234
|
+
stdio: "inherit",
|
|
235
|
+
shell: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (buildResult.error || buildResult.status !== 0) {
|
|
239
|
+
throw new Error("Failed to build component");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
spinner.text = `Adding component to manifest...`;
|
|
243
|
+
|
|
244
|
+
const componentId = uuidv4();
|
|
245
|
+
|
|
246
|
+
const newComponent: UntypedManifestComponent = {
|
|
247
|
+
type: validatedType,
|
|
248
|
+
title: componentName,
|
|
249
|
+
id: componentId,
|
|
250
|
+
path: `static/${componentName}/dist`,
|
|
251
|
+
settings: componentSettings,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (!manifest.components) {
|
|
255
|
+
manifest.components = [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
(manifest.components as unknown as UntypedManifestComponent[]).push(
|
|
259
|
+
newComponent
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const updatedYaml = yaml.dump(manifest, {
|
|
263
|
+
indent: 2,
|
|
264
|
+
lineWidth: -1,
|
|
265
|
+
noRefs: true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await fs.writeFile(manifestPath, updatedYaml, "utf-8");
|
|
269
|
+
|
|
270
|
+
spinner.succeed(
|
|
271
|
+
`Successfully created component "${chalk.cyan(componentName)}"!`
|
|
272
|
+
);
|
|
273
|
+
console.log(chalk.gray(`Component ID: ${componentId}`));
|
|
274
|
+
console.log(chalk.gray(`Type: ${validatedType}`));
|
|
275
|
+
console.log(chalk.gray(`Path: static/${componentName}/dist`));
|
|
276
|
+
console.log(chalk.green("\nš Your component is ready!"));
|
|
277
|
+
console.log(chalk.white(` cd static/${componentName}`));
|
|
278
|
+
console.log(chalk.white(` npm run dev # Start development server`));
|
|
279
|
+
console.log(chalk.white(` npm run build # Build for production`));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error instanceof Error) {
|
|
282
|
+
spinner.fail(chalk.red(`Failed to add component: ${error.message}`));
|
|
283
|
+
} else {
|
|
284
|
+
spinner.fail(
|
|
285
|
+
chalk.red(
|
|
286
|
+
`Failed to add component "${chalk.cyan(componentName || "unknown")}"`
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Button, Typography, DSThemeContextProvider } from "@fireberry/ds";
|
|
3
|
+
|
|
4
|
+
function App() {
|
|
5
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
6
|
+
|
|
7
|
+
const handleButtonClick = async () => {
|
|
8
|
+
setIsLoading(true);
|
|
9
|
+
// Add your logic here
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
setIsLoading(false);
|
|
12
|
+
}, 1000);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<DSThemeContextProvider isRtl={false}>
|
|
17
|
+
<div>
|
|
18
|
+
<Typography type="h3" color="information">Hello from Fireberry!</Typography>
|
|
19
|
+
<Typography>This is a Fireberry component built with React + Vite</Typography>
|
|
20
|
+
<p style={{ marginBottom: "10px" }}>
|
|
21
|
+
<Button onClick={handleButtonClick} label="Click Me" isLoading={isLoading}></Button>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
</DSThemeContextProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default App;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import FireberryClientSDK from "@fireberry/sdk/client";
|
|
3
|
+
import { Button, Typography, DSThemeContextProvider } from "@fireberry/ds";
|
|
4
|
+
|
|
5
|
+
function App() {
|
|
6
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
7
|
+
const [initError, setInitError] = useState(null);
|
|
8
|
+
const [context, setContext] = useState(null);
|
|
9
|
+
const [api, setApi] = useState(null);
|
|
10
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11
|
+
|
|
12
|
+
// Create a new instance (memoized to avoid recreating on every render)
|
|
13
|
+
const client = useMemo(() => new FireberryClientSDK(), []);
|
|
14
|
+
|
|
15
|
+
// Initialize context on component mount
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const initializeContext = async () => {
|
|
18
|
+
try {
|
|
19
|
+
await client.initializeContext();
|
|
20
|
+
setIsInitialized(true);
|
|
21
|
+
setContext(client.context);
|
|
22
|
+
setApi(client.api);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
setInitError(error);
|
|
25
|
+
console.error("Failed to initialize context:", error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
initializeContext();
|
|
30
|
+
}, [client]);
|
|
31
|
+
|
|
32
|
+
const handleButtonClick = async () => {
|
|
33
|
+
setIsLoading(true)
|
|
34
|
+
if (context.record && context?.user.id) {
|
|
35
|
+
await api.update(context.record.type, context.record.id, {
|
|
36
|
+
ownerid: context?.user.id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
setIsLoading(false)
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<DSThemeContextProvider isRtl={false}>
|
|
44
|
+
{initError ? (
|
|
45
|
+
<div>
|
|
46
|
+
<h1>Error Initializing Context</h1>
|
|
47
|
+
<p>Error: {initError.message || String(initError)}</p>
|
|
48
|
+
</div>
|
|
49
|
+
) : isInitialized ? (
|
|
50
|
+
<div>
|
|
51
|
+
<Typography type="h3" color="information">Hello {context?.user.fullName}!</Typography>
|
|
52
|
+
<Typography >Click <Typography color="success" >Assign</Typography> button to assign record to current user ({context?.user.fullName})</Typography>
|
|
53
|
+
<p style={{ marginBottom: "10px" }}><Button onClick={handleButtonClick} label="Assign" isLoading={isLoading}></Button></p>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<div>
|
|
57
|
+
<h1>Initializing Context...</h1>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</DSThemeContextProvider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default App;
|