-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbuild-module.mjs
More file actions
170 lines (160 loc) · 6.32 KB
/
build-module.mjs
File metadata and controls
170 lines (160 loc) · 6.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env node
/**
* Magic-Dashboard Custom-Module Builder
*
* Usage:
* node scripts/build-module.mjs <source.js|.tsx> [out-dir]
*
* Erwartet ein Modul-File mit:
* - default export der `render(ctx)`-Funktion (oder ein Objekt mit render+manifest)
* - optional ein named export `manifest` mit Modul-Metadaten
*
* Liefert:
* <out-dir>/module.json — Manifest
* <out-dir>/bundle.js — IIFE-Bundle das gegen window.MagicFrame.registerWidget() callt
* <out-dir>/<type>.zip — beides zusammen, hochladbar in der UI
*
* Beispiel-Modul:
*
* // hello-widget.js
* export const manifest = {
* type: "hello",
* label: "Hallo-Welt",
* iconEmoji: "👋",
* fields: [{ key: "name", label: "Name", type: "text", default: "Welt" }],
* };
* export default function render(ctx) {
* const h = ctx.createElement;
* const name = ctx.config.name || "Welt";
* return h("div", { className: "w-full h-full flex items-center justify-center text-[1.5em] font-bold" },
* `Hallo, ${name}!`);
* }
*/
import { build } from "esbuild";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: build-module.mjs <source.js|.tsx> [out-dir]");
process.exit(1);
}
const srcPath = path.resolve(args[0]);
const outDir = path.resolve(args[1] ?? "dist-module");
if (!existsSync(srcPath)) {
console.error(`Source-File nicht gefunden: ${srcPath}`);
process.exit(1);
}
// 1) Modul kompilieren — IIFE-Bundle, das die Default-Function gegen
// window.MagicFrame.registerWidget meldet. Wir bauen einen kleinen Wrapper
// der das Source-File importiert (esbuild inlined das), das Manifest +
// die render-Funktion extrahiert und registriert.
const wrapperSrc = `
import * as Mod from ${JSON.stringify(srcPath)};
(function () {
if (typeof window === "undefined" || !window.MagicFrame) {
console.warn("[CustomModule] window.MagicFrame fehlt — Modul wird ignoriert.");
return;
}
var manifest = Mod.manifest || (Mod.default && Mod.default.manifest);
var render = (typeof Mod.default === "function") ? Mod.default : (Mod.default && Mod.default.render);
if (!manifest || !render) {
console.warn("[CustomModule] Modul exportiert kein manifest + render — bitte 'export const manifest' und 'export default function(...)' setzen.");
return;
}
var type = String(manifest.type || "").trim();
if (!type) {
console.warn("[CustomModule] Manifest hat keinen type.");
return;
}
if (!type.startsWith("custom:")) type = "custom:" + type;
window.MagicFrame.registerWidget({ type: type, render: render });
})();
`;
// esbuild braucht ein "stdin" Eintrag — wir geben ihm den Wrapper als String,
// und es findet die echte Modul-Datei via dem import oben.
const result = await build({
stdin: {
contents: wrapperSrc,
resolveDir: path.dirname(srcPath),
sourcefile: "magic-module-entry.js",
loader: "ts",
},
bundle: true,
format: "iife",
platform: "browser",
target: ["es2020"],
minify: true,
loader: { ".ts": "ts", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
jsx: "automatic",
// React/ReactDOM sind im Host vorhanden — nicht ins Bundle ziehen,
// sondern aus ctx.createElement nutzen. Wer React-JSX nutzt, muss
// sicherstellen dass JSX-Runtime auf das ctx-API zeigt (siehe Doku).
write: false,
});
if (!result.outputFiles || result.outputFiles.length === 0) {
console.error("Build hat keine Output-Files produziert.");
process.exit(1);
}
const bundleJs = result.outputFiles[0].text;
// 2) Manifest aus dem Source extrahieren — wir kompilieren ein zweites Mal,
// diesmal als CommonJS, eval'en es und lesen das Manifest aus.
const manifestExtract = await build({
stdin: {
contents: `import * as Mod from ${JSON.stringify(srcPath)}; export const manifest = Mod.manifest || (Mod.default && Mod.default.manifest) || null;`,
resolveDir: path.dirname(srcPath),
sourcefile: "magic-manifest-extract.js",
loader: "ts",
},
bundle: true,
format: "cjs",
platform: "node",
target: ["es2020"],
loader: { ".ts": "ts", ".tsx": "tsx" },
jsx: "automatic",
write: false,
});
const manifestCjs = manifestExtract.outputFiles[0].text;
const exports = {};
const mod = { exports };
// eslint-disable-next-line no-new-func
new Function("module", "exports", manifestCjs)(mod, exports);
const manifest = mod.exports.manifest;
if (!manifest) {
console.error("Konnte `manifest` nicht aus dem Source extrahieren — bitte 'export const manifest = { type, label, ... }' setzen.");
process.exit(1);
}
// Sanity-Check der Felder
if (typeof manifest.type !== "string" || !manifest.type) {
console.error("Manifest braucht ein nicht-leeres `type`.");
process.exit(1);
}
if (typeof manifest.label !== "string" || !manifest.label) {
console.error("Manifest braucht ein nicht-leeres `label`.");
process.exit(1);
}
// 3) Outputs schreiben
await mkdir(outDir, { recursive: true });
const manifestOut = path.join(outDir, "module.json");
const bundleOut = path.join(outDir, "bundle.js");
await writeFile(manifestOut, JSON.stringify(manifest, null, 2), "utf-8");
await writeFile(bundleOut, bundleJs, "utf-8");
// 4) Tarball für Upload (statt ZIP — Node hat tar nicht built-in aber wir
// schreiben einfach beide Files separat; die Upload-UI nimmt eh einzelne Files)
const typeSafe = manifest.type.replace(/[^a-z0-9_-]/gi, "_");
console.log("");
console.log("✓ Modul gebaut:");
console.log(" Manifest: " + manifestOut);
console.log(" Bundle: " + bundleOut + " (" + bundleJs.length + " bytes)");
console.log("");
console.log("Upload via Settings → Module → 'Modul hochladen' (beide Files auswählen)");
console.log("oder via API:");
console.log(" curl -X POST http://localhost/api/admin/modules \\");
console.log(" -H 'Content-Type: application/json' \\");
console.log(" --cookie 'magic_session=...' \\");
console.log(" -d @<(jq -n --arg js \"$(cat " + bundleOut + ")\" --argjson m \"$(cat " + manifestOut + ")\" '{manifest:$m, bundleJs:$js}')");
console.log("");
console.log("Type-ID: " + (manifest.type.startsWith("custom:") ? manifest.type : "custom:" + manifest.type));