@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 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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.11/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.26.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.3.0",
21
- "@noble/hashes": "^2.3.0",
22
- "@tanstack/react-query": "^5.102.1",
23
- "@tiptap/core": "3.30.2",
24
- "@tiptap/react": "3.30.2",
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.0",
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.2",
31
- "clsx": "^2.1.1",
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.2",
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.3"
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.2.0",
50
+ "@types/node": "^26.4.1",
52
51
  "@types/react": "^19.2.18",
53
- "@vitejs/plugin-react": "^6.1.0",
52
+ "@vitejs/plugin-react": "^6.1.1",
54
53
  "typescript": "7.0.2",
55
54
  "vite": "^8.2.2",
56
- "vitest": "^4.1.11"
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
- readFileSync(resolve(entry.parentPath, entry.name), 'utf8').includes(
324
- 'createMarketingMetadata'
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(38);
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
- import { type ClassValue, clsx } from 'clsx';
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.42.0'); // x-release-please-version
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', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.42.0'; // x-release-please-version
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. */
@@ -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 Error('User not authenticated');
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 || !workspace) {
1113
- throw new Error('Personal workspace not found');
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;