@wealthfolio/addon-dev-tools 3.6.1 → 3.7.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 +32 -2
- package/dev-server.js +349 -14
- package/package.json +2 -2
- package/templates/CHANGELOG.md.template +3 -3
- package/templates/README.md.template +8 -0
- package/templates/addon.tsx.template +31 -28
- package/templates/manifest.json.template +19 -11
- package/templates/package.json.template +7 -7
- package/templates/vite.config.ts.template +32 -31
package/README.md
CHANGED
|
@@ -47,6 +47,11 @@ wealthfolio-addon test
|
|
|
47
47
|
|
|
48
48
|
## Development Server
|
|
49
49
|
|
|
50
|
+
> **Version compatibility:** Wealthfolio 3.7 requires
|
|
51
|
+
> `@wealthfolio/addon-dev-tools` 3.7 or newer. If the app reports that the
|
|
52
|
+
> server does not support v3.7 runtime packages, update this package and restart
|
|
53
|
+
> the development server.
|
|
54
|
+
|
|
50
55
|
The development server provides:
|
|
51
56
|
|
|
52
57
|
- Hot reload functionality
|
|
@@ -57,12 +62,37 @@ The development server provides:
|
|
|
57
62
|
### API Endpoints
|
|
58
63
|
|
|
59
64
|
- `GET /health` - Health check
|
|
60
|
-
- `GET /status` -
|
|
65
|
+
- `GET /status` - Build state and published runtime-package generation
|
|
61
66
|
- `GET /manifest.json` - Addon manifest
|
|
62
67
|
- `GET /addon.js` - Built addon code
|
|
68
|
+
- `GET /runtime-package` - One coherent manifest, code, and asset-metadata
|
|
69
|
+
snapshot
|
|
70
|
+
- `GET /runtime-files` - Built JavaScript and CSS modules
|
|
71
|
+
- `GET /runtime-assets` - Packaged asset metadata
|
|
72
|
+
- `GET /runtime-assets/:assetId?generation=<id>` - One asset from a published
|
|
73
|
+
generation
|
|
63
74
|
- `GET /files` - List of built files
|
|
64
75
|
- `GET /test` - Test connectivity
|
|
65
76
|
|
|
77
|
+
The host loads `/runtime-package` first, then requests asset bytes from the same
|
|
78
|
+
generation. The server retains the four most recent immutable generations so a
|
|
79
|
+
reload cannot mix new metadata with old bytes. A generation older than that
|
|
80
|
+
window is intentionally unavailable and the host must load the current package
|
|
81
|
+
snapshot again. `/manifest.json` and `/addon.js` remain diagnostic/legacy
|
|
82
|
+
endpoints; Wealthfolio 3.7 live loading does not assemble a runtime from them.
|
|
83
|
+
|
|
84
|
+
Files below `assets/**` and non-code files below `dist/assets/**` are published
|
|
85
|
+
as private asset metadata and lazy byte responses. JavaScript and CSS remain in
|
|
86
|
+
`/runtime-files`. The same 256-entry, 5 MiB-per-file, and 25 MiB-package limits
|
|
87
|
+
used during installation are enforced during development.
|
|
88
|
+
|
|
89
|
+
Generated projects pin `build.target` to Chrome/Edge 107, Firefox 104, and
|
|
90
|
+
Safari 16, matching Wealthfolio 3.7. Keep that explicit target when customizing
|
|
91
|
+
Vite so a future Vite default cannot silently raise the addon's browser floor.
|
|
92
|
+
The sandbox supports packaged images, fonts, media, CSS, and WebAssembly, but
|
|
93
|
+
does not allow Worker/service-worker entry points, popups, direct network
|
|
94
|
+
requests, or remote CSS imports.
|
|
95
|
+
|
|
66
96
|
## Usage in Addon Projects
|
|
67
97
|
|
|
68
98
|
Add to your addon's `package.json`:
|
|
@@ -73,7 +103,7 @@ Add to your addon's `package.json`:
|
|
|
73
103
|
"dev:server": "wealthfolio-addon dev"
|
|
74
104
|
},
|
|
75
105
|
"devDependencies": {
|
|
76
|
-
"@wealthfolio/addon-dev-tools": "^
|
|
106
|
+
"@wealthfolio/addon-dev-tools": "^3.7.0"
|
|
77
107
|
}
|
|
78
108
|
}
|
|
79
109
|
```
|
package/dev-server.js
CHANGED
|
@@ -15,8 +15,219 @@ const path = require("path");
|
|
|
15
15
|
const fs = require("fs");
|
|
16
16
|
const { exec } = require("child_process");
|
|
17
17
|
const { promisify } = require("util");
|
|
18
|
+
const { createHash } = require("crypto");
|
|
18
19
|
|
|
19
20
|
const execAsync = promisify(exec);
|
|
21
|
+
const MAX_RUNTIME_PACKAGE_ENTRIES = 256;
|
|
22
|
+
const MAX_RUNTIME_ASSET_FILE_SIZE = 5 * 1024 * 1024;
|
|
23
|
+
const MAX_RUNTIME_ASSET_TOTAL_SIZE = 25 * 1024 * 1024;
|
|
24
|
+
const MAX_RUNTIME_PACKAGE_GENERATIONS = 4;
|
|
25
|
+
|
|
26
|
+
function runtimeAssetMimeType(filePath) {
|
|
27
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
28
|
+
return (
|
|
29
|
+
{
|
|
30
|
+
".avif": "image/avif",
|
|
31
|
+
".bmp": "image/bmp",
|
|
32
|
+
".css": "text/css",
|
|
33
|
+
".csv": "text/csv",
|
|
34
|
+
".gif": "image/gif",
|
|
35
|
+
".html": "text/html",
|
|
36
|
+
".ico": "image/x-icon",
|
|
37
|
+
".jpeg": "image/jpeg",
|
|
38
|
+
".jpg": "image/jpeg",
|
|
39
|
+
".json": "application/json",
|
|
40
|
+
".md": "text/markdown",
|
|
41
|
+
".mp3": "audio/mpeg",
|
|
42
|
+
".mp4": "video/mp4",
|
|
43
|
+
".ogg": "audio/ogg",
|
|
44
|
+
".otf": "font/otf",
|
|
45
|
+
".pdf": "application/pdf",
|
|
46
|
+
".png": "image/png",
|
|
47
|
+
".svg": "image/svg+xml",
|
|
48
|
+
".ttf": "font/ttf",
|
|
49
|
+
".txt": "text/plain",
|
|
50
|
+
".wasm": "application/wasm",
|
|
51
|
+
".wav": "audio/wav",
|
|
52
|
+
".webm": "video/webm",
|
|
53
|
+
".webp": "image/webp",
|
|
54
|
+
".woff": "font/woff",
|
|
55
|
+
".woff2": "font/woff2",
|
|
56
|
+
".xml": "application/xml",
|
|
57
|
+
}[extension] || "application/octet-stream"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkRuntimeFiles(rootPath, visit) {
|
|
62
|
+
if (!fs.existsSync(rootPath)) return;
|
|
63
|
+
const rootStats = fs.lstatSync(rootPath);
|
|
64
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
65
|
+
throw new Error(`Runtime asset root is not a regular directory: ${rootPath}`);
|
|
66
|
+
}
|
|
67
|
+
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
|
68
|
+
const entryPath = path.join(rootPath, entry.name);
|
|
69
|
+
if (entry.isSymbolicLink()) {
|
|
70
|
+
throw new Error(`Runtime assets cannot contain symbolic links: ${entryPath}`);
|
|
71
|
+
}
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
walkRuntimeFiles(entryPath, visit);
|
|
74
|
+
} else if (entry.isFile()) {
|
|
75
|
+
visit(entryPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getRuntimePackageEntries(addonPath) {
|
|
81
|
+
const entries = [];
|
|
82
|
+
let totalSize = 0;
|
|
83
|
+
for (const root of [path.join(addonPath, "dist"), path.join(addonPath, "assets")]) {
|
|
84
|
+
walkRuntimeFiles(root, (filePath) => {
|
|
85
|
+
const logicalPath = path.relative(addonPath, filePath).split(path.sep).join("/");
|
|
86
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
87
|
+
if (extension === ".map") return;
|
|
88
|
+
if ([".gitkeep", ".ds_store"].includes(path.basename(filePath).toLowerCase())) return;
|
|
89
|
+
const kind =
|
|
90
|
+
extension === ".js" || extension === ".css"
|
|
91
|
+
? "text"
|
|
92
|
+
: logicalPath.startsWith("assets/") || logicalPath.startsWith("dist/assets/")
|
|
93
|
+
? "asset"
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!kind) return;
|
|
96
|
+
if (entries.length >= MAX_RUNTIME_PACKAGE_ENTRIES) {
|
|
97
|
+
throw new Error("Runtime package contains more than 256 files");
|
|
98
|
+
}
|
|
99
|
+
const size = fs.statSync(filePath).size;
|
|
100
|
+
if (size > MAX_RUNTIME_ASSET_FILE_SIZE) {
|
|
101
|
+
throw new Error(`Runtime package file exceeds 5 MiB: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
totalSize += size;
|
|
104
|
+
if (totalSize > MAX_RUNTIME_ASSET_TOTAL_SIZE) {
|
|
105
|
+
throw new Error("Runtime package exceeds the 25 MiB package limit");
|
|
106
|
+
}
|
|
107
|
+
entries.push({ extension, filePath, kind, logicalPath, size });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return entries.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runtimeAssetId(logicalPath, content) {
|
|
114
|
+
return createHash("sha256").update(logicalPath).update("\0").update(content).digest("hex");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readRuntimeEntry(entry) {
|
|
118
|
+
const content = fs.readFileSync(entry.filePath);
|
|
119
|
+
if (content.length !== entry.size) {
|
|
120
|
+
throw new Error(`Runtime package changed while it was being indexed: ${entry.filePath}`);
|
|
121
|
+
}
|
|
122
|
+
return content;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runtimeAssetsFromEntries(entries) {
|
|
126
|
+
return entries
|
|
127
|
+
.filter((entry) => entry.kind === "asset")
|
|
128
|
+
.map((entry) => {
|
|
129
|
+
const content = readRuntimeEntry(entry);
|
|
130
|
+
return {
|
|
131
|
+
content,
|
|
132
|
+
descriptor: {
|
|
133
|
+
id: runtimeAssetId(entry.logicalPath, content),
|
|
134
|
+
mimeType: runtimeAssetMimeType(entry.filePath),
|
|
135
|
+
path: entry.logicalPath,
|
|
136
|
+
size: content.length,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function runtimeTextFilesFromEntries(entries) {
|
|
143
|
+
return entries
|
|
144
|
+
.filter((entry) => entry.kind === "text")
|
|
145
|
+
.map((entry) => ({
|
|
146
|
+
content: readRuntimeEntry(entry).toString("utf8"),
|
|
147
|
+
isMain: entry.logicalPath === "dist/addon.js",
|
|
148
|
+
name: entry.logicalPath,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getRuntimeAssets(addonPath) {
|
|
153
|
+
return runtimeAssetsFromEntries(getRuntimePackageEntries(addonPath));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getRuntimeTextFiles(addonPath) {
|
|
157
|
+
return runtimeTextFilesFromEntries(getRuntimePackageEntries(addonPath));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function shouldPublishRuntimeFileChange(config, filePath) {
|
|
161
|
+
if (!config.buildCommand) return true;
|
|
162
|
+
const absolutePath = path.resolve(filePath);
|
|
163
|
+
const manifestPath = path.resolve(config.manifestPath);
|
|
164
|
+
const assetRoot = path.resolve(config.addonPath, "assets");
|
|
165
|
+
return absolutePath === manifestPath || absolutePath.startsWith(`${assetRoot}${path.sep}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class RuntimePackageRegistry {
|
|
169
|
+
constructor(addonPath, manifestPath = path.join(addonPath, "manifest.json")) {
|
|
170
|
+
this.addonPath = addonPath;
|
|
171
|
+
this.manifestPath = manifestPath;
|
|
172
|
+
this.currentGeneration = 0;
|
|
173
|
+
this.snapshots = new Map();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
createSnapshot() {
|
|
177
|
+
const entries = getRuntimePackageEntries(this.addonPath);
|
|
178
|
+
const manifest = fs.existsSync(this.manifestPath)
|
|
179
|
+
? JSON.parse(fs.readFileSync(this.manifestPath, "utf8"))
|
|
180
|
+
: null;
|
|
181
|
+
return {
|
|
182
|
+
assets: runtimeAssetsFromEntries(entries),
|
|
183
|
+
files: runtimeTextFilesFromEntries(entries),
|
|
184
|
+
manifest,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
refresh() {
|
|
189
|
+
const generation = this.currentGeneration + 1;
|
|
190
|
+
const snapshot = { generation, ...this.createSnapshot() };
|
|
191
|
+
this.snapshots.set(generation, snapshot);
|
|
192
|
+
this.currentGeneration = generation;
|
|
193
|
+
while (this.snapshots.size > MAX_RUNTIME_PACKAGE_GENERATIONS) {
|
|
194
|
+
this.snapshots.delete(this.snapshots.keys().next().value);
|
|
195
|
+
}
|
|
196
|
+
return snapshot;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getSnapshot(generation) {
|
|
200
|
+
if (this.currentGeneration === 0) {
|
|
201
|
+
throw new Error("No runtime package generation has been published");
|
|
202
|
+
}
|
|
203
|
+
const requestedGeneration = generation ?? this.currentGeneration;
|
|
204
|
+
const snapshot = this.snapshots.get(requestedGeneration);
|
|
205
|
+
if (!snapshot) {
|
|
206
|
+
throw new Error(`Runtime package generation is no longer available: ${requestedGeneration}`);
|
|
207
|
+
}
|
|
208
|
+
return snapshot;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getPackage(generation) {
|
|
212
|
+
return this.getSnapshot(generation);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getAssets(generation) {
|
|
216
|
+
return this.getSnapshot(generation).assets;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getTextFiles(generation) {
|
|
220
|
+
return this.getSnapshot(generation).files;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getManifest(generation) {
|
|
224
|
+
return this.getSnapshot(generation).manifest;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getGeneration() {
|
|
228
|
+
return this.currentGeneration;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
20
231
|
|
|
21
232
|
class AddonDevServer {
|
|
22
233
|
constructor(config) {
|
|
@@ -25,9 +236,16 @@ class AddonDevServer {
|
|
|
25
236
|
this.lastModified = new Date();
|
|
26
237
|
this.buildInProgress = false;
|
|
27
238
|
this.viteWatcher = null;
|
|
239
|
+
this.publishTimer = null;
|
|
240
|
+
this.runtimePackageRegistry = new RuntimePackageRegistry(
|
|
241
|
+
this.config.addonPath,
|
|
242
|
+
this.config.manifestPath,
|
|
243
|
+
);
|
|
244
|
+
this.publishRuntimePackage();
|
|
28
245
|
|
|
29
246
|
this.setupMiddleware();
|
|
30
247
|
this.setupRoutes();
|
|
248
|
+
this.app.use(express.static(this.config.addonPath));
|
|
31
249
|
this.setupFileWatcher();
|
|
32
250
|
this.startViteWatcher();
|
|
33
251
|
}
|
|
@@ -39,7 +257,6 @@ class AddonDevServer {
|
|
|
39
257
|
credentials: true,
|
|
40
258
|
}),
|
|
41
259
|
);
|
|
42
|
-
this.app.use(express.static(this.config.addonPath));
|
|
43
260
|
}
|
|
44
261
|
|
|
45
262
|
setupRoutes() {
|
|
@@ -57,6 +274,7 @@ class AddonDevServer {
|
|
|
57
274
|
res.json({
|
|
58
275
|
lastModified: this.lastModified.toISOString(),
|
|
59
276
|
buildInProgress: this.buildInProgress,
|
|
277
|
+
generation: this.runtimePackageRegistry.getGeneration(),
|
|
60
278
|
files: this.getFileList(),
|
|
61
279
|
});
|
|
62
280
|
});
|
|
@@ -64,9 +282,8 @@ class AddonDevServer {
|
|
|
64
282
|
// Serve addon manifest
|
|
65
283
|
this.app.get("/manifest.json", (req, res) => {
|
|
66
284
|
try {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
285
|
+
const manifest = this.runtimePackageRegistry.getManifest();
|
|
286
|
+
if (manifest) {
|
|
70
287
|
res.json(manifest);
|
|
71
288
|
} else {
|
|
72
289
|
res.status(404).json({ error: "Manifest not found" });
|
|
@@ -86,7 +303,13 @@ class AddonDevServer {
|
|
|
86
303
|
const fileExists = await this.waitForFile(addonFile, 3000);
|
|
87
304
|
|
|
88
305
|
if (fileExists) {
|
|
89
|
-
const code =
|
|
306
|
+
const code = this.runtimePackageRegistry
|
|
307
|
+
.getTextFiles()
|
|
308
|
+
.find((file) => file.isMain)?.content;
|
|
309
|
+
if (code === undefined) {
|
|
310
|
+
res.status(404).json({ error: "Addon entry point is not in the published package" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
90
313
|
res.type("application/javascript").send(code);
|
|
91
314
|
} else {
|
|
92
315
|
console.error(`❌ Addon file not found at: ${addonFile}`);
|
|
@@ -100,6 +323,70 @@ class AddonDevServer {
|
|
|
100
323
|
}
|
|
101
324
|
});
|
|
102
325
|
|
|
326
|
+
this.app.get("/runtime-package", (req, res) => {
|
|
327
|
+
try {
|
|
328
|
+
const runtimePackage = this.runtimePackageRegistry.getPackage();
|
|
329
|
+
const mainFile = runtimePackage.files.find((file) => file.isMain);
|
|
330
|
+
if (!mainFile) {
|
|
331
|
+
res.status(409).json({ error: "Runtime package has no published addon entry point" });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
res.set("Cache-Control", "no-store");
|
|
335
|
+
res.json({
|
|
336
|
+
assets: runtimePackage.assets.map((asset) => asset.descriptor),
|
|
337
|
+
files: runtimePackage.files,
|
|
338
|
+
generation: runtimePackage.generation,
|
|
339
|
+
manifest: runtimePackage.manifest,
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
res.status(500).json({ error: error.message });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Metadata-only package registry used by the opaque iframe asset broker.
|
|
347
|
+
this.app.get("/runtime-assets", (req, res) => {
|
|
348
|
+
try {
|
|
349
|
+
res.set("Cache-Control", "no-store");
|
|
350
|
+
res.json(this.runtimePackageRegistry.getAssets().map((asset) => asset.descriptor));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
res.status(500).json({ error: error.message });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
this.app.get("/runtime-assets/:assetId", (req, res) => {
|
|
357
|
+
try {
|
|
358
|
+
const requestedGeneration =
|
|
359
|
+
req.query.generation === undefined ? undefined : Number(req.query.generation);
|
|
360
|
+
if (
|
|
361
|
+
requestedGeneration !== undefined &&
|
|
362
|
+
(!Number.isSafeInteger(requestedGeneration) || requestedGeneration < 1)
|
|
363
|
+
) {
|
|
364
|
+
res.status(400).json({ error: "Invalid runtime package generation" });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const asset = this.runtimePackageRegistry
|
|
368
|
+
.getAssets(requestedGeneration)
|
|
369
|
+
.find((candidate) => candidate.descriptor.id === req.params.assetId);
|
|
370
|
+
if (!asset) {
|
|
371
|
+
res.status(404).json({ error: "Runtime asset not found" });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
res.set("Cache-Control", "no-store");
|
|
375
|
+
res.type(asset.descriptor.mimeType).send(asset.content);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
res.status(500).json({ error: error.message });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
this.app.get("/runtime-files", (req, res) => {
|
|
382
|
+
try {
|
|
383
|
+
res.set("Cache-Control", "no-store");
|
|
384
|
+
res.json(this.runtimePackageRegistry.getTextFiles());
|
|
385
|
+
} catch (error) {
|
|
386
|
+
res.status(500).json({ error: error.message });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
103
390
|
// Hot reload endpoint
|
|
104
391
|
this.app.get("/reload", (req, res) => {
|
|
105
392
|
res.json({
|
|
@@ -167,24 +454,57 @@ class AddonDevServer {
|
|
|
167
454
|
|
|
168
455
|
watcher.on("change", (filePath) => {
|
|
169
456
|
console.log(`📝 File changed: ${filePath}`);
|
|
170
|
-
|
|
171
|
-
// Just update the timestamp for status endpoint
|
|
172
|
-
this.lastModified = new Date();
|
|
457
|
+
this.handleRuntimeFileChange(filePath);
|
|
173
458
|
});
|
|
174
459
|
|
|
175
460
|
watcher.on("add", (filePath) => {
|
|
176
461
|
console.log(`➕ File added: ${filePath}`);
|
|
177
|
-
this.
|
|
462
|
+
this.handleRuntimeFileChange(filePath);
|
|
178
463
|
});
|
|
179
464
|
|
|
180
465
|
watcher.on("unlink", (filePath) => {
|
|
181
466
|
console.log(`➖ File removed: ${filePath}`);
|
|
182
|
-
this.
|
|
467
|
+
this.handleRuntimeFileChange(filePath);
|
|
183
468
|
});
|
|
184
469
|
|
|
185
470
|
console.log(`👀 Watching files: ${this.config.watchPaths.join(", ")}`);
|
|
186
471
|
}
|
|
187
472
|
|
|
473
|
+
handleRuntimeFileChange(filePath) {
|
|
474
|
+
// Source and dist events are intermediate Vite states. The Vite completion
|
|
475
|
+
// signal publishes their next coherent package generation.
|
|
476
|
+
if (shouldPublishRuntimeFileChange(this.config, filePath)) {
|
|
477
|
+
this.scheduleRuntimePackagePublish();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
scheduleRuntimePackagePublish() {
|
|
482
|
+
if (this.publishTimer) clearTimeout(this.publishTimer);
|
|
483
|
+
this.publishTimer = setTimeout(() => {
|
|
484
|
+
this.publishTimer = null;
|
|
485
|
+
if (this.buildInProgress) {
|
|
486
|
+
this.scheduleRuntimePackagePublish();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.publishRuntimePackage();
|
|
490
|
+
}, 100);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
publishRuntimePackage() {
|
|
494
|
+
if (this.publishTimer) {
|
|
495
|
+
clearTimeout(this.publishTimer);
|
|
496
|
+
this.publishTimer = null;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const runtimePackage = this.runtimePackageRegistry.refresh();
|
|
500
|
+
this.lastModified = new Date();
|
|
501
|
+
return runtimePackage;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error("❌ Failed to publish runtime package:", error);
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
188
508
|
async triggerBuild() {
|
|
189
509
|
if (this.buildInProgress || !this.config.buildCommand) return;
|
|
190
510
|
|
|
@@ -197,7 +517,7 @@ class AddonDevServer {
|
|
|
197
517
|
});
|
|
198
518
|
|
|
199
519
|
console.log("✅ Build completed successfully");
|
|
200
|
-
this.
|
|
520
|
+
this.publishRuntimePackage();
|
|
201
521
|
} catch (error) {
|
|
202
522
|
console.error("❌ Build failed:", error);
|
|
203
523
|
} finally {
|
|
@@ -277,7 +597,7 @@ class AddonDevServer {
|
|
|
277
597
|
|
|
278
598
|
if (output.includes("built in")) {
|
|
279
599
|
console.log(`✅ Vite rebuild completed`);
|
|
280
|
-
this.
|
|
600
|
+
this.publishRuntimePackage();
|
|
281
601
|
this.buildInProgress = false;
|
|
282
602
|
}
|
|
283
603
|
|
|
@@ -325,6 +645,10 @@ class AddonDevServer {
|
|
|
325
645
|
stop() {
|
|
326
646
|
console.log("🛑 Shutting down dev server...");
|
|
327
647
|
|
|
648
|
+
if (this.publishTimer) {
|
|
649
|
+
clearTimeout(this.publishTimer);
|
|
650
|
+
this.publishTimer = null;
|
|
651
|
+
}
|
|
328
652
|
if (this.viteWatcher) {
|
|
329
653
|
this.viteWatcher.kill("SIGTERM");
|
|
330
654
|
this.viteWatcher = null;
|
|
@@ -343,7 +667,12 @@ function main() {
|
|
|
343
667
|
addonPath: path.resolve(addonPath),
|
|
344
668
|
manifestPath: path.resolve(addonPath, "manifest.json"),
|
|
345
669
|
buildCommand: "pnpm run build",
|
|
346
|
-
watchPaths: [
|
|
670
|
+
watchPaths: [
|
|
671
|
+
path.resolve(addonPath, "src"),
|
|
672
|
+
path.resolve(addonPath, "assets"),
|
|
673
|
+
path.resolve(addonPath, "dist"),
|
|
674
|
+
path.resolve(addonPath, "manifest.json"),
|
|
675
|
+
],
|
|
347
676
|
};
|
|
348
677
|
|
|
349
678
|
// Check if addon directory exists
|
|
@@ -363,7 +692,13 @@ function main() {
|
|
|
363
692
|
}
|
|
364
693
|
|
|
365
694
|
// Export for use as a module
|
|
366
|
-
module.exports = {
|
|
695
|
+
module.exports = {
|
|
696
|
+
AddonDevServer,
|
|
697
|
+
RuntimePackageRegistry,
|
|
698
|
+
getRuntimeAssets,
|
|
699
|
+
getRuntimeTextFiles,
|
|
700
|
+
shouldPublishRuntimeFileChange,
|
|
701
|
+
};
|
|
367
702
|
|
|
368
703
|
// Run if called directly
|
|
369
704
|
if (require.main === module) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wealthfolio/addon-dev-tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"description": "Development tools for Wealthfolio addons - hot reload server and CLI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "
|
|
40
|
+
"test": "node --test dev-server.test.js scaffold.test.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"chokidar": "^4.0.3",
|
|
@@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
25
|
### Added
|
|
26
26
|
- Initial release of {{addonName}} addon
|
|
27
27
|
- Basic addon functionality and core features
|
|
28
|
-
- Integration with Wealthfolio addon SDK
|
|
28
|
+
- Integration with Wealthfolio addon SDK v3.7.0
|
|
29
29
|
- Sidebar navigation integration for easy access
|
|
30
30
|
- Responsive design for all screen sizes
|
|
31
31
|
|
|
@@ -34,5 +34,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
34
|
- User-friendly interface
|
|
35
35
|
- Compatible with Wealthfolio platform
|
|
36
36
|
|
|
37
|
-
###
|
|
38
|
-
-
|
|
37
|
+
### Compatibility
|
|
38
|
+
- Requires Wealthfolio 3.7.0 or newer
|
|
@@ -18,6 +18,14 @@ npm run build
|
|
|
18
18
|
npm run bundle
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
Wealthfolio 3.7 indexes private files below `assets/` and `dist/assets/` automatically. Use
|
|
22
|
+
`ctx.assets.getBlob(path)` or `ctx.assets.getUrl(path)` to load them in the sandbox; no manifest
|
|
23
|
+
asset list or permission is required. Blob URLs are valid only for the current addon lifetime.
|
|
24
|
+
|
|
25
|
+
The generated Vite config targets Chrome/Edge 107, Firefox 104, and Safari 16, matching Wealthfolio
|
|
26
|
+
3.7. The sandbox supports packaged images, fonts, media, CSS, and WebAssembly. Worker/service-worker
|
|
27
|
+
entry points, popups, direct network requests, and remote CSS imports are intentionally unavailable.
|
|
28
|
+
|
|
21
29
|
## Features
|
|
22
30
|
|
|
23
31
|
- Add your features here
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AddonContext } from '@wealthfolio/addon-sdk';
|
|
3
|
-
import { Card, CardContent
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import type { AddonContext, AddonEnableFunction } from '@wealthfolio/addon-sdk';
|
|
3
|
+
import { Card, CardContent } from '@wealthfolio/ui';
|
|
4
|
+
|
|
5
|
+
// The host owns a single React root per addon and mounts the route `component`
|
|
6
|
+
// itself (`createElement(Component, { location })`) with no access to the addon
|
|
7
|
+
// context. Capture it at enable time so the route wrapper can hand it down.
|
|
8
|
+
// (Do NOT call createRoot yourself — the host manages the lifecycle.)
|
|
9
|
+
let addonCtx: AddonContext | undefined;
|
|
4
10
|
|
|
5
11
|
function AddonExample({ ctx }: { ctx: AddonContext }) {
|
|
6
12
|
return (
|
|
@@ -17,35 +23,32 @@ function AddonExample({ ctx }: { ctx: AddonContext }) {
|
|
|
17
23
|
);
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
// Route component. The sidebar entry + route are declared in manifest.json
|
|
27
|
+
// (`contributes.routes` + `contributes.links`), so the host renders navigation
|
|
28
|
+
// without booting the addon; this component only runs when the route is first
|
|
29
|
+
// visited. The QueryClientProvider reuses this addon's isolated cache across
|
|
30
|
+
// route navigations; invalidations/refetches are bridged to the host.
|
|
31
|
+
const AddonRoute = () => (
|
|
32
|
+
<QueryClientProvider client={addonCtx!.api.query.getClient() as QueryClient}>
|
|
33
|
+
<AddonExample ctx={addonCtx!} />
|
|
34
|
+
</QueryClientProvider>
|
|
35
|
+
);
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
id: '{{addonId}}',
|
|
26
|
-
label: '{{addonName}}',
|
|
27
|
-
icon: 'blocks',
|
|
28
|
-
route: '/addon/{{addonId}}',
|
|
29
|
-
order: 100,
|
|
30
|
-
});
|
|
37
|
+
const enable: AddonEnableFunction = (ctx) => {
|
|
38
|
+
addonCtx = ctx;
|
|
31
39
|
|
|
32
|
-
//
|
|
40
|
+
// The route `id` MUST match `contributes.routes[].id` in manifest.json.
|
|
41
|
+
// The host derives this root path from the manifest addon id.
|
|
33
42
|
ctx.router.add({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
root.render(<AddonExample ctx={ctx} />);
|
|
38
|
-
},
|
|
43
|
+
id: '{{addonId}}',
|
|
44
|
+
path: '/addons/{{addonId}}',
|
|
45
|
+
component: AddonRoute,
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
//
|
|
48
|
+
// The host owns the React root, so there is nothing to unmount here.
|
|
42
49
|
ctx.onDisable(() => {
|
|
43
|
-
|
|
44
|
-
sidebarItem.remove();
|
|
45
|
-
root?.unmount();
|
|
46
|
-
root = null;
|
|
47
|
-
} catch (err) {
|
|
48
|
-
ctx.api.logger.error(`Failed to remove sidebar item: ${String(err)}`);
|
|
49
|
-
}
|
|
50
|
+
addonCtx = undefined;
|
|
50
51
|
});
|
|
51
|
-
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default enable;
|
|
@@ -5,26 +5,34 @@
|
|
|
5
5
|
"description": "{{description}}",
|
|
6
6
|
"author": "{{author}}",
|
|
7
7
|
"main": "dist/addon.js",
|
|
8
|
-
"sdkVersion": "3.
|
|
9
|
-
"minWealthfolioVersion": "3.
|
|
8
|
+
"sdkVersion": "3.7.0",
|
|
9
|
+
"minWealthfolioVersion": "3.7.0",
|
|
10
10
|
"enabled": true,
|
|
11
|
+
"contributes": {
|
|
12
|
+
"routes": [{ "id": "{{addonId}}" }],
|
|
13
|
+
"links": {
|
|
14
|
+
"sidebar": [
|
|
15
|
+
{
|
|
16
|
+
"id": "{{addonId}}",
|
|
17
|
+
"route": "{{addonId}}",
|
|
18
|
+
"label": "{{addonName}}",
|
|
19
|
+
"icon": "squares-four",
|
|
20
|
+
"order": 100
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
11
25
|
"hostDependencies": {
|
|
12
26
|
"@tanstack/react-query": "^5.90.0",
|
|
13
|
-
"@wealthfolio/addon-sdk": "^3.
|
|
14
|
-
"@wealthfolio/ui": "^3.
|
|
27
|
+
"@wealthfolio/addon-sdk": "^3.7.0",
|
|
28
|
+
"@wealthfolio/ui": "^3.7.0",
|
|
15
29
|
"date-fns": "^4.1.0",
|
|
16
30
|
"lucide-react": "^0.561.0",
|
|
17
31
|
"react": "^19.2.0",
|
|
18
32
|
"react-dom": "^19.2.0",
|
|
19
33
|
"recharts": "^3.7.0"
|
|
20
34
|
},
|
|
21
|
-
"permissions": [
|
|
22
|
-
{
|
|
23
|
-
"category": "ui",
|
|
24
|
-
"functions": ["sidebar.addItem", "router.add"],
|
|
25
|
-
"purpose": "Add navigation items and pages"
|
|
26
|
-
}
|
|
27
|
-
],
|
|
35
|
+
"permissions": [],
|
|
28
36
|
"keywords": ["wealthfolio", "addon"],
|
|
29
37
|
"license": "MIT"
|
|
30
38
|
}
|
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "vite build",
|
|
11
11
|
"dev": "vite build --watch",
|
|
12
|
-
"dev:server": "wealthfolio dev",
|
|
12
|
+
"dev:server": "wealthfolio-addon dev",
|
|
13
13
|
"clean": "rm -rf dist",
|
|
14
|
-
"package": "mkdir -p dist && find dist -name '*.map' -delete && zip -r dist/$npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md -x '*.map'",
|
|
14
|
+
"package": "mkdir -p dist assets && find dist -name '*.map' -delete && zip -r dist/$npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md -x '*.map'",
|
|
15
15
|
"bundle": "pnpm clean && pnpm build && pnpm package",
|
|
16
16
|
"lint": "tsc --noEmit",
|
|
17
17
|
"type-check": "tsc --noEmit"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@tanstack/react-query": "^5.90.0",
|
|
21
|
-
"@wealthfolio/addon-sdk": "^3.
|
|
22
|
-
"@wealthfolio/ui": "^3.
|
|
21
|
+
"@wealthfolio/addon-sdk": "^3.7.0",
|
|
22
|
+
"@wealthfolio/ui": "^3.7.0",
|
|
23
23
|
"date-fns": "^4.1.0",
|
|
24
24
|
"lucide-react": "^0.561.0",
|
|
25
25
|
"react": "^19.2.0",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@tanstack/react-query": "^5.90.20",
|
|
31
31
|
"@tailwindcss/vite": "^4.1.13",
|
|
32
|
-
"@wealthfolio/addon-dev-tools": "^3.
|
|
33
|
-
"@wealthfolio/addon-sdk": "^3.
|
|
34
|
-
"@wealthfolio/ui": "^3.
|
|
32
|
+
"@wealthfolio/addon-dev-tools": "^3.7.0",
|
|
33
|
+
"@wealthfolio/addon-sdk": "^3.7.0",
|
|
34
|
+
"@wealthfolio/ui": "^3.7.0",
|
|
35
35
|
"@types/node": "^20.0.0",
|
|
36
36
|
"@types/react": "^19.2.13",
|
|
37
37
|
"@types/react-dom": "^19.2.3",
|
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
import react from
|
|
2
|
-
import tailwindcss from
|
|
3
|
-
import { defineConfig } from
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import { defineConfig } from "vite";
|
|
4
4
|
|
|
5
5
|
const hostProvidedDependencies = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
"@tanstack/react-query",
|
|
7
|
+
"@wealthfolio/addon-sdk",
|
|
8
|
+
"@wealthfolio/addon-sdk/goal-progress",
|
|
9
|
+
"@wealthfolio/addon-sdk/host-api",
|
|
10
|
+
"@wealthfolio/addon-sdk/host-dependencies",
|
|
11
|
+
"@wealthfolio/addon-sdk/manifest",
|
|
12
|
+
"@wealthfolio/addon-sdk/permissions",
|
|
13
|
+
"@wealthfolio/addon-sdk/query-keys",
|
|
14
|
+
"@wealthfolio/addon-sdk/types",
|
|
15
|
+
"@wealthfolio/addon-sdk/utils",
|
|
16
|
+
"@wealthfolio/ui",
|
|
17
|
+
"@wealthfolio/ui/chart",
|
|
18
|
+
"date-fns",
|
|
19
|
+
"lucide-react",
|
|
20
|
+
"react",
|
|
21
|
+
"react-dom",
|
|
22
|
+
"react-dom/client",
|
|
23
|
+
"react/jsx-dev-runtime",
|
|
24
|
+
"react/jsx-runtime",
|
|
25
|
+
"recharts",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
export default defineConfig({
|
|
29
29
|
plugins: [react(), tailwindcss()],
|
|
30
30
|
define: {
|
|
31
|
-
|
|
31
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
32
32
|
},
|
|
33
33
|
build: {
|
|
34
|
+
target: ["chrome107", "edge107", "firefox104", "safari16"],
|
|
34
35
|
lib: {
|
|
35
|
-
entry:
|
|
36
|
-
fileName: () =>
|
|
37
|
-
formats: [
|
|
36
|
+
entry: "src/addon.tsx",
|
|
37
|
+
fileName: () => "addon.js",
|
|
38
|
+
formats: ["es"],
|
|
38
39
|
},
|
|
39
|
-
outDir:
|
|
40
|
+
outDir: "dist",
|
|
40
41
|
minify: true,
|
|
41
42
|
sourcemap: false,
|
|
42
43
|
rollupOptions: {
|
|
@@ -44,8 +45,8 @@ export default defineConfig({
|
|
|
44
45
|
},
|
|
45
46
|
watch: {
|
|
46
47
|
// Watch mode options for better hot reloading
|
|
47
|
-
include: [
|
|
48
|
-
exclude: [
|
|
49
|
-
}
|
|
48
|
+
include: ["src/**"],
|
|
49
|
+
exclude: ["node_modules/**", "dist/**"],
|
|
50
|
+
},
|
|
50
51
|
},
|
|
51
52
|
});
|