@supersoniks/concorde 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +163 -0
  2. package/build-infos.json +1 -1
  3. package/concorde-core.bundle.js +585 -670
  4. package/concorde-core.es.js +7165 -9505
  5. package/dist/concorde-core.bundle.js +585 -670
  6. package/dist/concorde-core.es.js +7165 -9505
  7. package/docs/assets/index-DP1oMukw.js +4949 -0
  8. package/docs/assets/index-DZtxIZCW.css +1 -0
  9. package/docs/index.html +2 -2
  10. package/{src/docs/_misc → docs/src/docs/_decorators}/ancestor-attribute.md +15 -31
  11. package/docs/src/docs/_decorators/bind.md +164 -0
  12. package/docs/src/docs/_decorators/get.md +65 -0
  13. package/docs/src/docs/_decorators/publish.md +54 -0
  14. package/docs/src/docs/_decorators/subscribe.md +36 -0
  15. package/docs/src/docs/_misc/dataProviderKey.md +135 -0
  16. package/docs/src/docs/_misc/endpoint.md +42 -0
  17. package/docs/src/docs/search/docs-search.json +850 -710
  18. package/docs/src/tsconfig.json +43 -4
  19. package/package.json +25 -4
  20. package/php/get-challenge.php +34 -0
  21. package/php/some-service.php +42 -0
  22. package/scripts/pre-build.mjs +4 -0
  23. package/src/core/_types/endpoint.ts +4 -0
  24. package/src/core/_types/key.ts +1 -0
  25. package/src/core/components/functional/example/example.ts +38 -6
  26. package/src/core/decorators/Subscriber.ts +2 -0
  27. package/src/core/decorators/api.spec.ts +150 -0
  28. package/src/core/decorators/api.ts +244 -0
  29. package/src/core/decorators/subscriber/bind.ts +57 -145
  30. package/src/core/decorators/subscriber/dynamicPath.ts +77 -0
  31. package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +105 -0
  32. package/src/core/decorators/subscriber/onAssign.ts +11 -147
  33. package/src/core/decorators/subscriber/publish.spec.ts +21 -0
  34. package/src/core/decorators/subscriber/publish.ts +148 -0
  35. package/src/core/decorators/subscriber/publisherPath.ts +13 -0
  36. package/src/core/decorators/subscriber/subscribe.spec.ts +21 -0
  37. package/src/core/decorators/subscriber/subscribe.ts +32 -0
  38. package/src/core/decorators/subscriber/subscribe.type-test.ts +32 -0
  39. package/src/core/utils/api.ts +83 -15
  40. package/src/core/utils/dataProviderKey.spec.ts +34 -0
  41. package/src/core/utils/dataProviderKey.ts +86 -0
  42. package/src/core/utils/endpoint.spec.ts +41 -0
  43. package/src/core/utils/endpoint.ts +87 -0
  44. package/src/decorators.ts +14 -0
  45. package/{docs/src/docs/_misc → src/docs/_decorators}/ancestor-attribute.md +15 -31
  46. package/src/docs/_decorators/bind.md +164 -0
  47. package/src/docs/_decorators/get.md +65 -0
  48. package/src/docs/_decorators/publish.md +54 -0
  49. package/src/docs/_decorators/subscribe.md +36 -0
  50. package/src/docs/_misc/dataProviderKey.md +135 -0
  51. package/src/docs/_misc/endpoint.md +42 -0
  52. package/src/docs/example/decorators-demo-bind-demos.ts +210 -0
  53. package/src/docs/example/decorators-demo-geo.ts +45 -0
  54. package/src/docs/example/decorators-demo-init.ts +228 -0
  55. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +324 -0
  56. package/src/docs/example/decorators-demo.ts +12 -459
  57. package/src/docs/navigation/navigation.ts +27 -10
  58. package/src/docs/search/docs-search.json +1059 -609
  59. package/src/tsconfig-model.json +1 -1
  60. package/src/tsconfig.json +65 -1
  61. package/src/tsconfig.tsbuildinfo +1 -1
  62. package/src/utils.ts +8 -1
  63. package/vite/config.js +25 -6
  64. package/vite.config.mts +13 -0
  65. package/docs/assets/index-B0IJ9I_B.js +0 -4918
  66. package/docs/assets/index-B3QHEJTV.css +0 -1
  67. package/docs/src/docs/_misc/bind.md +0 -436
  68. package/docs/src/docs/_misc/key.md +0 -135
  69. package/src/docs/_misc/bind.md +0 -362
  70. /package/docs/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
  71. /package/docs/src/docs/{_misc → _decorators}/on-assign.md +0 -0
  72. /package/docs/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
  73. /package/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
  74. /package/src/docs/{_misc → _decorators}/on-assign.md +0 -0
  75. /package/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
@@ -0,0 +1,244 @@
1
+ import type {
2
+ DataProviderKey,
3
+ DataProviderKeyHost,
4
+ } from "../utils/dataProviderKey";
5
+ import { Endpoint } from "../utils/endpoint";
6
+ import HTML, {
7
+ SearchableDomElement,
8
+ } from "@supersoniks/concorde/core/utils/HTML";
9
+ import API, {
10
+ APIConfiguration,
11
+ type ApiGetResult,
12
+ } from "@supersoniks/concorde/core/utils/api";
13
+ import DataProvider from "../utils/PublisherProxy";
14
+ import { ConnectedComponent, setSubscribable } from "./subscriber/common";
15
+ import {
16
+ extractDynamicDependencies,
17
+ resolveDynamicPath,
18
+ } from "./subscriber/dynamicPath";
19
+ import {
20
+ getDynamicWatchKeys,
21
+ registerDynamicPropertyWatcher,
22
+ } from "./subscriber/dynamicPropertyWatch";
23
+ import { getPublisherFromPath } from "./subscriber/publisherPath";
24
+
25
+ function asSearchableHost(component: unknown): SearchableDomElement | null {
26
+ if (component instanceof HTMLElement || component instanceof ShadowRoot) {
27
+ return component;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function readApiConfigurationFromPublisher(
33
+ publisher: DataProvider | null,
34
+ ): APIConfiguration | null {
35
+ if (!publisher || typeof publisher.get !== "function") return null;
36
+ const raw = publisher.get();
37
+ if (!raw || typeof raw !== "object") return null;
38
+ if (!("serviceURL" in (raw as object))) return null;
39
+ return raw as APIConfiguration;
40
+ }
41
+
42
+ function resolveScopedConfiguration(
43
+ component: unknown,
44
+ ): APIConfiguration | null {
45
+ const host = asSearchableHost(component);
46
+ if (!host) return null;
47
+ return HTML.getApiConfiguration(host);
48
+ }
49
+
50
+ type ApiGetState = {
51
+ cleanupWatchers: Array<() => void>;
52
+ requestGeneration: number;
53
+ configPublisher: DataProvider | null;
54
+ configMutationHandler: (() => void) | null;
55
+ };
56
+
57
+ function detachConfigPublisher(state: ApiGetState): void {
58
+ if (state.configPublisher && state.configMutationHandler) {
59
+ state.configPublisher.offInternalMutation(state.configMutationHandler);
60
+ }
61
+ state.configPublisher = null;
62
+ state.configMutationHandler = null;
63
+ }
64
+
65
+ /**
66
+ * Décorateur **`@get`** : charge des données via `API.getDetailed` et assigne un
67
+ * `ApiGetResult<T>` (`request`, `response`, `result` typé `T`) ou `null`.
68
+ * Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
69
+ *
70
+ * **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
71
+ * (`HTMLElement` / `ShadowRoot`). Sans hôte DOM valide, la propriété reste inchangée jusqu’à connexion.
72
+ *
73
+ * **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
74
+ * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
75
+ * Toute mutation interne du publisher (`onInternalMutation`) relance le GET.
76
+ *
77
+ * @example
78
+ * @get(new Endpoint<User, { userId: string }>("users/${userId}"))
79
+ * payload?: ApiGetResult<User>;
80
+ *
81
+ * @example
82
+ * const apiConf = new DataProviderKey<APIConfiguration>("myApiConf");
83
+ * PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... });
84
+ * @get(new Endpoint<Thing, { id: string }>("things/${id}"), apiConf)
85
+ * payload?: ApiGetResult<Thing>;
86
+ */
87
+ export function get<T, Ue = any>(
88
+ endpoint: Endpoint<T, Ue>,
89
+ ): <K extends string>(
90
+ target: DataProviderKeyHost<Ue> & {
91
+ [P in K]?: ApiGetResult<T> | null | undefined;
92
+ },
93
+ propertyKey: K,
94
+ ) => void;
95
+ export function get<T, Ue = any, Uk = any>(
96
+ endpoint: Endpoint<T, Ue>,
97
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
98
+ ): <K extends string>(
99
+ target: DataProviderKeyHost<Ue> &
100
+ DataProviderKeyHost<Uk> & {
101
+ [P in K]?: ApiGetResult<T> | null | undefined;
102
+ },
103
+ propertyKey: K,
104
+ ) => void;
105
+ export function get<T, Ue = any, Uk = any>(
106
+ endpoint: Endpoint<T, Ue>,
107
+ configurationKey?: DataProviderKey<APIConfiguration, Uk>,
108
+ ): <K extends string>(
109
+ target: object & { [P in K]?: ApiGetResult<T> | null | undefined },
110
+ propertyKey: K,
111
+ ) => void {
112
+ const pathTemplate = endpoint.path;
113
+ const configurationKeyPath = configurationKey?.path;
114
+ const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
115
+ const configKeyDynamicDependencies = configurationKeyPath
116
+ ? extractDynamicDependencies(configurationKeyPath)
117
+ : [];
118
+ const mergedDynamicDependencies = [
119
+ ...new Set([
120
+ ...endpointDynamicDependencies,
121
+ ...configKeyDynamicDependencies,
122
+ ]),
123
+ ];
124
+ const isDynamicPath = endpointDynamicDependencies.length > 0;
125
+ const usesPublisherConfig = Boolean(configurationKeyPath);
126
+
127
+ return function (target: object, propertyKey: string) {
128
+ if (!target) return;
129
+ setSubscribable(target);
130
+ const stateKey = `__get_state_${propertyKey}`;
131
+
132
+ (target as ConnectedComponent).__onConnected__((component) => {
133
+ const comp = component as Record<string, unknown>;
134
+ let state = comp[stateKey] as ApiGetState | undefined;
135
+ if (!state) {
136
+ state = {
137
+ cleanupWatchers: [],
138
+ requestGeneration: 0,
139
+ configPublisher: null,
140
+ configMutationHandler: null,
141
+ };
142
+ comp[stateKey] = state;
143
+ }
144
+
145
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
146
+ state.cleanupWatchers = [];
147
+ state.requestGeneration++;
148
+
149
+ const runFetch = () => {
150
+ const resolution = isDynamicPath
151
+ ? resolveDynamicPath(component, pathTemplate)
152
+ : { ready: true, path: pathTemplate };
153
+ if (!resolution.ready || !resolution.path) {
154
+ comp[propertyKey] = undefined;
155
+ return;
156
+ }
157
+ let config: APIConfiguration | null = null;
158
+ if (usesPublisherConfig && configurationKeyPath) {
159
+ const configRes = resolveDynamicPath(component, configurationKeyPath);
160
+ if (!configRes.ready || !configRes.path) {
161
+ comp[propertyKey] = undefined;
162
+ return;
163
+ }
164
+ const configPublisher = getPublisherFromPath(configRes.path);
165
+ config = readApiConfigurationFromPublisher(configPublisher);
166
+ } else {
167
+ config = resolveScopedConfiguration(component);
168
+ }
169
+ if (!config) {
170
+ comp[propertyKey] = undefined;
171
+ return;
172
+ }
173
+ const generation = ++state.requestGeneration;
174
+ const api = new API(config);
175
+ void api
176
+ .getDetailed<T>(resolution.path)
177
+ .then((payload?: ApiGetResult<T>) => {
178
+ if (generation !== state.requestGeneration) return;
179
+ comp[propertyKey] = payload;
180
+ });
181
+ };
182
+
183
+ const rebindPublisherConfig = () => {
184
+ if (!usesPublisherConfig || !configurationKeyPath) return;
185
+ detachConfigPublisher(state);
186
+ const configRes = resolveDynamicPath(component, configurationKeyPath);
187
+ if (!configRes.ready || !configRes.path) {
188
+ comp[propertyKey] = undefined;
189
+ return;
190
+ }
191
+ const publisher = getPublisherFromPath(configRes.path);
192
+ if (!publisher) {
193
+ comp[propertyKey] = undefined;
194
+ return;
195
+ }
196
+ const mutationHandler = () => {
197
+ runFetch();
198
+ };
199
+ publisher.onInternalMutation(mutationHandler);
200
+ state.configPublisher = publisher;
201
+ state.configMutationHandler = mutationHandler;
202
+ };
203
+
204
+ if (usesPublisherConfig) {
205
+ for (const dependency of mergedDynamicDependencies) {
206
+ const unsubscribe = registerDynamicPropertyWatcher(
207
+ getDynamicWatchKeys.watcherStore,
208
+ getDynamicWatchKeys.hooked,
209
+ component,
210
+ dependency,
211
+ () => rebindPublisherConfig(),
212
+ );
213
+ state.cleanupWatchers.push(unsubscribe);
214
+ }
215
+ rebindPublisherConfig();
216
+ } else {
217
+ if (isDynamicPath) {
218
+ for (const dependency of endpointDynamicDependencies) {
219
+ const unsubscribe = registerDynamicPropertyWatcher(
220
+ getDynamicWatchKeys.watcherStore,
221
+ getDynamicWatchKeys.hooked,
222
+ component,
223
+ dependency,
224
+ () => runFetch(),
225
+ );
226
+ state.cleanupWatchers.push(unsubscribe);
227
+ }
228
+ }
229
+ runFetch();
230
+ }
231
+ });
232
+
233
+ (target as ConnectedComponent).__onDisconnected__((component) => {
234
+ const comp = component as Record<string, unknown>;
235
+ const state = comp[stateKey] as ApiGetState | undefined;
236
+ if (!state) return;
237
+ detachConfigPublisher(state);
238
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
239
+ state.cleanupWatchers = [];
240
+ state.requestGeneration++;
241
+ comp[propertyKey] = undefined;
242
+ });
243
+ };
244
+ }
@@ -1,147 +1,23 @@
1
- import { Objects } from "@supersoniks/concorde/utils";
2
-
3
- import DataProvider, { PublisherManager } from "../../utils/PublisherProxy";
1
+ import type {
2
+ DataProviderKey,
3
+ DataProviderKeyHost,
4
+ } from "../../utils/dataProviderKey";
4
5
  import { ConnectedComponent, setSubscribable } from "./common";
5
-
6
- const dynamicWatcherStore = Symbol("__bindDynamicWatcherStore__");
7
- const dynamicWillUpdateHookedStore = Symbol("__bindDynamicWillUpdateHooked__");
8
-
9
- function registerDynamicWatcher(
10
- instance: any,
11
- propertyName: string,
12
- onChange: () => void
13
- ) {
14
- const key = String(propertyName);
15
- ensureWillUpdateHook(instance);
16
- if (!instance[dynamicWatcherStore]) {
17
- Object.defineProperty(instance, dynamicWatcherStore, {
18
- value: new Map<string, Set<() => void>>(),
19
- enumerable: false,
20
- configurable: false,
21
- writable: false,
22
- });
23
- }
24
- const watcherMap = instance[dynamicWatcherStore] as Map<
25
- string,
26
- Set<() => void>
27
- >;
28
- if (!watcherMap.has(key)) {
29
- watcherMap.set(key, new Set());
30
- }
31
- const watchers = watcherMap.get(key)!;
32
- watchers.add(onChange);
33
- return () => {
34
- watchers.delete(onChange);
35
- if (watchers.size === 0) {
36
- watcherMap.delete(key);
37
- }
38
- };
39
- }
40
-
41
- function ensureWillUpdateHook(instance: any) {
42
- const proto = Object.getPrototypeOf(instance);
43
- if (!proto || proto[dynamicWillUpdateHookedStore]) return;
44
- const originalWillUpdate = Object.prototype.hasOwnProperty.call(
45
- proto,
46
- "willUpdate"
47
- )
48
- ? proto.willUpdate
49
- : Object.getPrototypeOf(proto)?.willUpdate;
50
- proto.willUpdate = function (changedProperties?: Map<unknown, unknown>) {
51
- const handlers = this[dynamicWatcherStore] as
52
- | Map<string, Set<() => void>>
53
- | undefined;
54
- if (handlers && handlers.size > 0) {
55
- if (changedProperties && changedProperties.size > 0) {
56
- changedProperties.forEach((_value, dependency) => {
57
- const callbacks = handlers.get(String(dependency));
58
- if (callbacks) {
59
- callbacks.forEach((cb) => cb());
60
- }
61
- });
62
- } else {
63
- handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
64
- }
65
- }
66
- originalWillUpdate?.call(this, changedProperties);
67
- };
68
- proto[dynamicWillUpdateHookedStore] = true;
69
- }
70
-
71
- function extractDynamicDependencies(path: string) {
72
- const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
73
- const deps = new Set<string>();
74
- for (const pattern of patterns) {
75
- let match;
76
- while ((match = pattern.exec(path)) !== null) {
77
- const cleaned = cleanPlaceholder(match[1]);
78
- if (!cleaned) continue;
79
- const [root] = cleaned.split(".");
80
- if (root) deps.add(root);
81
- }
82
- }
83
- return Array.from(deps);
84
- }
85
-
86
- function cleanPlaceholder(value: string) {
87
- return value.trim().replace(/^this\./, "");
88
- }
89
-
90
- function resolveDynamicPath(component: any, template: string) {
91
- let missing = false;
92
- const replaceValue = (_match: string, expression: string) => {
93
- const cleaned = cleanPlaceholder(expression);
94
- const resolved = getValueFromExpression(component, cleaned);
95
- if (resolved === undefined || resolved === null) {
96
- missing = true;
97
- return "";
98
- }
99
- return `${resolved}`;
100
- };
101
- const resolvedPath = template
102
- .replace(/\$\{([^}]+)\}/g, replaceValue)
103
- .replace(/\{\$([^}]+)\}/g, replaceValue)
104
- .trim();
105
- if (missing || !resolvedPath.length) {
106
- return { ready: false, path: null };
107
- }
108
- const segments = resolvedPath.split(".").filter(Boolean);
109
- if (segments.length === 0 || !segments[0]) {
110
- return { ready: false, path: null };
111
- }
112
- return { ready: true, path: resolvedPath };
113
- }
114
-
115
- function getValueFromExpression(component: any, expression: string) {
116
- if (!expression) return undefined;
117
- const segments = expression.split(".").filter(Boolean);
118
- if (segments.length === 0) return undefined;
119
- let current: unknown = component;
120
- for (const segment of segments) {
121
- if (
122
- current === undefined ||
123
- current === null ||
124
- typeof current !== "object"
125
- ) {
126
- return undefined;
127
- }
128
- current = (current as Record<string, unknown>)[segment];
129
- }
130
- return current;
131
- }
132
-
133
- function getPublisherFromPath(path: string) {
134
- const segments = path.split(".").filter((segment) => segment.length > 0);
135
- if (segments.length === 0) return null;
136
- const dataProvider = segments.shift() || "";
137
- if (!dataProvider) return null;
138
- let publisher = PublisherManager.get(dataProvider);
139
- if (!publisher) return null;
140
- publisher = Objects.traverse(publisher, segments);
141
- return publisher as DataProvider | null;
142
- }
143
-
144
- export function bind(path: string, options?: { reflect?: boolean }) {
6
+ import {
7
+ extractDynamicDependencies,
8
+ hasPath,
9
+ resolveDynamicPath,
10
+ } from "./dynamicPath";
11
+ import {
12
+ bindDynamicWatchKeys,
13
+ registerDynamicPropertyWatcher,
14
+ } from "./dynamicPropertyWatch";
15
+ import { getPublisherFromPath } from "./publisherPath";
16
+
17
+ function bindImpl(
18
+ path: string,
19
+ options?: { reflect?: boolean }
20
+ ): (target: unknown, propertyKey: string) => void {
145
21
  const reflect = options?.reflect ?? false;
146
22
  const dynamicDependencies = extractDynamicDependencies(path);
147
23
  const isDynamicPath = dynamicDependencies.length > 0;
@@ -276,8 +152,10 @@ export function bind(path: string, options?: { reflect?: boolean }) {
276
152
 
277
153
  if (isDynamicPath) {
278
154
  for (const dependency of dynamicDependencies) {
279
- const unsubscribe = registerDynamicWatcher(
280
- component as Record<string, unknown>,
155
+ const unsubscribe = registerDynamicPropertyWatcher(
156
+ bindDynamicWatchKeys.watcherStore,
157
+ bindDynamicWatchKeys.hooked,
158
+ component,
281
159
  dependency,
282
160
  () => refreshSubscription()
283
161
  );
@@ -303,3 +181,37 @@ export function bind(path: string, options?: { reflect?: boolean }) {
303
181
  };
304
182
  }
305
183
 
184
+ /**
185
+ * Bidirectional binding to a publisher path. Subscribes to changes and optionally reflects writes back.
186
+ * Accepts either a string path (legacy) or DataProviderKey&lt;T&gt; for type-safe binding.
187
+ * Supports dynamic paths: use placeholders like "users.${userIndex}" in the path or DataProviderKey.
188
+ *
189
+ * @example
190
+ * // String path (legacy):
191
+ * @bind("demoData.firstName")
192
+ * @state()
193
+ * firstName = "";
194
+ *
195
+ * @example
196
+ * // DataProviderKey with type validation:
197
+ * const dataKey = new DataProviderKey<Data>("data");
198
+ * @bind(dataKey.count, { reflect: true })
199
+ * @state()
200
+ * count: number = 0;
201
+ */
202
+ export function bind(path: string, options?: { reflect?: boolean }): (target: unknown, propertyKey: string) => void;
203
+ export function bind<T, U = any>(
204
+ key: DataProviderKey<T, U>,
205
+ options?: { reflect?: boolean },
206
+ ): <K extends string>(
207
+ target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
208
+ propertyKey: K,
209
+ ) => void;
210
+ export function bind(
211
+ pathOrKey: string | DataProviderKey<unknown, unknown>,
212
+ options?: { reflect?: boolean },
213
+ ): (target: unknown, propertyKey: string) => void {
214
+ const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
215
+ return bindImpl(path, options);
216
+ }
217
+
@@ -0,0 +1,77 @@
1
+ /** Lit / décorateurs : chemins publisher avec `${prop}` ou `{$prop}`. */
2
+
3
+ export function cleanPlaceholder(value: string): string {
4
+ return value.trim().replace(/^this\./, "");
5
+ }
6
+
7
+ export function getValueFromExpression(
8
+ component: unknown,
9
+ expression: string,
10
+ ): unknown {
11
+ if (!expression) return undefined;
12
+ const segments = expression.split(".").filter(Boolean);
13
+ if (segments.length === 0) return undefined;
14
+ let current: unknown = component;
15
+ for (const segment of segments) {
16
+ if (
17
+ current === undefined ||
18
+ current === null ||
19
+ typeof current !== "object"
20
+ ) {
21
+ return undefined;
22
+ }
23
+ current = (current as Record<string, unknown>)[segment];
24
+ }
25
+ return current;
26
+ }
27
+
28
+ export function resolveDynamicPath(
29
+ component: unknown,
30
+ template: string,
31
+ ): { ready: boolean; path: string | null } {
32
+ let missing = false;
33
+ const replaceValue = (_match: string, expression: string) => {
34
+ const cleaned = cleanPlaceholder(expression);
35
+ const resolved = getValueFromExpression(component, cleaned);
36
+ if (resolved === undefined || resolved === null) {
37
+ missing = true;
38
+ return "";
39
+ }
40
+ return `${resolved}`;
41
+ };
42
+ const resolvedPath = template
43
+ .replace(/\$\{([^}]+)\}/g, replaceValue)
44
+ .replace(/\{\$([^}]+)\}/g, replaceValue)
45
+ .trim();
46
+ if (missing || !resolvedPath.length) {
47
+ return { ready: false, path: null };
48
+ }
49
+ const segments = resolvedPath.split(".").filter(Boolean);
50
+ if (segments.length === 0 || !segments[0]) {
51
+ return { ready: false, path: null };
52
+ }
53
+ return { ready: true, path: resolvedPath };
54
+ }
55
+
56
+ export function extractDynamicDependencies(path: string): string[] {
57
+ const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
58
+ const deps = new Set<string>();
59
+ for (const pattern of patterns) {
60
+ let match;
61
+ while ((match = pattern.exec(path)) !== null) {
62
+ const cleaned = (match[1] || "").trim().replace(/^this\./, "");
63
+ if (!cleaned) continue;
64
+ const [root] = cleaned.split(".");
65
+ if (root) deps.add(root);
66
+ }
67
+ }
68
+ return Array.from(deps);
69
+ }
70
+ export function hasPath(obj: unknown): obj is { path: string } {
71
+ return (
72
+ typeof obj === "object" &&
73
+ obj !== null &&
74
+ "path" in obj &&
75
+ typeof (obj as { path: unknown }).path === "string"
76
+ );
77
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Enregistre des callbacks sur des propriétés Lit (via `willUpdate`) pour
3
+ * réagir aux changements de segments dynamiques `${…}` dans les chemins.
4
+ * Chaque décorateur utilise des clés de stockage distinctes pour éviter les collisions.
5
+ */
6
+
7
+ type InstanceStores = Record<PropertyKey, unknown>;
8
+
9
+ export function registerDynamicPropertyWatcher(
10
+ watcherStoreKey: PropertyKey,
11
+ hookedStoreKey: PropertyKey,
12
+ instance: object,
13
+ propertyName: string,
14
+ onChange: () => void,
15
+ ): () => void {
16
+ const inst = instance as InstanceStores;
17
+ const key = String(propertyName);
18
+ ensureDynamicPropertiesWillUpdate(
19
+ watcherStoreKey,
20
+ hookedStoreKey,
21
+ instance,
22
+ );
23
+ if (!inst[watcherStoreKey]) {
24
+ Object.defineProperty(inst, watcherStoreKey, {
25
+ value: new Map<string, Set<() => void>>(),
26
+ enumerable: false,
27
+ configurable: false,
28
+ writable: false,
29
+ });
30
+ }
31
+ const watcherMap = inst[watcherStoreKey] as Map<string, Set<() => void>>;
32
+ if (!watcherMap.has(key)) {
33
+ watcherMap.set(key, new Set());
34
+ }
35
+ const watchers = watcherMap.get(key)!;
36
+ watchers.add(onChange);
37
+ return () => {
38
+ watchers.delete(onChange);
39
+ if (watchers.size === 0) {
40
+ watcherMap.delete(key);
41
+ }
42
+ };
43
+ }
44
+
45
+ export function ensureDynamicPropertiesWillUpdate(
46
+ watcherStoreKey: PropertyKey,
47
+ hookedStoreKey: PropertyKey,
48
+ instance: object,
49
+ ): void {
50
+ const proto = Object.getPrototypeOf(instance);
51
+ if (!proto || (proto as InstanceStores)[hookedStoreKey]) return;
52
+ const originalWillUpdate = Object.prototype.hasOwnProperty.call(
53
+ proto,
54
+ "willUpdate",
55
+ )
56
+ ? (proto as InstanceStores).willUpdate
57
+ : (Object.getPrototypeOf(proto) as InstanceStores)?.willUpdate;
58
+ (proto as InstanceStores).willUpdate = function (
59
+ changedProperties?: Map<unknown, unknown>,
60
+ ) {
61
+ const handlers = (this as InstanceStores)[watcherStoreKey] as
62
+ | Map<string, Set<() => void>>
63
+ | undefined;
64
+ if (handlers && handlers.size > 0) {
65
+ if (changedProperties && changedProperties.size > 0) {
66
+ changedProperties.forEach((_value, dependency) => {
67
+ const callbacks = handlers.get(String(dependency));
68
+ if (callbacks) {
69
+ callbacks.forEach((cb) => cb());
70
+ }
71
+ });
72
+ } else {
73
+ handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
74
+ }
75
+ }
76
+ if (typeof originalWillUpdate === "function") {
77
+ originalWillUpdate.call(this, changedProperties);
78
+ }
79
+ };
80
+ (proto as InstanceStores)[hookedStoreKey] = true;
81
+ }
82
+
83
+ /** Clés utilisées par `@bind`. */
84
+ export const bindDynamicWatchKeys = {
85
+ watcherStore: Symbol("__bindDynamicWatcherStore__"),
86
+ hooked: Symbol("__bindDynamicWillUpdateHooked__"),
87
+ } as const;
88
+
89
+ /** Clés utilisées par `@publish`. */
90
+ export const publishDynamicWatchKeys = {
91
+ watcherStore: "__publishDynamicWatcherStore__",
92
+ hooked: "__publishDynamicWillUpdateHooked__",
93
+ } as const;
94
+
95
+ /** Clés utilisées par `@get`. */
96
+ export const getDynamicWatchKeys = {
97
+ watcherStore: "__getDynamicWatcherStore__",
98
+ hooked: "__getDynamicWillUpdateHooked__",
99
+ } as const;
100
+
101
+ /** Clés utilisées par `@onAssign`. */
102
+ export const onAssignDynamicWatchKeys = {
103
+ watcherStore: Symbol("__onAssignDynamicWatcherStore__"),
104
+ hooked: Symbol("__onAssignDynamicWillUpdateHooked__"),
105
+ } as const;