webspresso 0.0.108 → 0.0.110

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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1401 -0
  24. package/core/client/api.js +1337 -0
  25. package/core/client/controller.js +1706 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/devtool.js +1134 -0
  28. package/core/client/forms.js +1164 -0
  29. package/core/client/fragments.js +894 -0
  30. package/core/client/index.d.ts +59 -0
  31. package/core/client/index.js +391 -0
  32. package/core/client/lite.js +175 -0
  33. package/core/client/server-fragments.js +868 -0
  34. package/core/client/storage.js +339 -0
  35. package/core/client/utils.js +281 -0
  36. package/core/compression/middleware.js +277 -26
  37. package/core/context/app-context.js +25 -3
  38. package/core/context/request.js +32 -4
  39. package/core/errors/middleware.js +14 -0
  40. package/core/events/dispatcher.js +33 -0
  41. package/core/forms/flash.js +136 -102
  42. package/core/health/middleware.js +36 -6
  43. package/core/http/pipeline.js +45 -6
  44. package/core/index.js +25 -7
  45. package/core/kernel/view.js +41 -10
  46. package/core/metrics/middleware.js +30 -6
  47. package/core/metrics/registry.js +13 -5
  48. package/core/orm/cache/fingerprint.js +32 -11
  49. package/core/orm/cache/layer.js +19 -2
  50. package/core/orm/hidden.js +145 -0
  51. package/core/orm/model.js +48 -2
  52. package/core/orm/query-builder.js +343 -6
  53. package/core/orm/query-filters.js +14 -2
  54. package/core/orm/repository.js +64 -13
  55. package/core/orm/schema-helpers.js +9 -6
  56. package/core/orm/transaction.js +50 -6
  57. package/core/orm/types.js +4 -0
  58. package/core/queue/adapters/database.js +184 -2
  59. package/core/queue/adapters/memory.js +153 -5
  60. package/core/queue/adapters/redis.js +59 -4
  61. package/core/queue/job.js +14 -1
  62. package/core/queue/manager.js +115 -0
  63. package/core/resilience/circuit-breaker.js +27 -8
  64. package/core/services/builtins/exchange.js +174 -7
  65. package/core/services/builtins/file-manager.js +36 -0
  66. package/core/services/builtins/mail.js +27 -0
  67. package/core/services/builtins/queue.js +144 -0
  68. package/core/services/builtins/security.js +118 -0
  69. package/core/services/builtins/webhook.js +110 -0
  70. package/core/services/executor.js +38 -16
  71. package/core/services/index.js +4 -1
  72. package/core/sse/stream.js +28 -5
  73. package/core/utils/timeout.js +8 -1
  74. package/core/validation/compiler.js +10 -7
  75. package/index.d.ts +1320 -74
  76. package/index.js +128 -8
  77. package/package.json +75 -27
  78. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  79. package/plugins/admin-panel/index.js +19 -8
  80. package/plugins/csrf/index.js +3 -3
  81. package/plugins/data-exchange/export.js +214 -0
  82. package/plugins/data-exchange/import.js +48 -5
  83. package/plugins/data-exchange/index.js +70 -28
  84. package/plugins/data-exchange/preview.js +266 -0
  85. package/plugins/data-exchange/template.js +242 -0
  86. package/plugins/email/admin-component.js +783 -141
  87. package/plugins/email/api-handlers.js +51 -1
  88. package/plugins/email/index.js +5 -0
  89. package/plugins/email/service.js +48 -0
  90. package/plugins/file-manager/admin-component.js +1546 -70
  91. package/plugins/file-manager/api-handlers.js +166 -1
  92. package/plugins/file-manager/core.js +598 -8
  93. package/plugins/file-manager/index.js +8 -0
  94. package/plugins/index.js +7 -0
  95. package/plugins/mcp/auth.js +3 -3
  96. package/plugins/oauth/index.js +36 -3
  97. package/plugins/queue/admin-component.js +1092 -0
  98. package/plugins/queue/api-handlers.js +248 -0
  99. package/plugins/queue/index.js +69 -0
  100. package/plugins/rest-resources/index.js +7 -2
  101. package/plugins/security/admin-component.js +689 -0
  102. package/plugins/security/api-handlers.js +169 -0
  103. package/plugins/security/audit.js +153 -0
  104. package/plugins/security/bruteforce.js +244 -0
  105. package/plugins/security/firewall.js +459 -0
  106. package/plugins/security/headers.js +214 -0
  107. package/plugins/security/honeypot.js +234 -0
  108. package/plugins/security/incident-recorder.js +137 -0
  109. package/plugins/security/index.js +211 -0
  110. package/plugins/seo-checker/analyzer.js +20 -7
  111. package/plugins/upload/index.js +36 -0
  112. package/plugins/webhooks/admin-component.js +702 -175
  113. package/plugins/webhooks/api-handlers.js +75 -1
  114. package/plugins/webhooks/dispatcher.js +290 -11
  115. package/plugins/webhooks/index.js +37 -2
  116. package/plugins/webhooks/sender.js +56 -0
  117. package/plugins/xlsx/engine.js +19 -4
  118. package/src/api/api-loader.js +19 -1
  119. package/src/bundle/index.js +443 -17
  120. package/src/client-runtime/bootstrap-swup.js +9 -0
  121. package/src/client-runtime/mount.js +15 -20
  122. package/src/client-runtime/resolve.js +3 -2
  123. package/src/dev/api-typegen.js +1300 -0
  124. package/src/dev/banner.js +10 -1
  125. package/src/dev/colors.js +1 -1
  126. package/src/dev/index.js +7 -1
  127. package/src/dev/typegen.js +173 -17
  128. package/src/dev-reload/index.js +1 -1
  129. package/src/discovery/file-route-parser.js +17 -3
  130. package/src/discovery/scan-routes.js +4 -1
  131. package/src/file-router.js +253 -69
  132. package/src/helpers.js +46 -26
  133. package/src/njk-frontmatter.js +30 -4
  134. package/src/routing/cascading.js +26 -8
  135. package/src/routing/mount-discovered-routes.js +76 -0
  136. package/src/server.js +231 -70
  137. package/views/partials/webspresso-client-runtime.njk +1 -9
  138. package/.agents/ADMIN.md +0 -426
  139. package/.agents/AGENTS.md +0 -96
  140. package/.agents/ARCHITECTURE.md +0 -163
  141. package/.agents/AUTH.md +0 -581
  142. package/.agents/CACHE.md +0 -148
  143. package/.agents/CLI.md +0 -649
  144. package/.agents/COMPRESSION.md +0 -59
  145. package/.agents/CONTEXT.md +0 -152
  146. package/.agents/CONVENTIONS.md +0 -50
  147. package/.agents/EARLY_HINTS.md +0 -120
  148. package/.agents/EMAIL.md +0 -223
  149. package/.agents/ERRORS.md +0 -260
  150. package/.agents/EVENTS.md +0 -179
  151. package/.agents/FLAGS.md +0 -118
  152. package/.agents/HEALTH.md +0 -118
  153. package/.agents/LIMITS.md +0 -109
  154. package/.agents/LOGGING.md +0 -93
  155. package/.agents/MCP.md +0 -103
  156. package/.agents/METRICS.md +0 -127
  157. package/.agents/MIGRATION_JOBS.md +0 -95
  158. package/.agents/ORM.md +0 -700
  159. package/.agents/PLUGINS.md +0 -729
  160. package/.agents/QUEUE.md +0 -290
  161. package/.agents/RATE_LIMIT.md +0 -147
  162. package/.agents/REALTIME.md +0 -213
  163. package/.agents/RESILIENCE.md +0 -235
  164. package/.agents/ROUTING.md +0 -605
  165. package/.agents/SCALE_OUT.md +0 -109
  166. package/.agents/SECRETS.md +0 -117
  167. package/.agents/SERVICES.md +0 -341
  168. package/.agents/SHUTDOWN.md +0 -74
  169. package/.agents/STREAMING.md +0 -108
  170. package/.agents/WORKFLOW.md +0 -27
  171. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  172. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  173. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  174. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  175. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  176. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  177. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  178. package/.cursorrules +0 -14
  179. package/AGENTS.md +0 -203
  180. package/CLAUDE.md +0 -54
  181. package/core/kernel/run-demo.js +0 -58
  182. package/plugins/admin-panel/styles.js +0 -9
  183. package/plugins/data-exchange/export-xlsx.js +0 -119
  184. package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
@@ -0,0 +1,1706 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Client Controller Runtime
5
+ *
6
+ * Implements a Stimulus-inspired, Datastar-powered controller architecture:
7
+ * - Deterministic controller naming & duplicate registration collision detection
8
+ * - Isolated per-instance state scope backed by Datastar signals
9
+ * - Scoped target resolution (`target(name)`, `targets(name)`) that never crosses into
10
+ * nested controllers and never caches stale DOM element references across patches
11
+ * - DOM CustomEvent `emit` / `on` communication without requiring a global event bus
12
+ * - Automatic cleanup of listeners (`on`), reactive `effect`s, `interval`s, `timeout`s,
13
+ * and in-flight `api` requests on disconnect
14
+ * - Protection against late async results mutating state after instance removal
15
+ * - Preservation of active controller instances and local state when DOM roots survive a fragment morph
16
+ */
17
+
18
+ const { createFormHandle } = require('./forms');
19
+ const { createApiClient } = require('./api');
20
+ const { defaultStorage, createClientStorage } = require('./storage');
21
+ const { decorateElement } = require('./utils');
22
+
23
+ let instanceSeq = 0;
24
+ let activeControllerContext = null;
25
+
26
+ function getActiveControllerContext() {
27
+ return activeControllerContext;
28
+ }
29
+
30
+ function resolveStatePath(root, pathStr) {
31
+ if (!root || !pathStr) return undefined;
32
+ const parts = String(pathStr).split('.');
33
+ let cur = root;
34
+ for (let i = 0; i < parts.length; i++) {
35
+ if (cur === null || typeof cur !== 'object') return undefined;
36
+ cur = cur[parts[i]];
37
+ }
38
+ return cur;
39
+ }
40
+
41
+ function deepTouch(val, visited = new Set()) {
42
+ if (val === null || typeof val !== 'object' || visited.has(val)) {
43
+ return;
44
+ }
45
+ visited.add(val);
46
+ if (Array.isArray(val)) {
47
+ for (let i = 0; i < val.length; i++) {
48
+ deepTouch(val[i], visited);
49
+ }
50
+ } else {
51
+ const keys = Object.keys(val);
52
+ for (let i = 0; i < keys.length; i++) {
53
+ deepTouch(val[keys[i]], visited);
54
+ }
55
+ }
56
+ }
57
+
58
+ function deepCloneOrSnapshot(val) {
59
+ if (val === null || typeof val !== 'object') return val;
60
+ try {
61
+ return JSON.parse(JSON.stringify(val));
62
+ } catch {
63
+ return Array.isArray(val) ? [...val] : { ...val };
64
+ }
65
+ }
66
+
67
+ function deepEqual(a, b) {
68
+ if (Object.is(a, b)) return true;
69
+ if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') return false;
70
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
71
+ const keysA = Object.keys(a);
72
+ const keysB = Object.keys(b);
73
+ if (keysA.length !== keysB.length) return false;
74
+ for (let i = 0; i < keysA.length; i++) {
75
+ const k = keysA[i];
76
+ if (!Object.prototype.hasOwnProperty.call(b, k) || !deepEqual(a[k], b[k])) {
77
+ return false;
78
+ }
79
+ }
80
+ return true;
81
+ }
82
+
83
+ function hasArrayChanged(newArr, oldArr) {
84
+ if (!Array.isArray(newArr) || !Array.isArray(oldArr)) return !Object.is(newArr, oldArr);
85
+ if (newArr.length !== oldArr.length) return true;
86
+ for (let i = 0; i < newArr.length; i++) {
87
+ if (!Object.is(newArr[i], oldArr[i])) return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ function runUntracked(engine, fn) {
93
+ if (engine && typeof engine.untracked === 'function') {
94
+ return engine.untracked(fn);
95
+ }
96
+ return fn();
97
+ }
98
+
99
+ function createStandaloneWatch(engine, source, callback, options = {}) {
100
+ if (typeof callback !== 'function') {
101
+ throw new TypeError('watch(source, callback) requires callback to be a function.');
102
+ }
103
+
104
+ const immediate = Boolean(options.immediate);
105
+ const deep = Boolean(options.deep);
106
+ const once = Boolean(options.once);
107
+
108
+ let getter;
109
+ if (typeof source === 'function') {
110
+ getter = source;
111
+ } else if (Array.isArray(source)) {
112
+ getter = () => source.map((s) => (typeof s === 'function' ? s() : s));
113
+ } else {
114
+ getter = () => source;
115
+ }
116
+
117
+ let oldValue = undefined;
118
+ let isFirstRun = true;
119
+ let stopped = false;
120
+ let userCleanup = null;
121
+
122
+ function runCleanup() {
123
+ if (typeof userCleanup === 'function') {
124
+ try {
125
+ userCleanup();
126
+ } catch (_e) {}
127
+ userCleanup = null;
128
+ }
129
+ }
130
+
131
+ function onCleanup(fn) {
132
+ if (typeof fn === 'function') {
133
+ userCleanup = fn;
134
+ }
135
+ }
136
+
137
+ function stopWatch() {
138
+ if (stopped) return;
139
+ stopped = true;
140
+ runCleanup();
141
+ stopEffect();
142
+ }
143
+
144
+ const stopEffect = engine.effect(() => {
145
+ if (stopped) return;
146
+
147
+ let newValue;
148
+ try {
149
+ newValue = getter();
150
+ if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !Array.isArray(source))) && newValue && typeof newValue === 'object') {
151
+ deepTouch(newValue);
152
+ }
153
+ } catch (_err) {
154
+ return;
155
+ }
156
+
157
+ const isArr = Array.isArray(source);
158
+
159
+ if (isFirstRun) {
160
+ isFirstRun = false;
161
+ oldValue = deep ? deepCloneOrSnapshot(newValue) : isArr ? [...newValue] : newValue;
162
+ if (immediate) {
163
+ runCleanup();
164
+ runUntracked(engine, () => {
165
+ callback(newValue, undefined, onCleanup);
166
+ });
167
+ if (once) stopWatch();
168
+ }
169
+ return;
170
+ }
171
+
172
+ const hasChanged = deep
173
+ ? !deepEqual(newValue, oldValue)
174
+ : isArr
175
+ ? hasArrayChanged(newValue, oldValue)
176
+ : !Object.is(newValue, oldValue);
177
+
178
+ if (!hasChanged) return;
179
+
180
+ const prev = oldValue;
181
+ oldValue = deep ? deepCloneOrSnapshot(newValue) : isArr ? [...newValue] : newValue;
182
+
183
+ runCleanup();
184
+ runUntracked(engine, () => {
185
+ callback(newValue, prev, onCleanup);
186
+ });
187
+
188
+ if (once) stopWatch();
189
+ });
190
+
191
+ return stopWatch;
192
+ }
193
+
194
+ /**
195
+ * Vue-style reactive watcher.
196
+ *
197
+ * @template T
198
+ * @param {(() => T) | string | Array<any>} source - Reactive getter function, state dot-path, or array of sources
199
+ * @param {(newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void} callback - Handler invoked when value changes
200
+ * @param {object} [options]
201
+ * @param {boolean} [options.immediate=false] - Trigger callback immediately on registration
202
+ * @param {boolean} [options.deep=false] - Deep watch nested object/array mutations
203
+ * @param {boolean} [options.once=false] - Stop watching automatically after first trigger
204
+ * @returns {() => void} Unwatch function
205
+ */
206
+ function watch(source, callback, options = {}) {
207
+ if (activeControllerContext && typeof activeControllerContext.watch === 'function') {
208
+ return activeControllerContext.watch(source, callback, options);
209
+ }
210
+ const { createDatastarAdapter } = require('./adapter/datastar');
211
+ const engine = createDatastarAdapter().engine;
212
+ if (!engine || typeof engine.effect !== 'function') {
213
+ return () => {};
214
+ }
215
+ return createStandaloneWatch(engine, source, callback, options);
216
+ }
217
+
218
+ /**
219
+ * Derive a deterministic controller name from a `.client.{js,ts}` file path.
220
+ *
221
+ * Examples:
222
+ * - `pages/notes.client.ts` -> `notes`
223
+ * - `pages/index.client.js` -> `index`
224
+ * - `pages/settings/profile.client.ts` -> `settings--profile` (alias `settings/profile`)
225
+ * - `views/components/dropdown.client.ts` -> `dropdown`
226
+ * - `views/modals/confirm-dialog.client.js` -> `confirm-dialog`
227
+ *
228
+ * @param {string} filePath
229
+ * @returns {string}
230
+ */
231
+ function stripBrackets(str) {
232
+ return str.replace(/[\[\]]/g, '');
233
+ }
234
+
235
+ function extractAfterMarker(path, marker) {
236
+ const candidates = ['/src/' + marker, 'src/' + marker, '/' + marker, marker];
237
+ for (const cand of candidates) {
238
+ const idx = path.lastIndexOf(cand);
239
+ if (idx !== -1) {
240
+ if (cand.startsWith('/') || idx === 0) {
241
+ return path.slice(idx + cand.length);
242
+ }
243
+ }
244
+ }
245
+ return null;
246
+ }
247
+
248
+ function stripClientExtension(path) {
249
+ const exts = ['.client.ts', '.client.tsx', '.client.js', '.client.jsx', '.client.mjs', '.client.cjs'];
250
+ const lower = path.toLowerCase();
251
+ for (const ext of exts) {
252
+ if (lower.endsWith(ext)) {
253
+ return path.slice(0, -ext.length);
254
+ }
255
+ }
256
+ return path;
257
+ }
258
+
259
+ function deriveControllerNameFromPath(filePath) {
260
+ if (!filePath || typeof filePath !== 'string') return '';
261
+ const normalized = filePath.replace(/\\/g, '/');
262
+ const withoutExt = stripClientExtension(normalized);
263
+
264
+ // Check if under views/components/
265
+ const compRel = extractAfterMarker(withoutExt, 'views/components/');
266
+ if (compRel) {
267
+ return stripBrackets(compRel.split('/').join('--'));
268
+ }
269
+
270
+ // Check if under views/
271
+ const viewsRel = extractAfterMarker(withoutExt, 'views/');
272
+ if (viewsRel) {
273
+ const base = viewsRel.split('/').pop() || viewsRel;
274
+ return stripBrackets(base);
275
+ }
276
+
277
+ // Check if under pages/
278
+ const pagesRel = extractAfterMarker(withoutExt, 'pages/');
279
+ if (pagesRel) {
280
+ let rel = pagesRel;
281
+ if (rel.endsWith('/index')) {
282
+ rel = rel.slice(0, -6);
283
+ }
284
+ if (!rel || rel === 'index') {
285
+ rel = 'index';
286
+ }
287
+ return stripBrackets(rel.split('/').join('--'));
288
+ }
289
+
290
+ const base = withoutExt.split('/').pop() || withoutExt;
291
+ return stripBrackets(base);
292
+ }
293
+
294
+ /**
295
+ * Define a client behavior controller.
296
+ *
297
+ * @template TState
298
+ * @param {object} definition
299
+ * @param {string} [definition.name] - Optional explicit controller name
300
+ * @param {(ctx: { el: Element }) => TState} [definition.state] - Factory returning initial local state per instance
301
+ * @param {(ctx: object) => (() => void)|void} [definition.connect] - Lifecycle setup hook; may return a cleanup function
302
+ * @param {(ctx: object) => void} [definition.disconnect] - Optional teardown hook
303
+ * @param {Record<string, (ctx: object, event?: Event, actionEl?: Element) => unknown>} [definition.actions] - Named action handlers
304
+ */
305
+ function defineController(definition = {}) {
306
+ if (!definition || typeof definition !== 'object') {
307
+ throw new TypeError('defineController() requires a configuration object.');
308
+ }
309
+ if (definition.state !== undefined && typeof definition.state !== 'function') {
310
+ throw new TypeError(
311
+ 'defineController({ state }) must be a factory function `() => ({ ... })` so each instance receives isolated state.'
312
+ );
313
+ }
314
+ if (definition.computed !== undefined && (typeof definition.computed !== 'object' || definition.computed === null)) {
315
+ throw new TypeError('defineController({ computed }) must be an object of computed property functions.');
316
+ }
317
+ if (definition.tasks !== undefined && (typeof definition.tasks !== 'object' || definition.tasks === null)) {
318
+ throw new TypeError('defineController({ tasks }) must be an object of task factory functions.');
319
+ }
320
+ if (definition.watch !== undefined && (typeof definition.watch !== 'object' || definition.watch === null)) {
321
+ throw new TypeError('defineController({ watch }) must be an object of watcher functions or descriptors.');
322
+ }
323
+ if (definition.onError !== undefined && typeof definition.onError !== 'function') {
324
+ throw new TypeError('defineController({ onError }) must be an error handler function `(err, ctx, meta) => void`.');
325
+ }
326
+ if (definition.persist !== undefined && definition.persist !== null) {
327
+ const p = definition.persist;
328
+ const isArr = Array.isArray(p);
329
+ const isStr = typeof p === 'string';
330
+ const isObj = typeof p === 'object' && p !== null && !isArr;
331
+ if (!isArr && !isStr && !isObj) {
332
+ throw new TypeError(
333
+ 'defineController({ persist }) must be an array of keys, storage type string ("local" | "session"), or a config object { keys?, storage?, key?, debounce? }.'
334
+ );
335
+ }
336
+ }
337
+
338
+ // Pre-validate collision between state and computed properties
339
+ if (definition.computed && typeof definition.computed === 'object') {
340
+ let dummyState = {};
341
+ if (typeof definition.state === 'function') {
342
+ try {
343
+ dummyState = definition.state({ el: null }) || {};
344
+ } catch (_e) {}
345
+ } else if (definition.state && typeof definition.state === 'object') {
346
+ dummyState = definition.state;
347
+ }
348
+ for (const key of Object.keys(definition.computed)) {
349
+ if (dummyState && Object.prototype.hasOwnProperty.call(dummyState, key)) {
350
+ throw new TypeError(`Cannot define "${key}" as both a state and computed property.`);
351
+ }
352
+ }
353
+ }
354
+ return Object.freeze({
355
+ __isWebspressoController: true,
356
+ name: definition.name ? String(definition.name).trim() : null,
357
+ state: definition.state || (() => ({})),
358
+ computed: definition.computed && typeof definition.computed === 'object' ? definition.computed : {},
359
+ tasks: definition.tasks && typeof definition.tasks === 'object' ? definition.tasks : {},
360
+ connect: typeof definition.connect === 'function' ? definition.connect : null,
361
+ disconnect: typeof definition.disconnect === 'function' ? definition.disconnect : null,
362
+ onError: typeof definition.onError === 'function' ? definition.onError : null,
363
+ persist: definition.persist || null,
364
+ watch: definition.watch && typeof definition.watch === 'object' ? definition.watch : null,
365
+ actions: definition.actions && typeof definition.actions === 'object' ? definition.actions : {},
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Normalize controller persist configuration.
371
+ *
372
+ * @param {any} persistOpt
373
+ * @param {string|null} controllerName
374
+ * @returns {{ keys: string[]|null, storageType: 'local'|'session'|'memory', storageKey: string, debounce: number }|null}
375
+ */
376
+ function normalizePersistConfig(persistOpt, controllerName) {
377
+ if (!persistOpt) return null;
378
+ let keys = null;
379
+ let storageType = 'local';
380
+ let storageKey = 'state';
381
+ let isGlobal = false;
382
+ let debounce = 50;
383
+
384
+ if (Array.isArray(persistOpt)) {
385
+ keys = persistOpt.map(String);
386
+ } else if (typeof persistOpt === 'string') {
387
+ const s = persistOpt.trim().toLowerCase();
388
+ storageType = s === 'session' ? 'session' : 'local';
389
+ } else if (typeof persistOpt === 'object' && persistOpt !== null) {
390
+ if (Array.isArray(persistOpt.keys)) {
391
+ keys = persistOpt.keys.map(String);
392
+ }
393
+ if (persistOpt.storage === 'session' || persistOpt.storage === 'memory') {
394
+ storageType = persistOpt.storage;
395
+ }
396
+ if (typeof persistOpt.key === 'string' && persistOpt.key.trim()) {
397
+ storageKey = persistOpt.key.trim();
398
+ }
399
+ if (Boolean(persistOpt.global)) {
400
+ isGlobal = true;
401
+ }
402
+ if (typeof persistOpt.debounce === 'number') {
403
+ debounce = Math.max(0, persistOpt.debounce);
404
+ }
405
+ }
406
+ return { keys, storageType, storageKey, isGlobal, debounce };
407
+ }
408
+
409
+ /**
410
+ * Check if `candidateEl` belongs to `rootEl`'s controller scope for `controllerName`,
411
+ * and is NOT inside a nested `[data-w-controller]` element.
412
+ */
413
+ function isTargetInControllerScope(rootEl, candidateEl) {
414
+ if (!rootEl || !candidateEl) return false;
415
+ if (candidateEl === rootEl) return true;
416
+ let cur = candidateEl.parentElement;
417
+ while (cur && cur !== rootEl) {
418
+ if (cur.hasAttribute && cur.hasAttribute('data-w-controller')) {
419
+ return false;
420
+ }
421
+ cur = cur.parentElement;
422
+ }
423
+ return cur === rootEl;
424
+ }
425
+
426
+ /**
427
+ * Dynamically resolve `[data-w-target]` elements inside `rootEl` strictly within the
428
+ * controller's scope. Never caches DOM references so DOM patches never leave stale pointers.
429
+ *
430
+ * Supports:
431
+ * - `data-w-target="input"`
432
+ * - `data-w-target="notes.input"` (controller-prefixed target syntax)
433
+ */
434
+ function queryScopedTargets(rootEl, controllerName, targetName) {
435
+ if (!rootEl || typeof rootEl.querySelectorAll !== 'function' || !targetName) {
436
+ return [];
437
+ }
438
+ const cleanTarget = String(targetName).trim();
439
+ const prefixedTarget = `${controllerName}.${cleanTarget}`;
440
+
441
+ const allTargets = rootEl.querySelectorAll('[data-w-target]');
442
+ const matched = [];
443
+
444
+ for (let i = 0; i < allTargets.length; i++) {
445
+ const el = allTargets[i];
446
+ if (!isTargetInControllerScope(rootEl, el)) continue;
447
+ const rawAttr = (el.getAttribute('data-w-target') || '').trim();
448
+ const tokens = rawAttr.split(/\s+/);
449
+ if (tokens.includes(cleanTarget) || tokens.includes(prefixedTarget)) {
450
+ matched.push(decorateElement(el));
451
+ }
452
+ }
453
+
454
+ return matched;
455
+ }
456
+
457
+ /**
458
+ * Create a Controller Runtime manager.
459
+ *
460
+ * @param {object} options
461
+ * @param {object} options.datastar - Datastar adapter instance
462
+ * @param {object} options.api - Base API client configuration or instance
463
+ * @param {object} [options.fragments] - Fragment coordinator instance
464
+ * @param {object} [options.debug] - Client debugger instance
465
+ * @param {(name: string) => Promise<object>} [options.lazyLoader] - Optional dynamic loader for lazy controllers
466
+ */
467
+ function createControllerRuntime(options = {}) {
468
+ const datastar = options.datastar;
469
+ const fragments = options.fragments || null;
470
+ const debug = options.debug || null;
471
+ const baseApiConfig = options.apiConfig || {};
472
+ const lazyLoader = typeof options.lazyLoader === 'function' ? options.lazyLoader : null;
473
+
474
+ // Registered controller definitions: name -> { name, definition, sourcePath }
475
+ const definitions = new Map();
476
+ // Active mounted controller instances per element: WeakMap<Element, Map<controllerName, InstanceRecord>>
477
+ const elementInstances = new WeakMap();
478
+ // All active instances by instanceId: Map<instanceId, InstanceRecord>
479
+ const activeInstances = new Map();
480
+ // Standalone forms (forms without a controller wrapper)
481
+ const standaloneForms = new WeakMap();
482
+
483
+ /**
484
+ * Register a controller definition with collision checking.
485
+ *
486
+ * @param {string} name
487
+ * @param {object} rawDef
488
+ * @param {object} [meta]
489
+ * @param {string} [meta.sourcePath]
490
+ * @param {string[]} [meta.aliases]
491
+ */
492
+ function register(nameOrDef, rawDef, meta = {}) {
493
+ let name = nameOrDef;
494
+ let actualDef = rawDef;
495
+ if (typeof nameOrDef === 'object' && nameOrDef !== null && !rawDef) {
496
+ actualDef = nameOrDef;
497
+ name = actualDef.name || (actualDef.default && actualDef.default.name) || null;
498
+ }
499
+
500
+ const def =
501
+ actualDef && actualDef.__isWebspressoController
502
+ ? actualDef
503
+ : defineController(actualDef && actualDef.default ? actualDef.default : actualDef);
504
+
505
+ const canonicalName = String(name || def.name || '').trim();
506
+ if (!canonicalName) {
507
+ throw new Error('Controller registration requires a non-empty controller name.');
508
+ }
509
+
510
+ const sourcePath = meta.sourcePath || null;
511
+ const existing = definitions.get(canonicalName);
512
+
513
+ if (existing) {
514
+ const sameDef = existing.definition === def;
515
+ const sameSource = Boolean(sourcePath && existing.sourcePath === sourcePath);
516
+ if (!sameDef && !sameSource) {
517
+ const err = new Error(
518
+ `Controller name collision detected for "${canonicalName}"` +
519
+ (existing.sourcePath && sourcePath
520
+ ? ` between "${existing.sourcePath}" and "${sourcePath}".`
521
+ : '.')
522
+ );
523
+ err.code = 'CONTROLLER_NAME_COLLISION';
524
+ err.controllerName = canonicalName;
525
+ throw err;
526
+ }
527
+ return def;
528
+ }
529
+
530
+ const entry = {
531
+ name: canonicalName,
532
+ definition: def,
533
+ sourcePath,
534
+ };
535
+ definitions.set(canonicalName, entry);
536
+
537
+ if (Array.isArray(meta.aliases)) {
538
+ for (const alias of meta.aliases) {
539
+ if (alias && !definitions.has(alias)) {
540
+ definitions.set(alias, entry);
541
+ }
542
+ }
543
+ }
544
+
545
+ return def;
546
+ }
547
+
548
+ /**
549
+ * Mount a single controller instance on `rootEl` if not already mounted.
550
+ */
551
+ function mountControllerOnElement(rootEl, controllerName) {
552
+ if (!rootEl || rootEl.nodeType !== 1) return null;
553
+ const reg = definitions.get(controllerName);
554
+ if (!reg) return null;
555
+
556
+ let elMap = elementInstances.get(rootEl);
557
+ if (!elMap) {
558
+ elMap = new Map();
559
+ elementInstances.set(rootEl, elMap);
560
+ }
561
+
562
+ // Prevent double-binding on the same DOM root!
563
+ if (elMap.has(controllerName)) {
564
+ const existingInstance = elMap.get(controllerName);
565
+ // Re-run DOM attribute binding for any newly morphed child nodes that aren't bound yet
566
+ existingInstance.rebindDom();
567
+ return existingInstance;
568
+ }
569
+
570
+ const instanceId = `ctrl_${controllerName}_${++instanceSeq}`;
571
+ let connected = true;
572
+ const lifecycleAbort = new AbortController();
573
+
574
+ const isConnected = () => connected && rootEl.isConnected !== false;
575
+
576
+ // 1. Controller-scoped API client (aborts on disconnect, routes invalidations to fragments)
577
+ const scopedApi = createApiClient({
578
+ ...baseApiConfig,
579
+ lifecycleSignal: lifecycleAbort.signal,
580
+ isConnected,
581
+ debug,
582
+ onInvalidate: fragments ? (tags, meta) => fragments.invalidate(tags, meta) : baseApiConfig.onInvalidate,
583
+ });
584
+
585
+ // 2. Lifecycle resource trackers
586
+ const cleanupCallbacks = [];
587
+ const scopedForms = new Map();
588
+ const scopedTasks = {};
589
+ const scopedTasksList = [];
590
+ let currentActionEl = null;
591
+ let lastError = null;
592
+ const dynamicErrorListeners = new Set();
593
+
594
+ function handleControllerError(err, meta = {}) {
595
+ if (!err || err.isCanceled) {
596
+ return;
597
+ }
598
+
599
+ lastError = err;
600
+
601
+ // 1. Controller definition onError hook
602
+ if (typeof reg.definition.onError === 'function') {
603
+ try {
604
+ reg.definition.onError(err, ctx, meta);
605
+ } catch (hookErr) {
606
+ if (debug) {
607
+ debug.trackCleanupError(instanceId, controllerName, 'onError', hookErr);
608
+ }
609
+ }
610
+ }
611
+
612
+ // 2. Dynamic ctx.onError listeners
613
+ for (const listener of Array.from(dynamicErrorListeners)) {
614
+ try {
615
+ listener(err, meta);
616
+ } catch (_listenerErr) {
617
+ // Ignore
618
+ }
619
+ }
620
+
621
+ // 3. Dispatch bubbling CustomEvent 'w-controller:error' on rootEl
622
+ emitEvent(
623
+ 'w-controller:error',
624
+ {
625
+ error: err,
626
+ instanceId,
627
+ controllerName,
628
+ ...meta,
629
+ },
630
+ { bubbles: true, cancelable: true }
631
+ );
632
+ }
633
+
634
+ let taskSeq = 0;
635
+ function createTaskInstance(runFnOrConfig, options = {}) {
636
+ let runFn = runFnOrConfig;
637
+ let taskOpts = { ...options };
638
+ if (typeof runFnOrConfig === 'object' && runFnOrConfig !== null && typeof runFnOrConfig.run === 'function') {
639
+ runFn = runFnOrConfig.run;
640
+ taskOpts = { ...runFnOrConfig, ...options };
641
+ }
642
+ if (typeof runFn !== 'function') {
643
+ throw new TypeError('task(fn) requires an async executor function or { run: async () => ... } config.');
644
+ }
645
+
646
+ const policy = taskOpts.concurrency || taskOpts.policy || 'latest';
647
+ const taskKey = taskOpts.key || `task_${++taskSeq}`;
648
+ const signalPrefix = `__w_inst_${instanceId}.task_${taskKey}`;
649
+
650
+ let currentRunVersion = 0;
651
+ let activeRuns = 0;
652
+ let inFlightAbort = null;
653
+ let queuedPromise = Promise.resolve();
654
+
655
+ datastar.engine.signals.setValue(`${signalPrefix}.pending`, false);
656
+ datastar.engine.signals.setValue(`${signalPrefix}.data`, null);
657
+ datastar.engine.signals.setValue(`${signalPrefix}.error`, null);
658
+ datastar.engine.signals.setValue(`${signalPrefix}.stale`, false);
659
+
660
+ let _pending = false;
661
+ let _data = null;
662
+ let _error = null;
663
+ let _stale = false;
664
+
665
+ function updateSignal(name, val) {
666
+ datastar.engine.signals.setValue(`${signalPrefix}.${name}`, val);
667
+ }
668
+
669
+ async function run(...args) {
670
+ if (!isConnected()) return undefined;
671
+
672
+ if (policy === 'drop' && activeRuns > 0) {
673
+ if (debug && typeof debug.trackDiscardedResponse === 'function') {
674
+ debug.trackDiscardedResponse({
675
+ source: 'task',
676
+ instanceId,
677
+ key: taskKey,
678
+ reason: 'dropped-by-policy',
679
+ });
680
+ }
681
+ return undefined;
682
+ }
683
+
684
+ if (policy === 'queue') {
685
+ const wait = queuedPromise;
686
+ const nextRun = wait.then(() => executeRun(...args)).catch(() => {});
687
+ queuedPromise = nextRun;
688
+ return nextRun;
689
+ }
690
+
691
+ return executeRun(...args);
692
+ }
693
+
694
+ async function executeRun(...args) {
695
+ if (!isConnected()) return undefined;
696
+
697
+ const runVersion = ++currentRunVersion;
698
+ activeRuns += 1;
699
+
700
+ if (policy === 'latest' && inFlightAbort) {
701
+ try {
702
+ inFlightAbort.abort();
703
+ } catch (_e) {}
704
+ }
705
+ const abortCtrl = new AbortController();
706
+ inFlightAbort = abortCtrl;
707
+
708
+ _pending = true;
709
+ updateSignal('pending', true);
710
+ if (_data !== null) {
711
+ _stale = true;
712
+ updateSignal('stale', true);
713
+ }
714
+
715
+ try {
716
+ const result = await runFn(...args);
717
+
718
+ if (!isConnected() || runVersion !== currentRunVersion) {
719
+ return undefined;
720
+ }
721
+
722
+ _data = result;
723
+ _error = null;
724
+ _stale = false;
725
+ updateSignal('data', result);
726
+ updateSignal('error', null);
727
+ updateSignal('stale', false);
728
+ return result;
729
+ } catch (err) {
730
+ if (!isConnected() || runVersion !== currentRunVersion) {
731
+ return undefined;
732
+ }
733
+ if (err && (err.isCanceled || err.name === 'AbortError')) {
734
+ return undefined;
735
+ }
736
+ _error = err;
737
+ updateSignal('error', err);
738
+ handleControllerError(err, {
739
+ task: taskKey,
740
+ phase: 'task',
741
+ });
742
+ throw err;
743
+ } finally {
744
+ if (inFlightAbort === abortCtrl) {
745
+ inFlightAbort = null;
746
+ }
747
+ activeRuns = Math.max(0, activeRuns - 1);
748
+ if (runVersion === currentRunVersion || activeRuns === 0) {
749
+ _pending = false;
750
+ _stale = false;
751
+ if (isConnected()) {
752
+ updateSignal('pending', false);
753
+ updateSignal('stale', false);
754
+ }
755
+ }
756
+ }
757
+ }
758
+
759
+ function cancel() {
760
+ if (inFlightAbort) {
761
+ try {
762
+ inFlightAbort.abort();
763
+ } catch (_e) {}
764
+ inFlightAbort = null;
765
+ }
766
+ _pending = false;
767
+ _stale = false;
768
+ if (isConnected()) {
769
+ updateSignal('pending', false);
770
+ updateSignal('stale', false);
771
+ }
772
+ }
773
+
774
+ const taskObj = {
775
+ __isWebspressoTask: true,
776
+ get pending() {
777
+ datastar.engine.signals.value(`${signalPrefix}.pending`);
778
+ return _pending;
779
+ },
780
+ get data() {
781
+ datastar.engine.signals.value(`${signalPrefix}.data`);
782
+ return _data;
783
+ },
784
+ get error() {
785
+ datastar.engine.signals.value(`${signalPrefix}.error`);
786
+ return _error;
787
+ },
788
+ get stale() {
789
+ datastar.engine.signals.value(`${signalPrefix}.stale`);
790
+ return _stale;
791
+ },
792
+ run,
793
+ cancel,
794
+ dispose() {
795
+ cancel();
796
+ datastar.engine.signals.remove(signalPrefix);
797
+ },
798
+ };
799
+
800
+ scopedTasksList.push(taskObj);
801
+ return taskObj;
802
+ }
803
+
804
+ function registerEffect(fn) {
805
+ if (!isConnected()) return () => {};
806
+ const stop = datastar.engine.effect(() => {
807
+ if (!isConnected()) return;
808
+ fn();
809
+ });
810
+ cleanupCallbacks.push(stop);
811
+ return stop;
812
+ }
813
+
814
+ function registerWatch(source, callback, options = {}) {
815
+ if (!isConnected()) return () => {};
816
+ if (typeof callback !== 'function') {
817
+ throw new TypeError('watch(source, callback) requires callback to be a function.');
818
+ }
819
+
820
+ const immediate = Boolean(options.immediate);
821
+ const deep = Boolean(options.deep);
822
+ const once = Boolean(options.once);
823
+
824
+ let getter;
825
+ if (typeof source === 'function') {
826
+ getter = () => source(ctx);
827
+ } else if (typeof source === 'string') {
828
+ getter = () => resolveStatePath(state, source);
829
+ } else if (Array.isArray(source)) {
830
+ getter = () =>
831
+ source.map((s) =>
832
+ typeof s === 'function'
833
+ ? s(ctx)
834
+ : typeof s === 'string'
835
+ ? resolveStatePath(state, s)
836
+ : s
837
+ );
838
+ } else if (source && typeof source === 'object') {
839
+ getter = () => source;
840
+ } else {
841
+ getter = () => source;
842
+ }
843
+
844
+ let oldValue = undefined;
845
+ let isFirstRun = true;
846
+ let stopped = false;
847
+ let userCleanup = null;
848
+
849
+ function runCleanup() {
850
+ if (typeof userCleanup === 'function') {
851
+ try {
852
+ userCleanup();
853
+ } catch (e) {
854
+ handleError(e, 'watch:cleanup');
855
+ }
856
+ userCleanup = null;
857
+ }
858
+ }
859
+
860
+ function onCleanup(fn) {
861
+ if (typeof fn === 'function') {
862
+ userCleanup = fn;
863
+ }
864
+ }
865
+
866
+ function stopWatch() {
867
+ if (stopped) return;
868
+ stopped = true;
869
+ runCleanup();
870
+ stopEffect();
871
+ }
872
+
873
+ const stopEffect = datastar.engine.effect(() => {
874
+ if (stopped || !isConnected()) return;
875
+
876
+ let newValue;
877
+ try {
878
+ newValue = getter();
879
+ if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !Array.isArray(source))) && newValue && typeof newValue === 'object') {
880
+ deepTouch(newValue);
881
+ }
882
+ } catch (err) {
883
+ handleError(err, 'watch:getter');
884
+ return;
885
+ }
886
+
887
+ const isArr = Array.isArray(source);
888
+
889
+ if (isFirstRun) {
890
+ isFirstRun = false;
891
+ oldValue = deep ? deepCloneOrSnapshot(newValue) : isArr ? [...newValue] : newValue;
892
+ if (immediate) {
893
+ try {
894
+ runCleanup();
895
+ runUntracked(datastar.engine, () => {
896
+ callback(newValue, undefined, onCleanup);
897
+ });
898
+ } catch (err) {
899
+ handleError(err, 'watch:callback');
900
+ }
901
+ if (once) {
902
+ stopWatch();
903
+ }
904
+ }
905
+ return;
906
+ }
907
+
908
+ const hasChanged = deep
909
+ ? !deepEqual(newValue, oldValue)
910
+ : isArr
911
+ ? hasArrayChanged(newValue, oldValue)
912
+ : !Object.is(newValue, oldValue);
913
+
914
+ if (!hasChanged) return;
915
+
916
+ const prev = oldValue;
917
+ oldValue = deep ? deepCloneOrSnapshot(newValue) : isArr ? [...newValue] : newValue;
918
+
919
+ try {
920
+ runCleanup();
921
+ runUntracked(datastar.engine, () => {
922
+ callback(newValue, prev, onCleanup);
923
+ });
924
+ } catch (err) {
925
+ handleError(err, 'watch:callback');
926
+ }
927
+
928
+ if (once) {
929
+ stopWatch();
930
+ }
931
+ });
932
+
933
+ cleanupCallbacks.push(stopWatch);
934
+ return stopWatch;
935
+ }
936
+
937
+ function registerListener(targetOrEventName, eventNameOrHandler, handlerOrOptions, maybeOptions) {
938
+ if (!isConnected()) return () => {};
939
+ let targetEl = rootEl;
940
+ let eventName;
941
+ let handler;
942
+ let listenerOptions;
943
+
944
+ if (typeof targetOrEventName === 'string') {
945
+ eventName = targetOrEventName;
946
+ handler = eventNameOrHandler;
947
+ listenerOptions = handlerOrOptions;
948
+ } else {
949
+ targetEl = targetOrEventName;
950
+ eventName = eventNameOrHandler;
951
+ handler = handlerOrOptions;
952
+ listenerOptions = maybeOptions;
953
+ }
954
+
955
+ if (!targetEl || typeof targetEl.addEventListener !== 'function' || typeof handler !== 'function') {
956
+ return () => {};
957
+ }
958
+
959
+ const wrappedHandler = (evt) => {
960
+ if (!isConnected()) return;
961
+ handler(evt);
962
+ };
963
+
964
+ targetEl.addEventListener(eventName, wrappedHandler, listenerOptions);
965
+ const remove = () => {
966
+ try {
967
+ targetEl.removeEventListener(eventName, wrappedHandler, listenerOptions);
968
+ } catch (_e) {
969
+ // Ignore
970
+ }
971
+ };
972
+ cleanupCallbacks.push(remove);
973
+ return remove;
974
+ }
975
+
976
+ function registerInterval(fn, ms) {
977
+ if (!isConnected()) return () => {};
978
+ const timerId = setInterval(() => {
979
+ if (!isConnected()) {
980
+ clearInterval(timerId);
981
+ return;
982
+ }
983
+ fn();
984
+ }, ms);
985
+ const clear = () => clearInterval(timerId);
986
+ cleanupCallbacks.push(clear);
987
+ return clear;
988
+ }
989
+
990
+ function registerTimeout(fn, ms) {
991
+ if (!isConnected()) return () => {};
992
+ const timerId = setTimeout(() => {
993
+ if (!isConnected()) return;
994
+ fn();
995
+ }, ms);
996
+ const clear = () => clearTimeout(timerId);
997
+ cleanupCallbacks.push(clear);
998
+ return clear;
999
+ }
1000
+
1001
+ function emitEvent(eventName, detail = undefined, eventOptions = {}) {
1002
+ if (!rootEl || typeof rootEl.dispatchEvent !== 'function') return false;
1003
+ const CustomEventCtor =
1004
+ (rootEl.ownerDocument && rootEl.ownerDocument.defaultView && rootEl.ownerDocument.defaultView.CustomEvent) ||
1005
+ (typeof CustomEvent !== 'undefined' ? CustomEvent : null);
1006
+ if (!CustomEventCtor) return false;
1007
+ const evt = new CustomEventCtor(eventName, {
1008
+ bubbles: eventOptions.bubbles !== undefined ? eventOptions.bubbles : true,
1009
+ cancelable: eventOptions.cancelable !== undefined ? eventOptions.cancelable : true,
1010
+ composed: eventOptions.composed !== undefined ? eventOptions.composed : true,
1011
+ detail,
1012
+ });
1013
+ return rootEl.dispatchEvent(evt);
1014
+ }
1015
+
1016
+ // 4. Scoped target & targets accessors (never cache stale DOM nodes)
1017
+ function getTarget(targetName) {
1018
+ const list = queryScopedTargets(rootEl, controllerName, targetName);
1019
+ return list[0] || null;
1020
+ }
1021
+
1022
+ function getTargets(targetName) {
1023
+ return queryScopedTargets(rootEl, controllerName, targetName);
1024
+ }
1025
+
1026
+ const targetsAccessor = new Proxy(getTargets, {
1027
+ get(_target, prop) {
1028
+ if (typeof prop === 'string' && prop !== 'name' && prop !== 'length' && prop !== 'prototype') {
1029
+ return queryScopedTargets(rootEl, controllerName, prop);
1030
+ }
1031
+ return Reflect.get(_target, prop);
1032
+ },
1033
+ });
1034
+
1035
+ const targetAccessor = new Proxy(getTarget, {
1036
+ get(_target, prop) {
1037
+ if (typeof prop === 'string' && prop !== 'name' && prop !== 'length' && prop !== 'prototype') {
1038
+ return getTarget(prop);
1039
+ }
1040
+ return Reflect.get(_target, prop);
1041
+ },
1042
+ });
1043
+
1044
+ // 5. Scoped forms accessor (`forms.get('create-note')` or `forms('create-note')`)
1045
+ function resolveFormHandle(formName, formOptions = {}) {
1046
+ let formEl = null;
1047
+ if (rootEl.tagName === 'FORM') {
1048
+ if (!formName || rootEl.getAttribute('data-w-form') === formName || rootEl.getAttribute('name') === formName) {
1049
+ formEl = rootEl;
1050
+ }
1051
+ }
1052
+ if (!formEl && typeof rootEl.querySelectorAll === 'function') {
1053
+ const candidates = rootEl.querySelectorAll('form[data-w-form], form');
1054
+ for (let i = 0; i < candidates.length; i++) {
1055
+ const cand = candidates[i];
1056
+ if (!isTargetInControllerScope(rootEl, cand)) continue;
1057
+ if (
1058
+ !formName ||
1059
+ cand.getAttribute('data-w-form') === formName ||
1060
+ cand.getAttribute('name') === formName
1061
+ ) {
1062
+ formEl = cand;
1063
+ break;
1064
+ }
1065
+ }
1066
+ }
1067
+ if (!formEl) return null;
1068
+ const handle = createFormHandle(formEl, {
1069
+ api: scopedApi,
1070
+ datastar,
1071
+ debug,
1072
+ ...formOptions,
1073
+ });
1074
+ scopedForms.set(handle.name, handle);
1075
+ return handle;
1076
+ }
1077
+
1078
+ const formsAccessor = Object.assign(
1079
+ (name, opts) => resolveFormHandle(name, opts),
1080
+ {
1081
+ get: (name, opts) => resolveFormHandle(name, opts),
1082
+ }
1083
+ );
1084
+
1085
+ function getTask(name) {
1086
+ if (scopedTasks[name]) return scopedTasks[name];
1087
+ if (reg.definition.tasks && typeof reg.definition.tasks === 'object' && reg.definition.tasks[name]) {
1088
+ const tDef = reg.definition.tasks[name];
1089
+ let taskInst = null;
1090
+ if (typeof tDef === 'function') {
1091
+ const res = tDef(ctx);
1092
+ if (res && res.__isWebspressoTask) {
1093
+ taskInst = res;
1094
+ } else if (res && typeof res === 'object' && typeof res.run === 'function') {
1095
+ taskInst = createTaskInstance(res.run, { key: name, ...res });
1096
+ } else if (typeof res === 'function') {
1097
+ taskInst = createTaskInstance(res, { key: name });
1098
+ }
1099
+ } else if (tDef && typeof tDef === 'object' && typeof tDef.run === 'function') {
1100
+ taskInst = createTaskInstance(tDef.run, { key: name, ...tDef });
1101
+ }
1102
+ if (taskInst) {
1103
+ scopedTasks[name] = taskInst;
1104
+ return taskInst;
1105
+ }
1106
+ }
1107
+ return undefined;
1108
+ }
1109
+
1110
+ const tasksProxy = new Proxy(scopedTasks, {
1111
+ get(target, prop) {
1112
+ if (typeof prop === 'string') {
1113
+ if (target[prop]) return target[prop];
1114
+ return getTask(prop);
1115
+ }
1116
+ return Reflect.get(target, prop);
1117
+ },
1118
+ has(target, prop) {
1119
+ if (target[prop]) return true;
1120
+ if (reg.definition.tasks && typeof reg.definition.tasks === 'object' && prop in reg.definition.tasks) {
1121
+ return true;
1122
+ }
1123
+ return Reflect.has(target, prop);
1124
+ },
1125
+ ownKeys(target) {
1126
+ const defKeys =
1127
+ reg.definition.tasks && typeof reg.definition.tasks === 'object'
1128
+ ? Object.keys(reg.definition.tasks)
1129
+ : [];
1130
+ return Array.from(new Set([...Reflect.ownKeys(target), ...defKeys]));
1131
+ },
1132
+ getOwnPropertyDescriptor(target, prop) {
1133
+ if (target[prop]) return Reflect.getOwnPropertyDescriptor(target, prop);
1134
+ if (reg.definition.tasks && typeof reg.definition.tasks === 'object' && prop in reg.definition.tasks) {
1135
+ return { configurable: true, enumerable: true, writable: true, value: getTask(prop) };
1136
+ }
1137
+ return Reflect.getOwnPropertyDescriptor(target, prop);
1138
+ },
1139
+ });
1140
+
1141
+ let state = null;
1142
+ let ctx = null;
1143
+
1144
+ const controllerStorage = defaultStorage.scoped(controllerName || 'anon');
1145
+ const storageCtx = Object.freeze({
1146
+ local: controllerStorage.local,
1147
+ session: controllerStorage.session,
1148
+ memory: controllerStorage.memory,
1149
+ scoped: (sub) => controllerStorage.scoped(sub),
1150
+ global: defaultStorage,
1151
+ root: defaultStorage,
1152
+ });
1153
+ const persistConfig = normalizePersistConfig(reg.definition.persist, controllerName);
1154
+ function resolvePersistDriver(pConfig) {
1155
+ if (!pConfig) return storageCtx.local;
1156
+ if (pConfig.isGlobal) {
1157
+ return defaultStorage[pConfig.storageType] || defaultStorage.local;
1158
+ }
1159
+ return storageCtx[pConfig.storageType] || storageCtx.local;
1160
+ }
1161
+
1162
+ // 6. Context object passed to `connect(ctx)` and `actions[name](ctx, event, actionEl)`
1163
+ ctx = Object.freeze({
1164
+ instanceId,
1165
+ name: controllerName,
1166
+ el: rootEl,
1167
+ get state() {
1168
+ return state;
1169
+ },
1170
+ api: scopedApi,
1171
+ forms: formsAccessor,
1172
+ target: targetAccessor,
1173
+ targets: targetsAccessor,
1174
+ tasks: tasksProxy,
1175
+ task: createTaskInstance,
1176
+ storage: storageCtx,
1177
+ clearPersistedState: () => {
1178
+ if (persistConfig) {
1179
+ const driver = resolvePersistDriver(persistConfig);
1180
+ driver.remove(persistConfig.storageKey);
1181
+ }
1182
+ },
1183
+ param: (name, maybeEl) => {
1184
+ if (!name) return null;
1185
+ const el = decorateElement(maybeEl || currentActionEl);
1186
+ if (!el) return null;
1187
+ if (typeof el.data === 'function') {
1188
+ return el.data(name);
1189
+ }
1190
+ const rawAttr = el.getAttribute ? el.getAttribute(`data-${name}`) : null;
1191
+ if (rawAttr !== null) return rawAttr;
1192
+ if (el.dataset && el.dataset[name] !== undefined) return el.dataset[name];
1193
+ return el.getAttribute ? el.getAttribute(name) : null;
1194
+ },
1195
+ on: registerListener,
1196
+ emit: emitEvent,
1197
+ effect: registerEffect,
1198
+ watch: registerWatch,
1199
+ interval: registerInterval,
1200
+ timeout: registerTimeout,
1201
+ signal: lifecycleAbort.signal,
1202
+ isConnected,
1203
+ isDisposed: () => !isConnected(),
1204
+ get lastError() {
1205
+ return lastError;
1206
+ },
1207
+ clearError() {
1208
+ lastError = null;
1209
+ },
1210
+ onError(listener) {
1211
+ if (typeof listener !== 'function') return () => {};
1212
+ dynamicErrorListeners.add(listener);
1213
+ const unregister = () => {
1214
+ dynamicErrorListeners.delete(listener);
1215
+ };
1216
+ cleanupCallbacks.push(unregister);
1217
+ return unregister;
1218
+ },
1219
+ invalidate: (tags) => (fragments ? fragments.invalidate(tags) : Promise.resolve({ ok: true, refreshed: [] })),
1220
+ refresh: async (idOrName, input, opts) => {
1221
+ if (!fragments) {
1222
+ const empty = { ok: true, refreshed: [], skipped: true };
1223
+ if (opts && typeof opts.onSuccess === 'function') await opts.onSuccess(empty);
1224
+ if (opts && typeof opts.onSettled === 'function') await opts.onSettled(empty, null);
1225
+ else if (opts && typeof opts.onComplete === 'function') await opts.onComplete(empty, null);
1226
+ return empty;
1227
+ }
1228
+ return fragments.refresh(idOrName, input, opts);
1229
+ },
1230
+ fragment: (idOrName) => ({
1231
+ get input() {
1232
+ const inst = fragments && fragments.getInstance ? fragments.getInstance(idOrName) : null;
1233
+ return inst && inst.input ? { ...inst.input } : {};
1234
+ },
1235
+ refresh: (input, opts) => {
1236
+ if (!fragments) {
1237
+ const empty = { ok: true, refreshed: [], skipped: true };
1238
+ if (opts && typeof opts.onSuccess === 'function') {
1239
+ return Promise.resolve(opts.onSuccess(empty)).then(() => empty);
1240
+ }
1241
+ return Promise.resolve(empty);
1242
+ }
1243
+ return fragments.refresh(idOrName, input, opts);
1244
+ },
1245
+ }),
1246
+ });
1247
+
1248
+ // Instantiate declarative tasks from controller definition before state/computed setup
1249
+ if (reg.definition.tasks && typeof reg.definition.tasks === 'object') {
1250
+ for (const tName of Object.keys(reg.definition.tasks)) {
1251
+ getTask(tName);
1252
+ }
1253
+ }
1254
+
1255
+ // 3. Isolated reactive state backed by Datastar signals + computed getters
1256
+ const computedDefs = reg.definition.computed || {};
1257
+ const initialRawState = reg.definition.state({ el: rootEl }) || {};
1258
+ const stateManager = datastar.createIsolatedControllerState(
1259
+ instanceId,
1260
+ initialRawState,
1261
+ isConnected,
1262
+ debug,
1263
+ computedDefs,
1264
+ () => ctx
1265
+ );
1266
+ state = stateManager.state;
1267
+
1268
+ // Hydrate persisted state if defined
1269
+ if (persistConfig) {
1270
+ const driver = resolvePersistDriver(persistConfig);
1271
+ const savedData = driver.get(persistConfig.storageKey, null);
1272
+ if (savedData && typeof savedData === 'object' && !Array.isArray(savedData)) {
1273
+ for (const [k, v] of Object.entries(savedData)) {
1274
+ if (persistConfig.keys && !persistConfig.keys.includes(k)) continue;
1275
+ if (Object.prototype.hasOwnProperty.call(initialRawState, k)) {
1276
+ state[k] = v;
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ // Register declarative watchers from definition
1283
+ if (reg.definition.watch && typeof reg.definition.watch === 'object') {
1284
+ for (const [watchKey, watchDef] of Object.entries(reg.definition.watch)) {
1285
+ if (typeof watchDef === 'function') {
1286
+ registerWatch(watchKey, (newVal, oldVal, onCleanup) => {
1287
+ watchDef.call(ctx, newVal, oldVal, onCleanup, ctx);
1288
+ });
1289
+ } else if (watchDef && typeof watchDef === 'object' && typeof watchDef.handler === 'function') {
1290
+ registerWatch(
1291
+ watchKey,
1292
+ (newVal, oldVal, onCleanup) => {
1293
+ watchDef.handler.call(ctx, newVal, oldVal, onCleanup, ctx);
1294
+ },
1295
+ {
1296
+ immediate: watchDef.immediate,
1297
+ deep: watchDef.deep,
1298
+ once: watchDef.once,
1299
+ }
1300
+ );
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ function invokeAction(actionName, event, actionEl) {
1306
+ if (!isConnected()) return undefined;
1307
+ const fn = reg.definition.actions[actionName];
1308
+ if (typeof fn !== 'function') {
1309
+ return undefined;
1310
+ }
1311
+ const decoratedEl = decorateElement(actionEl);
1312
+ currentActionEl = decoratedEl || null;
1313
+ const prevContext = activeControllerContext;
1314
+ activeControllerContext = ctx;
1315
+ try {
1316
+ const res = fn(ctx, event, decoratedEl);
1317
+ if (res && typeof res.then === 'function') {
1318
+ return res.catch((err) => {
1319
+ // Ignore canceled/superseded request errors on disconnected or superseded instances
1320
+ if (err && err.isCanceled) return undefined;
1321
+ handleControllerError(err, {
1322
+ action: actionName,
1323
+ event,
1324
+ actionEl: decoratedEl,
1325
+ phase: 'action',
1326
+ });
1327
+ throw err;
1328
+ });
1329
+ }
1330
+ return res;
1331
+ } catch (err) {
1332
+ if (err && err.isCanceled) return undefined;
1333
+ handleControllerError(err, {
1334
+ action: actionName,
1335
+ event,
1336
+ actionEl: decoratedEl,
1337
+ phase: 'action',
1338
+ });
1339
+ throw err;
1340
+ } finally {
1341
+ currentActionEl = null;
1342
+ activeControllerContext = prevContext;
1343
+ }
1344
+ }
1345
+
1346
+ // 7. Bind declarative DOM attributes within controller scope
1347
+ const domDisposers = [];
1348
+ function rebindDom() {
1349
+ if (!isConnected()) return;
1350
+ const unbind = datastar.bindControllerDom({
1351
+ instanceId,
1352
+ rootEl,
1353
+ state,
1354
+ invokeAction,
1355
+ isConnected,
1356
+ });
1357
+ domDisposers.push(unbind);
1358
+
1359
+ // Also auto-initialize any `<form data-w-form>` inside this controller's scope
1360
+ if (rootEl.tagName === 'FORM' && rootEl.hasAttribute('data-w-form')) {
1361
+ resolveFormHandle(rootEl.getAttribute('data-w-form'));
1362
+ }
1363
+ if (typeof rootEl.querySelectorAll === 'function') {
1364
+ const formsInScope = rootEl.querySelectorAll('form[data-w-form]');
1365
+ for (let i = 0; i < formsInScope.length; i++) {
1366
+ if (isTargetInControllerScope(rootEl, formsInScope[i])) {
1367
+ createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug });
1368
+ }
1369
+ }
1370
+ }
1371
+ }
1372
+
1373
+ rebindDom();
1374
+
1375
+ // Auto-sync persisted state changes back to storage
1376
+ if (persistConfig) {
1377
+ const driver = resolvePersistDriver(persistConfig);
1378
+ let persistTimer = null;
1379
+ let lastSavedJson = '';
1380
+
1381
+ const stopPersistEffect = registerEffect(() => {
1382
+ if (!isConnected()) return;
1383
+ const keysToTrack = persistConfig.keys || Object.keys(initialRawState);
1384
+ const payload = {};
1385
+ for (const k of keysToTrack) {
1386
+ payload[k] = state[k];
1387
+ }
1388
+
1389
+ const serialized = JSON.stringify(payload);
1390
+ if (serialized === lastSavedJson) return;
1391
+
1392
+ const writeToStorage = () => {
1393
+ if (!isConnected()) return;
1394
+ driver.set(persistConfig.storageKey, payload);
1395
+ lastSavedJson = serialized;
1396
+ };
1397
+
1398
+ if (persistConfig.debounce > 0) {
1399
+ if (persistTimer) clearTimeout(persistTimer);
1400
+ persistTimer = setTimeout(writeToStorage, persistConfig.debounce);
1401
+ } else {
1402
+ writeToStorage();
1403
+ }
1404
+ });
1405
+
1406
+ cleanupCallbacks.push(() => {
1407
+ if (persistTimer) clearTimeout(persistTimer);
1408
+ if (typeof stopPersistEffect === 'function') stopPersistEffect();
1409
+ });
1410
+ }
1411
+
1412
+ // 8. Run `connect(ctx)` lifecycle hook
1413
+ if (reg.definition.connect) {
1414
+ const prevContext = activeControllerContext;
1415
+ activeControllerContext = ctx;
1416
+ try {
1417
+ const userCleanup = reg.definition.connect(ctx);
1418
+ if (typeof userCleanup === 'function') {
1419
+ cleanupCallbacks.push(userCleanup);
1420
+ }
1421
+ } catch (err) {
1422
+ handleControllerError(err, { phase: 'connect' });
1423
+ if (debug) {
1424
+ debug.trackCleanupError(instanceId, controllerName, 'connect', err);
1425
+ }
1426
+ } finally {
1427
+ activeControllerContext = prevContext;
1428
+ }
1429
+ }
1430
+
1431
+ const instanceRecord = {
1432
+ instanceId,
1433
+ name: controllerName,
1434
+ el: rootEl,
1435
+ state,
1436
+ ctx,
1437
+ invokeAction,
1438
+ actions: new Proxy({}, {
1439
+ get(_target, prop) {
1440
+ if (typeof prop === 'string') {
1441
+ return (evt, el) => invokeAction(prop, evt, el);
1442
+ }
1443
+ return undefined;
1444
+ },
1445
+ }),
1446
+ rebindDom,
1447
+ get isConnected() {
1448
+ return isConnected();
1449
+ },
1450
+ destroy() {
1451
+ if (!connected) return;
1452
+ connected = false;
1453
+ lifecycleAbort.abort();
1454
+
1455
+ // Run user disconnect hook if provided
1456
+ if (reg.definition.disconnect) {
1457
+ try {
1458
+ reg.definition.disconnect(ctx);
1459
+ } catch (err) {
1460
+ handleControllerError(err, { phase: 'disconnect' });
1461
+ if (debug) {
1462
+ debug.trackCleanupError(instanceId, controllerName, 'disconnect', err);
1463
+ }
1464
+ }
1465
+ }
1466
+
1467
+ // Run all registered cleanup callbacks (listeners, effects, intervals, timeouts, connect return)
1468
+ for (let i = cleanupCallbacks.length - 1; i >= 0; i--) {
1469
+ try {
1470
+ cleanupCallbacks[i]();
1471
+ } catch (err) {
1472
+ if (debug) {
1473
+ debug.trackCleanupError(instanceId, controllerName, 'cleanup', err);
1474
+ }
1475
+ }
1476
+ }
1477
+ cleanupCallbacks.length = 0;
1478
+
1479
+ // Unbind declarative DOM bindings
1480
+ for (let i = 0; i < domDisposers.length; i++) {
1481
+ try {
1482
+ domDisposers[i]();
1483
+ } catch (_e) {
1484
+ // Ignore
1485
+ }
1486
+ }
1487
+ domDisposers.length = 0;
1488
+
1489
+ // Destroy scoped forms if root was removed
1490
+ for (const fh of scopedForms.values()) {
1491
+ try {
1492
+ fh.destroy();
1493
+ } catch (_e) {
1494
+ // Ignore
1495
+ }
1496
+ }
1497
+ scopedForms.clear();
1498
+
1499
+ // Dispose scoped tasks
1500
+ for (let i = 0; i < scopedTasksList.length; i++) {
1501
+ try {
1502
+ scopedTasksList[i].dispose();
1503
+ } catch (_e) {
1504
+ // Ignore
1505
+ }
1506
+ }
1507
+ scopedTasksList.length = 0;
1508
+
1509
+ // Dispose isolated Datastar signals for this instance
1510
+ stateManager.dispose();
1511
+ dynamicErrorListeners.clear();
1512
+ lastError = null;
1513
+
1514
+ const mapOnEl = elementInstances.get(rootEl);
1515
+ if (mapOnEl) {
1516
+ mapOnEl.delete(controllerName);
1517
+ }
1518
+ activeInstances.delete(instanceId);
1519
+
1520
+ if (debug) {
1521
+ debug.trackControllerUnmount(instanceId, controllerName);
1522
+ }
1523
+ },
1524
+ };
1525
+
1526
+ elMap.set(controllerName, instanceRecord);
1527
+ activeInstances.set(instanceId, instanceRecord);
1528
+
1529
+ if (debug) {
1530
+ debug.trackControllerMount(instanceId, controllerName, rootEl);
1531
+ }
1532
+
1533
+ return instanceRecord;
1534
+ }
1535
+
1536
+ /**
1537
+ * Unmount and clean up any controller instances rooted on `el` or inside `el`'s subtree.
1538
+ */
1539
+ function unmountSubtree(el) {
1540
+ if (!el || el.nodeType !== 1) return;
1541
+
1542
+ const elements = [el];
1543
+ if (typeof el.querySelectorAll === 'function') {
1544
+ const descendants = el.querySelectorAll('[data-w-controller], form[data-w-form]');
1545
+ for (let i = 0; i < descendants.length; i++) {
1546
+ elements.push(descendants[i]);
1547
+ }
1548
+ }
1549
+
1550
+ // Destroy from deepest child to parent
1551
+ for (let i = elements.length - 1; i >= 0; i--) {
1552
+ const node = elements[i];
1553
+ const elMap = elementInstances.get(node);
1554
+ if (elMap) {
1555
+ for (const inst of Array.from(elMap.values())) {
1556
+ inst.destroy();
1557
+ }
1558
+ }
1559
+ const standaloneForm = standaloneForms.get(node);
1560
+ if (standaloneForm) {
1561
+ standaloneForm.destroy();
1562
+ standaloneForms.delete(node);
1563
+ }
1564
+ }
1565
+ }
1566
+
1567
+ /**
1568
+ * Scan a DOM root or subtree, mounting controllers and standalone `[data-w-form]` elements.
1569
+ * Also supports lazy-loading controllers via `lazyLoader(controllerName)` when a controller
1570
+ * is not yet registered or has `data-w-lazy`.
1571
+ */
1572
+ async function mountSubtree(containerEl) {
1573
+ const root =
1574
+ containerEl || (typeof document !== 'undefined' ? document : null);
1575
+ if (!root) return [];
1576
+
1577
+ // 1. Prune any previously active instances whose DOM elements were removed externally
1578
+ for (const inst of Array.from(activeInstances.values())) {
1579
+ if (inst.el && inst.el.isConnected === false) {
1580
+ inst.destroy();
1581
+ }
1582
+ }
1583
+
1584
+ const controllerElements = [];
1585
+ if (root.nodeType === 1 && root.hasAttribute && root.hasAttribute('data-w-controller')) {
1586
+ controllerElements.push(root);
1587
+ }
1588
+ if (typeof root.querySelectorAll === 'function') {
1589
+ const found = root.querySelectorAll('[data-w-controller]');
1590
+ for (let i = 0; i < found.length; i++) {
1591
+ controllerElements.push(found[i]);
1592
+ }
1593
+ }
1594
+
1595
+ const mounted = [];
1596
+
1597
+ for (const el of controllerElements) {
1598
+ const rawAttr = (el.getAttribute('data-w-controller') || '').trim();
1599
+ if (!rawAttr) continue;
1600
+ const names = rawAttr.split(/\s+/).filter(Boolean);
1601
+
1602
+ for (const name of names) {
1603
+ if (!definitions.has(name) && lazyLoader) {
1604
+ try {
1605
+ const loadedMod = await lazyLoader(name, el);
1606
+ if (loadedMod && !definitions.has(name)) {
1607
+ register(name, loadedMod.default || loadedMod);
1608
+ }
1609
+ } catch (err) {
1610
+ if (debug) {
1611
+ debug.trackCleanupError(`lazy_${name}`, name, 'lazy-load', err);
1612
+ }
1613
+ }
1614
+ }
1615
+ const inst = mountControllerOnElement(el, name);
1616
+ if (inst) mounted.push(inst);
1617
+ }
1618
+ }
1619
+
1620
+ // 2. Auto-mount standalone `<form data-w-form>` elements that do not need a client controller file
1621
+ const formElements = [];
1622
+ if (root.nodeType === 1 && root.tagName === 'FORM' && root.hasAttribute('data-w-form')) {
1623
+ formElements.push(root);
1624
+ }
1625
+ if (typeof root.querySelectorAll === 'function') {
1626
+ const foundForms = root.querySelectorAll('form[data-w-form]');
1627
+ for (let i = 0; i < foundForms.length; i++) {
1628
+ formElements.push(foundForms[i]);
1629
+ }
1630
+ }
1631
+
1632
+ for (const formEl of formElements) {
1633
+ if (!formEl.__wFormHandle) {
1634
+ const defaultApi = createApiClient({
1635
+ ...baseApiConfig,
1636
+ debug,
1637
+ onInvalidate: fragments
1638
+ ? (tags, meta) => fragments.invalidate(tags, meta)
1639
+ : baseApiConfig.onInvalidate,
1640
+ });
1641
+ const handle = createFormHandle(formEl, {
1642
+ api: defaultApi,
1643
+ datastar,
1644
+ debug,
1645
+ });
1646
+ standaloneForms.set(formEl, handle);
1647
+ }
1648
+ }
1649
+
1650
+ // 3. Rebind any preserved ancestor controller if `containerEl` was patched inside it
1651
+ if (containerEl && containerEl.nodeType === 1) {
1652
+ let ancestor = containerEl;
1653
+ while (ancestor) {
1654
+ const elMap = elementInstances.get(ancestor);
1655
+ if (elMap) {
1656
+ for (const inst of elMap.values()) {
1657
+ inst.rebindDom();
1658
+ }
1659
+ }
1660
+ ancestor = ancestor.parentElement;
1661
+ }
1662
+ }
1663
+
1664
+ return mounted;
1665
+ }
1666
+
1667
+ return {
1668
+ register,
1669
+ hasController(name) {
1670
+ return definitions.has(name);
1671
+ },
1672
+ getDefinition(name) {
1673
+ const entry = definitions.get(name);
1674
+ return entry ? entry.definition : null;
1675
+ },
1676
+ mountSubtree,
1677
+ syncTree: mountSubtree,
1678
+ unmountSubtree,
1679
+ mountControllerOnElement,
1680
+ getInstanceForElement(el, name) {
1681
+ const map = elementInstances.get(el);
1682
+ if (!map) return null;
1683
+ if (name) return map.get(name) || null;
1684
+ return map.values().next().value || null;
1685
+ },
1686
+ getActiveInstances() {
1687
+ return Array.from(activeInstances.values());
1688
+ },
1689
+ destroyAll() {
1690
+ for (const inst of Array.from(activeInstances.values())) {
1691
+ inst.destroy();
1692
+ }
1693
+ },
1694
+ };
1695
+ }
1696
+
1697
+ module.exports = {
1698
+ defineController,
1699
+ deriveControllerNameFromPath,
1700
+ createControllerRuntime,
1701
+ queryScopedTargets,
1702
+ isTargetInControllerScope,
1703
+ watch,
1704
+ getActiveControllerContext,
1705
+ };
1706
+