veryfront 0.1.752 → 0.1.753

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.752",
3
+ "version": "0.1.753",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "minimumDependencyAge": "P2D",
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../../../src/src/modules/react-loader/ssr-module-loader/cache/memory.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAO5D,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAOnE,eAAO,MAAM,iBAAiB,oCAE5B,CAAC;AAEH,eAAO,MAAM,uBAAuB,oCAElC,CAAC;AAEH,eAAO,MAAM,gBAAgB,4BAAmC,CAAC;AAEjE,eAAO,MAAM,aAAa,0BAExB,CAAC;AAEH,eAAO,MAAM,gBAAgB,4BAAmC,CAAC;AAGjE,wBAAgB,qBAAqB,IAAI,SAAS,CAKjD;AAwBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,OAAO,CAY/E;AAKD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,UAAQ,GACb,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,IAAI,CAU5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC,CAOA;AAkDD,wBAAgB,mBAAmB,IAAI,IAAI,CAqB1C;AAED,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAiDrE"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../../../src/src/modules/react-loader/ssr-module-loader/cache/memory.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAO5D,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAOnE,eAAO,MAAM,iBAAiB,oCAE5B,CAAC;AAEH,eAAO,MAAM,uBAAuB,oCAElC,CAAC;AAEH,eAAO,MAAM,gBAAgB,4BAAmC,CAAC;AAEjE,eAAO,MAAM,aAAa,0BAExB,CAAC;AAEH,eAAO,MAAM,gBAAgB,4BAAmC,CAAC;AAGjE,wBAAgB,qBAAqB,IAAI,SAAS,CAKjD;AA+BD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,OAAO,CAY/E;AA2DD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,UAAQ,GACb,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,IAAI,CAY5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC,CAOA;AAkDD,wBAAgB,mBAAmB,IAAI,IAAI,CAsB1C;AAED,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkDrE"}
@@ -48,6 +48,7 @@ export function getTransformSemaphore() {
48
48
  * Only enforced when TRANSFORM_PER_PROJECT_LIMIT > 0.
49
49
  */
50
50
  const projectTransformCounts = new Map();
51
+ const projectTransformWaiters = new Map();
51
52
  /**
52
53
  * Projects that bypass per-project rate limiting.
53
54
  * - "__single__": Used for local development and tests where there's no multi-tenancy
@@ -88,8 +89,55 @@ export function acquireTransformSlot(projectId, bypass = false) {
88
89
  projectTransformCounts.set(projectId, current + 1);
89
90
  return true;
90
91
  }
91
- /** How long to wait between retry attempts for per-project slots */
92
- const PROJECT_SLOT_RETRY_INTERVAL_MS = 50;
92
+ function removeProjectTransformWaiter(projectId, waiter) {
93
+ const queue = projectTransformWaiters.get(projectId);
94
+ if (!queue)
95
+ return;
96
+ const index = queue.indexOf(waiter);
97
+ if (index !== -1)
98
+ queue.splice(index, 1);
99
+ if (queue.length === 0)
100
+ projectTransformWaiters.delete(projectId);
101
+ }
102
+ function settleProjectTransformWaiter(projectId, waiter, acquired) {
103
+ removeProjectTransformWaiter(projectId, waiter);
104
+ clearTimeout(waiter.timeoutId);
105
+ waiter.resolve(acquired);
106
+ }
107
+ function wakeNextProjectTransformWaiter(projectId) {
108
+ const limit = getTransformPerProjectLimit();
109
+ if (limit <= 0)
110
+ return;
111
+ const queue = projectTransformWaiters.get(projectId);
112
+ if (!queue?.length)
113
+ return;
114
+ const current = projectTransformCounts.get(projectId) ?? 0;
115
+ if (current >= limit)
116
+ return;
117
+ const waiter = queue.shift();
118
+ if (queue.length === 0)
119
+ projectTransformWaiters.delete(projectId);
120
+ if (!waiter)
121
+ return;
122
+ projectTransformCounts.set(projectId, current + 1);
123
+ clearTimeout(waiter.timeoutId);
124
+ waiter.resolve(true);
125
+ }
126
+ function rejectProjectTransformWaiters(projectId) {
127
+ const queue = projectTransformWaiters.get(projectId);
128
+ if (!queue?.length)
129
+ return;
130
+ projectTransformWaiters.delete(projectId);
131
+ for (const waiter of queue) {
132
+ clearTimeout(waiter.timeoutId);
133
+ waiter.resolve(false);
134
+ }
135
+ }
136
+ function rejectAllProjectTransformWaiters() {
137
+ const projectIds = Array.from(projectTransformWaiters.keys());
138
+ for (const projectId of projectIds)
139
+ rejectProjectTransformWaiters(projectId);
140
+ }
93
141
  /**
94
142
  * Try to acquire a project-level transform slot with retries.
95
143
  * Waits up to timeoutMs for a slot to become available.
@@ -98,13 +146,22 @@ const PROJECT_SLOT_RETRY_INTERVAL_MS = 50;
98
146
  export async function tryAcquireTransformSlot(projectId, timeoutMs, bypass = false) {
99
147
  if (acquireTransformSlot(projectId, bypass))
100
148
  return true;
101
- const deadline = Date.now() + timeoutMs;
102
- while (Date.now() < deadline) {
103
- await new Promise((resolve) => dntShim.setTimeout(resolve, PROJECT_SLOT_RETRY_INTERVAL_MS)); // no cleanup needed: one-shot
104
- if (acquireTransformSlot(projectId, bypass))
105
- return true;
106
- }
107
- return false;
149
+ if (timeoutMs <= 0)
150
+ return false;
151
+ return new Promise((resolve) => {
152
+ const waiter = {
153
+ resolve,
154
+ timeoutId: dntShim.setTimeout(() => {
155
+ settleProjectTransformWaiter(projectId, waiter, false);
156
+ }, timeoutMs),
157
+ };
158
+ const queue = projectTransformWaiters.get(projectId);
159
+ if (queue) {
160
+ queue.push(waiter);
161
+ return;
162
+ }
163
+ projectTransformWaiters.set(projectId, [waiter]);
164
+ });
108
165
  }
109
166
  /**
110
167
  * Release a project-level transform slot. `bypass` must match the value
@@ -117,9 +174,11 @@ export function releaseTransformSlot(projectId, bypass = false) {
117
174
  const current = projectTransformCounts.get(projectId) ?? 0;
118
175
  if (current <= 1) {
119
176
  projectTransformCounts.delete(projectId);
177
+ wakeNextProjectTransformWaiter(projectId);
120
178
  return;
121
179
  }
122
180
  projectTransformCounts.set(projectId, current - 1);
181
+ wakeNextProjectTransformWaiter(projectId);
123
182
  }
124
183
  /**
125
184
  * Get per-project transform statistics.
@@ -177,6 +236,7 @@ export function clearSSRModuleCache() {
177
236
  globalModuleCache.clear();
178
237
  failedComponents.clear();
179
238
  projectTransformCounts.clear();
239
+ rejectAllProjectTransformWaiters();
180
240
  verifiedHttpBundlePaths.clear();
181
241
  // Reset the transform semaphore and cached limits so leaked permits
182
242
  // from prior operations don't starve subsequent callers, and env var
@@ -225,6 +285,7 @@ export function clearSSRModuleCacheForProject(projectId) {
225
285
  }
226
286
  }
227
287
  projectTransformCounts.delete(projectId);
288
+ rejectProjectTransformWaiters(projectId);
228
289
  // Clear verified HTTP bundle paths — keys are tempPath:contentHash (not project-scoped),
229
290
  // so full clear is needed. This just forces re-verification on next access.
230
291
  verifiedHttpBundlePaths.clear();
@@ -8,7 +8,7 @@ interface TransformCacheEntry {
8
8
  }
9
9
  interface LocalFallbackLike<K, V> {
10
10
  get(key: K): V | undefined;
11
- set(key: K, value: V): void | this;
11
+ set(key: K, value: V): unknown;
12
12
  delete(key: K): boolean;
13
13
  has(key: K): boolean;
14
14
  clear(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"transform-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/transform-cache.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,wBAAwB,CAAC;AAmBhC,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAUD,UAAU,iBAAiB,CAAC,CAAC,EAAE,CAAC;IAC9B,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IACxB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IACrB,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,IAAI,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CACrC;AAcD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE;IACN,aAAa,CAAC,EAAE,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,IAAI,CAAC;IACtE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACpC,GAAG,IAAI,GACP,IAAI,CASN;AAmBD,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBjE;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,OAAe,EACpB,WAAW,GAAE,OAAe,EAC5B,OAAO,CAAC,EAAE,eAAe,GACxB,MAAM,CAER;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAgC1C;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAI/E;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,EACxC,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,IAAI,CAoBN;AAyBD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAKnF;AAED,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAChC,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,oBAAoB,CAAC,CA4B/B"}
1
+ {"version":3,"file":"transform-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/transforms/esm/transform-cache.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,wBAAwB,CAAC;AAmBhC,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAMD,UAAU,iBAAiB,CAAC,CAAC,EAAE,CAAC;IAC9B,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IACxB,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC;IACrB,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,IAAI,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CACrC;AA+DD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE;IACN,aAAa,CAAC,EAAE,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,IAAI,CAAC;IACtE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACpC,GAAG,IAAI,GACP,IAAI,CASN;AAmBD,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBjE;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,OAAe,EACpB,WAAW,GAAE,OAAe,EAC5B,OAAO,CAAC,EAAE,eAAe,GACxB,MAAM,CAER;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAgC1C;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAI/E;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,EACxC,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAA4B,GACvC,IAAI,CAoBN;AAWD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAKnF;AAED,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAChC,UAAU,GAAE,MAA4B,GACvC,OAAO,CAAC,oBAAoB,CAAC,CA4B/B"}
@@ -1,7 +1,6 @@
1
1
  import { registerCache } from "../../utils/memory/index.js";
2
2
  import { logger as baseLogger } from "../../utils/logger/logger.js";
3
3
  import { buildTransformCacheKey } from "../../cache/keys.js";
4
- import { LRUCache } from "../../utils/lru-wrapper.js";
5
4
  import { CacheBackends, MemoryCacheBackend, } from "../../cache/backend.js";
6
5
  import { hashCodeHex } from "../../utils/hash-utils.js";
7
6
  import { detokenizeAllCachePaths, tokenizeAllVeryFrontPaths } from "../../cache/paths.js";
@@ -19,9 +18,47 @@ const UNRESOLVED_VF_MODULES_PATTERN = /from\s*["']((?:file:\/\/)?\/?\/?_vf_modul
19
18
  let cacheGateway = null;
20
19
  let cacheInitialized = false;
21
20
  let cacheInitPromise = null;
22
- const defaultLocalFallback = new LRUCache({
23
- maxEntries: FALLBACK_MAX_ENTRIES,
24
- });
21
+ class EntryBoundedFallback {
22
+ maxEntries;
23
+ store = new Map();
24
+ constructor(maxEntries) {
25
+ this.maxEntries = maxEntries;
26
+ }
27
+ get(key) {
28
+ const value = this.store.get(key);
29
+ if (value === undefined)
30
+ return undefined;
31
+ this.store.delete(key);
32
+ this.store.set(key, value);
33
+ return value;
34
+ }
35
+ set(key, value) {
36
+ this.store.delete(key);
37
+ this.store.set(key, value);
38
+ while (this.store.size > this.maxEntries) {
39
+ const oldestKey = this.store.keys().next();
40
+ if (oldestKey.done)
41
+ break;
42
+ this.store.delete(oldestKey.value);
43
+ }
44
+ }
45
+ delete(key) {
46
+ return this.store.delete(key);
47
+ }
48
+ has(key) {
49
+ return this.store.has(key);
50
+ }
51
+ clear() {
52
+ this.store.clear();
53
+ }
54
+ get size() {
55
+ return this.store.size;
56
+ }
57
+ entries() {
58
+ return this.store.entries();
59
+ }
60
+ }
61
+ const defaultLocalFallback = new EntryBoundedFallback(FALLBACK_MAX_ENTRIES);
25
62
  /** Injected caches for testing */
26
63
  let injectedLocalFallback = null;
27
64
  let injectedCacheGateway = undefined;
@@ -176,21 +213,6 @@ function normalizeTtl(ttlSeconds) {
176
213
  function setLocalFallback(key, entry) {
177
214
  const fallback = getLocalFallback();
178
215
  fallback.set(key, entry);
179
- if (fallback === defaultLocalFallback)
180
- return;
181
- if (fallback.size > FALLBACK_MAX_ENTRIES)
182
- pruneLocalFallback();
183
- }
184
- function pruneLocalFallback() {
185
- const fallback = getLocalFallback();
186
- const excess = fallback.size - FALLBACK_MAX_ENTRIES;
187
- if (excess <= 0)
188
- return;
189
- const entries = Array.from(fallback.entries()).sort(([, a], [, b]) => a.timestamp - b.timestamp);
190
- for (let i = 0; i < excess; i++) {
191
- const [key] = entries[i];
192
- fallback.delete(key);
193
- }
194
216
  }
195
217
  export function destroyTransformCache() {
196
218
  getLocalFallback().clear();
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.752";
2
+ export declare const VERSION = "0.1.753";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.752";
4
+ export const VERSION = "0.1.753";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.752",
3
+ "version": "0.1.753",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",