@acmekit/acmekit 2.13.88 → 2.13.90
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/dist/commands/plugin/develop.d.ts.map +1 -1
- package/dist/commands/plugin/develop.js +92 -68
- package/dist/commands/plugin/develop.js.map +1 -1
- package/dist/templates/app/.claude/rules/admin-components.md +131 -111
- package/dist/templates/app/.claude/rules/admin-data.md +2 -0
- package/dist/templates/app/.claude/rules/admin-patterns.md +39 -31
- package/dist/templates/app/.claude/rules/admin-ui.md +28 -0
- package/dist/templates/app/.claude/rules/api-routes.md +30 -9
- package/dist/templates/app/.claude/rules/modules.md +119 -2
- package/dist/templates/app/.claude/skills/build-feature/SKILL.md +2 -0
- package/dist/templates/app/CLAUDE.md +1 -0
- package/dist/templates/plugin/.claude/rules/admin-components.md +131 -111
- package/dist/templates/plugin/.claude/rules/admin-data.md +2 -0
- package/dist/templates/plugin/.claude/rules/admin-patterns.md +39 -31
- package/dist/templates/plugin/.claude/rules/admin-ui.md +28 -0
- package/dist/templates/plugin/.claude/rules/api-routes.md +30 -9
- package/dist/templates/plugin/.claude/rules/modules.md +118 -1
- package/dist/templates/plugin/.claude/skills/build-feature/SKILL.md +2 -0
- package/dist/templates/plugin/CLAUDE.md +1 -0
- package/package.json +39 -39
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"develop.d.ts","sourceRoot":"","sources":["../../../src/commands/plugin/develop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"develop.d.ts","sourceRoot":"","sources":["../../../src/commands/plugin/develop.ts"],"names":[],"mappings":"AA+DA,wBAA8B,aAAa,CAAC,EAC1C,SAAS,GACV,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;CAClB,iBA8FA"}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
3
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
4
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
5
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
6
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
7
|
+
};
|
|
8
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
|
+
};
|
|
2
13
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
15
|
};
|
|
16
|
+
var _DebouncedTask_instances, _DebouncedTask_timer, _DebouncedTask_running, _DebouncedTask_queued, _DebouncedTask_fn, _DebouncedTask_delay, _DebouncedTask_run;
|
|
5
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
18
|
exports.default = developPlugin;
|
|
7
19
|
const build_tools_1 = require("@acmekit/framework/build-tools");
|
|
@@ -13,6 +25,53 @@ const ADMIN_REBUILD_DEBOUNCE_MS = 500;
|
|
|
13
25
|
const ROUTE_TYPES_DEBOUNCE_MS = 300;
|
|
14
26
|
const QUERY_TYPES_DEBOUNCE_MS = 300;
|
|
15
27
|
const YALC_PUSH_DEBOUNCE_MS = 300;
|
|
28
|
+
/**
|
|
29
|
+
* Debounces calls and serializes execution. If `schedule()` is called while
|
|
30
|
+
* the task is already running, a single follow-up execution is queued and
|
|
31
|
+
* runs after the current one completes — no concurrent executions.
|
|
32
|
+
*/
|
|
33
|
+
class DebouncedTask {
|
|
34
|
+
constructor(fn, delay) {
|
|
35
|
+
_DebouncedTask_instances.add(this);
|
|
36
|
+
_DebouncedTask_timer.set(this, null);
|
|
37
|
+
_DebouncedTask_running.set(this, false);
|
|
38
|
+
_DebouncedTask_queued.set(this, false);
|
|
39
|
+
_DebouncedTask_fn.set(this, void 0);
|
|
40
|
+
_DebouncedTask_delay.set(this, void 0);
|
|
41
|
+
__classPrivateFieldSet(this, _DebouncedTask_fn, fn, "f");
|
|
42
|
+
__classPrivateFieldSet(this, _DebouncedTask_delay, delay, "f");
|
|
43
|
+
}
|
|
44
|
+
schedule() {
|
|
45
|
+
if (__classPrivateFieldGet(this, _DebouncedTask_running, "f")) {
|
|
46
|
+
__classPrivateFieldSet(this, _DebouncedTask_queued, true, "f");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (__classPrivateFieldGet(this, _DebouncedTask_timer, "f"))
|
|
50
|
+
clearTimeout(__classPrivateFieldGet(this, _DebouncedTask_timer, "f"));
|
|
51
|
+
__classPrivateFieldSet(this, _DebouncedTask_timer, setTimeout(() => __classPrivateFieldGet(this, _DebouncedTask_instances, "m", _DebouncedTask_run).call(this), __classPrivateFieldGet(this, _DebouncedTask_delay, "f")), "f");
|
|
52
|
+
}
|
|
53
|
+
dispose() {
|
|
54
|
+
if (__classPrivateFieldGet(this, _DebouncedTask_timer, "f")) {
|
|
55
|
+
clearTimeout(__classPrivateFieldGet(this, _DebouncedTask_timer, "f"));
|
|
56
|
+
__classPrivateFieldSet(this, _DebouncedTask_timer, null, "f");
|
|
57
|
+
}
|
|
58
|
+
__classPrivateFieldSet(this, _DebouncedTask_queued, false, "f");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_DebouncedTask_timer = new WeakMap(), _DebouncedTask_running = new WeakMap(), _DebouncedTask_queued = new WeakMap(), _DebouncedTask_fn = new WeakMap(), _DebouncedTask_delay = new WeakMap(), _DebouncedTask_instances = new WeakSet(), _DebouncedTask_run = async function _DebouncedTask_run() {
|
|
62
|
+
__classPrivateFieldSet(this, _DebouncedTask_timer, null, "f");
|
|
63
|
+
__classPrivateFieldSet(this, _DebouncedTask_running, true, "f");
|
|
64
|
+
try {
|
|
65
|
+
await __classPrivateFieldGet(this, _DebouncedTask_fn, "f").call(this);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
__classPrivateFieldSet(this, _DebouncedTask_running, false, "f");
|
|
69
|
+
if (__classPrivateFieldGet(this, _DebouncedTask_queued, "f")) {
|
|
70
|
+
__classPrivateFieldSet(this, _DebouncedTask_queued, false, "f");
|
|
71
|
+
this.schedule();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
16
75
|
async function developPlugin({ directory, }) {
|
|
17
76
|
const compiler = new build_tools_1.Compiler(directory, logger_1.logger);
|
|
18
77
|
const parsedConfig = await compiler.loadTSConfigFile();
|
|
@@ -26,80 +85,49 @@ async function developPlugin({ directory, }) {
|
|
|
26
85
|
if (!adminBuilt) {
|
|
27
86
|
logger_1.logger.warn("[plugin:develop] Admin extensions build failed, continuing backend-only");
|
|
28
87
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
clearTimeout(yalcPushTimeout);
|
|
35
|
-
yalcPushTimeout = setTimeout(async () => {
|
|
36
|
-
yalcPushTimeout = null;
|
|
37
|
-
try {
|
|
38
|
-
const synced = await (0, require_yalc_1.yalcPublishAndPush)(directory);
|
|
39
|
-
if (synced > 0) {
|
|
40
|
-
logger_1.logger.info(`[plugin:develop] Synced to ${synced} app${synced > 1 ? "s" : ""}`);
|
|
41
|
-
}
|
|
88
|
+
const yalcPush = new DebouncedTask(async () => {
|
|
89
|
+
try {
|
|
90
|
+
const synced = await (0, require_yalc_1.yalcPublishAndPush)(directory);
|
|
91
|
+
if (synced > 0) {
|
|
92
|
+
logger_1.logger.info(`[plugin:develop] Synced to ${synced} app${synced > 1 ? "s" : ""}`);
|
|
42
93
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger_1.logger.warn(`[plugin:develop] Sync failed: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
}, YALC_PUSH_DEBOUNCE_MS);
|
|
48
99
|
// Sync to host apps immediately after the initial build so the
|
|
49
100
|
// developer doesn't have to save a file to trigger the first push.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
logger_1.logger.info("[plugin:develop] Route types updated");
|
|
60
|
-
}, ROUTE_TYPES_DEBOUNCE_MS);
|
|
61
|
-
};
|
|
62
|
-
// Debounced query-type regeneration triggered by module file changes
|
|
63
|
-
let queryTypesTimeout = null;
|
|
64
|
-
const scheduleQueryTypesRegen = () => {
|
|
65
|
-
if (queryTypesTimeout)
|
|
66
|
-
clearTimeout(queryTypesTimeout);
|
|
67
|
-
queryTypesTimeout = setTimeout(async () => {
|
|
68
|
-
queryTypesTimeout = null;
|
|
69
|
-
await compiler.generatePluginQueryTypes({ bustCache: true });
|
|
70
|
-
logger_1.logger.info("[plugin:develop] Query types updated");
|
|
71
|
-
}, QUERY_TYPES_DEBOUNCE_MS);
|
|
72
|
-
};
|
|
101
|
+
yalcPush.schedule();
|
|
102
|
+
const routeTypesRegen = new DebouncedTask(async () => {
|
|
103
|
+
await compiler.generatePluginRouteTypes();
|
|
104
|
+
logger_1.logger.info("[plugin:develop] Route types updated");
|
|
105
|
+
}, ROUTE_TYPES_DEBOUNCE_MS);
|
|
106
|
+
const queryTypesRegen = new DebouncedTask(async () => {
|
|
107
|
+
await compiler.generatePluginQueryTypes({ bustCache: true });
|
|
108
|
+
logger_1.logger.info("[plugin:develop] Query types updated");
|
|
109
|
+
}, QUERY_TYPES_DEBOUNCE_MS);
|
|
73
110
|
// Watch for backend changes (transpiles src/ → .acmekit/server/src/)
|
|
74
111
|
const transpiler = new build_tools_1.LocalPluginTranspiler({
|
|
75
112
|
pluginRoot: directory,
|
|
76
113
|
logger: logger_1.logger,
|
|
77
114
|
onFileChange: (transpiledFilePath) => {
|
|
78
|
-
// Only regenerate when an API route file changed
|
|
79
115
|
if (transpiledFilePath.includes(`${path_1.sep}api${path_1.sep}`)) {
|
|
80
|
-
|
|
116
|
+
routeTypesRegen.schedule();
|
|
81
117
|
}
|
|
82
|
-
// Regenerate query types when a module file changed
|
|
83
118
|
if (transpiledFilePath.includes(`${path_1.sep}modules${path_1.sep}`)) {
|
|
84
|
-
|
|
119
|
+
queryTypesRegen.schedule();
|
|
85
120
|
}
|
|
86
|
-
|
|
121
|
+
yalcPush.schedule();
|
|
87
122
|
},
|
|
88
123
|
});
|
|
89
124
|
await transpiler.writeOptionsFile();
|
|
90
125
|
await transpiler.watch();
|
|
91
126
|
// Watch admin extensions for rebuild
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
adminRebuildTimeout = setTimeout(async () => {
|
|
98
|
-
adminRebuildTimeout = null;
|
|
99
|
-
await compiler.buildPluginAdminExtensions(bundler);
|
|
100
|
-
scheduleYalcPush();
|
|
101
|
-
}, ADMIN_REBUILD_DEBOUNCE_MS);
|
|
102
|
-
};
|
|
127
|
+
const adminRebuild = new DebouncedTask(async () => {
|
|
128
|
+
await compiler.buildPluginAdminExtensions(bundler);
|
|
129
|
+
yalcPush.schedule();
|
|
130
|
+
}, ADMIN_REBUILD_DEBOUNCE_MS);
|
|
103
131
|
const adminWatcher = chokidar_1.default
|
|
104
132
|
.watch("src/admin", {
|
|
105
133
|
cwd: directory,
|
|
@@ -113,19 +141,15 @@ async function developPlugin({ directory, }) {
|
|
|
113
141
|
"**/__admin-extensions__.js",
|
|
114
142
|
],
|
|
115
143
|
})
|
|
116
|
-
.on("add",
|
|
117
|
-
.on("change",
|
|
118
|
-
.on("unlink",
|
|
144
|
+
.on("add", () => adminRebuild.schedule())
|
|
145
|
+
.on("change", () => adminRebuild.schedule())
|
|
146
|
+
.on("unlink", () => adminRebuild.schedule());
|
|
119
147
|
// Graceful shutdown
|
|
120
148
|
process.on("SIGINT", async () => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (queryTypesTimeout)
|
|
126
|
-
clearTimeout(queryTypesTimeout);
|
|
127
|
-
if (adminRebuildTimeout)
|
|
128
|
-
clearTimeout(adminRebuildTimeout);
|
|
149
|
+
yalcPush.dispose();
|
|
150
|
+
routeTypesRegen.dispose();
|
|
151
|
+
queryTypesRegen.dispose();
|
|
152
|
+
adminRebuild.dispose();
|
|
129
153
|
await Promise.all([transpiler.close(), adminWatcher.close()]);
|
|
130
154
|
process.exit(0);
|
|
131
155
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"develop.js","sourceRoot":"","sources":["../../../src/commands/plugin/develop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"develop.js","sourceRoot":"","sources":["../../../src/commands/plugin/develop.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AA+DA,gCAkGC;AAjKD,gEAGuC;AACvC,sDAAkD;AAClD,+BAA0B;AAC1B,wDAA+B;AAC/B,iDAAmD;AAEnD,MAAM,yBAAyB,GAAG,GAAG,CAAA;AACrC,MAAM,uBAAuB,GAAG,GAAG,CAAA;AACnC,MAAM,uBAAuB,GAAG,GAAG,CAAA;AACnC,MAAM,qBAAqB,GAAG,GAAG,CAAA;AAEjC;;;;GAIG;AACH,MAAM,aAAa;IAOjB,YAAY,EAAuB,EAAE,KAAa;;QANlD,+BAA+C,IAAI,EAAA;QACnD,iCAAW,KAAK,EAAA;QAChB,gCAAU,KAAK,EAAA;QACf,oCAAwB;QACxB,uCAAc;QAGZ,uBAAA,IAAI,qBAAO,EAAE,MAAA,CAAA;QACb,uBAAA,IAAI,wBAAU,KAAK,MAAA,CAAA;IACrB,CAAC;IAED,QAAQ;QACN,IAAI,uBAAA,IAAI,8BAAS,EAAE,CAAC;YAClB,uBAAA,IAAI,yBAAW,IAAI,MAAA,CAAA;YACnB,OAAM;QACR,CAAC;QACD,IAAI,uBAAA,IAAI,4BAAO;YAAE,YAAY,CAAC,uBAAA,IAAI,4BAAO,CAAC,CAAA;QAC1C,uBAAA,IAAI,wBAAU,UAAU,CAAC,GAAG,EAAE,CAAC,uBAAA,IAAI,oDAAK,MAAT,IAAI,CAAO,EAAE,uBAAA,IAAI,4BAAO,CAAC,MAAA,CAAA;IAC1D,CAAC;IAgBD,OAAO;QACL,IAAI,uBAAA,IAAI,4BAAO,EAAE,CAAC;YAChB,YAAY,CAAC,uBAAA,IAAI,4BAAO,CAAC,CAAA;YACzB,uBAAA,IAAI,wBAAU,IAAI,MAAA,CAAA;QACpB,CAAC;QACD,uBAAA,IAAI,yBAAW,KAAK,MAAA,CAAA;IACtB,CAAC;CACF;6PArBC,KAAK;IACH,uBAAA,IAAI,wBAAU,IAAI,MAAA,CAAA;IAClB,uBAAA,IAAI,0BAAY,IAAI,MAAA,CAAA;IACpB,IAAI,CAAC;QACH,MAAM,uBAAA,IAAI,yBAAI,MAAR,IAAI,CAAM,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,uBAAA,IAAI,0BAAY,KAAK,MAAA,CAAA;QACrB,IAAI,uBAAA,IAAI,6BAAQ,EAAE,CAAC;YACjB,uBAAA,IAAI,yBAAW,KAAK,MAAA,CAAA;YACpB,IAAI,CAAC,QAAQ,EAAE,CAAA;QACjB,CAAC;IACH,CAAC;AACH,CAAC;AAWY,KAAK,UAAU,aAAa,CAAC,EAC1C,SAAS,GAGV;IACC,MAAM,QAAQ,GAAG,IAAI,sBAAQ,CAAC,SAAS,EAAE,eAAM,CAAC,CAAA;IAChD,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAA;IACtD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAM;IACR,CAAC;IAED,wBAAwB;IACxB,MAAM,QAAQ,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA;IAE/C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAA;IACtD,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,0BAA0B,CAAC,OAAO,CAAC,CAAA;IACrE,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,eAAM,CAAC,IAAI,CACT,yEAAyE,CAC1E,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,aAAa,CAAC,KAAK,IAAI,EAAE;QAC5C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAA,iCAAkB,EAAC,SAAS,CAAC,CAAA;YAClD,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBACf,eAAM,CAAC,IAAI,CACT,8BAA8B,MAAM,OAAO,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACnE,CAAA;YACH,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,eAAM,CAAC,IAAI,CAAC,iCAAiC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,EAAE,qBAAqB,CAAC,CAAA;IAEzB,+DAA+D;IAC/D,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,CAAA;IAEnB,MAAM,eAAe,GAAG,IAAI,aAAa,CAAC,KAAK,IAAI,EAAE;QACnD,MAAM,QAAQ,CAAC,wBAAwB,EAAE,CAAA;QACzC,eAAM,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAA;IACrD,CAAC,EAAE,uBAAuB,CAAC,CAAA;IAE3B,MAAM,eAAe,GAAG,IAAI,aAAa,CAAC,KAAK,IAAI,EAAE;QACnD,MAAM,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5D,eAAM,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAA;IACrD,CAAC,EAAE,uBAAuB,CAAC,CAAA;IAE3B,qEAAqE;IACrE,MAAM,UAAU,GAAG,IAAI,mCAAqB,CAAC;QAC3C,UAAU,EAAE,SAAS;QACrB,MAAM,EAAN,eAAM;QACN,YAAY,EAAE,CAAC,kBAAkB,EAAE,EAAE;YACnC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,GAAG,UAAG,MAAM,UAAG,EAAE,CAAC,EAAE,CAAC;gBACnD,eAAe,CAAC,QAAQ,EAAE,CAAA;YAC5B,CAAC;YACD,IAAI,kBAAkB,CAAC,QAAQ,CAAC,GAAG,UAAG,UAAU,UAAG,EAAE,CAAC,EAAE,CAAC;gBACvD,eAAe,CAAC,QAAQ,EAAE,CAAA;YAC5B,CAAC;YACD,QAAQ,CAAC,QAAQ,EAAE,CAAA;QACrB,CAAC;KACF,CAAC,CAAA;IACF,MAAM,UAAU,CAAC,gBAAgB,EAAE,CAAA;IACnC,MAAM,UAAU,CAAC,KAAK,EAAE,CAAA;IAExB,qCAAqC;IACrC,MAAM,YAAY,GAAG,IAAI,aAAa,CAAC,KAAK,IAAI,EAAE;QAChD,MAAM,QAAQ,CAAC,0BAA0B,CAAC,OAAO,CAAC,CAAA;QAClD,QAAQ,CAAC,QAAQ,EAAE,CAAA;IACrB,CAAC,EAAE,yBAAyB,CAAC,CAAA;IAE7B,MAAM,YAAY,GAAG,kBAAQ;SAC1B,KAAK,CAAC,WAAW,EAAE;QAClB,GAAG,EAAE,SAAS;QACd,aAAa,EAAE,IAAI;QACnB,OAAO,EAAE;YACP,cAAc;YACd,MAAM;YACN,UAAU;YACV,gBAAgB;YAChB,mCAAmC;YACnC,4BAA4B;SAC7B;KACF,CAAC;SACD,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;SACxC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;SAC3C,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAA;IAE9C,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAC9B,QAAQ,CAAC,OAAO,EAAE,CAAA;QAClB,eAAe,CAAC,OAAO,EAAE,CAAA;QACzB,eAAe,CAAC,OAAO,EAAE,CAAA;QACzB,YAAY,CAAC,OAAO,EAAE,CAAA;QACtB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -420,6 +420,8 @@ File: `src/admin/routes/custom/[id]/edit/page.tsx`
|
|
|
420
420
|
|
|
421
421
|
**Critical structure**: Use `RouteDrawer.Form` wrapping `KeyboundForm` — this handles dirty form blocking on close and Cmd/Ctrl+Enter submit. The `KeyboundForm` wraps BOTH `RouteDrawer.Body` AND `RouteDrawer.Footer` so `type="submit"` works. Use `useRouteModal()` for `handleSuccess` to close the modal after success.
|
|
422
422
|
|
|
423
|
+
**IMPORTANT**: `useRouteModal()` must be called in a **child component** rendered inside `<RouteDrawer>`, not in the same component that renders it. The `RouteModalProvider` context is created inside `RouteDrawer`, so calling the hook in the parent component crashes with "useRouteModal must be used within a RouteModalProvider".
|
|
424
|
+
|
|
423
425
|
```tsx
|
|
424
426
|
import { Button, RouteDrawer, Form, Heading, Input, Textarea, toast, KeyboundForm, useRouteModal } from "@acmekit/ui"
|
|
425
427
|
import { useForm } from "react-hook-form"
|
|
@@ -434,39 +436,54 @@ const EditSchema = zod.object({
|
|
|
434
436
|
description: zod.string().optional(),
|
|
435
437
|
})
|
|
436
438
|
|
|
439
|
+
// Page component — renders RouteDrawer, delegates form to child
|
|
437
440
|
const EditPostPage = () => {
|
|
438
441
|
const { id } = useParams()
|
|
439
|
-
const queryClient = useQueryClient()
|
|
440
|
-
const { handleSuccess } = useRouteModal()
|
|
441
442
|
|
|
442
443
|
const { data, isLoading, isError, error } = useQuery({
|
|
443
444
|
queryKey: ["posts", id],
|
|
444
445
|
queryFn: () => sdk.admin.fetch("/posts/:id", { method: "GET", params: { id: id! } }),
|
|
445
446
|
})
|
|
446
447
|
|
|
447
|
-
// Throw query errors to error boundary
|
|
448
448
|
if (isError) {
|
|
449
449
|
throw error
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
return (
|
|
453
|
+
<RouteDrawer>
|
|
454
|
+
<RouteDrawer.Header>
|
|
455
|
+
<Heading>Edit Post</Heading>
|
|
456
|
+
</RouteDrawer.Header>
|
|
457
|
+
{!isLoading && data?.post && (
|
|
458
|
+
<EditPostForm post={data.post} />
|
|
459
|
+
)}
|
|
460
|
+
</RouteDrawer>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Form component — useRouteModal() is valid here (inside RouteDrawer)
|
|
465
|
+
const EditPostForm = ({ post }: { post: { id: string; title: string; description?: string } }) => {
|
|
466
|
+
const queryClient = useQueryClient()
|
|
467
|
+
const { handleSuccess } = useRouteModal()
|
|
468
|
+
|
|
452
469
|
const form = useForm<zod.infer<typeof EditSchema>>({
|
|
453
470
|
defaultValues: {
|
|
454
|
-
title:
|
|
455
|
-
description:
|
|
471
|
+
title: post.title ?? "",
|
|
472
|
+
description: post.description ?? "",
|
|
456
473
|
},
|
|
457
474
|
resolver: zodResolver(EditSchema),
|
|
458
475
|
})
|
|
459
476
|
|
|
460
477
|
const { mutateAsync, isPending } = useMutation({
|
|
461
478
|
mutationFn: (payload: zod.infer<typeof EditSchema>) =>
|
|
462
|
-
sdk.admin.fetch("/posts/:id", { method: "POST", params: { id: id
|
|
479
|
+
sdk.admin.fetch("/posts/:id", { method: "POST", params: { id: post.id }, body: payload }),
|
|
463
480
|
})
|
|
464
481
|
|
|
465
482
|
const handleSubmit = form.handleSubmit(async (values) => {
|
|
466
483
|
await mutateAsync(values, {
|
|
467
484
|
onSuccess: () => {
|
|
468
485
|
queryClient.invalidateQueries({ queryKey: ["posts"] })
|
|
469
|
-
queryClient.invalidateQueries({ queryKey: ["posts", id] })
|
|
486
|
+
queryClient.invalidateQueries({ queryKey: ["posts", post.id] })
|
|
470
487
|
toast.success("Post updated")
|
|
471
488
|
handleSuccess()
|
|
472
489
|
},
|
|
@@ -475,57 +492,50 @@ const EditPostPage = () => {
|
|
|
475
492
|
})
|
|
476
493
|
|
|
477
494
|
return (
|
|
478
|
-
<RouteDrawer>
|
|
479
|
-
<
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
</
|
|
514
|
-
</RouteDrawer.
|
|
515
|
-
<
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
</Button>
|
|
523
|
-
</div>
|
|
524
|
-
</RouteDrawer.Footer>
|
|
525
|
-
</KeyboundForm>
|
|
526
|
-
</RouteDrawer.Form>
|
|
527
|
-
)}
|
|
528
|
-
</RouteDrawer>
|
|
495
|
+
<RouteDrawer.Form form={form}>
|
|
496
|
+
<KeyboundForm onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
|
497
|
+
<RouteDrawer.Body>
|
|
498
|
+
<div className="flex flex-col gap-y-4">
|
|
499
|
+
<Form.Field
|
|
500
|
+
control={form.control}
|
|
501
|
+
name="title"
|
|
502
|
+
render={({ field }) => (
|
|
503
|
+
<Form.Item>
|
|
504
|
+
<Form.Label>Title</Form.Label>
|
|
505
|
+
<Form.Control>
|
|
506
|
+
<Input autoComplete="off" {...field} />
|
|
507
|
+
</Form.Control>
|
|
508
|
+
<Form.ErrorMessage />
|
|
509
|
+
</Form.Item>
|
|
510
|
+
)}
|
|
511
|
+
/>
|
|
512
|
+
<Form.Field
|
|
513
|
+
control={form.control}
|
|
514
|
+
name="description"
|
|
515
|
+
render={({ field }) => (
|
|
516
|
+
<Form.Item>
|
|
517
|
+
<Form.Label optional>Description</Form.Label>
|
|
518
|
+
<Form.Control>
|
|
519
|
+
<Textarea {...field} />
|
|
520
|
+
</Form.Control>
|
|
521
|
+
<Form.ErrorMessage />
|
|
522
|
+
</Form.Item>
|
|
523
|
+
)}
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
</RouteDrawer.Body>
|
|
527
|
+
<RouteDrawer.Footer>
|
|
528
|
+
<div className="flex items-center justify-end gap-x-2">
|
|
529
|
+
<RouteDrawer.Close asChild>
|
|
530
|
+
<Button size="small" variant="secondary">Cancel</Button>
|
|
531
|
+
</RouteDrawer.Close>
|
|
532
|
+
<Button size="small" type="submit" isLoading={isPending}>
|
|
533
|
+
Save
|
|
534
|
+
</Button>
|
|
535
|
+
</div>
|
|
536
|
+
</RouteDrawer.Footer>
|
|
537
|
+
</KeyboundForm>
|
|
538
|
+
</RouteDrawer.Form>
|
|
529
539
|
)
|
|
530
540
|
}
|
|
531
541
|
|
|
@@ -553,6 +563,8 @@ File: `src/admin/routes/custom/create/page.tsx`
|
|
|
553
563
|
|
|
554
564
|
**Critical structure**: Use `RouteFocusModal.Form` wrapping `KeyboundForm` — this handles dirty form blocking on close and Cmd/Ctrl+Enter submit. The `KeyboundForm` wraps Header + Body + Footer. Use `useRouteModal()` for `handleSuccess` to close the modal after success.
|
|
555
565
|
|
|
566
|
+
**IMPORTANT**: `useRouteModal()` must be called in a **child component** rendered inside `<RouteFocusModal>`, not in the same component that renders it. The `RouteModalProvider` context is created inside `RouteFocusModal`, so calling the hook in the parent component crashes with "useRouteModal must be used within a RouteModalProvider".
|
|
567
|
+
|
|
556
568
|
```tsx
|
|
557
569
|
import { Button, RouteFocusModal, Form, Heading, Input, Text, Textarea, toast, KeyboundForm, useRouteModal } from "@acmekit/ui"
|
|
558
570
|
import { useForm } from "react-hook-form"
|
|
@@ -566,7 +578,17 @@ const CreateSchema = zod.object({
|
|
|
566
578
|
description: zod.string().optional(),
|
|
567
579
|
})
|
|
568
580
|
|
|
581
|
+
// Page component — renders RouteFocusModal, delegates form to child
|
|
569
582
|
const CreatePostPage = () => {
|
|
583
|
+
return (
|
|
584
|
+
<RouteFocusModal>
|
|
585
|
+
<CreatePostForm />
|
|
586
|
+
</RouteFocusModal>
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Form component — useRouteModal() is valid here (inside RouteFocusModal)
|
|
591
|
+
const CreatePostForm = () => {
|
|
570
592
|
const queryClient = useQueryClient()
|
|
571
593
|
const { handleSuccess } = useRouteModal()
|
|
572
594
|
|
|
@@ -592,61 +614,59 @@ const CreatePostPage = () => {
|
|
|
592
614
|
})
|
|
593
615
|
|
|
594
616
|
return (
|
|
595
|
-
<RouteFocusModal>
|
|
596
|
-
<
|
|
597
|
-
<
|
|
598
|
-
|
|
599
|
-
<
|
|
600
|
-
<div
|
|
601
|
-
<
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
</Text>
|
|
606
|
-
</div>
|
|
607
|
-
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
608
|
-
<Form.Field
|
|
609
|
-
control={form.control}
|
|
610
|
-
name="title"
|
|
611
|
-
render={({ field }) => (
|
|
612
|
-
<Form.Item>
|
|
613
|
-
<Form.Label>Title</Form.Label>
|
|
614
|
-
<Form.Control>
|
|
615
|
-
<Input autoComplete="off" {...field} />
|
|
616
|
-
</Form.Control>
|
|
617
|
-
<Form.ErrorMessage />
|
|
618
|
-
</Form.Item>
|
|
619
|
-
)}
|
|
620
|
-
/>
|
|
621
|
-
<Form.Field
|
|
622
|
-
control={form.control}
|
|
623
|
-
name="description"
|
|
624
|
-
render={({ field }) => (
|
|
625
|
-
<Form.Item>
|
|
626
|
-
<Form.Label optional>Description</Form.Label>
|
|
627
|
-
<Form.Control>
|
|
628
|
-
<Textarea {...field} />
|
|
629
|
-
</Form.Control>
|
|
630
|
-
<Form.ErrorMessage />
|
|
631
|
-
</Form.Item>
|
|
632
|
-
)}
|
|
633
|
-
/>
|
|
634
|
-
</div>
|
|
617
|
+
<RouteFocusModal.Form form={form}>
|
|
618
|
+
<KeyboundForm onSubmit={handleSubmit} className="flex flex-1 flex-col overflow-hidden">
|
|
619
|
+
<RouteFocusModal.Header />
|
|
620
|
+
<RouteFocusModal.Body className="flex flex-1 flex-col items-center overflow-y-auto py-16">
|
|
621
|
+
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
|
622
|
+
<div>
|
|
623
|
+
<Heading>Create Post</Heading>
|
|
624
|
+
<Text size="small" className="text-ui-fg-subtle">
|
|
625
|
+
Add a new blog post.
|
|
626
|
+
</Text>
|
|
635
627
|
</div>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
628
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
629
|
+
<Form.Field
|
|
630
|
+
control={form.control}
|
|
631
|
+
name="title"
|
|
632
|
+
render={({ field }) => (
|
|
633
|
+
<Form.Item>
|
|
634
|
+
<Form.Label>Title</Form.Label>
|
|
635
|
+
<Form.Control>
|
|
636
|
+
<Input autoComplete="off" {...field} />
|
|
637
|
+
</Form.Control>
|
|
638
|
+
<Form.ErrorMessage />
|
|
639
|
+
</Form.Item>
|
|
640
|
+
)}
|
|
641
|
+
/>
|
|
642
|
+
<Form.Field
|
|
643
|
+
control={form.control}
|
|
644
|
+
name="description"
|
|
645
|
+
render={({ field }) => (
|
|
646
|
+
<Form.Item>
|
|
647
|
+
<Form.Label optional>Description</Form.Label>
|
|
648
|
+
<Form.Control>
|
|
649
|
+
<Textarea {...field} />
|
|
650
|
+
</Form.Control>
|
|
651
|
+
<Form.ErrorMessage />
|
|
652
|
+
</Form.Item>
|
|
653
|
+
)}
|
|
654
|
+
/>
|
|
645
655
|
</div>
|
|
646
|
-
</
|
|
647
|
-
</
|
|
648
|
-
|
|
649
|
-
|
|
656
|
+
</div>
|
|
657
|
+
</RouteFocusModal.Body>
|
|
658
|
+
<RouteFocusModal.Footer>
|
|
659
|
+
<div className="flex items-center justify-end gap-x-2">
|
|
660
|
+
<RouteFocusModal.Close asChild>
|
|
661
|
+
<Button size="small" variant="secondary">Cancel</Button>
|
|
662
|
+
</RouteFocusModal.Close>
|
|
663
|
+
<Button size="small" type="submit" isLoading={isPending}>
|
|
664
|
+
Create
|
|
665
|
+
</Button>
|
|
666
|
+
</div>
|
|
667
|
+
</RouteFocusModal.Footer>
|
|
668
|
+
</KeyboundForm>
|
|
669
|
+
</RouteFocusModal.Form>
|
|
650
670
|
)
|
|
651
671
|
}
|
|
652
672
|
|
|
@@ -313,6 +313,7 @@ const confirmed = await prompt({
|
|
|
313
313
|
**Imports & dependencies:**
|
|
314
314
|
- Importing `zod` from `"zod"` in admin code — use `import * as zod from "@acmekit/deps/zod"` to share the same zod instance as acmekit
|
|
315
315
|
- Adding `react-hook-form` or `@hookform/resolvers` to `devDependencies` — they are provided by acmekit. In plugins: list them in `peerDependencies` (+ `devDependencies` mirror). In apps: they are available transitively, no extra dependency needed
|
|
316
|
+
- Installing `@hookform/resolvers@5.x` — this version requires Zod v4 (`zod/v4/core`), but AcmeKit ships Zod v3. Causes `Could not resolve "zod/v4/core"` at build time. Never install your own version — use what AcmeKit provides
|
|
316
317
|
|
|
317
318
|
**Form context:**
|
|
318
319
|
- Using `Form.Field`, `Form.Label`, `SwitchBox`, or any `Form.*` sub-component without a form provider — they call `useFormContext()` and crash with "Cannot destructure property 'getFieldState' of 'useFormContext(...)' as it is null". `RouteDrawer.Form` / `RouteFocusModal.Form` provide this automatically; standalone pages must wrap with `<Form {...form}>` from `@acmekit/ui`
|
|
@@ -326,6 +327,7 @@ const confirmed = await prompt({
|
|
|
326
327
|
- Using `onClick={handleSubmit}` on submit button — use `type="submit"` inside a `KeyboundForm` tag
|
|
327
328
|
- Missing `className="flex flex-1 flex-col"` on the `KeyboundForm` tag (breaks layout)
|
|
328
329
|
- Missing `overflow-hidden` on RouteFocusModal's `KeyboundForm` tag
|
|
330
|
+
- Calling `useRouteModal()` in the same component that renders `RouteFocusModal`/`RouteDrawer` — the hook must be in a **child component** rendered inside the modal/drawer (the provider mounts inside it)
|
|
329
331
|
- Using `navigate("..")` to close overlays instead of `handleSuccess()` from `useRouteModal()`
|
|
330
332
|
- Putting footer buttons directly in Footer — wrap in `<div className="flex items-center justify-end gap-x-2">`
|
|
331
333
|
- Using grid layout in Drawer forms — drawers are narrow, use single column `flex flex-col gap-y-4`
|