@tuturuuu/utils 0.0.3 → 0.6.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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,269 @@
1
+ const articles = new Set(['van', 'von', 'der', 'den', 'de', 'la', 'les', 'le']);
2
+
3
+ export const getInitials = (name?: string | null): string => {
4
+ if (!name) return '';
5
+
6
+ // Replace tabs and newlines with spaces and trim
7
+ const cleanName = name.replace(/[\t\n]/g, ' ').trim();
8
+ if (!cleanName) return '';
9
+
10
+ // Split by spaces, hyphens, and apostrophes
11
+ const nameParts = cleanName.split(/[\s\-']+/).filter(Boolean);
12
+ if (nameParts.length === 0) return '';
13
+
14
+ // For single word names
15
+ if (nameParts.length === 1) {
16
+ return nameParts[0]!.charAt(0).toUpperCase();
17
+ }
18
+
19
+ const firstPart = nameParts[0]!.toLowerCase();
20
+
21
+ // Special case: if the name starts with a capitalized prefix like "Van", use it
22
+ if (nameParts[0]![0]?.toUpperCase() === nameParts[0]![0]) {
23
+ return (
24
+ nameParts[0]![0]! + nameParts[nameParts.length - 1]![0]
25
+ ).toUpperCase();
26
+ }
27
+
28
+ // Otherwise look for the first non-prefix word and last word
29
+ if (articles.has(firstPart)) {
30
+ const lastPart = nameParts[nameParts.length - 1]!;
31
+ return (nameParts[1] ? nameParts[1][0] : '') + lastPart[0]!.toUpperCase();
32
+ }
33
+
34
+ // Otherwise use first and last word
35
+ return (
36
+ (nameParts?.[0]?.[0] ?? '') + (nameParts?.[nameParts.length - 1]?.[0] ?? '')
37
+ ).toUpperCase();
38
+ };
39
+
40
+ export function getAvatarPlaceholder(name: string) {
41
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}`;
42
+ }
43
+
44
+ /**
45
+ * Generates a consistent fun name from a user ID
46
+ */
47
+ export function generateFunName({
48
+ id,
49
+ locale = 'en',
50
+ }: {
51
+ id: string;
52
+ locale?: 'en' | 'vi' | string;
53
+ }): string {
54
+ // List of adjectives and animals for fun names
55
+ const adjectives = {
56
+ en: [
57
+ 'Happy',
58
+ 'Silly',
59
+ 'Clever',
60
+ 'Brave',
61
+ 'Curious',
62
+ 'Playful',
63
+ 'Friendly',
64
+ 'Gentle',
65
+ 'Jolly',
66
+ 'Witty',
67
+ 'Mighty',
68
+ 'Dazzling',
69
+ 'Adventurous',
70
+ 'Bouncy',
71
+ 'Cheerful',
72
+ 'Daring',
73
+ 'Energetic',
74
+ 'Fuzzy',
75
+ 'Goofy',
76
+ 'Hilarious',
77
+ 'Intelligent',
78
+ 'Jumpy',
79
+ 'Kind',
80
+ 'Lively',
81
+ 'Magical',
82
+ 'Noble',
83
+ 'Optimistic',
84
+ 'Quirky',
85
+ 'Radiant',
86
+ 'Sassy',
87
+ 'Talented',
88
+ 'Unique',
89
+ 'Vibrant',
90
+ 'Whimsical',
91
+ 'Zealous',
92
+ 'Adorable',
93
+ ],
94
+ vi: [
95
+ 'Vui Vẻ', // Happy
96
+ 'Ngốc Nghếch', // Silly
97
+ 'Thông Minh', // Clever
98
+ 'Dũng Cảm', // Brave
99
+ 'Tò Mò', // Curious
100
+ 'Tinh Nghịch', // Playful
101
+ 'Thân Thiện', // Friendly
102
+ 'Dịu Dàng', // Gentle
103
+ 'Hạnh Phúc', // Jolly
104
+ 'Hóm Hỉnh', // Witty
105
+ 'Mạnh Mẽ', // Mighty
106
+ 'Lấp Lánh', // Dazzling
107
+ 'Phiêu Lưu', // Adventurous
108
+ 'Nhảy Nhót', // Bouncy
109
+ 'Vui Tươi', // Cheerful
110
+ 'Táo Bạo', // Daring
111
+ 'Năng Động', // Energetic
112
+ 'Xù Xì', // Fuzzy
113
+ 'Ngớ Ngẩn', // Goofy
114
+ 'Buồn Cười', // Hilarious
115
+ 'Thông Tuệ', // Intelligent
116
+ 'Nhảy Nhót', // Jumpy
117
+ 'Tốt Bụng', // Kind
118
+ 'Sống Động', // Lively
119
+ 'Kỳ Diệu', // Magical
120
+ 'Cao Quý', // Noble
121
+ 'Lạc Quan', // Optimistic
122
+ 'Kỳ Lạ', // Quirky
123
+ 'Rực Rỡ', // Radiant
124
+ 'Bướng Bỉnh', // Sassy
125
+ 'Tài Năng', // Talented
126
+ 'Độc Đáo', // Unique
127
+ 'Sôi Động', // Vibrant
128
+ 'Kỳ Quặc', // Whimsical
129
+ 'Nhiệt Tình', // Zealous
130
+ 'Đáng Yêu', // Adorable
131
+ ],
132
+ };
133
+
134
+ const animals = {
135
+ en: [
136
+ { name: 'Octopus', emoji: '🐙' },
137
+ { name: 'Cat', emoji: '🐱' },
138
+ { name: 'Penguin', emoji: '🐧' },
139
+ { name: 'Fox', emoji: '🦊' },
140
+ { name: 'Panda', emoji: '🐼' },
141
+ { name: 'Dolphin', emoji: '🐬' },
142
+ { name: 'Koala', emoji: '🐨' },
143
+ { name: 'Owl', emoji: '🦉' },
144
+ { name: 'Tiger', emoji: '🐯' },
145
+ { name: 'Rabbit', emoji: '🐰' },
146
+ { name: 'Monkey', emoji: '🐵' },
147
+ { name: 'Wolf', emoji: '🐺' },
148
+ { name: 'Alligator', emoji: '🐊' },
149
+ { name: 'Beaver', emoji: '🦫' },
150
+ { name: 'Chameleon', emoji: '🦎' },
151
+ { name: 'Duck', emoji: '🦆' },
152
+ { name: 'Elephant', emoji: '🐘' },
153
+ { name: 'Flamingo', emoji: '🦩' },
154
+ { name: 'Giraffe', emoji: '🦒' },
155
+ { name: 'Hedgehog', emoji: '🦔' },
156
+ { name: 'Iguana', emoji: '🦎' },
157
+ { name: 'Jellyfish', emoji: '🪼' },
158
+ { name: 'Kangaroo', emoji: '🦘' },
159
+ { name: 'Lion', emoji: '🦁' },
160
+ { name: 'Meerkat', emoji: '🦝' },
161
+ { name: 'Narwhal', emoji: '🦭' },
162
+ { name: 'Otter', emoji: '🦦' },
163
+ { name: 'Peacock', emoji: '🦚' },
164
+ { name: 'Quokka', emoji: '🦘' },
165
+ { name: 'Raccoon', emoji: '🦝' },
166
+ { name: 'Sloth', emoji: '🦥' },
167
+ { name: 'Turtle', emoji: '🐢' },
168
+ { name: 'Unicorn', emoji: '🦄' },
169
+ { name: 'Vulture', emoji: '🦅' },
170
+ { name: 'Walrus', emoji: '🦭' },
171
+ { name: 'Yak', emoji: '🐃' },
172
+ { name: 'Zebra', emoji: '🦓' },
173
+ { name: 'Badger', emoji: '🦡' },
174
+ { name: 'Cheetah', emoji: '🐆' },
175
+ { name: 'Dingo', emoji: '🐕' },
176
+ { name: 'Ferret', emoji: '🦡' },
177
+ { name: 'Gorilla', emoji: '🦍' },
178
+ ],
179
+ vi: [
180
+ { name: 'Bạch Tuộc', emoji: '🐙' },
181
+ { name: 'Mèo', emoji: '🐱' },
182
+ { name: 'Chim Cánh Cụt', emoji: '🐧' },
183
+ { name: 'Cáo', emoji: '🦊' },
184
+ { name: 'Gấu Trúc', emoji: '🐼' },
185
+ { name: 'Cá Heo', emoji: '🐬' },
186
+ { name: 'Gấu Koala', emoji: '🐨' },
187
+ { name: 'Cú Mèo', emoji: '🦉' },
188
+ { name: 'Hổ', emoji: '🐯' },
189
+ { name: 'Thỏ', emoji: '🐰' },
190
+ { name: 'Khỉ', emoji: '🐵' },
191
+ { name: 'Sói', emoji: '🐺' },
192
+ { name: 'Cá Sấu', emoji: '🐊' },
193
+ { name: 'Hải Ly', emoji: '🦫' },
194
+ { name: 'Tắc Kè', emoji: '🦎' },
195
+ { name: 'Vịt', emoji: '🦆' },
196
+ { name: 'Voi', emoji: '🐘' },
197
+ { name: 'Hồng Hạc', emoji: '🦩' },
198
+ { name: 'Hươu Cao Cổ', emoji: '🦒' },
199
+ { name: 'Nhím', emoji: '🦔' },
200
+ { name: 'Kỳ Đà', emoji: '🦎' },
201
+ { name: 'Sứa', emoji: '🪼' },
202
+ { name: 'Kangaroo', emoji: '🦘' },
203
+ { name: 'Sư Tử', emoji: '🦁' },
204
+ { name: 'Cầy Meerkat', emoji: '🦝' },
205
+ { name: 'Kỳ Lân Biển', emoji: '🦭' },
206
+ { name: 'Rái Cá', emoji: '🦦' },
207
+ { name: 'Công', emoji: '🦚' },
208
+ { name: 'Quokka', emoji: '🦘' },
209
+ { name: 'Gấu Mèo', emoji: '🦝' },
210
+ { name: 'Lười', emoji: '🦥' },
211
+ { name: 'Rùa', emoji: '🐢' },
212
+ { name: 'Kỳ Lân', emoji: '🦄' },
213
+ { name: 'Kền Kền', emoji: '🦅' },
214
+ { name: 'Hải Mã', emoji: '🦭' },
215
+ { name: 'Bò Yak', emoji: '🐃' },
216
+ { name: 'Ngựa Vằn', emoji: '🦓' },
217
+ { name: 'Lửng', emoji: '🦡' },
218
+ { name: 'Báo', emoji: '🐆' },
219
+ { name: 'Chó Dingo', emoji: '🐕' },
220
+ { name: 'Chồn', emoji: '🦡' },
221
+ { name: 'Khỉ Đột', emoji: '🦍' },
222
+ ],
223
+ };
224
+
225
+ // Improved hash function for more randomness
226
+ const hash = (str: string): number => {
227
+ let h1 = 0xdeadbeef;
228
+ let h2 = 0x41c6ce57;
229
+
230
+ for (let i = 0; i < str.length; i++) {
231
+ const char = str.charCodeAt(i);
232
+ h1 = Math.imul(h1 ^ char, 2654435761);
233
+ h2 = Math.imul(h2 ^ char, 1597334677);
234
+ }
235
+
236
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
237
+ h1 = Math.imul(h1 ^ (h1 >>> 13), 3266489909);
238
+
239
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
240
+ h2 = Math.imul(h2 ^ (h2 >>> 13), 3266489909);
241
+
242
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
243
+ };
244
+
245
+ // Use the appropriate language lists
246
+ const selectedLocale = locale in adjectives ? locale : 'en';
247
+ const adjectivesList =
248
+ adjectives?.[selectedLocale as keyof typeof adjectives];
249
+ const animalsList = animals?.[selectedLocale as keyof typeof animals];
250
+
251
+ // Generate consistent indices
252
+ const combinedHash = hash(id);
253
+ const adjIndex = combinedHash % adjectivesList.length;
254
+ const animalIndex = (combinedHash * 31) % animalsList.length;
255
+
256
+ const adjective =
257
+ adjectivesList[adjIndex] || (locale === 'en' ? 'Happy' : 'Vui');
258
+ const animal = animalsList[animalIndex];
259
+
260
+ if (!animal) {
261
+ return locale === 'en'
262
+ ? `${adjective} Mystery ❓`
263
+ : `${adjective} Kỳ Bí ❓`;
264
+ }
265
+
266
+ return locale === 'vi'
267
+ ? `${animal.name} ${adjective} ${animal.emoji}`
268
+ : `${adjective} ${animal.name} ${animal.emoji}`;
269
+ }
@@ -0,0 +1,234 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createTuturuuuNextConfig,
4
+ isTuturuuuNextCacheComponentsEnabled,
5
+ isTuturuuuNextReactCompilerEnabled,
6
+ resolveTuturuuuWebAppUrl,
7
+ TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
8
+ TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
9
+ trimTrailingSlashes,
10
+ } from './next-config';
11
+
12
+ describe('createTuturuuuNextConfig', () => {
13
+ it('applies shared local development defaults', () => {
14
+ const config = createTuturuuuNextConfig();
15
+
16
+ expect(config.allowedDevOrigins).toContain('tuturuuu.localhost');
17
+ expect(config.cacheComponents).toBe(true);
18
+ expect(config.images?.remotePatterns).toEqual(
19
+ TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS
20
+ );
21
+ expect(config.poweredByHeader).toBe(false);
22
+ expect(config.reactStrictMode).toBe(true);
23
+ expect(config.typescript?.ignoreBuildErrors).toBe(true);
24
+ });
25
+
26
+ it('allows the exact worktree-prefixed Portless host for Next dev assets', () => {
27
+ const originalPortlessUrl = process.env.PORTLESS_URL;
28
+
29
+ try {
30
+ process.env.PORTLESS_URL =
31
+ 'https://zalo-qr-chat-setup.chat.tuturuuu.localhost';
32
+
33
+ const config = createTuturuuuNextConfig();
34
+
35
+ expect(config.allowedDevOrigins).toContain(
36
+ 'zalo-qr-chat-setup.chat.tuturuuu.localhost'
37
+ );
38
+ } finally {
39
+ if (originalPortlessUrl === undefined) {
40
+ delete process.env.PORTLESS_URL;
41
+ } else {
42
+ process.env.PORTLESS_URL = originalPortlessUrl;
43
+ }
44
+ }
45
+ });
46
+
47
+ it('dedupes optimized package imports while preserving app additions', () => {
48
+ const config = createTuturuuuNextConfig({
49
+ experimental: {
50
+ optimizePackageImports: ['lucide-react', '@tuturuuu/ui'],
51
+ },
52
+ });
53
+
54
+ expect(config.experimental?.optimizePackageImports).toEqual([
55
+ ...TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
56
+ '@tuturuuu/ui',
57
+ ]);
58
+ });
59
+
60
+ it('dedupes image remote patterns while preserving app additions', () => {
61
+ const config = createTuturuuuNextConfig({
62
+ images: {
63
+ remotePatterns: [
64
+ ...TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
65
+ {
66
+ protocol: 'https',
67
+ hostname: 'models.dev',
68
+ },
69
+ ],
70
+ },
71
+ });
72
+
73
+ expect(config.images?.remotePatterns).toEqual([
74
+ ...TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
75
+ {
76
+ protocol: 'https',
77
+ hostname: 'models.dev',
78
+ },
79
+ ]);
80
+ });
81
+
82
+ it('preserves app-specific config overrides', () => {
83
+ const config = createTuturuuuNextConfig({
84
+ cacheComponents: false,
85
+ reactCompiler: true,
86
+ transpilePackages: ['@tuturuuu/ui'],
87
+ experimental: {
88
+ cpus: 2,
89
+ },
90
+ typescript: {
91
+ ignoreBuildErrors: false,
92
+ },
93
+ });
94
+
95
+ expect(config.cacheComponents).toBe(false);
96
+ expect(config.reactCompiler).toBe(true);
97
+ expect(config.transpilePackages).toEqual(['@tuturuuu/ui']);
98
+ expect(config.experimental?.cpus).toBe(2);
99
+ expect(config.typescript?.ignoreBuildErrors).toBe(false);
100
+ });
101
+ });
102
+
103
+ describe('isTuturuuuNextCacheComponentsEnabled', () => {
104
+ it('keeps cache components enabled by default', () => {
105
+ expect(isTuturuuuNextCacheComponentsEnabled({})).toBe(true);
106
+ });
107
+
108
+ it('honors explicit environment overrides', () => {
109
+ expect(
110
+ isTuturuuuNextCacheComponentsEnabled({
111
+ TUTURUUU_NEXT_CACHE_COMPONENTS: '0',
112
+ })
113
+ ).toBe(false);
114
+ expect(
115
+ isTuturuuuNextCacheComponentsEnabled({
116
+ TUTURUUU_NEXT_CACHE_COMPONENTS: '1',
117
+ })
118
+ ).toBe(true);
119
+ });
120
+ });
121
+
122
+ describe('isTuturuuuNextReactCompilerEnabled', () => {
123
+ it('disables React Compiler by default for next dev', () => {
124
+ expect(
125
+ isTuturuuuNextReactCompilerEnabled({ NODE_ENV: 'development' })
126
+ ).toBe(false);
127
+ });
128
+
129
+ it('keeps React Compiler enabled by default outside next dev', () => {
130
+ expect(isTuturuuuNextReactCompilerEnabled({ NODE_ENV: 'production' })).toBe(
131
+ true
132
+ );
133
+ });
134
+
135
+ it('honors explicit environment overrides', () => {
136
+ expect(
137
+ isTuturuuuNextReactCompilerEnabled({
138
+ NODE_ENV: 'production',
139
+ TUTURUUU_NEXT_REACT_COMPILER: '0',
140
+ })
141
+ ).toBe(false);
142
+ expect(
143
+ isTuturuuuNextReactCompilerEnabled({
144
+ NODE_ENV: 'development',
145
+ TUTURUUU_NEXT_REACT_COMPILER: '1',
146
+ })
147
+ ).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe('resolveTuturuuuWebAppUrl', () => {
152
+ it('trims configured web app origins', () => {
153
+ expect(
154
+ resolveTuturuuuWebAppUrl({
155
+ env: {
156
+ INTERNAL_WEB_API_ORIGIN: 'https://example.com///',
157
+ NODE_ENV: 'development',
158
+ },
159
+ })
160
+ ).toBe('https://example.com');
161
+ });
162
+
163
+ it('uses the local Portless platform app in development', () => {
164
+ expect(
165
+ resolveTuturuuuWebAppUrl({
166
+ centralPort: 7903,
167
+ env: {
168
+ NODE_ENV: 'development',
169
+ },
170
+ })
171
+ ).toBe('https://tuturuuu.localhost');
172
+ });
173
+
174
+ it('allows a custom local fallback URL', () => {
175
+ expect(
176
+ resolveTuturuuuWebAppUrl({
177
+ env: {
178
+ NODE_ENV: 'development',
179
+ },
180
+ localFallbackUrl: 'https://tuturuuu.localhost/',
181
+ })
182
+ ).toBe('https://tuturuuu.localhost');
183
+ });
184
+
185
+ it('uses the production platform app in deployed environments', () => {
186
+ expect(
187
+ resolveTuturuuuWebAppUrl({
188
+ env: {
189
+ VERCEL_ENV: 'production',
190
+ },
191
+ })
192
+ ).toBe('https://tuturuuu.com');
193
+ });
194
+
195
+ it('ignores known satellite NEXT_PUBLIC_APP_URL values when resolving the Web app URL', () => {
196
+ expect(
197
+ resolveTuturuuuWebAppUrl({
198
+ env: {
199
+ NEXT_PUBLIC_APP_URL: 'https://chat.tuturuuu.com',
200
+ NODE_ENV: 'production',
201
+ },
202
+ })
203
+ ).toBe('https://tuturuuu.com');
204
+ });
205
+
206
+ it('keeps explicit Web origins authoritative over satellite app URLs', () => {
207
+ expect(
208
+ resolveTuturuuuWebAppUrl({
209
+ env: {
210
+ NEXT_PUBLIC_APP_URL: 'https://chat.tuturuuu.com',
211
+ WEB_APP_URL: 'https://web.internal.example.com/',
212
+ },
213
+ })
214
+ ).toBe('https://web.internal.example.com');
215
+ });
216
+
217
+ it('allows platform NEXT_PUBLIC_APP_URL values as Web app URLs', () => {
218
+ expect(
219
+ resolveTuturuuuWebAppUrl({
220
+ env: {
221
+ NEXT_PUBLIC_APP_URL: 'https://tuturuuu.com/',
222
+ },
223
+ })
224
+ ).toBe('https://tuturuuu.com');
225
+ });
226
+ });
227
+
228
+ describe('trimTrailingSlashes', () => {
229
+ it('removes only trailing slash characters', () => {
230
+ expect(trimTrailingSlashes('https://example.com/path///')).toBe(
231
+ 'https://example.com/path'
232
+ );
233
+ });
234
+ });
@@ -0,0 +1,203 @@
1
+ import type { NextConfig } from 'next';
2
+ import { resolveInternalAppUrl } from './app-url';
3
+ import { getLocalInternalAppUrl } from './internal-domains';
4
+ import { getTuturuuuPortlessAllowedDevOrigins } from './portless';
5
+
6
+ type Environment = Record<string, string | undefined>;
7
+
8
+ export const TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS = [
9
+ '@lucide/lab',
10
+ '@tuturuuu/icons',
11
+ '@tuturuuu/icons/lab',
12
+ '@tuturuuu/icons/lucide',
13
+ 'lucide-react',
14
+ ] as const;
15
+
16
+ type NextImageConfig = NonNullable<NextConfig['images']>;
17
+ type NextImageRemotePattern = NonNullable<
18
+ NextImageConfig['remotePatterns']
19
+ >[number];
20
+
21
+ export const TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS = [
22
+ {
23
+ protocol: 'http',
24
+ hostname: 'localhost',
25
+ },
26
+ {
27
+ protocol: 'http',
28
+ hostname: '127.0.0.1',
29
+ },
30
+ {
31
+ protocol: 'https',
32
+ hostname: '**.supabase.co',
33
+ },
34
+ {
35
+ protocol: 'https',
36
+ hostname: 'avatars.githubusercontent.com',
37
+ },
38
+ {
39
+ protocol: 'https',
40
+ hostname: 'tuturuuu.com',
41
+ },
42
+ ] satisfies NextImageRemotePattern[];
43
+
44
+ const TRUTHY_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
45
+ const FALSY_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
46
+
47
+ function mergeStringArrays(
48
+ first: readonly string[] | undefined,
49
+ second: readonly string[] | undefined
50
+ ) {
51
+ return Array.from(new Set([...(first ?? []), ...(second ?? [])]));
52
+ }
53
+
54
+ function getRemotePatternKey(pattern: NextImageRemotePattern) {
55
+ return pattern instanceof URL ? pattern.toString() : JSON.stringify(pattern);
56
+ }
57
+
58
+ function mergeRemotePatterns(
59
+ first: readonly NextImageRemotePattern[] | undefined,
60
+ second: readonly NextImageRemotePattern[] | undefined
61
+ ) {
62
+ const merged = new Map<string, NextImageRemotePattern>();
63
+
64
+ for (const pattern of [...(first ?? []), ...(second ?? [])]) {
65
+ merged.set(getRemotePatternKey(pattern), pattern);
66
+ }
67
+
68
+ return Array.from(merged.values());
69
+ }
70
+
71
+ function readBooleanEnvOverride(value: string | undefined) {
72
+ const normalizedValue = value?.trim().toLowerCase();
73
+
74
+ if (!normalizedValue) {
75
+ return undefined;
76
+ }
77
+
78
+ if (TRUTHY_ENV_VALUES.has(normalizedValue)) {
79
+ return true;
80
+ }
81
+
82
+ if (FALSY_ENV_VALUES.has(normalizedValue)) {
83
+ return false;
84
+ }
85
+
86
+ return undefined;
87
+ }
88
+
89
+ export function isTuturuuuNextReactCompilerEnabled(
90
+ env: Environment = process.env
91
+ ) {
92
+ const override = readBooleanEnvOverride(env.TUTURUUU_NEXT_REACT_COMPILER);
93
+
94
+ if (override !== undefined) {
95
+ return override;
96
+ }
97
+
98
+ return env.NODE_ENV !== 'development';
99
+ }
100
+
101
+ export function isTuturuuuNextCacheComponentsEnabled(
102
+ env: Environment = process.env
103
+ ) {
104
+ const override = readBooleanEnvOverride(env.TUTURUUU_NEXT_CACHE_COMPONENTS);
105
+
106
+ return override ?? true;
107
+ }
108
+
109
+ export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
110
+ const experimentalConfig = config.experimental ?? {};
111
+ const imageConfig = config.images ?? {};
112
+
113
+ return {
114
+ reactCompiler: isTuturuuuNextReactCompilerEnabled(),
115
+ reactStrictMode: true,
116
+ poweredByHeader: false,
117
+ ...config,
118
+ cacheComponents:
119
+ config.cacheComponents ?? isTuturuuuNextCacheComponentsEnabled(),
120
+ allowedDevOrigins: mergeStringArrays(
121
+ getTuturuuuPortlessAllowedDevOrigins(),
122
+ config.allowedDevOrigins
123
+ ),
124
+ images: {
125
+ ...imageConfig,
126
+ remotePatterns: mergeRemotePatterns(
127
+ TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
128
+ imageConfig.remotePatterns
129
+ ),
130
+ },
131
+ typescript: {
132
+ ignoreBuildErrors: true,
133
+ ...config.typescript,
134
+ },
135
+ experimental: {
136
+ ...experimentalConfig,
137
+ optimizePackageImports: mergeStringArrays(
138
+ TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
139
+ experimentalConfig.optimizePackageImports
140
+ ),
141
+ },
142
+ };
143
+ }
144
+
145
+ export function trimTrailingSlashes(value: string) {
146
+ let end = value.length;
147
+
148
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
149
+ end -= 1;
150
+ }
151
+
152
+ return end === value.length ? value : value.slice(0, end);
153
+ }
154
+
155
+ export function isTuturuuuNextDeployedEnvironment(
156
+ env: Environment = process.env
157
+ ) {
158
+ return (
159
+ env.VERCEL === '1' ||
160
+ env.VERCEL_ENV === 'preview' ||
161
+ env.VERCEL_ENV === 'production' ||
162
+ env.NODE_ENV === 'production'
163
+ );
164
+ }
165
+
166
+ function resolveNextPublicPlatformAppUrl(env: Environment) {
167
+ if (!env.NEXT_PUBLIC_APP_URL) {
168
+ return undefined;
169
+ }
170
+
171
+ const resolvedUrl = resolveInternalAppUrl({
172
+ appName: 'platform',
173
+ candidates: [env.NEXT_PUBLIC_APP_URL],
174
+ fallback: '',
175
+ });
176
+
177
+ return resolvedUrl || undefined;
178
+ }
179
+
180
+ export function resolveTuturuuuWebAppUrl({
181
+ centralPort,
182
+ env = process.env,
183
+ localFallbackUrl,
184
+ productionUrl = 'https://tuturuuu.com',
185
+ }: {
186
+ centralPort?: number | string;
187
+ env?: Environment;
188
+ localFallbackUrl?: string;
189
+ productionUrl?: string;
190
+ } = {}) {
191
+ const localCentralPort = centralPort ?? env.CENTRAL_PORT ?? 7803;
192
+ const localUrl =
193
+ localFallbackUrl ??
194
+ getLocalInternalAppUrl('platform', `http://localhost:${localCentralPort}`);
195
+
196
+ return trimTrailingSlashes(
197
+ env.INTERNAL_WEB_API_ORIGIN ||
198
+ env.WEB_APP_URL ||
199
+ env.NEXT_PUBLIC_WEB_APP_URL ||
200
+ resolveNextPublicPlatformAppUrl(env) ||
201
+ (isTuturuuuNextDeployedEnvironment(env) ? productionUrl : localUrl)
202
+ );
203
+ }