@tuturuuu/utils 0.25.0 → 0.25.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.2](https://github.com/tutur3u/platform/compare/utils-v0.25.1...utils-v0.25.2) (2026-08-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tasks:** reconcile external task ordering ([2ebeefa](https://github.com/tutur3u/platform/commit/2ebeefa4c13ea9d0b62c33e50cf4f6fed7fac76a))
9
+
10
+ ## [0.25.1](https://github.com/tutur3u/platform/compare/utils-v0.25.0...utils-v0.25.1) (2026-08-15)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **contacts:** harden invited profile access ([19878d0](https://github.com/tutur3u/platform/commit/19878d06466eafc8cef74af52e14ddbdb6eef510))
16
+
3
17
  ## [0.25.0](https://github.com/tutur3u/platform/compare/utils-v0.24.2...utils-v0.25.0) (2026-08-14)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.25.0",
4
+ "version": "0.25.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -24,7 +24,7 @@
24
24
  "@tiptap/react": "3.30.0",
25
25
  "@tuturuuu/google": "0.1.0",
26
26
  "@tuturuuu/icons": "0.1.0",
27
- "@tuturuuu/internal-api": "0.30.0",
27
+ "@tuturuuu/internal-api": "0.31.0",
28
28
  "@tuturuuu/supabase": "0.5.0",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
30
  "@upstash/redis": "^1.38.2",
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getTicketIdentifier } from '../task-helper';
3
+
4
+ describe('getTicketIdentifier', () => {
5
+ it('keeps only the prefix while an optimistic task has no display number', () => {
6
+ expect(getTicketIdentifier('VHP', undefined)).toBe('VHP');
7
+ expect(getTicketIdentifier(null, undefined)).toBe('TASK');
8
+ });
9
+
10
+ it('keeps the full identifier after the display number is assigned', () => {
11
+ expect(getTicketIdentifier('VHP', 247)).toBe('VHP-247');
12
+ });
13
+ });
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
3
3
  import {
4
4
  createTuturuuuNextConfig,
5
5
  createTuturuuuWebWorkspaceApiRewrites,
6
+ getTuturuuuNextOptimizePackageImports,
6
7
  isTuturuuuNextCacheComponentsEnabled,
7
8
  isTuturuuuNextReactCompilerEnabled,
8
9
  isTuturuuuTurbopackRustReactCompilerEnabled,
@@ -174,6 +175,25 @@ describe('createTuturuuuNextConfig', () => {
174
175
  ]);
175
176
  });
176
177
 
178
+ it('stabilizes the webpack-only E2E satellite fallback', () => {
179
+ const originalWebpackBuild = process.env.NEXT_WEBPACK_BUILD;
180
+
181
+ try {
182
+ process.env.NEXT_WEBPACK_BUILD = '1';
183
+ const config = createTuturuuuNextConfig();
184
+
185
+ expect(config.experimental?.devMemoryThresholdRestart).toBe(false);
186
+ expect(config.experimental?.optimizePackageImports).toEqual([]);
187
+ expect(config.experimental?.turbopackRustReactCompiler).toBe(false);
188
+ } finally {
189
+ if (originalWebpackBuild === undefined) {
190
+ delete process.env.NEXT_WEBPACK_BUILD;
191
+ } else {
192
+ process.env.NEXT_WEBPACK_BUILD = originalWebpackBuild;
193
+ }
194
+ }
195
+ });
196
+
177
197
  it('dedupes image remote patterns while preserving app additions', () => {
178
198
  const config = createTuturuuuNextConfig({
179
199
  images: {
@@ -307,6 +327,22 @@ describe('isTuturuuuTurbopackRustReactCompilerEnabled', () => {
307
327
  });
308
328
  });
309
329
 
330
+ describe('getTuturuuuNextOptimizePackageImports', () => {
331
+ it('keeps shared package optimization for Turbopack', () => {
332
+ expect(getTuturuuuNextOptimizePackageImports(['@tuturuuu/ui'], {})).toEqual(
333
+ [...TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS, '@tuturuuu/ui']
334
+ );
335
+ });
336
+
337
+ it('avoids webpack React Server Consumer Manifest barrel corruption', () => {
338
+ expect(
339
+ getTuturuuuNextOptimizePackageImports(['@tuturuuu/ui'], {
340
+ NEXT_WEBPACK_BUILD: '1',
341
+ })
342
+ ).toEqual(['@tuturuuu/ui']);
343
+ });
344
+ });
345
+
310
346
  describe('isTuturuuuNextReactCompilerEnabled', () => {
311
347
  it('keeps React Compiler enabled for next dev', () => {
312
348
  expect(
@@ -96,6 +96,20 @@ export function isTuturuuuTurbopackRustReactCompilerEnabled(
96
96
  return env.NEXT_WEBPACK_BUILD !== '1';
97
97
  }
98
98
 
99
+ export function getTuturuuuNextOptimizePackageImports(
100
+ appImports: readonly string[] | undefined,
101
+ env: Environment = process.env
102
+ ) {
103
+ // Next's webpack dev server can leave optimized workspace-package barrels
104
+ // out of the React Server Consumer Manifest after enough on-demand route
105
+ // compilations. E2E-owned satellites deliberately use webpack, so retain
106
+ // only explicit app imports there and keep the shared optimization for
107
+ // normal Turbopack development and production builds.
108
+ return env.NEXT_WEBPACK_BUILD === '1'
109
+ ? [...(appImports ?? [])]
110
+ : mergeStringArrays(TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS, appImports);
111
+ }
112
+
99
113
  export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
100
114
  const experimentalConfig = config.experimental ?? {};
101
115
  const imageConfig = config.images ?? {};
@@ -134,8 +148,10 @@ export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
134
148
  turbopackRustReactCompiler:
135
149
  experimentalConfig.turbopackRustReactCompiler ??
136
150
  isTuturuuuTurbopackRustReactCompilerEnabled(),
137
- optimizePackageImports: mergeStringArrays(
138
- TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
151
+ devMemoryThresholdRestart:
152
+ experimentalConfig.devMemoryThresholdRestart ??
153
+ process.env.NEXT_WEBPACK_BUILD !== '1',
154
+ optimizePackageImports: getTuturuuuNextOptimizePackageImports(
139
155
  experimentalConfig.optimizePackageImports
140
156
  ),
141
157
  },
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  describe('platform release metadata', () => {
9
9
  it('uses the centralized shared browser app version', () => {
10
- expect(TUTURUUU_PLATFORM_VERSION).toBe('0.37.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.39.0'); // x-release-please-version
11
11
  });
12
12
 
13
13
  it('preserves the source timestamp in builtAt and derives a short hash', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.37.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.39.0'; // x-release-please-version
4
4
 
5
5
  export type PlatformBuildMetadataInput = {
6
6
  /** ISO timestamp of the source commit used for the build, not deployment time. */
@@ -4,9 +4,12 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
4
4
 
5
5
  export function getTicketIdentifier(
6
6
  prefix: string | null | undefined,
7
- displayNumber: number
7
+ displayNumber: number | null | undefined
8
8
  ): string {
9
9
  const effectivePrefix = prefix?.trim() || 'TASK';
10
+ if (typeof displayNumber !== 'number' || !Number.isFinite(displayNumber)) {
11
+ return effectivePrefix.toUpperCase();
12
+ }
10
13
  return `${effectivePrefix}-${displayNumber}`.toUpperCase();
11
14
  }
12
15
 
@@ -50,4 +50,55 @@ describe('getWorkspaceUserLinkForUser', () => {
50
50
  expect(mocks.createClient).not.toHaveBeenCalled();
51
51
  expect(mocks.createAdminClient).not.toHaveBeenCalled();
52
52
  });
53
+
54
+ it('returns a link created by a concurrent repair even when its own RPC errors', async () => {
55
+ const repairedLink = {
56
+ created_at: '2026-08-14T00:00:00.000Z',
57
+ platform_user_id: 'platform-user-1',
58
+ virtual_user_id: 'virtual-user-1',
59
+ ws_id: 'workspace-1',
60
+ };
61
+ const initialMaybeSingle = vi
62
+ .fn()
63
+ .mockResolvedValue({ data: null, error: null });
64
+ const repairedMaybeSingle = vi
65
+ .fn()
66
+ .mockResolvedValue({ data: repairedLink, error: null });
67
+ const createLookupClient = (maybeSingle: ReturnType<typeof vi.fn>) => ({
68
+ from: vi.fn(() => ({
69
+ select: vi.fn(() => ({
70
+ eq: vi.fn(() => ({
71
+ eq: vi.fn(() => ({
72
+ limit: vi.fn(() => ({ maybeSingle })),
73
+ })),
74
+ })),
75
+ })),
76
+ })),
77
+ });
78
+ const authorizationClient = createLookupClient(initialMaybeSingle);
79
+ const adminClient = {
80
+ ...createLookupClient(repairedMaybeSingle),
81
+ rpc: vi.fn().mockResolvedValue({
82
+ error: new Error('duplicate repair already completed'),
83
+ }),
84
+ };
85
+ mocks.verifyWorkspaceMembershipType.mockResolvedValue({
86
+ membershipType: 'MEMBER',
87
+ ok: true,
88
+ });
89
+ mocks.createAdminClient.mockResolvedValue(adminClient);
90
+
91
+ await expect(
92
+ getWorkspaceUserLinkForUser('workspace-1', 'platform-user-1', {
93
+ authorizationClient:
94
+ authorizationClient as unknown as TypedSupabaseClient,
95
+ })
96
+ ).resolves.toEqual(repairedLink);
97
+
98
+ expect(adminClient.rpc).toHaveBeenCalledWith('ensure_workspace_user_link', {
99
+ target_user_id: 'platform-user-1',
100
+ target_ws_id: 'workspace-1',
101
+ });
102
+ expect(repairedMaybeSingle).toHaveBeenCalledOnce();
103
+ });
53
104
  });
@@ -37,6 +37,25 @@ function toLink(row: LinkRow): WorkspaceUserLink {
37
37
  };
38
38
  }
39
39
 
40
+ async function readWorkspaceUserLink(
41
+ supabase: TypedSupabaseClient,
42
+ wsId: string,
43
+ userId: string
44
+ ) {
45
+ const { data, error } = await supabase
46
+ .from('workspace_user_linked_users')
47
+ .select(LINK_SELECT)
48
+ .eq('platform_user_id', userId)
49
+ .eq('ws_id', wsId)
50
+ .limit(1)
51
+ .maybeSingle();
52
+
53
+ return {
54
+ error,
55
+ link: data ? toLink(data as LinkRow) : null,
56
+ };
57
+ }
58
+
40
59
  /**
41
60
  * Resolves the workspace-user link for an EXPLICIT platform user id.
42
61
  *
@@ -60,15 +79,13 @@ export async function getWorkspaceUserLinkForUser(
60
79
  const supabase = authorizationClient ?? (await createClient());
61
80
  const resolvedWsId = resolveWorkspaceId(wsId);
62
81
 
63
- const { data: workspaceUser } = await supabase
64
- .from('workspace_user_linked_users')
65
- .select(LINK_SELECT)
66
- .eq('platform_user_id', userId)
67
- .eq('ws_id', resolvedWsId)
68
- .limit(1)
69
- .maybeSingle();
82
+ const initialLookup = await readWorkspaceUserLink(
83
+ supabase,
84
+ resolvedWsId,
85
+ userId
86
+ );
70
87
 
71
- if (workspaceUser) return toLink(workspaceUser as LinkRow);
88
+ if (initialLookup.link) return initialLookup.link;
72
89
 
73
90
  if (!autoRepair) return null;
74
91
 
@@ -94,23 +111,29 @@ export async function getWorkspaceUserLinkForUser(
94
111
  target_ws_id: resolvedWsId,
95
112
  });
96
113
 
114
+ // Always re-read with the service-role client, including after an RPC
115
+ // error. Another layout/page/API request may have repaired the same member
116
+ // concurrently, and returning null here would turn a valid membership into
117
+ // a route-level 404 or an empty profile-scoped module.
118
+ const repairedLookup = await readWorkspaceUserLink(
119
+ sbAdmin,
120
+ resolvedWsId,
121
+ userId
122
+ );
123
+
124
+ if (repairedLookup.link) return repairedLookup.link;
125
+
97
126
  if (repairError) {
98
127
  console.error(
99
128
  '[getWorkspaceUserLinkForUser] Failed to auto-repair workspace user link:',
100
129
  repairError
101
130
  );
102
- return null;
131
+ } else if (repairedLookup.error) {
132
+ console.error(
133
+ '[getWorkspaceUserLinkForUser] Failed to fetch repaired workspace user link:',
134
+ repairedLookup.error
135
+ );
103
136
  }
104
-
105
- const { data: repairedUser } = await supabase
106
- .from('workspace_user_linked_users')
107
- .select(LINK_SELECT)
108
- .eq('platform_user_id', userId)
109
- .eq('ws_id', resolvedWsId)
110
- .limit(1)
111
- .maybeSingle();
112
-
113
- if (repairedUser) return toLink(repairedUser as LinkRow);
114
137
  } catch (err) {
115
138
  console.error(
116
139
  '[getWorkspaceUserLinkForUser] Error during auto-repair:',