nexu-app 2.0.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.
- package/README.md +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
capitalize,
|
|
5
|
+
slugify,
|
|
6
|
+
truncate,
|
|
7
|
+
pick,
|
|
8
|
+
omit,
|
|
9
|
+
isEmpty,
|
|
10
|
+
chunk,
|
|
11
|
+
unique,
|
|
12
|
+
isEmail,
|
|
13
|
+
isUrl,
|
|
14
|
+
clamp,
|
|
15
|
+
daysBetween,
|
|
16
|
+
} from './index';
|
|
17
|
+
|
|
18
|
+
describe('String utilities', () => {
|
|
19
|
+
describe('capitalize', () => {
|
|
20
|
+
it('should capitalize first letter', () => {
|
|
21
|
+
expect(capitalize('hello')).toBe('Hello');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return empty string for empty input', () => {
|
|
25
|
+
expect(capitalize('')).toBe('');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('slugify', () => {
|
|
30
|
+
it('should convert string to slug', () => {
|
|
31
|
+
expect(slugify('Hello World')).toBe('hello-world');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should remove special characters', () => {
|
|
35
|
+
expect(slugify('Hello! World?')).toBe('hello-world');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('truncate', () => {
|
|
40
|
+
it('should truncate long strings', () => {
|
|
41
|
+
expect(truncate('Hello World', 8)).toBe('Hello...');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should not truncate short strings', () => {
|
|
45
|
+
expect(truncate('Hello', 10)).toBe('Hello');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Object utilities', () => {
|
|
51
|
+
describe('pick', () => {
|
|
52
|
+
it('should pick specified keys', () => {
|
|
53
|
+
const obj = { a: 1, b: 2, c: 3 };
|
|
54
|
+
expect(pick(obj, ['a', 'c'])).toEqual({ a: 1, c: 3 });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('omit', () => {
|
|
59
|
+
it('should omit specified keys', () => {
|
|
60
|
+
const obj = { a: 1, b: 2, c: 3 };
|
|
61
|
+
expect(omit(obj, ['b'])).toEqual({ a: 1, c: 3 });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('isEmpty', () => {
|
|
66
|
+
it('should return true for empty values', () => {
|
|
67
|
+
expect(isEmpty(null)).toBe(true);
|
|
68
|
+
expect(isEmpty(undefined)).toBe(true);
|
|
69
|
+
expect(isEmpty('')).toBe(true);
|
|
70
|
+
expect(isEmpty([])).toBe(true);
|
|
71
|
+
expect(isEmpty({})).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return false for non-empty values', () => {
|
|
75
|
+
expect(isEmpty('hello')).toBe(false);
|
|
76
|
+
expect(isEmpty([1])).toBe(false);
|
|
77
|
+
expect(isEmpty({ a: 1 })).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Array utilities', () => {
|
|
83
|
+
describe('chunk', () => {
|
|
84
|
+
it('should split array into chunks', () => {
|
|
85
|
+
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('unique', () => {
|
|
90
|
+
it('should remove duplicates', () => {
|
|
91
|
+
expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Validation utilities', () => {
|
|
97
|
+
describe('isEmail', () => {
|
|
98
|
+
it('should validate emails', () => {
|
|
99
|
+
expect(isEmail('test@example.com')).toBe(true);
|
|
100
|
+
expect(isEmail('invalid')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('isUrl', () => {
|
|
105
|
+
it('should validate URLs', () => {
|
|
106
|
+
expect(isUrl('https://example.com')).toBe(true);
|
|
107
|
+
expect(isUrl('not-a-url')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('Number utilities', () => {
|
|
113
|
+
describe('clamp', () => {
|
|
114
|
+
it('should clamp values', () => {
|
|
115
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
116
|
+
expect(clamp(-5, 0, 10)).toBe(0);
|
|
117
|
+
expect(clamp(15, 0, 10)).toBe(10);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Date utilities', () => {
|
|
123
|
+
describe('daysBetween', () => {
|
|
124
|
+
it('should calculate days between dates', () => {
|
|
125
|
+
const date1 = new Date('2024-01-01');
|
|
126
|
+
const date2 = new Date('2024-01-10');
|
|
127
|
+
expect(daysBetween(date1, date2)).toBe(9);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// String utilities
|
|
2
|
+
export function capitalize(str: string): string {
|
|
3
|
+
if (!str) return '';
|
|
4
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function slugify(str: string): string {
|
|
8
|
+
return str
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/[^\w\s-]/g, '')
|
|
12
|
+
.replace(/[\s_-]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function truncate(str: string, length: number, suffix = '...'): string {
|
|
17
|
+
if (str.length <= length) return str;
|
|
18
|
+
return str.slice(0, length - suffix.length) + suffix;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Object utilities
|
|
22
|
+
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
|
23
|
+
const result = {} as Pick<T, K>;
|
|
24
|
+
keys.forEach(key => {
|
|
25
|
+
if (key in obj) {
|
|
26
|
+
result[key] = obj[key];
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
|
33
|
+
const result = { ...obj };
|
|
34
|
+
keys.forEach(key => {
|
|
35
|
+
delete result[key];
|
|
36
|
+
});
|
|
37
|
+
return result as Omit<T, K>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isEmpty(value: unknown): boolean {
|
|
41
|
+
if (value == null) return true;
|
|
42
|
+
if (typeof value === 'string' || Array.isArray(value)) return value.length === 0;
|
|
43
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Array utilities
|
|
48
|
+
export function chunk<T>(array: T[], size: number): T[][] {
|
|
49
|
+
const chunks: T[][] = [];
|
|
50
|
+
for (let i = 0; i < array.length; i += size) {
|
|
51
|
+
chunks.push(array.slice(i, i + size));
|
|
52
|
+
}
|
|
53
|
+
return chunks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function unique<T>(array: T[]): T[] {
|
|
57
|
+
return [...new Set(array)];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function groupBy<T, K extends string | number | symbol>(
|
|
61
|
+
array: T[],
|
|
62
|
+
keyFn: (item: T) => K
|
|
63
|
+
): Record<K, T[]> {
|
|
64
|
+
return array.reduce(
|
|
65
|
+
(acc, item) => {
|
|
66
|
+
const key = keyFn(item);
|
|
67
|
+
if (!acc[key]) {
|
|
68
|
+
acc[key] = [];
|
|
69
|
+
}
|
|
70
|
+
acc[key].push(item);
|
|
71
|
+
return acc;
|
|
72
|
+
},
|
|
73
|
+
{} as Record<K, T[]>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Date utilities
|
|
78
|
+
export function formatDate(date: Date, locale = 'en-US'): string {
|
|
79
|
+
return new Intl.DateTimeFormat(locale, {
|
|
80
|
+
year: 'numeric',
|
|
81
|
+
month: 'long',
|
|
82
|
+
day: 'numeric',
|
|
83
|
+
}).format(date);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isValidDate(date: unknown): date is Date {
|
|
87
|
+
return date instanceof Date && !isNaN(date.getTime());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function daysBetween(date1: Date, date2: Date): number {
|
|
91
|
+
const oneDay = 24 * 60 * 60 * 1000;
|
|
92
|
+
return Math.round(Math.abs((date1.getTime() - date2.getTime()) / oneDay));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Async utilities
|
|
96
|
+
export function sleep(ms: number): Promise<void> {
|
|
97
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function retry<T>(
|
|
101
|
+
fn: () => Promise<T>,
|
|
102
|
+
options: { attempts?: number; delay?: number } = {}
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
const { attempts = 3, delay = 1000 } = options;
|
|
105
|
+
let lastError: Error | undefined;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < attempts; i++) {
|
|
108
|
+
try {
|
|
109
|
+
return await fn();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
if (i < attempts - 1) {
|
|
113
|
+
await sleep(delay);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw lastError;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validation utilities
|
|
122
|
+
export function isEmail(email: string): boolean {
|
|
123
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
124
|
+
return emailRegex.test(email);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isUrl(url: string): boolean {
|
|
128
|
+
try {
|
|
129
|
+
new URL(url);
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Number utilities
|
|
137
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
138
|
+
return Math.min(Math.max(value, min), max);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function randomInt(min: number, max: number): number {
|
|
142
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatNumber(num: number, locale = 'en-US'): string {
|
|
146
|
+
return new Intl.NumberFormat(locale).format(num);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatCurrency(amount: number, currency = 'USD', locale = 'en-US'): string {
|
|
150
|
+
return new Intl.NumberFormat(locale, {
|
|
151
|
+
style: 'currency',
|
|
152
|
+
currency,
|
|
153
|
+
}).format(amount);
|
|
154
|
+
}
|