create-middag-ui 0.16.0 → 0.18.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 +16 -5
- package/lib/scaffold.js +264 -51
- package/lib/templates/shared/moodle-admin-shell.tsx +108 -0
- package/lib/templates/shared/page-resolver-moodle.tsx +86 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
scaffoldDemoFiles,
|
|
30
30
|
scaffoldDevShell,
|
|
31
31
|
scaffoldEslintConfig,
|
|
32
|
+
scaffoldGitignore,
|
|
33
|
+
scaffoldInjectPlaceholders,
|
|
32
34
|
scaffoldFreeAdapters,
|
|
33
35
|
scaffoldFreeApp,
|
|
34
36
|
scaffoldFreeRegister,
|
|
@@ -38,6 +40,8 @@ import {
|
|
|
38
40
|
scaffoldHostViteConfig,
|
|
39
41
|
scaffoldIndexHtml,
|
|
40
42
|
scaffoldMoodleAdapters,
|
|
43
|
+
scaffoldMoodleAdminShell,
|
|
44
|
+
scaffoldMoodlePageResolver,
|
|
41
45
|
scaffoldMoodlePlugin,
|
|
42
46
|
scaffoldMoodleTailwind,
|
|
43
47
|
scaffoldPackageJson,
|
|
@@ -166,11 +170,13 @@ if (!dirCreated) {
|
|
|
166
170
|
|
|
167
171
|
heading(5, TOTAL_STEPS, "Scaffolding config files");
|
|
168
172
|
|
|
173
|
+
const isPro = registryPath === "github";
|
|
169
174
|
scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey);
|
|
170
175
|
scaffoldTsconfig(targetDir);
|
|
171
176
|
scaffoldViteConfig(targetDir, host, registryPath);
|
|
172
|
-
scaffoldEslintConfig(targetDir);
|
|
177
|
+
scaffoldEslintConfig(targetDir, isPro);
|
|
173
178
|
scaffoldPrettierConfig(targetDir);
|
|
179
|
+
scaffoldGitignore(targetDir, isPro);
|
|
174
180
|
scaffoldIndexHtml(targetDir);
|
|
175
181
|
|
|
176
182
|
// ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
|
|
@@ -192,13 +198,16 @@ scaffoldDemoFiles(targetDir);
|
|
|
192
198
|
|
|
193
199
|
// ── Step 8: Scaffold app + page examples (PRO vs FREE) ─────────────────
|
|
194
200
|
|
|
195
|
-
const isPro = registryPath === "github";
|
|
196
201
|
heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
|
|
197
202
|
|
|
198
203
|
scaffoldPageExamples(targetDir);
|
|
199
204
|
|
|
200
|
-
//
|
|
201
|
-
|
|
205
|
+
// Page resolver — Moodle gets AMD version (RequireJS + extension globs)
|
|
206
|
+
if (hostKey === "moodle") {
|
|
207
|
+
scaffoldMoodlePageResolver(targetDir);
|
|
208
|
+
} else {
|
|
209
|
+
scaffoldPageResolver(targetDir);
|
|
210
|
+
}
|
|
202
211
|
scaffoldDemoDirectPage(targetDir);
|
|
203
212
|
// Convert frankenstyle "local_middag" → Moodle path "local/middag"
|
|
204
213
|
const moodlePath = moodleComponent ? moodleComponent.replace("_", "/") : null;
|
|
@@ -217,6 +226,7 @@ if (isPro) {
|
|
|
217
226
|
// PRO Inertia adapters — re-export from @middag-io/react/mock so usePage()
|
|
218
227
|
// shares the same React context as MockPageProvider (no context mismatch).
|
|
219
228
|
scaffoldProAdapters(targetDir);
|
|
229
|
+
scaffoldInjectPlaceholders(targetDir);
|
|
220
230
|
success("PRO: using MockProductShell from @middag-io/react/mock");
|
|
221
231
|
} catch {
|
|
222
232
|
// npm version — PRO file excluded, fall back to FREE
|
|
@@ -245,7 +255,8 @@ if (hostKey === "moodle" && moodleComponent) {
|
|
|
245
255
|
scaffoldMoodlePlugin(targetDir, moodleComponent);
|
|
246
256
|
scaffoldMoodleTailwind(targetDir);
|
|
247
257
|
scaffoldMoodleAdapters(targetDir, moodleComponent);
|
|
248
|
-
|
|
258
|
+
scaffoldMoodleAdminShell(targetDir);
|
|
259
|
+
success(`Moodle: AMD plugin + Tailwind + adapters + AdminShell for ${ moodleComponent }`);
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
// ── Step 9: npm install ──────────────────────────────────────────────────
|
package/lib/scaffold.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* scaffold.js — File creation for all scaffolded files.
|
|
3
3
|
*
|
|
4
4
|
* Creates directory structure, config files, page examples, app entry,
|
|
5
|
-
* and Inertia mock adapters.
|
|
5
|
+
* and Inertia mock adapters. Dev-only adapters live under mock/.
|
|
6
6
|
*
|
|
7
7
|
* Every I/O operation is wrapped with error handling.
|
|
8
8
|
*/
|
|
@@ -127,17 +127,30 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
|
|
|
127
127
|
if (hostKey === "wordpress") {
|
|
128
128
|
scripts["build:wp"] = "vite build --config vite.config.wordpress.ts";
|
|
129
129
|
scripts["watch:wp"] = "vite build --config vite.config.wordpress.ts --watch";
|
|
130
|
+
if (isPro) {
|
|
131
|
+
scripts["build:licensed"] =
|
|
132
|
+
"vite build --config vite.config.wordpress.ts && node scripts/inject-placeholders.mjs";
|
|
133
|
+
}
|
|
130
134
|
} else if (hostKey === "moodle") {
|
|
131
135
|
scripts["build:moodle"] = "vite build --config vite.config.moodle.ts";
|
|
132
136
|
scripts["watch:moodle"] = "vite build --config vite.config.moodle.ts --watch";
|
|
137
|
+
if (isPro) {
|
|
138
|
+
scripts["build:licensed"] =
|
|
139
|
+
"vite build --config vite.config.moodle.ts && node scripts/inject-placeholders.mjs";
|
|
140
|
+
}
|
|
133
141
|
} else if (hostKey === "custom") {
|
|
134
142
|
scripts["build:host"] = "vite build --config vite.config.custom.ts";
|
|
135
143
|
scripts["watch:host"] = "vite build --config vite.config.custom.ts --watch";
|
|
144
|
+
if (isPro) {
|
|
145
|
+
scripts["build:licensed"] =
|
|
146
|
+
"vite build --config vite.config.custom.ts && node scripts/inject-placeholders.mjs";
|
|
147
|
+
}
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
const pkg = {
|
|
139
151
|
name: `${projectName}-ui`,
|
|
140
152
|
private: true,
|
|
153
|
+
version: "0.1.0",
|
|
141
154
|
type: "module",
|
|
142
155
|
scripts,
|
|
143
156
|
dependencies: deps,
|
|
@@ -190,6 +203,11 @@ export function scaffoldTsconfig(targetDir) {
|
|
|
190
203
|
};
|
|
191
204
|
|
|
192
205
|
writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
|
|
206
|
+
|
|
207
|
+
const envFilePath = join(targetDir, "src", "vite-env.d.ts");
|
|
208
|
+
if (!skipIfExists(envFilePath, "src/vite-env.d.ts")) {
|
|
209
|
+
writeFile(envFilePath, `/// <reference types="vite/client" />\n`, "src/vite-env.d.ts");
|
|
210
|
+
}
|
|
193
211
|
}
|
|
194
212
|
|
|
195
213
|
/**
|
|
@@ -226,9 +244,10 @@ export default defineConfig({
|
|
|
226
244
|
resolve: {
|
|
227
245
|
alias: {
|
|
228
246
|
"@/": resolve(__dirname, "src") + "/",
|
|
247
|
+
"@mock/": resolve(__dirname, "mock") + "/",
|
|
229
248
|
${adapterComment}
|
|
230
|
-
"@inertiajs/react": resolve(__dirname, "
|
|
231
|
-
"@inertiajs/core": resolve(__dirname, "
|
|
249
|
+
"@inertiajs/react": resolve(__dirname, "mock/adapters/inertia-react.ts"),
|
|
250
|
+
"@inertiajs/core": resolve(__dirname, "mock/adapters/inertia-core.ts"),
|
|
232
251
|
},
|
|
233
252
|
},
|
|
234
253
|
});
|
|
@@ -240,10 +259,13 @@ export default defineConfig({
|
|
|
240
259
|
/**
|
|
241
260
|
* Scaffold eslint.config.js.
|
|
242
261
|
*/
|
|
243
|
-
export function scaffoldEslintConfig(targetDir) {
|
|
262
|
+
export function scaffoldEslintConfig(targetDir, isPro = false) {
|
|
244
263
|
const filePath = join(targetDir, "eslint.config.js");
|
|
245
264
|
if (skipIfExists(filePath, "eslint.config.js")) return;
|
|
246
265
|
|
|
266
|
+
const ignores = ["dist/", "node_modules/", "scripts/", ...(isPro ? ["dist-licensed/"] : [])];
|
|
267
|
+
const ignoresStr = ignores.map((i) => `"${i}"`).join(", ");
|
|
268
|
+
|
|
247
269
|
writeFile(
|
|
248
270
|
filePath,
|
|
249
271
|
`import js from "@eslint/js";
|
|
@@ -253,7 +275,7 @@ import reactRefresh from "eslint-plugin-react-refresh";
|
|
|
253
275
|
import prettierConfig from "eslint-config-prettier";
|
|
254
276
|
|
|
255
277
|
export default tseslint.config(
|
|
256
|
-
{ ignores: [
|
|
278
|
+
{ ignores: [${ignoresStr}] },
|
|
257
279
|
js.configs.recommended,
|
|
258
280
|
...tseslint.configs.recommended,
|
|
259
281
|
{
|
|
@@ -263,7 +285,8 @@ export default tseslint.config(
|
|
|
263
285
|
"react-refresh": reactRefresh,
|
|
264
286
|
},
|
|
265
287
|
rules: {
|
|
266
|
-
|
|
288
|
+
"react-hooks/rules-of-hooks": "error",
|
|
289
|
+
"react-hooks/exhaustive-deps": "warn",
|
|
267
290
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
268
291
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
269
292
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -293,6 +316,7 @@ export function scaffoldPrettierConfig(targetDir) {
|
|
|
293
316
|
trailingComma: "all",
|
|
294
317
|
printWidth: 100,
|
|
295
318
|
plugins: ["prettier-plugin-tailwindcss"],
|
|
319
|
+
tailwindFunctions: ["cn", "cva"],
|
|
296
320
|
},
|
|
297
321
|
null,
|
|
298
322
|
2,
|
|
@@ -301,6 +325,99 @@ export function scaffoldPrettierConfig(targetDir) {
|
|
|
301
325
|
);
|
|
302
326
|
}
|
|
303
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Scaffold .gitignore with UI-specific dist patterns.
|
|
330
|
+
*/
|
|
331
|
+
export function scaffoldGitignore(targetDir, isPro = false) {
|
|
332
|
+
const filePath = join(targetDir, ".gitignore");
|
|
333
|
+
if (skipIfExists(filePath, ".gitignore")) return;
|
|
334
|
+
|
|
335
|
+
const lines = [
|
|
336
|
+
"node_modules/",
|
|
337
|
+
"dist/",
|
|
338
|
+
...(isPro ? ["dist-licensed/"] : []),
|
|
339
|
+
"dist-master/",
|
|
340
|
+
"dist-mock/",
|
|
341
|
+
"dist-lib/",
|
|
342
|
+
"*.log",
|
|
343
|
+
".vite/",
|
|
344
|
+
];
|
|
345
|
+
writeFile(filePath, lines.join("\n") + "\n", ".gitignore");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Scaffold scripts/inject-placeholders.mjs (PRO only).
|
|
350
|
+
* Injects opaque placeholder variables into middag-* bundles for CDN delivery.
|
|
351
|
+
* The Cloudflare Worker replaces placeholders with per-installation values.
|
|
352
|
+
*/
|
|
353
|
+
export function scaffoldInjectPlaceholders(targetDir) {
|
|
354
|
+
const scriptsDir = join(targetDir, "scripts");
|
|
355
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
356
|
+
const filePath = join(scriptsDir, "inject-placeholders.mjs");
|
|
357
|
+
if (skipIfExists(filePath, "scripts/inject-placeholders.mjs")) return;
|
|
358
|
+
|
|
359
|
+
writeFile(
|
|
360
|
+
filePath,
|
|
361
|
+
`/**
|
|
362
|
+
* inject-placeholders — generate licensed bundles for CDN delivery.
|
|
363
|
+
*
|
|
364
|
+
* Takes compiled Vite output from dist/ and produces dist-licensed/ with
|
|
365
|
+
* placeholder variables injected into middag-* bundles. The Cloudflare
|
|
366
|
+
* Worker replaces these placeholders with real values per installation.
|
|
367
|
+
*
|
|
368
|
+
* Usage: node scripts/inject-placeholders.mjs
|
|
369
|
+
* Called by: npm run build:licensed
|
|
370
|
+
*/
|
|
371
|
+
import fs from "node:fs";
|
|
372
|
+
import path from "node:path";
|
|
373
|
+
|
|
374
|
+
const DIST_DIR = path.resolve(import.meta.dirname, "../dist");
|
|
375
|
+
const LICENSED_DIR = path.resolve(import.meta.dirname, "../dist-licensed");
|
|
376
|
+
|
|
377
|
+
// Variable names are intentionally opaque to discourage reverse engineering.
|
|
378
|
+
// The mapping is documented only here and in the Worker source:
|
|
379
|
+
// _0x7a3f → wwwroot (replaced by Worker with installation URL)
|
|
380
|
+
// _0x9b1e → expiry (replaced by Worker with unix timestamp)
|
|
381
|
+
// _0x4d2c → tier (replaced by Worker with license tier)
|
|
382
|
+
// _0x6e8a → domain hash (replaced by Worker with HMAC of wwwroot)
|
|
383
|
+
const PLACEHOLDER_HEADER = [
|
|
384
|
+
'var _0x7a3f="__PH_0x7a3f__";',
|
|
385
|
+
"var _0x9b1e=0;",
|
|
386
|
+
'var _0x4d2c="__PH_0x4d2c__";',
|
|
387
|
+
'var _0x6e8a="__PH_0x6e8a__";',
|
|
388
|
+
].join("") + "\\n";
|
|
389
|
+
|
|
390
|
+
if (fs.existsSync(LICENSED_DIR)) fs.rmSync(LICENSED_DIR, { recursive: true });
|
|
391
|
+
fs.mkdirSync(LICENSED_DIR, { recursive: true });
|
|
392
|
+
|
|
393
|
+
const files = fs.readdirSync(DIST_DIR).filter((f) => f.endsWith(".js"));
|
|
394
|
+
let injected = 0;
|
|
395
|
+
let copied = 0;
|
|
396
|
+
|
|
397
|
+
for (const file of files) {
|
|
398
|
+
const srcPath = path.join(DIST_DIR, file);
|
|
399
|
+
const destPath = path.join(LICENSED_DIR, file);
|
|
400
|
+
const content = fs.readFileSync(srcPath, "utf-8");
|
|
401
|
+
|
|
402
|
+
if (file.startsWith("middag-")) {
|
|
403
|
+
fs.writeFileSync(destPath, PLACEHOLDER_HEADER + content);
|
|
404
|
+
injected++;
|
|
405
|
+
console.log(\`[licensed] \${file} — placeholders injected\`);
|
|
406
|
+
} else {
|
|
407
|
+
fs.copyFileSync(srcPath, destPath);
|
|
408
|
+
copied++;
|
|
409
|
+
console.log(\`[licensed] \${file} — copied (vendor/shared)\`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(\`\\nLicensed bundles ready in \${LICENSED_DIR}/\`);
|
|
414
|
+
console.log(\` \${injected} bundle(s) with placeholders\`);
|
|
415
|
+
console.log(\` \${copied} bundle(s) copied as-is\`);
|
|
416
|
+
`,
|
|
417
|
+
"scripts/inject-placeholders.mjs",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
304
421
|
/**
|
|
305
422
|
* Scaffold index.html at project root.
|
|
306
423
|
*/
|
|
@@ -934,12 +1051,12 @@ export const settingsContract: PageContract = {
|
|
|
934
1051
|
*/
|
|
935
1052
|
export function scaffoldProApp(targetDir) {
|
|
936
1053
|
ensureDir(join(targetDir, "src"));
|
|
937
|
-
ensureDir(join(targetDir, "
|
|
1054
|
+
ensureDir(join(targetDir, "mock", "adapters"));
|
|
938
1055
|
|
|
939
1056
|
// Inertia adapter shims — re-export from pre-built mock bundle
|
|
940
1057
|
// so usePage() shares the same MockPageContext as MockPageProvider.
|
|
941
|
-
const adapterReactPath = join(targetDir, "
|
|
942
|
-
if (!skipIfExists(adapterReactPath, "
|
|
1058
|
+
const adapterReactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
|
|
1059
|
+
if (!skipIfExists(adapterReactPath, "mock/adapters/inertia-react.ts")) {
|
|
943
1060
|
writeFile(adapterReactPath, `/**
|
|
944
1061
|
* Mock @inertiajs/react — re-exports from pre-built @middag-io/react/mock.
|
|
945
1062
|
*
|
|
@@ -947,16 +1064,16 @@ export function scaffoldProApp(targetDir) {
|
|
|
947
1064
|
* MockPageProvider (both live in the pre-built ESM bundle).
|
|
948
1065
|
*/
|
|
949
1066
|
export { usePage, Head, Link, router } from "@middag-io/react/mock";
|
|
950
|
-
`, "
|
|
1067
|
+
`, "mock/adapters/inertia-react.ts");
|
|
951
1068
|
}
|
|
952
1069
|
|
|
953
|
-
const adapterCorePath = join(targetDir, "
|
|
954
|
-
if (!skipIfExists(adapterCorePath, "
|
|
1070
|
+
const adapterCorePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
|
|
1071
|
+
if (!skipIfExists(adapterCorePath, "mock/adapters/inertia-core.ts")) {
|
|
955
1072
|
writeFile(adapterCorePath, `/**
|
|
956
1073
|
* Mock @inertiajs/core — re-exports from pre-built @middag-io/react/mock.
|
|
957
1074
|
*/
|
|
958
1075
|
export { router, setMockNavigate } from "@middag-io/react/mock";
|
|
959
|
-
`, "
|
|
1076
|
+
`, "mock/adapters/inertia-core.ts");
|
|
960
1077
|
}
|
|
961
1078
|
|
|
962
1079
|
const mainPath = join(targetDir, "src", "main.tsx");
|
|
@@ -1053,13 +1170,13 @@ export function App() {
|
|
|
1053
1170
|
}
|
|
1054
1171
|
|
|
1055
1172
|
/**
|
|
1056
|
-
* Scaffold FREE adapters:
|
|
1173
|
+
* Scaffold FREE adapters: mock/adapters/ with react-router.
|
|
1057
1174
|
*/
|
|
1058
1175
|
export function scaffoldFreeAdapters(targetDir) {
|
|
1059
|
-
ensureDir(join(targetDir, "
|
|
1176
|
+
ensureDir(join(targetDir, "mock", "adapters"));
|
|
1060
1177
|
|
|
1061
|
-
const corePath = join(targetDir, "
|
|
1062
|
-
if (!skipIfExists(corePath, "
|
|
1178
|
+
const corePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
|
|
1179
|
+
if (!skipIfExists(corePath, "mock/adapters/inertia-core.ts")) {
|
|
1063
1180
|
writeFile(corePath, `/**
|
|
1064
1181
|
* Mock @inertiajs/core for standalone dev server.
|
|
1065
1182
|
* Vite alias redirects @inertiajs/core here.
|
|
@@ -1078,11 +1195,11 @@ export const router = {
|
|
|
1078
1195
|
visit: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] VISIT", url); },
|
|
1079
1196
|
on: () => () => {},
|
|
1080
1197
|
};
|
|
1081
|
-
`, "
|
|
1198
|
+
`, "mock/adapters/inertia-core.ts");
|
|
1082
1199
|
}
|
|
1083
1200
|
|
|
1084
|
-
const reactPath = join(targetDir, "
|
|
1085
|
-
if (!skipIfExists(reactPath, "
|
|
1201
|
+
const reactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
|
|
1202
|
+
if (!skipIfExists(reactPath, "mock/adapters/inertia-react.ts")) {
|
|
1086
1203
|
writeFile(reactPath, `/**
|
|
1087
1204
|
* Mock @inertiajs/react for standalone dev server (FREE).
|
|
1088
1205
|
* Context-based usePage + react-router Link.
|
|
@@ -1127,7 +1244,7 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
|
|
|
1127
1244
|
});
|
|
1128
1245
|
|
|
1129
1246
|
export { router };
|
|
1130
|
-
`, "
|
|
1247
|
+
`, "mock/adapters/inertia-react.ts");
|
|
1131
1248
|
}
|
|
1132
1249
|
}
|
|
1133
1250
|
|
|
@@ -1139,20 +1256,20 @@ export { router };
|
|
|
1139
1256
|
* defining their own context (which would cause context mismatch).
|
|
1140
1257
|
*/
|
|
1141
1258
|
export function scaffoldProAdapters(targetDir) {
|
|
1142
|
-
ensureDir(join(targetDir, "
|
|
1259
|
+
ensureDir(join(targetDir, "mock", "adapters"));
|
|
1143
1260
|
|
|
1144
|
-
const corePath = join(targetDir, "
|
|
1145
|
-
if (!skipIfExists(corePath, "
|
|
1261
|
+
const corePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
|
|
1262
|
+
if (!skipIfExists(corePath, "mock/adapters/inertia-core.ts")) {
|
|
1146
1263
|
writeFile(corePath, `/**
|
|
1147
1264
|
* Mock @inertiajs/core — PRO re-export from @middag-io/react/mock.
|
|
1148
1265
|
* Shares the same navigate function as MockProductShell.
|
|
1149
1266
|
*/
|
|
1150
1267
|
export { router, setMockNavigate } from "@middag-io/react/mock";
|
|
1151
|
-
`, "
|
|
1268
|
+
`, "mock/adapters/inertia-core.ts (PRO)");
|
|
1152
1269
|
}
|
|
1153
1270
|
|
|
1154
|
-
const reactPath = join(targetDir, "
|
|
1155
|
-
if (!skipIfExists(reactPath, "
|
|
1271
|
+
const reactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
|
|
1272
|
+
if (!skipIfExists(reactPath, "mock/adapters/inertia-react.ts")) {
|
|
1156
1273
|
writeFile(reactPath, `/**
|
|
1157
1274
|
* Mock @inertiajs/react — PRO re-export from @middag-io/react/mock.
|
|
1158
1275
|
* Shares the same React context as MockPageProvider so usePage()
|
|
@@ -1160,7 +1277,7 @@ export { router, setMockNavigate } from "@middag-io/react/mock";
|
|
|
1160
1277
|
*/
|
|
1161
1278
|
export { usePage, Head, Link } from "@middag-io/react/mock";
|
|
1162
1279
|
export { router } from "./inertia-core";
|
|
1163
|
-
`, "
|
|
1280
|
+
`, "mock/adapters/inertia-react.ts (PRO)");
|
|
1164
1281
|
}
|
|
1165
1282
|
}
|
|
1166
1283
|
|
|
@@ -1270,8 +1387,8 @@ createRoot(document.getElementById("root")!).render(
|
|
|
1270
1387
|
import { BrowserRouter, Routes, Route, useNavigate } from "react-router";
|
|
1271
1388
|
import { ContractPage, I18nProvider } from "@middag-io/react";
|
|
1272
1389
|
import type { PageContract } from "@middag-io/react";
|
|
1273
|
-
import { PageProvider } from "
|
|
1274
|
-
import { setMockNavigate } from "
|
|
1390
|
+
import { PageProvider } from "@mock/adapters/inertia-react";
|
|
1391
|
+
import { setMockNavigate } from "@mock/adapters/inertia-core";
|
|
1275
1392
|
import { dashboardContract } from "./pages/dashboard";
|
|
1276
1393
|
import { connectorsContract } from "./pages/connectors";
|
|
1277
1394
|
import { settingsContract } from "./pages/settings";
|
|
@@ -1374,26 +1491,91 @@ export function scaffoldHostEntry(targetDir, hostKey) {
|
|
|
1374
1491
|
});
|
|
1375
1492
|
observer.observe(document.body, { childList: true, subtree: true });`;
|
|
1376
1493
|
} else if (hostKey === "moodle") {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1494
|
+
// Moodle uses a dedicated content block — see below.
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Moodle: fully separate entry (asyncResolver, full provider stack, theme init, AdminShell)
|
|
1498
|
+
if (hostKey === "moodle") {
|
|
1499
|
+
const moodleContent = `/**
|
|
1500
|
+
* Production entry point for Moodle.
|
|
1501
|
+
*
|
|
1502
|
+
* AMD module loaded by Moodle's RequireJS via js_call_amd().
|
|
1503
|
+
* Uses real createInertiaApp — Moodle serves the HTML and page props.
|
|
1504
|
+
* This is the build target for \`npm run build\`.
|
|
1505
|
+
*
|
|
1506
|
+
* NOT used by \`npm run dev\` — that uses src/main.tsx with mock adapters.
|
|
1507
|
+
*/
|
|
1508
|
+
import "./tailwind.css";
|
|
1509
|
+
import "@fontsource-variable/figtree";
|
|
1510
|
+
|
|
1511
|
+
import { createRoot } from "react-dom/client";
|
|
1512
|
+
import { createInertiaApp } from "@inertiajs/react";
|
|
1513
|
+
import {
|
|
1514
|
+
I18nProvider,
|
|
1515
|
+
ProgressProvider,
|
|
1516
|
+
AuthProvider,
|
|
1517
|
+
FlashProvider,
|
|
1518
|
+
ScopeProvider,
|
|
1519
|
+
setAppearance,
|
|
1520
|
+
getStoredAppearance,
|
|
1521
|
+
getEffectiveTheme,
|
|
1522
|
+
onSystemThemeChange,
|
|
1523
|
+
} from "@middag-io/react";
|
|
1524
|
+
import "@middag-io/react/style.css";
|
|
1525
|
+
import { getString } from "./lib/moodle/strings";
|
|
1526
|
+
import { registerDefaults } from "./app/register";
|
|
1527
|
+
import { resolvePageComponent } from "./app/page-resolver";
|
|
1528
|
+
|
|
1529
|
+
registerDefaults();
|
|
1530
|
+
setAppearance(getStoredAppearance());
|
|
1531
|
+
onSystemThemeChange();
|
|
1532
|
+
|
|
1533
|
+
createInertiaApp({
|
|
1534
|
+
id: "middag-app",
|
|
1535
|
+
resolve: (name) => resolvePageComponent(name),
|
|
1536
|
+
setup({ el, App, props }) {
|
|
1537
|
+
el.classList.add("middag-root");
|
|
1538
|
+
el.classList.add("middag-active");
|
|
1539
|
+
el.setAttribute("data-theme", getEffectiveTheme(getStoredAppearance()));
|
|
1380
1540
|
|
|
1381
1541
|
// Portal container for Radix UI (modals, popovers, toasts).
|
|
1382
1542
|
// Radix portals default to document.body which is outside .middag-root,
|
|
1383
|
-
//
|
|
1384
|
-
// container with .middag-root so portal content inherits design tokens.
|
|
1543
|
+
// so this sibling container ensures portal content inherits design tokens.
|
|
1385
1544
|
if (!document.getElementById("middag-portals")) {
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
document.body.appendChild(
|
|
1390
|
-
}
|
|
1545
|
+
const portals = document.createElement("div");
|
|
1546
|
+
portals.id = "middag-portals";
|
|
1547
|
+
portals.classList.add("middag-root");
|
|
1548
|
+
document.body.appendChild(portals);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
createRoot(el).render(
|
|
1552
|
+
<ProgressProvider>
|
|
1553
|
+
<App {...props}>
|
|
1554
|
+
{({ Component, props: pageProps, key }) => (
|
|
1555
|
+
<AuthProvider>
|
|
1556
|
+
<ScopeProvider>
|
|
1557
|
+
<FlashProvider>
|
|
1558
|
+
<I18nProvider asyncResolver={getString}>
|
|
1559
|
+
<Component key={key} {...pageProps} />
|
|
1560
|
+
</I18nProvider>
|
|
1561
|
+
</FlashProvider>
|
|
1562
|
+
</ScopeProvider>
|
|
1563
|
+
</AuthProvider>
|
|
1564
|
+
)}
|
|
1565
|
+
</App>
|
|
1566
|
+
</ProgressProvider>,
|
|
1567
|
+
);
|
|
1568
|
+
},
|
|
1569
|
+
});
|
|
1570
|
+
`;
|
|
1571
|
+
writeFile(filePath, moodleContent, label);
|
|
1572
|
+
return;
|
|
1391
1573
|
}
|
|
1392
1574
|
|
|
1393
1575
|
const content = `/**
|
|
1394
|
-
* Production entry point for ${hostKey === "wordpress" ? "WordPress" :
|
|
1576
|
+
* Production entry point for ${hostKey === "wordpress" ? "WordPress" : "custom host"}.
|
|
1395
1577
|
*
|
|
1396
|
-
* Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" :
|
|
1578
|
+
* Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : "your backend"})
|
|
1397
1579
|
* serves the HTML and Inertia page props. This file is the build target
|
|
1398
1580
|
* for \`npm run build:${hostKey === "custom" ? "host" : hostKey}\`.
|
|
1399
1581
|
*
|
|
@@ -1726,7 +1908,7 @@ body.middag-active [data-slot="sidebar-container"] {
|
|
|
1726
1908
|
/** @deprecated Use scaffoldFreeApp + scaffoldFreeAdapters instead */
|
|
1727
1909
|
export function scaffoldAppFiles(targetDir) {
|
|
1728
1910
|
ensureDir(join(targetDir, "src"));
|
|
1729
|
-
ensureDir(join(targetDir, "
|
|
1911
|
+
ensureDir(join(targetDir, "mock", "adapters"));
|
|
1730
1912
|
|
|
1731
1913
|
// src/main.tsx — entry point
|
|
1732
1914
|
const mainPath = join(targetDir, "src", "main.tsx");
|
|
@@ -1821,9 +2003,9 @@ export function App() {
|
|
|
1821
2003
|
);
|
|
1822
2004
|
}
|
|
1823
2005
|
|
|
1824
|
-
//
|
|
1825
|
-
const inertiaReactPath = join(targetDir, "
|
|
1826
|
-
if (!skipIfExists(inertiaReactPath, "
|
|
2006
|
+
// mock/adapters/inertia-react.ts
|
|
2007
|
+
const inertiaReactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
|
|
2008
|
+
if (!skipIfExists(inertiaReactPath, "mock/adapters/inertia-react.ts")) {
|
|
1827
2009
|
writeFile(
|
|
1828
2010
|
inertiaReactPath,
|
|
1829
2011
|
`/**
|
|
@@ -1934,13 +2116,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
|
|
|
1934
2116
|
|
|
1935
2117
|
export { router };
|
|
1936
2118
|
`,
|
|
1937
|
-
"
|
|
2119
|
+
"mock/adapters/inertia-react.ts",
|
|
1938
2120
|
);
|
|
1939
2121
|
}
|
|
1940
2122
|
|
|
1941
|
-
//
|
|
1942
|
-
const inertiaCorePath = join(targetDir, "
|
|
1943
|
-
if (!skipIfExists(inertiaCorePath, "
|
|
2123
|
+
// mock/adapters/inertia-core.ts
|
|
2124
|
+
const inertiaCorePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
|
|
2125
|
+
if (!skipIfExists(inertiaCorePath, "mock/adapters/inertia-core.ts")) {
|
|
1944
2126
|
writeFile(
|
|
1945
2127
|
inertiaCorePath,
|
|
1946
2128
|
`/**
|
|
@@ -1960,7 +2142,7 @@ export const router = {
|
|
|
1960
2142
|
on: () => () => {},
|
|
1961
2143
|
};
|
|
1962
2144
|
`,
|
|
1963
|
-
"
|
|
2145
|
+
"mock/adapters/inertia-core.ts",
|
|
1964
2146
|
);
|
|
1965
2147
|
}
|
|
1966
2148
|
}
|
|
@@ -1995,6 +2177,37 @@ export function scaffoldPageResolver(targetDir) {
|
|
|
1995
2177
|
writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
|
|
1996
2178
|
}
|
|
1997
2179
|
|
|
2180
|
+
/**
|
|
2181
|
+
* Scaffold Moodle AMD page resolver: src/app/page-resolver.tsx.
|
|
2182
|
+
*
|
|
2183
|
+
* Overwrites the generic resolver with a Moodle-specific version that supports:
|
|
2184
|
+
* - Eager core pages (../extensions/core/pages/**)
|
|
2185
|
+
* - Lazy non-core extension pages (separate AMD chunks)
|
|
2186
|
+
* - External plugin pages via RequireJS (frankenstyle prefix)
|
|
2187
|
+
*
|
|
2188
|
+
* @param {string} targetDir - Absolute path to UI project root
|
|
2189
|
+
*/
|
|
2190
|
+
export function scaffoldMoodlePageResolver(targetDir) {
|
|
2191
|
+
ensureDir(join(targetDir, "src", "app"));
|
|
2192
|
+
const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
|
|
2193
|
+
writeFile(filePath, readTemplate("templates/shared/page-resolver-moodle.tsx"), "src/app/page-resolver.tsx (Moodle AMD)");
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/**
|
|
2197
|
+
* Scaffold Moodle admin shell: src/shells/AdminShell.tsx.
|
|
2198
|
+
*
|
|
2199
|
+
* Moodle-specific shell that supports the admin_tabs shared prop.
|
|
2200
|
+
* Registered as 'admin' shell in src/entry-moodle.tsx.
|
|
2201
|
+
*
|
|
2202
|
+
* @param {string} targetDir - Absolute path to UI project root
|
|
2203
|
+
*/
|
|
2204
|
+
export function scaffoldMoodleAdminShell(targetDir) {
|
|
2205
|
+
ensureDir(join(targetDir, "src", "shells"));
|
|
2206
|
+
const filePath = join(targetDir, "src", "shells", "AdminShell.tsx");
|
|
2207
|
+
if (skipIfExists(filePath, "src/shells/AdminShell.tsx")) return;
|
|
2208
|
+
writeFile(filePath, readTemplate("templates/shared/moodle-admin-shell.tsx"), "src/shells/AdminShell.tsx");
|
|
2209
|
+
}
|
|
2210
|
+
|
|
1998
2211
|
/**
|
|
1999
2212
|
* Scaffold route helper: src/lib/routes.ts.
|
|
2000
2213
|
* Abstracts host admin URL vs dev mock path.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminShell — Moodle-specific admin shell.
|
|
3
|
+
*
|
|
4
|
+
* Extends ProductShell architecture with an optional admin tab bar
|
|
5
|
+
* rendered when the `admin_tabs` Inertia shared prop is present.
|
|
6
|
+
*
|
|
7
|
+
* Registered as 'admin' shell in src/entry-moodle.tsx.
|
|
8
|
+
* Edit this file to customise the admin sidebar header, tab bar, or layout.
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, type ReactElement } from "react";
|
|
11
|
+
import { Link, usePage } from "@inertiajs/react";
|
|
12
|
+
import {
|
|
13
|
+
Toaster,
|
|
14
|
+
Sidebar,
|
|
15
|
+
SidebarHeader,
|
|
16
|
+
SidebarProvider,
|
|
17
|
+
useSidebar,
|
|
18
|
+
SidebarNav,
|
|
19
|
+
PageHeader,
|
|
20
|
+
NavErrorBoundary,
|
|
21
|
+
type ShellProps,
|
|
22
|
+
type SharedProps,
|
|
23
|
+
type PageMeta,
|
|
24
|
+
type AdminTabsProps,
|
|
25
|
+
} from "@middag-io/react";
|
|
26
|
+
import { Tabs, TabsList, TabsTrigger } from "@middag-io/react/reui/tabs";
|
|
27
|
+
|
|
28
|
+
// ── Inner shell (must be inside SidebarProvider) ──────────────────────────────
|
|
29
|
+
|
|
30
|
+
function AdminShellInner({ children }: ShellProps): ReactElement {
|
|
31
|
+
const { setOpenMobile } = useSidebar();
|
|
32
|
+
const { props } = usePage<SharedProps>();
|
|
33
|
+
|
|
34
|
+
const page: PageMeta = (props as SharedProps & { contract?: { page?: PageMeta } }).contract
|
|
35
|
+
?.page ?? { key: "unknown", title: "", breadcrumbs: [], actions: [] };
|
|
36
|
+
|
|
37
|
+
const adminTabs = (props as SharedProps & { admin_tabs?: AdminTabsProps }).admin_tabs;
|
|
38
|
+
|
|
39
|
+
const handleMobileMenuClick = useCallback(() => {
|
|
40
|
+
setOpenMobile(true);
|
|
41
|
+
}, [setOpenMobile]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Sidebar
|
|
46
|
+
aria-label="Navegação"
|
|
47
|
+
collapsible="icon"
|
|
48
|
+
className="border-sidebar-border bg-sidebar border-r"
|
|
49
|
+
>
|
|
50
|
+
<SidebarHeader className="border-sidebar-border border-b px-4 py-3">
|
|
51
|
+
{/* Replace with your plugin name or logo */}
|
|
52
|
+
<p className="text-sidebar-foreground text-sm font-semibold">Admin</p>
|
|
53
|
+
</SidebarHeader>
|
|
54
|
+
|
|
55
|
+
<NavErrorBoundary>
|
|
56
|
+
<SidebarNav />
|
|
57
|
+
</NavErrorBoundary>
|
|
58
|
+
</Sidebar>
|
|
59
|
+
|
|
60
|
+
<div className="admin-shell__content flex min-h-screen flex-1 flex-col overflow-hidden">
|
|
61
|
+
<NavErrorBoundary>
|
|
62
|
+
<PageHeader page={page} onMobileMenuClick={handleMobileMenuClick} />
|
|
63
|
+
</NavErrorBoundary>
|
|
64
|
+
|
|
65
|
+
{adminTabs && (
|
|
66
|
+
<div className="border-b px-6">
|
|
67
|
+
<Tabs value={adminTabs.active}>
|
|
68
|
+
<TabsList variant="line" className="w-full justify-start">
|
|
69
|
+
{adminTabs.items.map((tab) => (
|
|
70
|
+
<TabsTrigger key={tab.key} value={tab.key} asChild>
|
|
71
|
+
<Link href={tab.href} preserveState>
|
|
72
|
+
{tab.label}
|
|
73
|
+
</Link>
|
|
74
|
+
</TabsTrigger>
|
|
75
|
+
))}
|
|
76
|
+
</TabsList>
|
|
77
|
+
</Tabs>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
<main className="flex-1 overflow-auto p-6" aria-live="polite" aria-busy="false">
|
|
82
|
+
{children}
|
|
83
|
+
</main>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<Toaster position="bottom-right" richColors />
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── AdminShell — public export ──────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export function AdminShell({ children }: ShellProps): ReactElement {
|
|
94
|
+
return (
|
|
95
|
+
<SidebarProvider
|
|
96
|
+
defaultOpen={true}
|
|
97
|
+
style={
|
|
98
|
+
{
|
|
99
|
+
"--sidebar-width": "var(--sidebar-width)",
|
|
100
|
+
"--sidebar-width-icon": "var(--sidebar-width-collapsed)",
|
|
101
|
+
} as React.CSSProperties
|
|
102
|
+
}
|
|
103
|
+
className="bg-background text-foreground flex min-h-screen"
|
|
104
|
+
>
|
|
105
|
+
<AdminShellInner>{children}</AdminShellInner>
|
|
106
|
+
</SidebarProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* page-resolver — Moodle AMD page resolver.
|
|
3
|
+
*
|
|
4
|
+
* Three resolution modes:
|
|
5
|
+
*
|
|
6
|
+
* 1. Core pages (no prefix): "admin/Dashboard" resolves to
|
|
7
|
+
* "../extensions/core/pages/admin/Dashboard.tsx" via eager glob.
|
|
8
|
+
* Bundled into middag-app.js.
|
|
9
|
+
*
|
|
10
|
+
* 2. Extension pages (extension prefix): "ecommerce/pages/Index" resolves
|
|
11
|
+
* to "../extensions/ecommerce/pages/Index.tsx" via lazy glob.
|
|
12
|
+
* Each non-core extension gets its own AMD chunk.
|
|
13
|
+
*
|
|
14
|
+
* 3. External plugin pages (frankenstyle prefix): "local_yourplugin/admin/Index"
|
|
15
|
+
* loads the AMD module via RequireJS. Any Moodle plugin can register
|
|
16
|
+
* React pages in the MIDDAG shell using this mechanism.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Core pages — eager (bundled into middag-app.js)
|
|
20
|
+
const corePages = import.meta.glob("../extensions/core/pages/**/*.tsx", { eager: true }) as Record<
|
|
21
|
+
string,
|
|
22
|
+
Record<string, unknown>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
// Non-core extension pages — lazy (separate AMD chunks)
|
|
26
|
+
const extensionPages = import.meta.glob(
|
|
27
|
+
["../extensions/*/pages/**/*.tsx", "!../extensions/core/**"],
|
|
28
|
+
{ eager: false },
|
|
29
|
+
) as Record<string, () => Promise<Record<string, unknown>>>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Frankenstyle pattern: type_name (e.g. local_yourplugin, mod_assign).
|
|
33
|
+
* A name like "local_yourplugin/admin/Index" is external.
|
|
34
|
+
* A name like "admin/Dashboard" is local (no underscore in first segment).
|
|
35
|
+
*/
|
|
36
|
+
function isExternalPage(name: string): boolean {
|
|
37
|
+
const firstSegment = name.split("/")[0];
|
|
38
|
+
return /^[a-z]+_[a-z]/.test(firstSegment);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load an external plugin page via RequireJS (Moodle AMD).
|
|
43
|
+
*
|
|
44
|
+
* The plugin must build its page as an AMD module that exports a
|
|
45
|
+
* default React component. The module ID matches the page name directly
|
|
46
|
+
* (e.g. "local_yourplugin/admin/Index").
|
|
47
|
+
*/
|
|
48
|
+
function loadExternalPage(name: string): Promise<Record<string, unknown>> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- RequireJS global
|
|
51
|
+
const req = (window as any).require;
|
|
52
|
+
if (!req) {
|
|
53
|
+
reject(new Error(`RequireJS not available. Cannot load external page: ${name}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
req(
|
|
57
|
+
[name],
|
|
58
|
+
(mod: Record<string, unknown>) => resolve(mod),
|
|
59
|
+
(err: Error) => reject(new Error(`Failed to load external page "${name}": ${err.message}`)),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolvePageComponent(name: string) {
|
|
65
|
+
if (isExternalPage(name)) {
|
|
66
|
+
return loadExternalPage(name).then((mod) => mod.default ?? mod);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Try core pages first (eager/sync)
|
|
70
|
+
const corePath = `../extensions/core/pages/${name}.tsx`;
|
|
71
|
+
const coreMod = corePages[corePath];
|
|
72
|
+
if (coreMod) {
|
|
73
|
+
return coreMod.default ?? coreMod;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try non-core extension pages (lazy/async)
|
|
77
|
+
const extPath = `../extensions/${name}.tsx`;
|
|
78
|
+
const extLoader = extensionPages[extPath];
|
|
79
|
+
if (extLoader) {
|
|
80
|
+
return extLoader().then((mod) => mod.default ?? mod);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Page not found: ${name}. Available: ${[...Object.keys(corePages), ...Object.keys(extensionPages)].join(", ")}`,
|
|
85
|
+
);
|
|
86
|
+
}
|