@tuturuuu/utils 0.26.0 → 0.27.1
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 +15 -0
- package/biome.json +1 -1
- package/package.json +14 -15
- package/src/__tests__/format.test.ts +43 -0
- package/src/__tests__/workspace-helper.test.ts +38 -0
- package/src/common/metadata.test.ts +7 -3
- package/src/format.ts +1 -6
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
- package/src/workspace-helper.ts +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.27.1](https://github.com/tutur3u/platform/compare/utils-v0.27.0...utils-v0.27.1) (2026-09-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **ai:** type workspace resolution failures ([10b9141](https://github.com/tutur3u/platform/commit/10b9141999894f25f7c4e0ca81a9d7e75dbfb2c1))
|
|
9
|
+
* **rewise:** bind AI work to selected workspace ([#5208](https://github.com/tutur3u/platform/issues/5208)) ([5f1d8f3](https://github.com/tutur3u/platform/commit/5f1d8f3544e87e78b6555cd679c36f2878d0dab9))
|
|
10
|
+
|
|
11
|
+
## [0.27.0](https://github.com/tutur3u/platform/compare/utils-v0.26.0...utils-v0.27.0) (2026-08-27)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **web:** localize metadata for every product page ([#5159](https://github.com/tutur3u/platform/issues/5159)) ([2862e0c](https://github.com/tutur3u/platform/commit/2862e0c771495fa7f2f7a6cc83937272d68336b5))
|
|
17
|
+
|
|
3
18
|
## [0.26.0](https://github.com/tutur3u/platform/compare/utils-v0.25.5...utils-v0.26.0) (2026-08-26)
|
|
4
19
|
|
|
5
20
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -17,43 +17,42 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@dnd-kit/core": "^6.3.1",
|
|
20
|
-
"@noble/ciphers": "^2.
|
|
21
|
-
"@noble/hashes": "^2.
|
|
22
|
-
"@tanstack/react-query": "^5.102.
|
|
23
|
-
"@tiptap/core": "3.
|
|
24
|
-
"@tiptap/react": "3.
|
|
20
|
+
"@noble/ciphers": "^2.4.0",
|
|
21
|
+
"@noble/hashes": "^2.4.0",
|
|
22
|
+
"@tanstack/react-query": "^5.102.8",
|
|
23
|
+
"@tiptap/core": "3.31.0",
|
|
24
|
+
"@tiptap/react": "3.31.0",
|
|
25
25
|
"@tuturuuu/google": "0.1.0",
|
|
26
26
|
"@tuturuuu/icons": "0.1.0",
|
|
27
|
-
"@tuturuuu/internal-api": "0.33.
|
|
27
|
+
"@tuturuuu/internal-api": "0.33.1",
|
|
28
28
|
"@tuturuuu/supabase": "0.5.1",
|
|
29
29
|
"@upstash/ratelimit": "^2.0.8",
|
|
30
|
-
"@upstash/redis": "^1.38.
|
|
31
|
-
"
|
|
30
|
+
"@upstash/redis": "^1.38.3",
|
|
31
|
+
"cn": "^0.2.3",
|
|
32
32
|
"date-fns": "^4.4.0",
|
|
33
33
|
"dayjs": "^1.11.23",
|
|
34
34
|
"diff": "^9.0.0",
|
|
35
35
|
"effect": "^3.22.1",
|
|
36
36
|
"moment": "^2.30.1",
|
|
37
|
-
"next": "^16.3.
|
|
37
|
+
"next": "^16.3.4",
|
|
38
38
|
"prosemirror-model": "^1.25.11",
|
|
39
39
|
"react": "^19.2.8",
|
|
40
40
|
"react-dom": "^19.2.8",
|
|
41
|
-
"tailwind-merge": "^3.6.0",
|
|
42
41
|
"uuid": "^14.0.2",
|
|
43
42
|
"y-prosemirror": "^1.3.7",
|
|
44
43
|
"yjs": "^13.6.32",
|
|
45
|
-
"zod": "^4.4
|
|
44
|
+
"zod": "^4.5.4"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@tuturuuu/types": "0.28.0",
|
|
49
48
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
50
49
|
"@types/diff": "^8.0.0",
|
|
51
|
-
"@types/node": "^26.
|
|
50
|
+
"@types/node": "^26.4.1",
|
|
52
51
|
"@types/react": "^19.2.18",
|
|
53
|
-
"@vitejs/plugin-react": "^6.1.
|
|
52
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
54
53
|
"typescript": "7.0.2",
|
|
55
54
|
"vite": "^8.2.2",
|
|
56
|
-
"vitest": "^
|
|
55
|
+
"vitest": "^5.0.0"
|
|
57
56
|
},
|
|
58
57
|
"exports": {
|
|
59
58
|
"./timezones": "./src/timezones.json",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
capitalize,
|
|
4
|
+
cn,
|
|
4
5
|
formatBytes,
|
|
5
6
|
formatCurrency,
|
|
6
7
|
formatDuration,
|
|
@@ -10,6 +11,48 @@ import {
|
|
|
10
11
|
} from '../format';
|
|
11
12
|
|
|
12
13
|
describe('Format Utilities', () => {
|
|
14
|
+
describe('cn', () => {
|
|
15
|
+
it('joins conditional class values with clsx-compatible semantics', () => {
|
|
16
|
+
expect(
|
|
17
|
+
cn('flex', false, null, undefined, [
|
|
18
|
+
'items-center',
|
|
19
|
+
{ 'opacity-50': true, hidden: false },
|
|
20
|
+
])
|
|
21
|
+
).toBe('flex items-center opacity-50');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('resolves conflicting Tailwind utilities using the last value', () => {
|
|
25
|
+
expect(cn('px-2 py-1', 'px-4', 'py-3')).toBe('px-4 py-3');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps independent responsive and state variants', () => {
|
|
29
|
+
expect(
|
|
30
|
+
cn(
|
|
31
|
+
'text-sm hover:text-base md:text-lg',
|
|
32
|
+
'text-xl hover:text-2xl md:text-3xl'
|
|
33
|
+
)
|
|
34
|
+
).toBe('text-xl hover:text-2xl md:text-3xl');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('preserves non-conflicting variants from separate inputs', () => {
|
|
38
|
+
expect(cn('text-sm hover:text-base', 'md:text-lg')).toBe(
|
|
39
|
+
'text-sm hover:text-base md:text-lg'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('resolves arbitrary values and important modifiers', () => {
|
|
44
|
+
expect(cn('w-[10px] w-[var(--panel-width)]', '!p-4 p-2')).toBe(
|
|
45
|
+
'w-[var(--panel-width)] !p-4 p-2'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves non-Tailwind application classes', () => {
|
|
50
|
+
expect(cn('app-shell', 'feature-card', { selected: true })).toBe(
|
|
51
|
+
'app-shell feature-card selected'
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
13
56
|
describe('capitalize', () => {
|
|
14
57
|
it('capitalizes first letter of a word', () => {
|
|
15
58
|
expect(capitalize('hello')).toBe('Hello');
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
isPersonalWorkspace,
|
|
18
18
|
isWorkspaceUuidLiteral,
|
|
19
19
|
normalizeWorkspaceId,
|
|
20
|
+
WorkspaceAuthError,
|
|
21
|
+
WorkspaceNotFoundError,
|
|
22
|
+
WorkspaceResolutionError,
|
|
20
23
|
} from '../workspace-helper';
|
|
21
24
|
|
|
22
25
|
describe('isWorkspaceUuidLiteral', () => {
|
|
@@ -410,6 +413,41 @@ describe('normalizeWorkspaceId', () => {
|
|
|
410
413
|
expect(query.eq).toHaveBeenCalledWith('workspace_members.type', 'MEMBER');
|
|
411
414
|
});
|
|
412
415
|
|
|
416
|
+
it('uses a typed error when personal workspace authentication is missing', async () => {
|
|
417
|
+
mockCreateClient.mockResolvedValue({
|
|
418
|
+
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await expect(normalizeWorkspaceId('personal')).rejects.toBeInstanceOf(
|
|
422
|
+
WorkspaceAuthError
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it.each([
|
|
427
|
+
[new Error('database unavailable'), WorkspaceResolutionError],
|
|
428
|
+
[null, WorkspaceNotFoundError],
|
|
429
|
+
])('distinguishes personal lookup failures', async (error, ErrorType) => {
|
|
430
|
+
const query = {
|
|
431
|
+
eq: vi.fn(),
|
|
432
|
+
maybeSingle: vi.fn().mockResolvedValue({ data: null, error }),
|
|
433
|
+
select: vi.fn(),
|
|
434
|
+
};
|
|
435
|
+
query.select.mockReturnValue(query);
|
|
436
|
+
query.eq.mockReturnValue(query);
|
|
437
|
+
mockCreateClient.mockResolvedValue({
|
|
438
|
+
auth: {
|
|
439
|
+
getUser: vi.fn().mockResolvedValue({
|
|
440
|
+
data: { user: { id: 'user-1' } },
|
|
441
|
+
}),
|
|
442
|
+
},
|
|
443
|
+
from: vi.fn(() => query),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await expect(normalizeWorkspaceId('personal')).rejects.toBeInstanceOf(
|
|
447
|
+
ErrorType
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
413
451
|
it('resolves handle via admin fallback when request-scoped lookup cannot see workspace', async () => {
|
|
414
452
|
const requestScopedQuery = {
|
|
415
453
|
select: vi.fn(),
|
|
@@ -320,13 +320,17 @@ describe('application metadata coverage', () => {
|
|
|
320
320
|
(entry) =>
|
|
321
321
|
entry.isFile() &&
|
|
322
322
|
entry.name === 'layout.tsx' &&
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
// Either sanctioned helper counts. `createLocalizedMarketingMetadata`
|
|
324
|
+
// resolves the copy from the message bundles instead of a literal,
|
|
325
|
+
// but produces the same metadata contract — matching only the exact
|
|
326
|
+
// string silently dropped every localized layout from this check.
|
|
327
|
+
/create(?:Localized)?MarketingMetadata/.test(
|
|
328
|
+
readFileSync(resolve(entry.parentPath, entry.name), 'utf8')
|
|
325
329
|
)
|
|
326
330
|
)
|
|
327
331
|
.map((entry) => resolve(entry.parentPath, entry.name));
|
|
328
332
|
|
|
329
|
-
expect(marketingLayouts).toHaveLength(
|
|
333
|
+
expect(marketingLayouts).toHaveLength(45);
|
|
330
334
|
for (const layoutPath of marketingLayouts) {
|
|
331
335
|
expect(readFileSync(layoutPath, 'utf8')).toMatch(/pathname:\s*['`]/);
|
|
332
336
|
}
|
package/src/format.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import { twMerge } from 'tailwind-merge';
|
|
1
|
+
export { cn } from 'cn';
|
|
3
2
|
|
|
4
3
|
import { getCurrencyLocale as getCurrencyLocaleFromConfig } from './currencies';
|
|
5
4
|
|
|
6
|
-
export function cn(...inputs: ClassValue[]): string {
|
|
7
|
-
return twMerge(clsx(inputs));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
export const capitalize = (s?: string | null): string => {
|
|
11
6
|
if (!s) return '';
|
|
12
7
|
if (s.length === 0) return s;
|
|
@@ -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.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.45.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('preserves the source timestamp in builtAt and derives a short hash', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.45.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. */
|
package/src/workspace-helper.ts
CHANGED
|
@@ -28,6 +28,20 @@ export class WorkspaceAuthError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export class WorkspaceNotFoundError extends Error {
|
|
32
|
+
constructor(message = 'Workspace not found') {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'WorkspaceNotFoundError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class WorkspaceResolutionError extends Error {
|
|
39
|
+
constructor(message = 'Workspace resolution failed') {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'WorkspaceResolutionError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export class WorkspaceAccessError extends Error {
|
|
32
46
|
constructor(message = 'Workspace access denied') {
|
|
33
47
|
super(message);
|
|
@@ -1096,7 +1110,7 @@ export async function normalizeWorkspaceId(
|
|
|
1096
1110
|
const principal = await resolveAuthenticatedPrincipal(sb);
|
|
1097
1111
|
|
|
1098
1112
|
if (!principal) {
|
|
1099
|
-
throw new
|
|
1113
|
+
throw new WorkspaceAuthError();
|
|
1100
1114
|
}
|
|
1101
1115
|
|
|
1102
1116
|
const userId = principal.id;
|
|
@@ -1109,8 +1123,11 @@ export async function normalizeWorkspaceId(
|
|
|
1109
1123
|
.eq('workspace_members.type', 'MEMBER')
|
|
1110
1124
|
.maybeSingle();
|
|
1111
1125
|
|
|
1112
|
-
if (error
|
|
1113
|
-
throw new
|
|
1126
|
+
if (error) {
|
|
1127
|
+
throw new WorkspaceResolutionError('Personal workspace lookup failed');
|
|
1128
|
+
}
|
|
1129
|
+
if (!workspace) {
|
|
1130
|
+
throw new WorkspaceNotFoundError('Personal workspace not found');
|
|
1114
1131
|
}
|
|
1115
1132
|
|
|
1116
1133
|
return workspace.id;
|