create-top-secret-starter 0.0.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.
Files changed (57) hide show
  1. package/index.js +117 -0
  2. package/package.json +33 -0
  3. package/templates/template-router/.env.example +1 -0
  4. package/templates/template-router/.oxfmtrc.json +13 -0
  5. package/templates/template-router/.oxlintrc.json +8 -0
  6. package/templates/template-router/_gitignore +39 -0
  7. package/templates/template-router/components.json +25 -0
  8. package/templates/template-router/index.html +24 -0
  9. package/templates/template-router/package.json +58 -0
  10. package/templates/template-router/public/favicon.svg +1 -0
  11. package/templates/template-router/src/api/auth/auth.test.ts +46 -0
  12. package/templates/template-router/src/api/auth/guards.ts +18 -0
  13. package/templates/template-router/src/api/auth/index.ts +46 -0
  14. package/templates/template-router/src/api/auth/refresh.test.ts +89 -0
  15. package/templates/template-router/src/api/auth/router-bridge.test.ts +74 -0
  16. package/templates/template-router/src/api/auth/router-bridge.ts +26 -0
  17. package/templates/template-router/src/api/auth/schema.ts +20 -0
  18. package/templates/template-router/src/api/auth/session-store.test.ts +101 -0
  19. package/templates/template-router/src/api/auth/session-store.ts +56 -0
  20. package/templates/template-router/src/api/index.ts +51 -0
  21. package/templates/template-router/src/components/status-page.tsx +68 -0
  22. package/templates/template-router/src/components/ui/button.tsx +58 -0
  23. package/templates/template-router/src/components/ui/dialog.tsx +136 -0
  24. package/templates/template-router/src/components/ui/empty.tsx +94 -0
  25. package/templates/template-router/src/components/ui/field.tsx +222 -0
  26. package/templates/template-router/src/components/ui/input.tsx +20 -0
  27. package/templates/template-router/src/components/ui/label.tsx +18 -0
  28. package/templates/template-router/src/components/ui/select.tsx +188 -0
  29. package/templates/template-router/src/components/ui/separator.tsx +21 -0
  30. package/templates/template-router/src/components/ui/skeleton.tsx +13 -0
  31. package/templates/template-router/src/components/ui/sonner.tsx +43 -0
  32. package/templates/template-router/src/components/ui/tooltip.tsx +52 -0
  33. package/templates/template-router/src/env.ts +20 -0
  34. package/templates/template-router/src/index.css +134 -0
  35. package/templates/template-router/src/lib/query-client.test.ts +82 -0
  36. package/templates/template-router/src/lib/query-client.ts +30 -0
  37. package/templates/template-router/src/lib/single-flight.test.ts +48 -0
  38. package/templates/template-router/src/lib/single-flight.ts +9 -0
  39. package/templates/template-router/src/lib/utils.ts +1 -0
  40. package/templates/template-router/src/main.tsx +26 -0
  41. package/templates/template-router/src/mocks/mock-server.ts +149 -0
  42. package/templates/template-router/src/providers/index.tsx +27 -0
  43. package/templates/template-router/src/providers/theme.test.tsx +68 -0
  44. package/templates/template-router/src/providers/theme.tsx +76 -0
  45. package/templates/template-router/src/routeTree.gen.ts +102 -0
  46. package/templates/template-router/src/router.tsx +29 -0
  47. package/templates/template-router/src/routes/-error.tsx +34 -0
  48. package/templates/template-router/src/routes/-not-found.tsx +27 -0
  49. package/templates/template-router/src/routes/__root.tsx +15 -0
  50. package/templates/template-router/src/routes/_authenticated/index.tsx +9 -0
  51. package/templates/template-router/src/routes/_authenticated.tsx +38 -0
  52. package/templates/template-router/src/routes/sign-in.tsx +100 -0
  53. package/templates/template-router/src/test/setup.ts +5 -0
  54. package/templates/template-router/tsconfig.app.json +29 -0
  55. package/templates/template-router/tsconfig.json +7 -0
  56. package/templates/template-router/tsconfig.node.json +23 -0
  57. package/templates/template-router/vite.config.ts +33 -0
@@ -0,0 +1,102 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
13
+ import { Route as SignInRouteImport } from './routes/sign-in'
14
+ import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
15
+
16
+ const AuthenticatedRoute = AuthenticatedRouteImport.update({
17
+ id: '/_authenticated',
18
+ getParentRoute: () => rootRouteImport,
19
+ } as any)
20
+ const SignInRoute = SignInRouteImport.update({
21
+ id: '/sign-in',
22
+ path: '/sign-in',
23
+ getParentRoute: () => rootRouteImport,
24
+ } as any)
25
+ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
26
+ id: '/',
27
+ path: '/',
28
+ getParentRoute: () => AuthenticatedRoute,
29
+ } as any)
30
+
31
+ export interface FileRoutesByFullPath {
32
+ '/': typeof AuthenticatedIndexRoute
33
+ '/sign-in': typeof SignInRoute
34
+ }
35
+ export interface FileRoutesByTo {
36
+ '/sign-in': typeof SignInRoute
37
+ '/': typeof AuthenticatedIndexRoute
38
+ }
39
+ export interface FileRoutesById {
40
+ __root__: typeof rootRouteImport
41
+ '/_authenticated': typeof AuthenticatedRouteWithChildren
42
+ '/sign-in': typeof SignInRoute
43
+ '/_authenticated/': typeof AuthenticatedIndexRoute
44
+ }
45
+ export interface FileRouteTypes {
46
+ fileRoutesByFullPath: FileRoutesByFullPath
47
+ fullPaths: '/' | '/sign-in'
48
+ fileRoutesByTo: FileRoutesByTo
49
+ to: '/sign-in' | '/'
50
+ id: '__root__' | '/_authenticated' | '/sign-in' | '/_authenticated/'
51
+ fileRoutesById: FileRoutesById
52
+ }
53
+ export interface RootRouteChildren {
54
+ AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
55
+ SignInRoute: typeof SignInRoute
56
+ }
57
+
58
+ declare module '@tanstack/react-router' {
59
+ interface FileRoutesByPath {
60
+ '/_authenticated': {
61
+ id: '/_authenticated'
62
+ path: ''
63
+ fullPath: '/'
64
+ preLoaderRoute: typeof AuthenticatedRouteImport
65
+ parentRoute: typeof rootRouteImport
66
+ }
67
+ '/sign-in': {
68
+ id: '/sign-in'
69
+ path: '/sign-in'
70
+ fullPath: '/sign-in'
71
+ preLoaderRoute: typeof SignInRouteImport
72
+ parentRoute: typeof rootRouteImport
73
+ }
74
+ '/_authenticated/': {
75
+ id: '/_authenticated/'
76
+ path: '/'
77
+ fullPath: '/'
78
+ preLoaderRoute: typeof AuthenticatedIndexRouteImport
79
+ parentRoute: typeof AuthenticatedRoute
80
+ }
81
+ }
82
+ }
83
+
84
+ interface AuthenticatedRouteChildren {
85
+ AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
86
+ }
87
+
88
+ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
89
+ AuthenticatedIndexRoute: AuthenticatedIndexRoute,
90
+ }
91
+
92
+ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
93
+ AuthenticatedRouteChildren,
94
+ )
95
+
96
+ const rootRouteChildren: RootRouteChildren = {
97
+ AuthenticatedRoute: AuthenticatedRouteWithChildren,
98
+ SignInRoute: SignInRoute,
99
+ }
100
+ export const routeTree = rootRouteImport
101
+ ._addFileChildren(rootRouteChildren)
102
+ ._addFileTypes<FileRouteTypes>()
@@ -0,0 +1,29 @@
1
+ import { createRouter } from '@tanstack/react-router'
2
+
3
+ import { bindSessionToRouter } from '@/api/auth/router-bridge'
4
+ import { ErrorPage } from '@/routes/-error'
5
+ import { NotFoundPage } from '@/routes/-not-found'
6
+ import { routeTree } from './routeTree.gen'
7
+
8
+ import { queryClient } from './lib/query-client'
9
+
10
+ export const router = createRouter({
11
+ routeTree,
12
+ defaultPreload: 'intent',
13
+ context: { queryClient },
14
+ scrollRestoration: true,
15
+ defaultStructuralSharing: true,
16
+ defaultPreloadStaleTime: 5_000,
17
+ defaultNotFoundComponent: NotFoundPage,
18
+ defaultErrorComponent: ErrorPage
19
+ })
20
+
21
+ // Why the router reacts to session changes is auth knowledge; it lives with the
22
+ // rest of the session policy in src/api/auth/router-bridge.ts.
23
+ bindSessionToRouter(router)
24
+
25
+ declare module '@tanstack/react-router' {
26
+ interface Register {
27
+ router: typeof router
28
+ }
29
+ }
@@ -0,0 +1,34 @@
1
+ import type { ErrorComponentProps } from '@tanstack/react-router'
2
+ import { RotateCcw, TriangleAlert } from 'lucide-react'
3
+
4
+ import { BackHomeButton, StatusPage } from '@/components/status-page'
5
+ import { Button } from '@/components/ui/button'
6
+
7
+ export const ErrorPage = ({ error, reset }: ErrorComponentProps) => {
8
+ const message = error instanceof Error ? error.message : String(error)
9
+
10
+ return (
11
+ <StatusPage
12
+ code='500'
13
+ icon={<TriangleAlert />}
14
+ mediaClassName='bg-destructive/10 text-destructive'
15
+ title='Something went wrong'
16
+ description='An unexpected error occurred. You can try again, or head back home.'
17
+ actions={
18
+ <>
19
+ <BackHomeButton variant='outline' />
20
+ <Button onClick={() => reset()}>
21
+ <RotateCcw data-icon='inline-start' />
22
+ Try again
23
+ </Button>
24
+ </>
25
+ }
26
+ >
27
+ {import.meta.env.DEV && message && (
28
+ <pre className='text-muted-foreground bg-muted mt-2 max-h-40 w-full overflow-auto rounded-lg border p-3 text-left font-mono text-xs whitespace-pre-wrap'>
29
+ {message}
30
+ </pre>
31
+ )}
32
+ </StatusPage>
33
+ )
34
+ }
@@ -0,0 +1,27 @@
1
+ import { useRouter } from '@tanstack/react-router'
2
+ import { ArrowLeft, SearchX } from 'lucide-react'
3
+
4
+ import { BackHomeButton, StatusPage } from '@/components/status-page'
5
+ import { Button } from '@/components/ui/button'
6
+
7
+ export const NotFoundPage = () => {
8
+ const router = useRouter()
9
+
10
+ return (
11
+ <StatusPage
12
+ code='404'
13
+ icon={<SearchX />}
14
+ title='Page not found'
15
+ description='The page you’re looking for doesn’t exist or may have been moved.'
16
+ actions={
17
+ <>
18
+ <Button variant='outline' onClick={() => router.history.back()}>
19
+ <ArrowLeft data-icon='inline-start' />
20
+ Go back
21
+ </Button>
22
+ <BackHomeButton />
23
+ </>
24
+ }
25
+ />
26
+ )
27
+ }
@@ -0,0 +1,15 @@
1
+ import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
2
+
3
+ import type { QueryClient } from '@tanstack/react-query'
4
+
5
+ export type RouterContext = {
6
+ queryClient: QueryClient
7
+ }
8
+
9
+ const RootComponent = () => {
10
+ return <Outlet />
11
+ }
12
+
13
+ export const Route = createRootRouteWithContext<RouterContext>()({
14
+ component: RootComponent
15
+ })
@@ -0,0 +1,9 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ const HomePage = () => {
4
+ return <h1 className='text-2xl font-semibold'>top-secret</h1>
5
+ }
6
+
7
+ export const Route = createFileRoute('/_authenticated/')({
8
+ component: HomePage
9
+ })
@@ -0,0 +1,38 @@
1
+ import { Outlet, createFileRoute, useRouter } from '@tanstack/react-router'
2
+ import { LogOut } from 'lucide-react'
3
+
4
+ import { useLogout } from '@/api/auth'
5
+ import { requireSession } from '@/api/auth/guards'
6
+ import { useSession } from '@/api/auth/session-store'
7
+ import { Button } from '@/components/ui/button'
8
+
9
+ export const Route = createFileRoute('/_authenticated')({
10
+ beforeLoad: ({ location }) => requireSession(location),
11
+ component: AuthenticatedLayout
12
+ })
13
+
14
+ function AuthenticatedLayout() {
15
+ const session = useSession()
16
+ const logout = useLogout()
17
+ const router = useRouter()
18
+
19
+ const signOut = async () => {
20
+ await logout.mutateAsync()
21
+ await router.navigate({ to: '/sign-in' })
22
+ }
23
+
24
+ return (
25
+ <div className='flex min-h-svh flex-col'>
26
+ <header className='flex h-14 items-center justify-between border-b px-6'>
27
+ <span className='text-muted-foreground text-sm'>{session?.user.email}</span>
28
+ <Button variant='outline' size='sm' onClick={signOut} disabled={logout.isPending}>
29
+ <LogOut data-icon='inline-start' />
30
+ Sign out
31
+ </Button>
32
+ </header>
33
+ <main className='flex flex-1 items-center justify-center p-6'>
34
+ <Outlet />
35
+ </main>
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,100 @@
1
+ import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
2
+ import { createFileRoute, useRouter } from '@tanstack/react-router'
3
+ import { useForm } from 'react-hook-form'
4
+ import * as z from 'zod/mini'
5
+
6
+ import { useLogin, type Credentials } from '@/api/auth'
7
+ import { redirectIfAuthenticated } from '@/api/auth/guards'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
10
+ import { Input } from '@/components/ui/input'
11
+
12
+ const searchSchema = z.object({
13
+ redirect: z.catch(z.optional(z.string()), undefined)
14
+ })
15
+
16
+ const formSchema = z.object({
17
+ email: z.string().check(z.minLength(1, 'Email is required'), z.email('Enter a valid email')),
18
+ password: z.string().check(z.minLength(1, 'Password is required'))
19
+ })
20
+
21
+ // Seeded in src/mocks/mock-server.ts — removed alongside the mock backend.
22
+ // `import.meta.env.DEV` is statically replaced at build time, so the literals are
23
+ // tree-shaken out of production bundles rather than merely unused there.
24
+ const DEMO_CREDENTIALS: Credentials = import.meta.env.DEV
25
+ ? { email: 'demo@example.com', password: 'demo1234' }
26
+ : { email: '', password: '' }
27
+
28
+ const FIELDS = [
29
+ { name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
30
+ { name: 'password', label: 'Password', type: 'password', autoComplete: 'current-password' }
31
+ ] as const satisfies ReadonlyArray<{
32
+ name: keyof Credentials
33
+ label: string
34
+ type: string
35
+ autoComplete: string
36
+ }>
37
+
38
+ export const Route = createFileRoute('/sign-in')({
39
+ validateSearch: searchSchema,
40
+ beforeLoad: ({ search }) => redirectIfAuthenticated(search),
41
+ component: SignInPage
42
+ })
43
+
44
+ function SignInPage() {
45
+ const router = useRouter()
46
+ const { redirect: redirectTo } = Route.useSearch()
47
+ const login = useLogin()
48
+
49
+ const { register, handleSubmit, formState } = useForm<Credentials>({
50
+ resolver: standardSchemaResolver(formSchema),
51
+ defaultValues: DEMO_CREDENTIALS
52
+ })
53
+
54
+ const onSubmit = handleSubmit(async values => {
55
+ await login.mutateAsync(values)
56
+ await router.navigate({ to: redirectTo ?? '/' })
57
+ })
58
+
59
+ return (
60
+ <main className='flex min-h-svh items-center justify-center p-6'>
61
+ <form onSubmit={onSubmit} className='w-full max-w-sm'>
62
+ <FieldGroup>
63
+ <div className='flex flex-col gap-1 text-center'>
64
+ <h1 className='text-xl font-semibold'>Sign in</h1>
65
+ {import.meta.env.DEV && (
66
+ <p className='text-muted-foreground text-sm'>Sign in with the seeded demo account.</p>
67
+ )}
68
+ </div>
69
+
70
+ {import.meta.env.DEV && (
71
+ <div className='bg-muted text-muted-foreground rounded-lg border px-3 py-2 text-center font-mono text-xs'>
72
+ {DEMO_CREDENTIALS.email} · {DEMO_CREDENTIALS.password}
73
+ </div>
74
+ )}
75
+
76
+ {FIELDS.map(field => {
77
+ const error = formState.errors[field.name]
78
+ return (
79
+ <Field key={field.name} data-invalid={Boolean(error)}>
80
+ <FieldLabel htmlFor={field.name}>{field.label}</FieldLabel>
81
+ <Input
82
+ id={field.name}
83
+ type={field.type}
84
+ autoComplete={field.autoComplete}
85
+ aria-invalid={Boolean(error)}
86
+ {...register(field.name)}
87
+ />
88
+ <FieldError errors={[error]} />
89
+ </Field>
90
+ )
91
+ })}
92
+
93
+ <Button type='submit' disabled={formState.isSubmitting}>
94
+ {formState.isSubmitting ? 'Signing in…' : 'Sign in'}
95
+ </Button>
96
+ </FieldGroup>
97
+ </form>
98
+ </main>
99
+ )
100
+ }
@@ -0,0 +1,5 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { cleanup } from '@testing-library/react'
3
+ import { afterEach } from 'vitest'
4
+
5
+ afterEach(cleanup)
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM"],
6
+ "module": "esnext",
7
+ "types": ["vite/client", "vitest/globals", "vitest/jsdom", "@testing-library/jest-dom"],
8
+ "allowArbitraryExtensions": true,
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+ "noImplicitOverride": true,
19
+ "paths": { "@/*": ["./src/*"] },
20
+
21
+ /* Linting */
22
+ "noUnusedLocals": true,
23
+ "noUnusedParameters": true,
24
+ "erasableSyntaxOnly": true,
25
+ "noFallthroughCasesInSwitch": true,
26
+ "noUncheckedIndexedAccess": true
27
+ },
28
+ "include": ["src"]
29
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
4
+ "compilerOptions": {
5
+ "paths": { "@/*": ["./src/*"] }
6
+ }
7
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023"],
6
+ "types": ["node"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "module": "nodenext",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["vite.config.ts"]
23
+ }
@@ -0,0 +1,33 @@
1
+ import babel from '@rolldown/plugin-babel'
2
+ import tailwindcss from '@tailwindcss/vite'
3
+ import { tanstackRouter } from '@tanstack/router-plugin/vite'
4
+ import react, { reactCompilerPreset } from '@vitejs/plugin-react'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { configDefaults, defineConfig } from 'vitest/config'
7
+
8
+ export default defineConfig({
9
+ resolve: {
10
+ alias: {
11
+ '@': fileURLToPath(new URL('./src', import.meta.url))
12
+ }
13
+ },
14
+ plugins: [
15
+ tanstackRouter({
16
+ target: 'react',
17
+ autoCodeSplitting: true
18
+ }),
19
+ react(),
20
+ tailwindcss(),
21
+ babel({ presets: [reactCompilerPreset()] })
22
+ ],
23
+ test: {
24
+ globals: true,
25
+ environment: 'happy-dom',
26
+ environmentOptions: { happyDOM: { url: 'http://localhost' } },
27
+ env: { VITE_API_URL: 'http://localhost/api/' },
28
+ setupFiles: ['./src/test/setup.ts'],
29
+ restoreMocks: true,
30
+ css: false,
31
+ exclude: [...configDefaults.exclude, '**/e2e/**', 'create-cli/**']
32
+ }
33
+ })