@wealthfolio/addon-dev-tools 1.0.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 +80 -0
- package/cli.js +318 -0
- package/dev-server.js +370 -0
- package/index.js +3 -0
- package/package.json +51 -0
- package/scaffold.js +163 -0
- package/templates/CHANGELOG.md.template +38 -0
- package/templates/README.md.template +27 -0
- package/templates/addon.tsx.template +45 -0
- package/templates/components-index.ts.template +3 -0
- package/templates/hooks-index.ts.template +3 -0
- package/templates/lib-index.ts.template +5 -0
- package/templates/manifest.json.template +19 -0
- package/templates/package.json.template +35 -0
- package/templates/pages-index.ts.template +4 -0
- package/templates/tsconfig.json.template +14 -0
- package/templates/types-index.ts.template +10 -0
- package/templates/vite.config.ts.template +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @wealthfolio/addon-dev-tools
|
|
2
|
+
|
|
3
|
+
Development tools for Wealthfolio addons including hot reload server and CLI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @wealthfolio/addon-dev-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CLI Commands
|
|
12
|
+
|
|
13
|
+
### Create New Addon
|
|
14
|
+
```bash
|
|
15
|
+
wealthfolio create my-awesome-addon
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Start Development Server
|
|
19
|
+
```bash
|
|
20
|
+
# In your addon directory
|
|
21
|
+
wealthfolio dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Build Addon
|
|
25
|
+
```bash
|
|
26
|
+
wealthfolio build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Package for Distribution
|
|
30
|
+
```bash
|
|
31
|
+
wealthfolio package
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Test Setup
|
|
35
|
+
```bash
|
|
36
|
+
wealthfolio test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development Server
|
|
40
|
+
|
|
41
|
+
The development server provides:
|
|
42
|
+
- Hot reload functionality
|
|
43
|
+
- File watching
|
|
44
|
+
- Auto-building
|
|
45
|
+
- Health check endpoints
|
|
46
|
+
|
|
47
|
+
### API Endpoints
|
|
48
|
+
|
|
49
|
+
- `GET /health` - Health check
|
|
50
|
+
- `GET /status` - Addon status and last modified time
|
|
51
|
+
- `GET /manifest.json` - Addon manifest
|
|
52
|
+
- `GET /addon.js` - Built addon code
|
|
53
|
+
- `GET /files` - List of built files
|
|
54
|
+
- `GET /test` - Test connectivity
|
|
55
|
+
|
|
56
|
+
## Usage in Addon Projects
|
|
57
|
+
|
|
58
|
+
Add to your addon's `package.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"scripts": {
|
|
63
|
+
"dev:server": "wealthfolio dev"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@wealthfolio/addon-dev-tools": "^1.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
This package is separate from `@wealthfolio/addon-sdk` to:
|
|
74
|
+
- Keep the SDK lightweight for production
|
|
75
|
+
- Avoid unnecessary dependencies in addon bundles
|
|
76
|
+
- Provide optional development tooling
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wealthfolio Addon CLI
|
|
5
|
+
*
|
|
6
|
+
* Command-line tool for developing, building, and managing addons
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { program } = require('commander');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { exec, spawn } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const readline = require('node:readline/promises');
|
|
15
|
+
const { stdin, stdout } = require('node:process');
|
|
16
|
+
const { AddonScaffold } = require('./scaffold');
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
// Colors for console output
|
|
21
|
+
const colors = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bright: '\x1b[1m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
blue: '\x1b[34m',
|
|
28
|
+
magenta: '\x1b[35m',
|
|
29
|
+
cyan: '\x1b[36m'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function log(message, color = colors.reset) {
|
|
33
|
+
console.log(`${color}${message}${colors.reset}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function error(message) {
|
|
37
|
+
log(`❌ ${message}`, colors.red);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function success(message) {
|
|
41
|
+
log(`✅ ${message}`, colors.green);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function info(message) {
|
|
45
|
+
log(`ℹ️ ${message}`, colors.blue);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function warn(message) {
|
|
49
|
+
log(`⚠️ ${message}`, colors.yellow);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize scaffold service
|
|
53
|
+
const scaffold = new AddonScaffold();
|
|
54
|
+
|
|
55
|
+
// Command: create
|
|
56
|
+
async function createAddon(name, options) {
|
|
57
|
+
try {
|
|
58
|
+
info(`Creating new addon: ${name}`);
|
|
59
|
+
|
|
60
|
+
// Prepare configuration
|
|
61
|
+
const config = {
|
|
62
|
+
name,
|
|
63
|
+
description: options.description,
|
|
64
|
+
author: options.author
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Validate configuration
|
|
68
|
+
const validationErrors = scaffold.validateConfig(config);
|
|
69
|
+
if (validationErrors.length > 0) {
|
|
70
|
+
error('Configuration errors:');
|
|
71
|
+
validationErrors.forEach(err => error(` - ${err}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Interactive prompts for missing information
|
|
76
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
77
|
+
if (interactive && (!config.description || !config.author)) {
|
|
78
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
79
|
+
try {
|
|
80
|
+
if (!config.description) {
|
|
81
|
+
const defaultDesc = `A Wealthfolio addon for ${name}`;
|
|
82
|
+
const answer = (await rl.question(`Description [${defaultDesc}]: `)).trim();
|
|
83
|
+
config.description = answer.length > 0 ? answer : defaultDesc;
|
|
84
|
+
}
|
|
85
|
+
if (!config.author) {
|
|
86
|
+
const defaultAuthor = 'Anonymous';
|
|
87
|
+
const answer = (await rl.question(`Author [${defaultAuthor}]: `)).trim();
|
|
88
|
+
config.author = answer.length > 0 ? answer : defaultAuthor;
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
rl.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Set defaults for non-interactive mode
|
|
96
|
+
if (!config.description) {
|
|
97
|
+
config.description = `A Wealthfolio addon for ${name}`;
|
|
98
|
+
}
|
|
99
|
+
if (!config.author) {
|
|
100
|
+
config.author = 'Anonymous';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const addonId = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
104
|
+
const addonDir = path.resolve(process.cwd(), addonId);
|
|
105
|
+
|
|
106
|
+
// Check if directory already exists
|
|
107
|
+
if (fs.existsSync(addonDir)) {
|
|
108
|
+
error(`Directory ${addonId} already exists`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add current date for changelog
|
|
113
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
114
|
+
config.currentDate = currentDate;
|
|
115
|
+
|
|
116
|
+
// Create addon using scaffold service
|
|
117
|
+
const result = await scaffold.createAddon(config, addonDir);
|
|
118
|
+
|
|
119
|
+
success(`Addon ${name} created successfully!`);
|
|
120
|
+
info(`Directory: ${result.addonDir}`);
|
|
121
|
+
info(`Addon ID: ${result.addonId}`);
|
|
122
|
+
info(`Package name: ${result.packageName}`);
|
|
123
|
+
info(`Structure created:`);
|
|
124
|
+
info(` ├── src/`);
|
|
125
|
+
info(` │ ├── addon.tsx # Main addon entry point`);
|
|
126
|
+
info(` │ ├── components/ # React components`);
|
|
127
|
+
info(` │ ├── hooks/ # React hooks`);
|
|
128
|
+
info(` │ ├── pages/ # Addon pages`);
|
|
129
|
+
info(` │ ├── lib/ # Utility functions and shared logic`);
|
|
130
|
+
info(` │ └── types/ # Type definitions`);
|
|
131
|
+
info(` ├── dist/ # Built files (generated)`);
|
|
132
|
+
info(` ├── manifest.json # Addon metadata and permissions`);
|
|
133
|
+
info(` ├── package.json # NPM package configuration`);
|
|
134
|
+
info(` ├── vite.config.ts # Build configuration`);
|
|
135
|
+
info(` ├── tsconfig.json # TypeScript configuration`);
|
|
136
|
+
info(` ├── CHANGELOG.md # Version history and release notes`);
|
|
137
|
+
info(` └── README.md # Documentation`);
|
|
138
|
+
info(`Next steps:`);
|
|
139
|
+
info(` 1. cd ${addonId}`);
|
|
140
|
+
info(` 2. pnpm install`);
|
|
141
|
+
info(` 3. pnpm run dev:server`);
|
|
142
|
+
|
|
143
|
+
} catch (err) {
|
|
144
|
+
error(`Failed to create addon: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Command: dev
|
|
149
|
+
async function startDev(port = 3001) {
|
|
150
|
+
try {
|
|
151
|
+
const manifestPath = path.resolve(process.cwd(), 'manifest.json');
|
|
152
|
+
|
|
153
|
+
if (!fs.existsSync(manifestPath)) {
|
|
154
|
+
error('No manifest.json found. Are you in an addon directory?');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
159
|
+
|
|
160
|
+
info(`Starting development server for ${manifest.name}`);
|
|
161
|
+
info(`Server will run on http://localhost:${port}`);
|
|
162
|
+
|
|
163
|
+
// Start the development server
|
|
164
|
+
const devServerPath = path.resolve(__dirname, 'dev-server.js');
|
|
165
|
+
const child = spawn('node', [devServerPath, process.cwd(), port.toString()], {
|
|
166
|
+
stdio: 'inherit'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Handle cleanup on exit
|
|
170
|
+
process.on('SIGINT', () => {
|
|
171
|
+
child.kill('SIGINT');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
child.on('exit', (code) => {
|
|
176
|
+
process.exit(code);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
} catch (err) {
|
|
180
|
+
error(`Failed to start development server: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Command: build
|
|
185
|
+
async function buildAddon() {
|
|
186
|
+
try {
|
|
187
|
+
info('Building addon...');
|
|
188
|
+
|
|
189
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
190
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
191
|
+
error('No package.json found. Are you in an addon directory?');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await execAsync('pnpm run build');
|
|
196
|
+
success('Addon built successfully!');
|
|
197
|
+
|
|
198
|
+
} catch (err) {
|
|
199
|
+
error(`Build failed: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Command: package
|
|
204
|
+
async function packageAddon() {
|
|
205
|
+
try {
|
|
206
|
+
info('Packaging addon...');
|
|
207
|
+
|
|
208
|
+
// Build first
|
|
209
|
+
await buildAddon();
|
|
210
|
+
|
|
211
|
+
// Create package
|
|
212
|
+
await execAsync('pnpm run package');
|
|
213
|
+
success('Addon packaged successfully!');
|
|
214
|
+
|
|
215
|
+
} catch (err) {
|
|
216
|
+
error(`Packaging failed: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Command: test
|
|
221
|
+
async function testSetup() {
|
|
222
|
+
try {
|
|
223
|
+
info('Testing addon development setup...');
|
|
224
|
+
|
|
225
|
+
// Check if manifest exists
|
|
226
|
+
const manifestPath = path.resolve(process.cwd(), 'manifest.json');
|
|
227
|
+
if (!fs.existsSync(manifestPath)) {
|
|
228
|
+
error('❌ No manifest.json found. Are you in an addon directory?');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
233
|
+
success(`✅ Found manifest for: ${manifest.name}`);
|
|
234
|
+
|
|
235
|
+
// Check if dist exists
|
|
236
|
+
const distPath = path.resolve(process.cwd(), 'dist');
|
|
237
|
+
if (!fs.existsSync(distPath)) {
|
|
238
|
+
warn('⚠️ No dist directory found. Run `pnpm run build` first.');
|
|
239
|
+
} else {
|
|
240
|
+
success('✅ Dist directory exists');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if dev server is running
|
|
244
|
+
try {
|
|
245
|
+
const response = await fetch('http://localhost:3001/test');
|
|
246
|
+
if (response.ok) {
|
|
247
|
+
const data = await response.json();
|
|
248
|
+
success('✅ Development server is running');
|
|
249
|
+
info(` Server message: ${data.message}`);
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
warn('⚠️ Development server not running on port 3001');
|
|
253
|
+
info(' Start it with: pnpm run dev:server');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
info('\nNext steps:');
|
|
257
|
+
info('1. Start dev server: pnpm run dev:server');
|
|
258
|
+
info('2. Start main app in dev mode');
|
|
259
|
+
info('3. Check console: discoverAddons()');
|
|
260
|
+
|
|
261
|
+
} catch (err) {
|
|
262
|
+
error(`Test failed: ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Command: install
|
|
267
|
+
async function installAddon(zipPath) {
|
|
268
|
+
try {
|
|
269
|
+
info(`Installing addon from ${zipPath}`);
|
|
270
|
+
|
|
271
|
+
// This would integrate with the main app's addon installation
|
|
272
|
+
warn('Install command not yet implemented. Use the main app to install.');
|
|
273
|
+
|
|
274
|
+
} catch (err) {
|
|
275
|
+
error(`Installation failed: ${err.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// CLI Setup
|
|
280
|
+
program
|
|
281
|
+
.name('wealthfolio')
|
|
282
|
+
.description('Wealthfolio Addon Development CLI')
|
|
283
|
+
.version('1.0.0');
|
|
284
|
+
|
|
285
|
+
program
|
|
286
|
+
.command('create <name>')
|
|
287
|
+
.description('Create a new addon')
|
|
288
|
+
.option('-d, --description <desc>', 'Addon description')
|
|
289
|
+
.option('-a, --author <author>', 'Addon author')
|
|
290
|
+
.action(createAddon);
|
|
291
|
+
|
|
292
|
+
program
|
|
293
|
+
.command('dev')
|
|
294
|
+
.description('Start development server')
|
|
295
|
+
.option('-p, --port <port>', 'Port number', '3001')
|
|
296
|
+
.action((options) => startDev(parseInt(options.port)));
|
|
297
|
+
|
|
298
|
+
program
|
|
299
|
+
.command('build')
|
|
300
|
+
.description('Build the addon')
|
|
301
|
+
.action(buildAddon);
|
|
302
|
+
|
|
303
|
+
program
|
|
304
|
+
.command('package')
|
|
305
|
+
.description('Package the addon for distribution')
|
|
306
|
+
.action(packageAddon);
|
|
307
|
+
|
|
308
|
+
program
|
|
309
|
+
.command('test')
|
|
310
|
+
.description('Test addon development setup')
|
|
311
|
+
.action(testSetup);
|
|
312
|
+
|
|
313
|
+
program
|
|
314
|
+
.command('install <zip>')
|
|
315
|
+
.description('Install an addon from zip file')
|
|
316
|
+
.action(installAddon);
|
|
317
|
+
|
|
318
|
+
program.parse();
|
package/dev-server.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Addon Development Server
|
|
6
|
+
*
|
|
7
|
+
* A simple development server for hot reloading addons during development.
|
|
8
|
+
* This server watches for file changes and provides a hot reload endpoint.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const express = require('express');
|
|
12
|
+
const cors = require('cors');
|
|
13
|
+
const chokidar = require('chokidar');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const { exec } = require('child_process');
|
|
17
|
+
const { promisify } = require('util');
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
|
|
21
|
+
class AddonDevServer {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.app = express();
|
|
25
|
+
this.lastModified = new Date();
|
|
26
|
+
this.buildInProgress = false;
|
|
27
|
+
this.viteWatcher = null;
|
|
28
|
+
|
|
29
|
+
this.setupMiddleware();
|
|
30
|
+
this.setupRoutes();
|
|
31
|
+
this.setupFileWatcher();
|
|
32
|
+
this.startViteWatcher();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setupMiddleware() {
|
|
36
|
+
this.app.use(cors({
|
|
37
|
+
origin: ['http://localhost:1420', 'http://localhost:3000'],
|
|
38
|
+
credentials: true
|
|
39
|
+
}));
|
|
40
|
+
this.app.use(express.static(this.config.addonPath));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setupRoutes() {
|
|
44
|
+
// Health check endpoint
|
|
45
|
+
this.app.get('/health', (req, res) => {
|
|
46
|
+
res.json({
|
|
47
|
+
status: 'ok',
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
addonPath: this.config.addonPath
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Addon status endpoint
|
|
54
|
+
this.app.get('/status', (req, res) => {
|
|
55
|
+
res.json({
|
|
56
|
+
lastModified: this.lastModified.toISOString(),
|
|
57
|
+
buildInProgress: this.buildInProgress,
|
|
58
|
+
files: this.getFileList()
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Serve addon manifest
|
|
63
|
+
this.app.get('/manifest.json', (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const manifestPath = path.resolve(this.config.manifestPath);
|
|
66
|
+
if (fs.existsSync(manifestPath)) {
|
|
67
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
68
|
+
res.json(manifest);
|
|
69
|
+
} else {
|
|
70
|
+
res.status(404).json({ error: 'Manifest not found' });
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
res.status(500).json({ error: 'Failed to read manifest' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Serve addon code
|
|
78
|
+
this.app.get('/addon.js', async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const addonFile = path.resolve(this.config.addonPath, 'dist/addon.js');
|
|
81
|
+
console.log(`📦 Serving addon.js from: ${addonFile}`);
|
|
82
|
+
|
|
83
|
+
// Wait for file to exist (with timeout)
|
|
84
|
+
const fileExists = await this.waitForFile(addonFile, 3000);
|
|
85
|
+
|
|
86
|
+
if (fileExists) {
|
|
87
|
+
const code = fs.readFileSync(addonFile, 'utf-8');
|
|
88
|
+
res.type('application/javascript').send(code);
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`❌ Addon file not found at: ${addonFile}`);
|
|
91
|
+
res.status(404).json({ error: 'Addon file not found. Run build first.', path: addonFile });
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`❌ Error serving addon.js:`, error);
|
|
95
|
+
res.status(500).json({ error: 'Failed to read addon file', details: error.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Hot reload endpoint
|
|
100
|
+
this.app.get('/reload', (req, res) => {
|
|
101
|
+
res.json({
|
|
102
|
+
message: 'Reload triggered',
|
|
103
|
+
timestamp: new Date().toISOString()
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Trigger rebuild if configured
|
|
107
|
+
if (this.config.buildCommand) {
|
|
108
|
+
this.triggerBuild();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// File listing for debugging
|
|
113
|
+
this.app.get('/files', (req, res) => {
|
|
114
|
+
res.json({
|
|
115
|
+
files: this.getFileList(),
|
|
116
|
+
watchPaths: this.config.watchPaths
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Test endpoint for connectivity
|
|
121
|
+
this.app.get('/test', (req, res) => {
|
|
122
|
+
res.json({
|
|
123
|
+
message: 'Addon development server is working!',
|
|
124
|
+
addonPath: this.config.addonPath,
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
manifest: this.getManifestInfo()
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Debug endpoint for troubleshooting
|
|
131
|
+
this.app.get('/debug', (req, res) => {
|
|
132
|
+
const addonFile = path.resolve(this.config.addonPath, 'dist/addon.js');
|
|
133
|
+
res.json({
|
|
134
|
+
lastModified: this.lastModified.toISOString(),
|
|
135
|
+
buildInProgress: this.buildInProgress,
|
|
136
|
+
files: this.getFileList(),
|
|
137
|
+
watchPaths: this.config.watchPaths,
|
|
138
|
+
viteWatcherRunning: this.viteWatcher !== null,
|
|
139
|
+
addonFile: {
|
|
140
|
+
path: addonFile,
|
|
141
|
+
exists: fs.existsSync(addonFile),
|
|
142
|
+
size: fs.existsSync(addonFile) ? fs.statSync(addonFile).size : 0
|
|
143
|
+
},
|
|
144
|
+
config: {
|
|
145
|
+
port: this.config.port,
|
|
146
|
+
buildCommand: this.config.buildCommand
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Simple ping endpoint
|
|
152
|
+
this.app.get('/ping', (req, res) => {
|
|
153
|
+
res.json({ message: 'pong', timestamp: new Date().toISOString() });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setupFileWatcher() {
|
|
158
|
+
const watcher = chokidar.watch(this.config.watchPaths, {
|
|
159
|
+
ignored: /node_modules|\.git/,
|
|
160
|
+
persistent: true,
|
|
161
|
+
ignoreInitial: true
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
watcher.on('change', (filePath) => {
|
|
165
|
+
console.log(`📝 File changed: ${filePath}`);
|
|
166
|
+
// Don't trigger manual build since Vite is already watching
|
|
167
|
+
// Just update the timestamp for status endpoint
|
|
168
|
+
this.lastModified = new Date();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watcher.on('add', (filePath) => {
|
|
172
|
+
console.log(`➕ File added: ${filePath}`);
|
|
173
|
+
this.lastModified = new Date();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
watcher.on('unlink', (filePath) => {
|
|
177
|
+
console.log(`➖ File removed: ${filePath}`);
|
|
178
|
+
this.lastModified = new Date();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log(`👀 Watching files: ${this.config.watchPaths.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async triggerBuild() {
|
|
185
|
+
if (this.buildInProgress || !this.config.buildCommand) return;
|
|
186
|
+
|
|
187
|
+
this.buildInProgress = true;
|
|
188
|
+
console.log(`🔨 Building addon with: ${this.config.buildCommand}`);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await execAsync(this.config.buildCommand, {
|
|
192
|
+
cwd: this.config.addonPath
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log('✅ Build completed successfully');
|
|
196
|
+
this.lastModified = new Date();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('❌ Build failed:', error);
|
|
199
|
+
} finally {
|
|
200
|
+
this.buildInProgress = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getFileList() {
|
|
205
|
+
try {
|
|
206
|
+
const distPath = path.resolve(this.config.addonPath, 'dist');
|
|
207
|
+
if (fs.existsSync(distPath)) {
|
|
208
|
+
return fs.readdirSync(distPath).map(file => `dist/${file}`);
|
|
209
|
+
}
|
|
210
|
+
return [];
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getManifestInfo() {
|
|
217
|
+
try {
|
|
218
|
+
const manifestPath = path.resolve(this.config.manifestPath);
|
|
219
|
+
if (fs.existsSync(manifestPath)) {
|
|
220
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Wait for a file to exist with timeout
|
|
230
|
+
*/
|
|
231
|
+
async waitForFile(filePath, timeout = 3000) {
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
const checkInterval = 100;
|
|
234
|
+
|
|
235
|
+
while (Date.now() - startTime < timeout) {
|
|
236
|
+
if (fs.existsSync(filePath)) {
|
|
237
|
+
// Additional check to ensure file is fully written
|
|
238
|
+
try {
|
|
239
|
+
const stats = fs.statSync(filePath);
|
|
240
|
+
if (stats.size > 0) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
// File might be in the process of being written
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
startViteWatcher() {
|
|
255
|
+
if (!this.config.buildCommand) return;
|
|
256
|
+
|
|
257
|
+
console.log('🔨 Starting Vite in watch mode...');
|
|
258
|
+
|
|
259
|
+
// Start vite build in watch mode
|
|
260
|
+
const { spawn } = require('child_process');
|
|
261
|
+
this.viteWatcher = spawn('npm', ['run', 'dev'], {
|
|
262
|
+
cwd: this.config.addonPath,
|
|
263
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.viteWatcher.stdout.on('data', (data) => {
|
|
267
|
+
const output = data.toString();
|
|
268
|
+
console.log(`Vite output: ${output.trim()}`);
|
|
269
|
+
|
|
270
|
+
if (output.includes('build started')) {
|
|
271
|
+
this.buildInProgress = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (output.includes('built in')) {
|
|
275
|
+
console.log(`✅ Vite rebuild completed`);
|
|
276
|
+
this.lastModified = new Date();
|
|
277
|
+
this.buildInProgress = false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (output.includes('watching for file changes')) {
|
|
281
|
+
console.log(`✅ Vite watcher ready`);
|
|
282
|
+
this.buildInProgress = false;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this.viteWatcher.stderr.on('data', (data) => {
|
|
287
|
+
console.error(`Vite error: ${data}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this.viteWatcher.on('close', (code) => {
|
|
291
|
+
if (code !== 0) {
|
|
292
|
+
console.error(`Vite watcher exited with code ${code}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
start() {
|
|
298
|
+
this.app.listen(this.config.port, () => {
|
|
299
|
+
console.log(`🚀 Addon dev server running on http://localhost:${this.config.port}`);
|
|
300
|
+
console.log(`📁 Serving from: ${this.config.addonPath}`);
|
|
301
|
+
console.log(`📋 Manifest: ${this.config.manifestPath}`);
|
|
302
|
+
console.log(`👀 Watching files: ${this.config.watchPaths.join(', ')}`);
|
|
303
|
+
|
|
304
|
+
if (this.config.buildCommand) {
|
|
305
|
+
console.log(`🔨 Build command: ${this.config.buildCommand}`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Handle graceful shutdown
|
|
310
|
+
process.on('SIGINT', () => {
|
|
311
|
+
this.stop();
|
|
312
|
+
process.exit(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
process.on('SIGTERM', () => {
|
|
316
|
+
this.stop();
|
|
317
|
+
process.exit(0);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
stop() {
|
|
322
|
+
console.log('🛑 Shutting down dev server...');
|
|
323
|
+
|
|
324
|
+
if (this.viteWatcher) {
|
|
325
|
+
this.viteWatcher.kill('SIGTERM');
|
|
326
|
+
this.viteWatcher = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// CLI interface
|
|
332
|
+
function main() {
|
|
333
|
+
const args = process.argv.slice(2);
|
|
334
|
+
const addonPath = args[0] || process.cwd();
|
|
335
|
+
const port = parseInt(args[1]) || 3001;
|
|
336
|
+
|
|
337
|
+
const config = {
|
|
338
|
+
port,
|
|
339
|
+
addonPath: path.resolve(addonPath),
|
|
340
|
+
manifestPath: path.resolve(addonPath, 'manifest.json'),
|
|
341
|
+
buildCommand: 'npm run build',
|
|
342
|
+
watchPaths: [
|
|
343
|
+
path.resolve(addonPath, 'src'),
|
|
344
|
+
path.resolve(addonPath, 'manifest.json')
|
|
345
|
+
]
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Check if addon directory exists
|
|
349
|
+
if (!fs.existsSync(config.addonPath)) {
|
|
350
|
+
console.error(`❌ Addon directory not found: ${config.addonPath}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check if manifest exists
|
|
355
|
+
if (!fs.existsSync(config.manifestPath)) {
|
|
356
|
+
console.error(`❌ Manifest not found: ${config.manifestPath}`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const server = new AddonDevServer(config);
|
|
361
|
+
server.start();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Export for use as a module
|
|
365
|
+
module.exports = { AddonDevServer };
|
|
366
|
+
|
|
367
|
+
// Run if called directly
|
|
368
|
+
if (require.main === module) {
|
|
369
|
+
main();
|
|
370
|
+
}
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wealthfolio/addon-dev-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Development tools for Wealthfolio addons - hot reload server and CLI",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wealthfolio": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"wealthfolio",
|
|
11
|
+
"addon",
|
|
12
|
+
"development",
|
|
13
|
+
"cli",
|
|
14
|
+
"hot-reload"
|
|
15
|
+
],
|
|
16
|
+
"author": "Wealthfolio Team",
|
|
17
|
+
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/afadil/wealthfolio.git",
|
|
22
|
+
"directory": "packages/addon-dev-tools"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/afadil/wealthfolio/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/afadil/wealthfolio/tree/main/packages/addon-dev-tools#readme",
|
|
28
|
+
"files": [
|
|
29
|
+
"cli.js",
|
|
30
|
+
"dev-server.js",
|
|
31
|
+
"index.js",
|
|
32
|
+
"scaffold.js",
|
|
33
|
+
"templates/",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^14.0.0",
|
|
44
|
+
"express": "^4.21.2",
|
|
45
|
+
"cors": "^2.8.5",
|
|
46
|
+
"chokidar": "^4.0.3"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/scaffold.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scaffold service for creating new addons from templates
|
|
6
|
+
*/
|
|
7
|
+
class AddonScaffold {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.templatesDir = path.join(__dirname, 'templates');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get all available templates
|
|
14
|
+
*/
|
|
15
|
+
getAvailableTemplates() {
|
|
16
|
+
if (!fs.existsSync(this.templatesDir)) {
|
|
17
|
+
throw new Error('Templates directory not found');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return fs.readdirSync(this.templatesDir)
|
|
21
|
+
.filter(file => file.endsWith('.template'))
|
|
22
|
+
.map(file => file.replace('.template', ''));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load a template file
|
|
27
|
+
*/
|
|
28
|
+
loadTemplate(templateName) {
|
|
29
|
+
const templatePath = path.join(this.templatesDir, `${templateName}.template`);
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(templatePath)) {
|
|
32
|
+
throw new Error(`Template ${templateName} not found`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Replace template variables with actual values
|
|
40
|
+
*/
|
|
41
|
+
processTemplate(content, replacements) {
|
|
42
|
+
let result = content;
|
|
43
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
44
|
+
const pattern = new RegExp(`{{${key}}}`, 'g');
|
|
45
|
+
result = result.replace(pattern, value);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate replacements object from addon config
|
|
52
|
+
*/
|
|
53
|
+
generateReplacements(config) {
|
|
54
|
+
const addonId = config.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
55
|
+
const packageName = `wealthfolio-${addonId}-addon`;
|
|
56
|
+
const componentName = config.name.replace(/[^a-zA-Z0-9]/g, '');
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
addonId,
|
|
60
|
+
addonName: config.name,
|
|
61
|
+
packageName,
|
|
62
|
+
componentName,
|
|
63
|
+
description: config.description || `A Wealthfolio addon for ${config.name}`,
|
|
64
|
+
author: config.author || 'Anonymous'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create addon structure
|
|
70
|
+
*/
|
|
71
|
+
async createAddon(config, targetDir) {
|
|
72
|
+
const replacements = this.generateReplacements(config);
|
|
73
|
+
|
|
74
|
+
// Create directory structure
|
|
75
|
+
if (!fs.existsSync(targetDir)) {
|
|
76
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create src directory and subdirectories
|
|
80
|
+
const srcDir = path.join(targetDir, 'src');
|
|
81
|
+
const srcSubDirs = ['components', 'hooks', 'pages', 'lib', 'types'];
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(srcDir)) {
|
|
84
|
+
fs.mkdirSync(srcDir);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create all subdirectories
|
|
88
|
+
srcSubDirs.forEach(subDir => {
|
|
89
|
+
const subDirPath = path.join(srcDir, subDir);
|
|
90
|
+
if (!fs.existsSync(subDirPath)) {
|
|
91
|
+
fs.mkdirSync(subDirPath, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Template file mappings
|
|
96
|
+
const fileTemplates = [
|
|
97
|
+
{ template: 'manifest.json', output: 'manifest.json' },
|
|
98
|
+
{ template: 'package.json', output: 'package.json' },
|
|
99
|
+
{ template: 'vite.config.ts', output: 'vite.config.ts' },
|
|
100
|
+
{ template: 'tsconfig.json', output: 'tsconfig.json' },
|
|
101
|
+
{ template: 'README.md', output: 'README.md' },
|
|
102
|
+
{ template: 'CHANGELOG.md', output: 'CHANGELOG.md' },
|
|
103
|
+
{ template: 'addon.tsx', output: 'src/addon.tsx' },
|
|
104
|
+
{ template: 'components-index.ts', output: 'src/components/index.ts' },
|
|
105
|
+
{ template: 'hooks-index.ts', output: 'src/hooks/index.ts' },
|
|
106
|
+
{ template: 'pages-index.ts', output: 'src/pages/index.ts' },
|
|
107
|
+
{ template: 'lib-index.ts', output: 'src/lib/index.ts' },
|
|
108
|
+
{ template: 'types-index.ts', output: 'src/types/index.ts' }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Process and write each template
|
|
112
|
+
for (const { template, output } of fileTemplates) {
|
|
113
|
+
try {
|
|
114
|
+
const templateContent = this.loadTemplate(template);
|
|
115
|
+
const processedContent = this.processTemplate(templateContent, replacements);
|
|
116
|
+
const outputPath = path.join(targetDir, output);
|
|
117
|
+
|
|
118
|
+
// Ensure directory exists
|
|
119
|
+
const outputDir = path.dirname(outputPath);
|
|
120
|
+
if (!fs.existsSync(outputDir)) {
|
|
121
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(outputPath, processedContent);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to process template ${template}: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
addonDir: targetDir,
|
|
132
|
+
addonId: replacements.addonId,
|
|
133
|
+
packageName: replacements.packageName
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate addon configuration
|
|
139
|
+
*/
|
|
140
|
+
validateConfig(config) {
|
|
141
|
+
const errors = [];
|
|
142
|
+
|
|
143
|
+
if (!config.name || typeof config.name !== 'string') {
|
|
144
|
+
errors.push('Addon name is required and must be a string');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (config.name && config.name.trim().length === 0) {
|
|
148
|
+
errors.push('Addon name cannot be empty');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (config.description && typeof config.description !== 'string') {
|
|
152
|
+
errors.push('Description must be a string');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (config.author && typeof config.author !== 'string') {
|
|
156
|
+
errors.push('Author must be a string');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return errors;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { AddonScaffold };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the {{name}} addon will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial addon structure and setup
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
### Deprecated
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - {{currentDate}}
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Initial release of {{name}} addon
|
|
27
|
+
- Basic addon functionality and core features
|
|
28
|
+
- Integration with Wealthfolio addon SDK v1.0.0
|
|
29
|
+
- Sidebar navigation integration for easy access
|
|
30
|
+
- Responsive design for all screen sizes
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
- {{description}}
|
|
34
|
+
- User-friendly interface
|
|
35
|
+
- Compatible with Wealthfolio platform
|
|
36
|
+
|
|
37
|
+
### Permissions
|
|
38
|
+
- UI components access for sidebar and routing
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{addonName}}
|
|
2
|
+
|
|
3
|
+
{{description}}
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
npm install
|
|
10
|
+
|
|
11
|
+
# Start development server
|
|
12
|
+
npm run dev:server
|
|
13
|
+
|
|
14
|
+
# Build for production
|
|
15
|
+
npm run build
|
|
16
|
+
|
|
17
|
+
# Package addon
|
|
18
|
+
npm run bundle
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Add your features here
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AddonContext } from '@wealthfolio/addon-sdk';
|
|
3
|
+
import { Card, CardContent, Icons } from '@wealthfolio/ui';
|
|
4
|
+
|
|
5
|
+
function AddonExample({ ctx }: { ctx: AddonContext }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="p-6">
|
|
8
|
+
<Card>
|
|
9
|
+
<CardContent className="p-6">
|
|
10
|
+
<h1 className="text-2xl font-semibold mb-2">{{addonName}}</h1>
|
|
11
|
+
<p className="text-muted-foreground">
|
|
12
|
+
Welcome to your new Wealthfolio addon! Start building amazing features.
|
|
13
|
+
</p>
|
|
14
|
+
</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function enable(ctx: AddonContext) {
|
|
21
|
+
// Add a sidebar item
|
|
22
|
+
const sidebarItem = ctx.sidebar.addItem({
|
|
23
|
+
id: '{{addonId}}',
|
|
24
|
+
label: '{{addonName}}',
|
|
25
|
+
icon: <Icons.Blocks className="h-5 w-5" />,
|
|
26
|
+
route: '/addon/{{addonId}}',
|
|
27
|
+
order: 100,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Add a route
|
|
31
|
+
const Wrapper = () => <AddonExample ctx={ctx} />;
|
|
32
|
+
ctx.router.add({
|
|
33
|
+
path: '/addon/{{addonId}}',
|
|
34
|
+
component: React.lazy(() => Promise.resolve({ default: Wrapper })),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Cleanup on disable
|
|
38
|
+
ctx.onDisable(() => {
|
|
39
|
+
try {
|
|
40
|
+
sidebarItem.remove();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
ctx.api.logger.error('Failed to remove sidebar item:', err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "{{addonId}}",
|
|
3
|
+
"name": "{{addonName}}",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "{{description}}",
|
|
6
|
+
"author": "{{author}}",
|
|
7
|
+
"main": "dist/addon.js",
|
|
8
|
+
"sdkVersion": "1.0.0",
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"permissions": [
|
|
11
|
+
{
|
|
12
|
+
"category": "ui",
|
|
13
|
+
"functions": ["sidebar.addItem"],
|
|
14
|
+
"purpose": "Add navigation items to the sidebar"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"keywords": ["wealthfolio", "addon"],
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "{{description}}",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/addon.js",
|
|
7
|
+
"keywords": ["wealthfolio", "addon"],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"dev": "vite build --watch",
|
|
12
|
+
"dev:server": "wealthfolio dev",
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"package": "mkdir -p dist && zip -r dist/$npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md",
|
|
15
|
+
"bundle": "pnpm clean && pnpm build && pnpm package",
|
|
16
|
+
"lint": "tsc --noEmit",
|
|
17
|
+
"type-check": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@wealthfolio/addon-sdk": "^1.0.0",
|
|
21
|
+
"@wealthfolio/ui": "^1.0.0",
|
|
22
|
+
"react": "^18.3.1",
|
|
23
|
+
"react-dom": "^18.3.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@wealthfolio/addon-dev-tools": "^1.0.0",
|
|
27
|
+
"@types/node": "^22.14.0",
|
|
28
|
+
"@types/react": "^18.3.11",
|
|
29
|
+
"@types/react-dom": "^18.3.0",
|
|
30
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
31
|
+
"rollup-plugin-external-globals": "^0.13.0",
|
|
32
|
+
"typescript": "^5.8.3",
|
|
33
|
+
"vite": "^6.2.7"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"types": ["node", "react", "react-dom"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Export your type definitions here
|
|
2
|
+
// Example:
|
|
3
|
+
// export interface AddonConfig {
|
|
4
|
+
// enabled: boolean;
|
|
5
|
+
// settings: Record<string, any>;
|
|
6
|
+
// }
|
|
7
|
+
//
|
|
8
|
+
// export type AddonPageProps = {
|
|
9
|
+
// onSettingsChange: (settings: Record<string, any>) => void;
|
|
10
|
+
// };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import externalGlobals from 'rollup-plugin-external-globals';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
define: {
|
|
8
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
9
|
+
},
|
|
10
|
+
build: {
|
|
11
|
+
lib: {
|
|
12
|
+
entry: 'src/addon.tsx',
|
|
13
|
+
fileName: () => 'addon.js',
|
|
14
|
+
formats: ['es'],
|
|
15
|
+
},
|
|
16
|
+
rollupOptions: {
|
|
17
|
+
external: ['react', 'react-dom'],
|
|
18
|
+
plugins: [
|
|
19
|
+
externalGlobals({
|
|
20
|
+
react: 'React',
|
|
21
|
+
'react-dom': 'ReactDOM'
|
|
22
|
+
})
|
|
23
|
+
],
|
|
24
|
+
output: {
|
|
25
|
+
globals: {
|
|
26
|
+
react: 'React',
|
|
27
|
+
'react-dom': 'ReactDOM',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
outDir: 'dist',
|
|
32
|
+
minify: false,
|
|
33
|
+
sourcemap: false,
|
|
34
|
+
watch: {
|
|
35
|
+
// Watch mode options for better hot reloading
|
|
36
|
+
include: ['src/**'],
|
|
37
|
+
exclude: ['node_modules/**', 'dist/**']
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|