create-middag-ui 0.10.3 → 0.12.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/cli.js +47 -4
- package/lib/detect.js +73 -7
- package/lib/scaffold.js +383 -10
- package/lib/scaffoldPRO.js +228 -0
- package/lib/templates/pro/app.tsx +50 -0
- package/lib/templates/pro/main.tsx +16 -0
- package/lib/templates/pro/mock-data.ts +28 -0
- package/lib/templates/pro/mock-entities.ts +13 -0
- package/lib/templates/pro/mock-navigation.ts +59 -0
- package/lib/templates/pro/mock-routes.tsx +90 -0
- package/lib/templates/pro/register-pro.ts +57 -0
- package/lib/templates/shared/demo-page.tsx +45 -0
- package/lib/templates/shared/page-resolver.tsx +50 -0
- package/lib/templates/shared/register-free.ts +52 -0
- package/lib/templates/shared/route-helper-wp.ts +26 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -33,10 +33,16 @@ import {
|
|
|
33
33
|
scaffoldIndexHtml,
|
|
34
34
|
scaffoldDemoFiles,
|
|
35
35
|
scaffoldPageExamples,
|
|
36
|
-
scaffoldProApp,
|
|
37
36
|
scaffoldFreeApp,
|
|
38
37
|
scaffoldFreeAdapters,
|
|
39
38
|
scaffoldDevShell,
|
|
39
|
+
scaffoldHostEntry,
|
|
40
|
+
scaffoldHostViteConfig,
|
|
41
|
+
scaffoldHostThemeCSS,
|
|
42
|
+
scaffoldFreeRegister,
|
|
43
|
+
scaffoldPageResolver,
|
|
44
|
+
scaffoldRouteHelper,
|
|
45
|
+
scaffoldDemoDirectPage,
|
|
40
46
|
} from "./lib/scaffold.js";
|
|
41
47
|
import { runNpmInstall } from "./lib/install.js";
|
|
42
48
|
import { log, success, heading, blank, info } from "./lib/ui.js";
|
|
@@ -116,7 +122,7 @@ if (!dirCreated) {
|
|
|
116
122
|
|
|
117
123
|
heading(5, TOTAL_STEPS, "Scaffolding config files");
|
|
118
124
|
|
|
119
|
-
scaffoldPackageJson(targetDir, host, cwd, registryPath);
|
|
125
|
+
scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey);
|
|
120
126
|
scaffoldTsconfig(targetDir);
|
|
121
127
|
scaffoldViteConfig(targetDir, host, registryPath);
|
|
122
128
|
scaffoldEslintConfig(targetDir);
|
|
@@ -147,16 +153,44 @@ heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
|
|
|
147
153
|
|
|
148
154
|
scaffoldPageExamples(targetDir);
|
|
149
155
|
|
|
156
|
+
// Shared files (both PRO and FREE)
|
|
157
|
+
scaffoldPageResolver(targetDir);
|
|
158
|
+
scaffoldDemoDirectPage(targetDir);
|
|
159
|
+
scaffoldRouteHelper(targetDir, hostKey);
|
|
160
|
+
|
|
150
161
|
if (isPro) {
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
try {
|
|
163
|
+
const pro = await import("./lib/scaffoldPRO.js");
|
|
164
|
+
pro.scaffoldProRegister(targetDir);
|
|
165
|
+
pro.scaffoldProApp(targetDir);
|
|
166
|
+
pro.scaffoldMockNavigation(targetDir);
|
|
167
|
+
pro.scaffoldMockData(targetDir);
|
|
168
|
+
pro.scaffoldMockEntities(targetDir);
|
|
169
|
+
pro.scaffoldMockPageContracts(targetDir);
|
|
170
|
+
pro.scaffoldMockRoutes(targetDir);
|
|
171
|
+
success("PRO: using MockProductShell from @middag-io/react/mock");
|
|
172
|
+
} catch {
|
|
173
|
+
// npm version — PRO file excluded, fall back to FREE
|
|
174
|
+
info("PRO scaffold not available — using FREE path");
|
|
175
|
+
scaffoldFreeRegister(targetDir);
|
|
176
|
+
scaffoldFreeAdapters(targetDir);
|
|
177
|
+
scaffoldDevShell(targetDir);
|
|
178
|
+
scaffoldFreeApp(targetDir);
|
|
179
|
+
success("FREE: generated DevShell + local Inertia adapters");
|
|
180
|
+
}
|
|
153
181
|
} else {
|
|
182
|
+
scaffoldFreeRegister(targetDir);
|
|
154
183
|
scaffoldFreeAdapters(targetDir);
|
|
155
184
|
scaffoldDevShell(targetDir);
|
|
156
185
|
scaffoldFreeApp(targetDir);
|
|
157
186
|
success("FREE: generated DevShell + local Inertia adapters");
|
|
158
187
|
}
|
|
159
188
|
|
|
189
|
+
// Host-specific production files (entry, vite config, theme CSS)
|
|
190
|
+
scaffoldHostEntry(targetDir, hostKey);
|
|
191
|
+
scaffoldHostViteConfig(targetDir, hostKey, host);
|
|
192
|
+
scaffoldHostThemeCSS(targetDir, hostKey, host);
|
|
193
|
+
|
|
160
194
|
// ── Step 9: npm install ──────────────────────────────────────────────────
|
|
161
195
|
|
|
162
196
|
heading(9, TOTAL_STEPS, "Installing dependencies");
|
|
@@ -173,6 +207,10 @@ heading(10, TOTAL_STEPS, "Done!");
|
|
|
173
207
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
174
208
|
blank();
|
|
175
209
|
|
|
210
|
+
// Determine host-specific build script name for display
|
|
211
|
+
const hostBuildScript = hostKey === "wordpress" ? "build:wp" : hostKey === "moodle" ? "build:moodle" : "build:host";
|
|
212
|
+
const hostWatchScript = hostKey === "wordpress" ? "watch:wp" : hostKey === "moodle" ? "watch:moodle" : "watch:host";
|
|
213
|
+
|
|
176
214
|
if (installOk) {
|
|
177
215
|
log(`MIDDAG React UI ready in ${dirName}/ (${elapsed}s)\n`);
|
|
178
216
|
console.log(" Start developing:");
|
|
@@ -194,6 +232,11 @@ console.log(" src/pages/settings.ts \u2190 advanced: tabbed_panel + fo
|
|
|
194
232
|
console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
|
|
195
233
|
console.log(" src/app.tsx \u2190 hash-based page router");
|
|
196
234
|
|
|
235
|
+
blank();
|
|
236
|
+
console.log(` Production build for ${host.name}:`);
|
|
237
|
+
console.log(` npm run ${hostBuildScript} \u2192 build for ${host.name}`);
|
|
238
|
+
console.log(` npm run ${hostWatchScript} \u2192 rebuild on change`);
|
|
239
|
+
|
|
197
240
|
blank();
|
|
198
241
|
console.log(` Integrate with your ${host.name} plugin:`);
|
|
199
242
|
console.log(" 1. Import { ContractPage } from '@middag-io/react'");
|
package/lib/detect.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* detect.js — Host detection (Moodle / WordPress / Custom).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Detection strategy:
|
|
5
|
+
* - Moodle plugin: cwd has version.php with $plugin->component
|
|
6
|
+
* - WordPress: wp-config.php found in ancestor dirs (plugin/theme is 3+ levels deep)
|
|
7
|
+
* - Moodle root: ancestor has version.php WITHOUT $plugin->component
|
|
8
|
+
*
|
|
9
|
+
* Walks up to MAX_DEPTH ancestor directories from cwd.
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
|
|
15
|
+
/** Max ancestor levels to walk when searching for platform markers. */
|
|
16
|
+
const MAX_DEPTH = 5;
|
|
9
17
|
|
|
10
18
|
export const HOSTS = {
|
|
11
19
|
moodle: { name: "Moodle", detect: "version.php", port: 5174, headerHeight: 50 },
|
|
@@ -14,16 +22,74 @@ export const HOSTS = {
|
|
|
14
22
|
};
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
|
-
*
|
|
25
|
+
* Check if a version.php file contains $plugin->component (Moodle plugin marker).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} filePath - Path to version.php
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function isMoodlePluginVersion(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(filePath, "utf-8");
|
|
33
|
+
return /\$plugin\s*->\s*component\s*=/.test(content);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a version.php file is a Moodle root version.php.
|
|
41
|
+
* Root has $version but NOT $plugin->component.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} filePath - Path to version.php
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isMoodleRootVersion(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(filePath, "utf-8");
|
|
49
|
+
if (/\$plugin\s*->\s*component\s*=/.test(content)) return false;
|
|
50
|
+
// Moodle root has both $version and $release — distinguishes from generic version.php
|
|
51
|
+
return /\$version\s*=/.test(content) && /\$release\s*=/.test(content);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Detect host platform by checking cwd and ancestor directories.
|
|
59
|
+
*
|
|
60
|
+
* Priority:
|
|
61
|
+
* 1. Moodle plugin — cwd has version.php with $plugin->component
|
|
62
|
+
* 2. WordPress — wp-config.php in any ancestor (up to MAX_DEPTH)
|
|
63
|
+
* 3. Moodle root — version.php without $plugin->component in any ancestor
|
|
18
64
|
*
|
|
19
65
|
* @param {string} cwd - Directory to check
|
|
20
66
|
* @returns {string|null} Host key ('moodle', 'wordpress') or null
|
|
21
67
|
*/
|
|
22
68
|
export function detectHost(cwd) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
69
|
+
// 1. Moodle plugin — version.php in cwd with $plugin->component
|
|
70
|
+
const cwdVersion = join(cwd, "version.php");
|
|
71
|
+
if (existsSync(cwdVersion) && isMoodlePluginVersion(cwdVersion)) {
|
|
72
|
+
return "moodle";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Walk ancestors for WordPress (wp-config.php) or Moodle root (version.php)
|
|
76
|
+
let dir = cwd;
|
|
77
|
+
for (let i = 0; i < MAX_DEPTH; i++) {
|
|
78
|
+
const parent = dirname(dir);
|
|
79
|
+
if (parent === dir) break;
|
|
80
|
+
dir = parent;
|
|
81
|
+
|
|
82
|
+
// WordPress — wp-config.php
|
|
83
|
+
if (existsSync(join(dir, "wp-config.php"))) {
|
|
84
|
+
return "wordpress";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Moodle root — version.php without $plugin->component
|
|
88
|
+
const versionFile = join(dir, "version.php");
|
|
89
|
+
if (existsSync(versionFile) && isMoodleRootVersion(versionFile)) {
|
|
90
|
+
return "moodle";
|
|
26
91
|
}
|
|
27
92
|
}
|
|
93
|
+
|
|
28
94
|
return null;
|
|
29
95
|
}
|
package/lib/scaffold.js
CHANGED
|
@@ -77,8 +77,9 @@ export function createTargetDir(targetDir) {
|
|
|
77
77
|
*/
|
|
78
78
|
/**
|
|
79
79
|
* @param {string} registryPath - "github" (PRO) or "public" (FREE)
|
|
80
|
+
* @param {string} [hostKey] - 'wordpress' | 'moodle' | 'custom' (adds host build scripts)
|
|
80
81
|
*/
|
|
81
|
-
export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
|
|
82
|
+
export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey) {
|
|
82
83
|
const filePath = join(targetDir, "package.json");
|
|
83
84
|
if (skipIfExists(filePath, "package.json")) return;
|
|
84
85
|
|
|
@@ -93,19 +94,33 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
|
|
|
93
94
|
deps["sonner"] = "^2.0.0";
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
const scripts = {
|
|
98
|
+
dev: "vite",
|
|
99
|
+
build: "vite build",
|
|
100
|
+
typecheck: "tsc --noEmit",
|
|
101
|
+
lint: "eslint .",
|
|
102
|
+
"lint:fix": "eslint . --fix",
|
|
103
|
+
format: 'prettier --write "src/**/*.{ts,tsx,css}"',
|
|
104
|
+
"format:check": 'prettier --check "src/**/*.{ts,tsx,css}"',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Add host-specific build/watch scripts
|
|
108
|
+
if (hostKey === "wordpress") {
|
|
109
|
+
scripts["build:wp"] = "vite build --config vite.config.wordpress.ts";
|
|
110
|
+
scripts["watch:wp"] = "vite build --config vite.config.wordpress.ts --watch";
|
|
111
|
+
} else if (hostKey === "moodle") {
|
|
112
|
+
scripts["build:moodle"] = "vite build --config vite.config.moodle.ts";
|
|
113
|
+
scripts["watch:moodle"] = "vite build --config vite.config.moodle.ts --watch";
|
|
114
|
+
} else if (hostKey === "custom") {
|
|
115
|
+
scripts["build:host"] = "vite build --config vite.config.custom.ts";
|
|
116
|
+
scripts["watch:host"] = "vite build --config vite.config.custom.ts --watch";
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
const pkg = {
|
|
97
120
|
name: `${projectName}-ui`,
|
|
98
121
|
private: true,
|
|
99
122
|
type: "module",
|
|
100
|
-
scripts
|
|
101
|
-
dev: "vite",
|
|
102
|
-
build: "vite build",
|
|
103
|
-
typecheck: "tsc --noEmit",
|
|
104
|
-
lint: "eslint .",
|
|
105
|
-
"lint:fix": "eslint . --fix",
|
|
106
|
-
format: 'prettier --write "src/**/*.{ts,tsx,css}"',
|
|
107
|
-
"format:check": 'prettier --check "src/**/*.{ts,tsx,css}"',
|
|
108
|
-
},
|
|
123
|
+
scripts,
|
|
109
124
|
dependencies: deps,
|
|
110
125
|
devDependencies: {
|
|
111
126
|
"@types/react": "^19.0.0",
|
|
@@ -185,6 +200,9 @@ import { resolve } from "path";
|
|
|
185
200
|
export default defineConfig({
|
|
186
201
|
plugins: [react()],
|
|
187
202
|
server: { port: ${host.port} },
|
|
203
|
+
optimizeDeps: {
|
|
204
|
+
include: ["@middag-io/react", "@middag-io/react/mock"],
|
|
205
|
+
},
|
|
188
206
|
resolve: {
|
|
189
207
|
alias: {
|
|
190
208
|
"@/": resolve(__dirname, "src") + "/",
|
|
@@ -294,6 +312,7 @@ export function scaffoldIndexHtml(targetDir) {
|
|
|
294
312
|
-->
|
|
295
313
|
<div id="root" class="middag-root"></div>
|
|
296
314
|
<div id="middag-portals" class="middag-root"></div>
|
|
315
|
+
<script>window.__MIDDAG_MOCK_NAVIGATE__ = true;</script>
|
|
297
316
|
<script type="module" src="/src/main.tsx"></script>
|
|
298
317
|
</body>
|
|
299
318
|
</html>
|
|
@@ -1231,6 +1250,293 @@ export function App() {
|
|
|
1231
1250
|
}
|
|
1232
1251
|
}
|
|
1233
1252
|
|
|
1253
|
+
// ── Host-specific production files ─────────────────────────────────────
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Scaffold production entry point: src/entry-{hostKey}.tsx.
|
|
1257
|
+
* Uses real createInertiaApp — no mocks.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {string} targetDir - Absolute path to UI dir
|
|
1260
|
+
* @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
|
|
1261
|
+
*/
|
|
1262
|
+
export function scaffoldHostEntry(targetDir, hostKey) {
|
|
1263
|
+
ensureDir(join(targetDir, "src"));
|
|
1264
|
+
|
|
1265
|
+
const filePath = join(targetDir, "src", `entry-${hostKey}.tsx`);
|
|
1266
|
+
const label = `src/entry-${hostKey}.tsx`;
|
|
1267
|
+
if (skipIfExists(filePath, label)) return;
|
|
1268
|
+
|
|
1269
|
+
// Host-specific setup and post-mount code
|
|
1270
|
+
let setupCode = "";
|
|
1271
|
+
let postMountCode = "";
|
|
1272
|
+
|
|
1273
|
+
if (hostKey === "wordpress") {
|
|
1274
|
+
setupCode = ` document.body.classList.add("middag-active");`;
|
|
1275
|
+
postMountCode = `
|
|
1276
|
+
// Relocate WP admin notices into the product shell content area
|
|
1277
|
+
const noticeContainer = document.createElement("div");
|
|
1278
|
+
noticeContainer.className = "middag-wp-notices";
|
|
1279
|
+
const observer = new MutationObserver(() => {
|
|
1280
|
+
const content = document.querySelector(".product-shell__content");
|
|
1281
|
+
if (!content) return;
|
|
1282
|
+
const notices = document.querySelectorAll(
|
|
1283
|
+
"#wpbody-content > .notice, #wpbody-content > .update-nag, #wpbody-content > .updated, #wpbody-content > .error",
|
|
1284
|
+
);
|
|
1285
|
+
if (notices.length > 0) {
|
|
1286
|
+
notices.forEach((n) => noticeContainer.appendChild(n));
|
|
1287
|
+
if (!noticeContainer.parentElement) {
|
|
1288
|
+
content.prepend(noticeContainer);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
observer.observe(document.body, { childList: true, subtree: true });`;
|
|
1293
|
+
} else if (hostKey === "moodle") {
|
|
1294
|
+
setupCode = ` document.body.classList.add("middag-active");`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const content = `/**
|
|
1298
|
+
* Production entry point for ${hostKey === "wordpress" ? "WordPress" : hostKey === "moodle" ? "Moodle" : "custom host"}.
|
|
1299
|
+
*
|
|
1300
|
+
* Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : hostKey === "moodle" ? "Moodle" : "your backend"})
|
|
1301
|
+
* serves the HTML and Inertia page props. This file is the build target
|
|
1302
|
+
* for \`npm run build:${hostKey === "custom" ? "host" : hostKey}\`.
|
|
1303
|
+
*
|
|
1304
|
+
* NOT used by \`npm run dev\` — that uses src/main.tsx with mock adapters.
|
|
1305
|
+
*/
|
|
1306
|
+
import { createRoot } from "react-dom/client";
|
|
1307
|
+
import { createInertiaApp } from "@inertiajs/react";
|
|
1308
|
+
import { I18nProvider, ProgressProvider } from "@middag-io/react";
|
|
1309
|
+
import "@middag-io/react/style.css";
|
|
1310
|
+
import "./theme.css";
|
|
1311
|
+
import { registerDefaults } from "./app/register";
|
|
1312
|
+
import { resolvePageComponent } from "./app/page-resolver";
|
|
1313
|
+
|
|
1314
|
+
registerDefaults();
|
|
1315
|
+
|
|
1316
|
+
createInertiaApp({
|
|
1317
|
+
id: "middag-app",
|
|
1318
|
+
resolve: (name) => resolvePageComponent(name),
|
|
1319
|
+
setup({ el, App, props }) {
|
|
1320
|
+
el.classList.add("middag-root");
|
|
1321
|
+
${setupCode}
|
|
1322
|
+
createRoot(el).render(
|
|
1323
|
+
<ProgressProvider>
|
|
1324
|
+
<App {...props}>
|
|
1325
|
+
{({ Component, props: pageProps, key }) => (
|
|
1326
|
+
<I18nProvider>
|
|
1327
|
+
<Component key={key} {...pageProps} />
|
|
1328
|
+
</I18nProvider>
|
|
1329
|
+
)}
|
|
1330
|
+
</App>
|
|
1331
|
+
</ProgressProvider>,
|
|
1332
|
+
);
|
|
1333
|
+
${postMountCode}
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
`;
|
|
1337
|
+
|
|
1338
|
+
writeFile(filePath, content, label);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Scaffold host-specific Vite build config: vite.config.{hostKey}.ts.
|
|
1343
|
+
*
|
|
1344
|
+
* @param {string} targetDir - Absolute path to UI dir
|
|
1345
|
+
* @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
|
|
1346
|
+
* @param {object} host - HOSTS[hostKey] object
|
|
1347
|
+
*/
|
|
1348
|
+
export function scaffoldHostViteConfig(targetDir, hostKey, host) {
|
|
1349
|
+
const filePath = join(targetDir, `vite.config.${hostKey}.ts`);
|
|
1350
|
+
const label = `vite.config.${hostKey}.ts`;
|
|
1351
|
+
if (skipIfExists(filePath, label)) return;
|
|
1352
|
+
|
|
1353
|
+
let outDir, formats, libName, fileName, extraRollup;
|
|
1354
|
+
|
|
1355
|
+
if (hostKey === "wordpress") {
|
|
1356
|
+
outDir = `resolve(__dirname, "../assets/dist")`;
|
|
1357
|
+
formats = `["iife"]`;
|
|
1358
|
+
libName = `"MiddagUI"`;
|
|
1359
|
+
fileName = `() => "app.js"`;
|
|
1360
|
+
extraRollup = `
|
|
1361
|
+
output: {
|
|
1362
|
+
assetFileNames: (assetInfo) => {
|
|
1363
|
+
if (assetInfo.name?.endsWith(".css")) return "style.css";
|
|
1364
|
+
return assetInfo.name || "[name]-[hash][extname]";
|
|
1365
|
+
},
|
|
1366
|
+
},`;
|
|
1367
|
+
} else if (hostKey === "moodle") {
|
|
1368
|
+
outDir = `resolve(__dirname, "../amd/build")`;
|
|
1369
|
+
formats = `["iife"]`;
|
|
1370
|
+
libName = `"MiddagUI"`;
|
|
1371
|
+
fileName = `() => "app.js"`;
|
|
1372
|
+
extraRollup = `
|
|
1373
|
+
output: {
|
|
1374
|
+
assetFileNames: (assetInfo) => {
|
|
1375
|
+
if (assetInfo.name?.endsWith(".css")) return "style.css";
|
|
1376
|
+
return assetInfo.name || "[name]-[hash][extname]";
|
|
1377
|
+
},
|
|
1378
|
+
},`;
|
|
1379
|
+
} else {
|
|
1380
|
+
outDir = `resolve(__dirname, "../dist")`;
|
|
1381
|
+
formats = `["es"]`;
|
|
1382
|
+
libName = `"MiddagUI"`;
|
|
1383
|
+
fileName = `() => "app.js"`;
|
|
1384
|
+
extraRollup = `
|
|
1385
|
+
output: {
|
|
1386
|
+
assetFileNames: (assetInfo) => {
|
|
1387
|
+
if (assetInfo.name?.endsWith(".css")) return "style.css";
|
|
1388
|
+
return assetInfo.name || "[name]-[hash][extname]";
|
|
1389
|
+
},
|
|
1390
|
+
},`;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const content = `/**
|
|
1394
|
+
* Vite build config for ${host.name} — production build target.
|
|
1395
|
+
*
|
|
1396
|
+
* Usage:
|
|
1397
|
+
* npm run build:${hostKey === "custom" ? "host" : hostKey} \u2192 single build
|
|
1398
|
+
* npm run watch:${hostKey === "custom" ? "host" : hostKey} \u2192 rebuild on change
|
|
1399
|
+
*
|
|
1400
|
+
* This config builds src/entry-${hostKey}.tsx into a${hostKey === "custom" ? "n ESM" : "n IIFE"} bundle.
|
|
1401
|
+
* The dev server (\`npm run dev\`) uses vite.config.ts instead.
|
|
1402
|
+
*/
|
|
1403
|
+
import { defineConfig } from "vite";
|
|
1404
|
+
import react from "@vitejs/plugin-react";
|
|
1405
|
+
import { resolve } from "path";
|
|
1406
|
+
|
|
1407
|
+
export default defineConfig({
|
|
1408
|
+
plugins: [react()],
|
|
1409
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
1410
|
+
resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
|
|
1411
|
+
build: {
|
|
1412
|
+
outDir: ${outDir},
|
|
1413
|
+
emptyOutDir: true,
|
|
1414
|
+
lib: {
|
|
1415
|
+
entry: resolve(__dirname, "src/entry-${hostKey}.tsx"),
|
|
1416
|
+
formats: ${formats},
|
|
1417
|
+
name: ${libName},
|
|
1418
|
+
fileName: ${fileName},
|
|
1419
|
+
},
|
|
1420
|
+
cssCodeSplit: false,
|
|
1421
|
+
rollupOptions: {${extraRollup}
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
`;
|
|
1426
|
+
|
|
1427
|
+
writeFile(filePath, content, label);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Append host-specific CSS to src/theme.css.
|
|
1432
|
+
* If theme.css doesn't exist yet, it will be created by scaffoldDemoFiles.
|
|
1433
|
+
*
|
|
1434
|
+
* @param {string} targetDir - Absolute path to UI dir
|
|
1435
|
+
* @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
|
|
1436
|
+
* @param {object} host - HOSTS[hostKey] object
|
|
1437
|
+
*/
|
|
1438
|
+
export function scaffoldHostThemeCSS(targetDir, hostKey, host) {
|
|
1439
|
+
const themePath = join(targetDir, "src", "theme.css");
|
|
1440
|
+
|
|
1441
|
+
let hostSection;
|
|
1442
|
+
|
|
1443
|
+
if (hostKey === "wordpress") {
|
|
1444
|
+
hostSection = `
|
|
1445
|
+
|
|
1446
|
+
/* \u2500\u2500 WordPress admin integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1447
|
+
* Active when MIDDAG mounts inside wp-admin (body.middag-active).
|
|
1448
|
+
*/
|
|
1449
|
+
|
|
1450
|
+
body.middag-active #wpbody-content {
|
|
1451
|
+
padding-bottom: 0;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
body.middag-active [data-slot="sidebar-container"] {
|
|
1455
|
+
left: 160px !important;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
body.folded.middag-active [data-slot="sidebar-container"] {
|
|
1459
|
+
left: 36px !important;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
@media screen and (max-width: 782px) {
|
|
1463
|
+
body.middag-active [data-slot="sidebar-container"] {
|
|
1464
|
+
left: 0 !important;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
body.middag-active #wpbody-content > .notice,
|
|
1469
|
+
body.middag-active #wpbody-content > .update-nag,
|
|
1470
|
+
body.middag-active #wpbody-content > .updated,
|
|
1471
|
+
body.middag-active #wpbody-content > .error {
|
|
1472
|
+
display: none !important;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.middag-wp-notices {
|
|
1476
|
+
padding: 0.75rem 1.5rem 0;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.middag-wp-notices .notice {
|
|
1480
|
+
display: block !important;
|
|
1481
|
+
margin: 0 0 0.5rem;
|
|
1482
|
+
border-radius: var(--radius, 0.5rem);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
body.middag-active #wpfooter {
|
|
1486
|
+
display: none !important;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
body.middag-active {
|
|
1490
|
+
--host-header-height: 32px;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
@media screen and (max-width: 782px) {
|
|
1494
|
+
body.middag-active {
|
|
1495
|
+
--host-header-height: 46px;
|
|
1496
|
+
}
|
|
1497
|
+
}`;
|
|
1498
|
+
} else if (hostKey === "moodle") {
|
|
1499
|
+
hostSection = `
|
|
1500
|
+
|
|
1501
|
+
/* \u2500\u2500 Moodle Boost integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1502
|
+
* Active when MIDDAG mounts inside Moodle admin (body.middag-active).
|
|
1503
|
+
*/
|
|
1504
|
+
|
|
1505
|
+
body.middag-active {
|
|
1506
|
+
--host-header-height: 50px;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
body.middag-active [data-slot="sidebar-container"] {
|
|
1510
|
+
left: 0 !important;
|
|
1511
|
+
}`;
|
|
1512
|
+
} else {
|
|
1513
|
+
hostSection = `
|
|
1514
|
+
|
|
1515
|
+
/* \u2500\u2500 Host integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1516
|
+
* Set --host-header-height and --host-sidebar-width as needed.
|
|
1517
|
+
*/
|
|
1518
|
+
|
|
1519
|
+
:root {
|
|
1520
|
+
--host-header-height: 0px;
|
|
1521
|
+
--host-sidebar-width: 0px;
|
|
1522
|
+
}`;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// If theme.css exists, append; otherwise create with host section only
|
|
1526
|
+
if (existsSync(themePath)) {
|
|
1527
|
+
try {
|
|
1528
|
+
const existing = readFileSync(themePath, "utf-8");
|
|
1529
|
+
writeFileSync(themePath, existing + hostSection + "\n");
|
|
1530
|
+
success(`Appended ${host.name} integration CSS to src/theme.css`);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
error(`Failed to append to src/theme.css: ${err.message}`);
|
|
1533
|
+
}
|
|
1534
|
+
} else {
|
|
1535
|
+
ensureDir(join(targetDir, "src"));
|
|
1536
|
+
writeFile(themePath, hostSection.trimStart() + "\n", "src/theme.css (host integration)");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1234
1540
|
// ── LEGACY (kept for backward compat, delegates to FREE) ────────────────
|
|
1235
1541
|
|
|
1236
1542
|
/** @deprecated Use scaffoldFreeApp + scaffoldFreeAdapters instead */
|
|
@@ -1474,3 +1780,70 @@ export const router = {
|
|
|
1474
1780
|
);
|
|
1475
1781
|
}
|
|
1476
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
// ── Template reader ─────────────────────────────────────────────────────
|
|
1785
|
+
|
|
1786
|
+
/** Read a template file relative to this script's directory. */
|
|
1787
|
+
function readTemplate(relativePath) {
|
|
1788
|
+
return readFileSync(join(__dirname, relativePath), "utf-8");
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// ── Shared: register, page-resolver, route helper, demo page ────────────
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Scaffold FREE register: src/app/register.ts — minimal (5 blocks).
|
|
1795
|
+
*/
|
|
1796
|
+
export function scaffoldFreeRegister(targetDir) {
|
|
1797
|
+
ensureDir(join(targetDir, "src", "app"));
|
|
1798
|
+
const filePath = join(targetDir, "src", "app", "register.ts");
|
|
1799
|
+
if (skipIfExists(filePath, "src/app/register.ts")) return;
|
|
1800
|
+
writeFile(filePath, readTemplate("templates/shared/register-free.ts"), "src/app/register.ts (FREE)");
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Scaffold page resolver: src/app/page-resolver.tsx.
|
|
1805
|
+
* Supports Contract: prefix pages (ContractPage) and direct pages (glob).
|
|
1806
|
+
*/
|
|
1807
|
+
export function scaffoldPageResolver(targetDir) {
|
|
1808
|
+
ensureDir(join(targetDir, "src", "app"));
|
|
1809
|
+
const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
|
|
1810
|
+
if (skipIfExists(filePath, "src/app/page-resolver.tsx")) return;
|
|
1811
|
+
writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Scaffold route helper: src/lib/routes.ts.
|
|
1816
|
+
* Abstracts host admin URL vs dev mock path.
|
|
1817
|
+
*
|
|
1818
|
+
* @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
|
|
1819
|
+
*/
|
|
1820
|
+
export function scaffoldRouteHelper(targetDir, hostKey) {
|
|
1821
|
+
ensureDir(join(targetDir, "src", "lib"));
|
|
1822
|
+
const filePath = join(targetDir, "src", "lib", "routes.ts");
|
|
1823
|
+
if (skipIfExists(filePath, "src/lib/routes.ts")) return;
|
|
1824
|
+
|
|
1825
|
+
const templateMap = {
|
|
1826
|
+
wordpress: "templates/shared/route-helper-wp.ts",
|
|
1827
|
+
moodle: "templates/shared/route-helper-moodle.ts",
|
|
1828
|
+
custom: "templates/shared/route-helper-custom.ts",
|
|
1829
|
+
};
|
|
1830
|
+
const template = templateMap[hostKey] || templateMap.wordpress;
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
writeFile(filePath, readTemplate(template), "src/lib/routes.ts");
|
|
1834
|
+
} catch {
|
|
1835
|
+
// Fallback to WP template if host-specific one doesn't exist
|
|
1836
|
+
writeFile(filePath, readTemplate("templates/shared/route-helper-wp.ts"), "src/lib/routes.ts");
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Scaffold demo direct page: src/pages/DemoPage.tsx.
|
|
1842
|
+
* Shows the direct page pattern (usePage + custom React).
|
|
1843
|
+
*/
|
|
1844
|
+
export function scaffoldDemoDirectPage(targetDir) {
|
|
1845
|
+
ensureDir(join(targetDir, "src", "pages"));
|
|
1846
|
+
const filePath = join(targetDir, "src", "pages", "DemoPage.tsx");
|
|
1847
|
+
if (skipIfExists(filePath, "src/pages/DemoPage.tsx")) return;
|
|
1848
|
+
writeFile(filePath, readTemplate("templates/shared/demo-page.tsx"), "src/pages/DemoPage.tsx");
|
|
1849
|
+
}
|