create-velox-app 0.6.31 → 0.6.51
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 +120 -0
- package/GUIDE.md +230 -0
- package/dist/cli.js +1 -0
- package/dist/index.js +14 -4
- package/dist/templates/auth.js +10 -0
- package/dist/templates/index.js +30 -1
- package/dist/templates/placeholders.js +0 -3
- package/dist/templates/rsc-auth.d.ts +12 -0
- package/dist/templates/rsc-auth.js +208 -0
- package/dist/templates/rsc.js +40 -1
- package/dist/templates/shared/css-generator.d.ts +26 -0
- package/dist/templates/shared/css-generator.js +553 -0
- package/dist/templates/shared/index.d.ts +3 -0
- package/dist/templates/shared/index.js +3 -0
- package/dist/templates/shared/rsc-styles.d.ts +54 -0
- package/dist/templates/shared/rsc-styles.js +68 -0
- package/dist/templates/shared/theme.d.ts +133 -0
- package/dist/templates/shared/theme.js +141 -0
- package/dist/templates/spa.js +10 -0
- package/dist/templates/trpc.js +10 -0
- package/dist/templates/types.d.ts +2 -1
- package/dist/templates/types.js +6 -0
- package/package.json +6 -3
- package/src/templates/source/api/config/database.ts +13 -32
- package/src/templates/source/api/docker-compose.yml +21 -0
- package/src/templates/source/root/CLAUDE.auth.md +6 -0
- package/src/templates/source/root/CLAUDE.default.md +6 -0
- package/src/templates/source/rsc/CLAUDE.md +56 -2
- package/src/templates/source/rsc/app/actions/posts.ts +1 -1
- package/src/templates/source/rsc/app/actions/users.ts +111 -20
- package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
- package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
- package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
- package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
- package/src/templates/source/rsc/docker-compose.yml +21 -0
- package/src/templates/source/rsc/package.json +3 -3
- package/src/templates/source/rsc/src/api/database.ts +13 -32
- package/src/templates/source/rsc/src/api/handler.ts +1 -1
- package/src/templates/source/rsc/src/entry.client.tsx +65 -18
- package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
- package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
- package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
- package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
- package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
- package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
- package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
- package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
- package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
- package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
- package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
- package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
- package/src/templates/source/rsc-auth/app.config.ts +12 -0
- package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
- package/src/templates/source/rsc-auth/env.example +11 -0
- package/src/templates/source/rsc-auth/gitignore +34 -0
- package/src/templates/source/rsc-auth/package.json +44 -0
- package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
- package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
- package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
- package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
- package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
- package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
- package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
- package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
- package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
- package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
- package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
- package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
- package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
- package/src/templates/source/rsc-auth/tsconfig.json +24 -0
- package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Hydrates React client components on the client side.
|
|
5
|
+
* Only pages marked with 'use client' need hydration.
|
|
6
|
+
* Server-only components remain static HTML.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: The hydration tree must match exactly what the server rendered.
|
|
9
|
+
* Server renders: <div id="root"><MinimalContent><Page /></MinimalContent></div>
|
|
10
|
+
* Client hydrates: <MinimalContent><Page /></MinimalContent>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { StrictMode } from 'react';
|
|
14
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
15
|
+
|
|
16
|
+
// Import content layouts (shared between server and client)
|
|
17
|
+
import MinimalContent from '../app/layouts/minimal-content.tsx';
|
|
18
|
+
// Import client pages (only pages with 'use client' directive)
|
|
19
|
+
import LoginPage from '../app/pages/auth/login.tsx';
|
|
20
|
+
import RegisterPage from '../app/pages/auth/register.tsx';
|
|
21
|
+
|
|
22
|
+
// Route mapping for client pages with their layouts
|
|
23
|
+
// The layout must match what the server rendered inside <div id="root">
|
|
24
|
+
interface ClientRoute {
|
|
25
|
+
Page: React.ComponentType;
|
|
26
|
+
Layout: React.ComponentType<{ children: React.ReactNode }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const clientRoutes: Record<string, ClientRoute> = {
|
|
30
|
+
'/auth/login': { Page: LoginPage, Layout: MinimalContent },
|
|
31
|
+
'/auth/register': { Page: RegisterPage, Layout: MinimalContent },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const rootElement = document.getElementById('root');
|
|
35
|
+
const pathname = window.location.pathname;
|
|
36
|
+
|
|
37
|
+
// Only hydrate if this is a client page
|
|
38
|
+
const match = clientRoutes[pathname];
|
|
39
|
+
|
|
40
|
+
if (rootElement && match) {
|
|
41
|
+
const { Page, Layout } = match;
|
|
42
|
+
|
|
43
|
+
// Hydrate with the exact same tree structure as server rendered
|
|
44
|
+
hydrateRoot(
|
|
45
|
+
rootElement,
|
|
46
|
+
<StrictMode>
|
|
47
|
+
<Layout>
|
|
48
|
+
<Page />
|
|
49
|
+
</Layout>
|
|
50
|
+
</StrictMode>,
|
|
51
|
+
{
|
|
52
|
+
onRecoverableError: (error: unknown) => {
|
|
53
|
+
// Log hydration mismatches in development
|
|
54
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
55
|
+
console.warn('[VeloxTS] Hydration warning:', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
// Server-only page, no hydration needed
|
|
62
|
+
console.debug('[VeloxTS] Server-rendered page, no client hydration needed');
|
|
63
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Handles server-side rendering of React Server Components.
|
|
5
|
+
* Uses h3 event handler for Vinxi compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PassThrough } from 'node:stream';
|
|
9
|
+
|
|
10
|
+
import type { ComponentType, ReactElement, ReactNode } from 'react';
|
|
11
|
+
import { renderToPipeableStream } from 'react-dom/server';
|
|
12
|
+
|
|
13
|
+
// Static imports for layout components
|
|
14
|
+
import DashboardLayout from '../app/layouts/dashboard.tsx';
|
|
15
|
+
import MarketingLayout from '../app/layouts/marketing.tsx';
|
|
16
|
+
import MinimalLayout from '../app/layouts/minimal.tsx';
|
|
17
|
+
import RootLayout from '../app/layouts/root.tsx';
|
|
18
|
+
// Static imports for page components
|
|
19
|
+
import NotFoundPage from '../app/pages/_not-found.tsx';
|
|
20
|
+
import LoginPage from '../app/pages/auth/login.tsx';
|
|
21
|
+
import RegisterPage from '../app/pages/auth/register.tsx';
|
|
22
|
+
import DashboardPage from '../app/pages/dashboard/index.tsx';
|
|
23
|
+
import HomePage from '../app/pages/index.tsx';
|
|
24
|
+
import UsersPage from '../app/pages/users.tsx';
|
|
25
|
+
|
|
26
|
+
// Page props type
|
|
27
|
+
interface PageProps {
|
|
28
|
+
params: Record<string, string>;
|
|
29
|
+
searchParams: Record<string, string | string[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Layout props type
|
|
33
|
+
interface LayoutProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
params: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type LayoutComponent = ComponentType<LayoutProps>;
|
|
39
|
+
|
|
40
|
+
// Route definition with pattern matching
|
|
41
|
+
interface RouteDefinition {
|
|
42
|
+
pattern: string;
|
|
43
|
+
component: ComponentType<PageProps>;
|
|
44
|
+
regex: RegExp;
|
|
45
|
+
paramNames: string[];
|
|
46
|
+
layouts: LayoutComponent[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Route match result
|
|
50
|
+
interface RouteMatch {
|
|
51
|
+
component: ComponentType<PageProps>;
|
|
52
|
+
params: Record<string, string>;
|
|
53
|
+
layouts: LayoutComponent[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compiles a file-based route pattern into a regex
|
|
58
|
+
*/
|
|
59
|
+
function compileRoute(pattern: string): { regex: RegExp; paramNames: string[] } {
|
|
60
|
+
const paramNames: string[] = [];
|
|
61
|
+
|
|
62
|
+
const regexStr = pattern
|
|
63
|
+
.replace(/\[\.\.\.(\w+)\]/g, (_, name) => {
|
|
64
|
+
paramNames.push(name);
|
|
65
|
+
return '___CATCH_ALL___';
|
|
66
|
+
})
|
|
67
|
+
.replace(/\[(\w+)\]/g, (_, name) => {
|
|
68
|
+
paramNames.push(name);
|
|
69
|
+
return '___DYNAMIC___';
|
|
70
|
+
})
|
|
71
|
+
.replace(/[.+?^${}()|\\]/g, '\\$&')
|
|
72
|
+
.replace(/___CATCH_ALL___/g, '(.+)')
|
|
73
|
+
.replace(/___DYNAMIC___/g, '([^/]+)');
|
|
74
|
+
|
|
75
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
76
|
+
return { regex, paramNames };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a route definition from pattern and component
|
|
81
|
+
*/
|
|
82
|
+
function defineRoute(
|
|
83
|
+
pattern: string,
|
|
84
|
+
component: ComponentType<PageProps>,
|
|
85
|
+
layouts: LayoutComponent[] = [RootLayout]
|
|
86
|
+
): RouteDefinition {
|
|
87
|
+
const { regex, paramNames } = compileRoute(pattern);
|
|
88
|
+
return { pattern, component, regex, paramNames, layouts };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wraps a page element with its layout chain
|
|
93
|
+
*/
|
|
94
|
+
function wrapWithLayouts(
|
|
95
|
+
pageElement: ReactElement,
|
|
96
|
+
layouts: LayoutComponent[],
|
|
97
|
+
params: Record<string, string>
|
|
98
|
+
): ReactElement {
|
|
99
|
+
return layouts.reduceRight(
|
|
100
|
+
(children, Layout) => (
|
|
101
|
+
<Layout key={Layout.name || Layout.displayName || 'layout'} params={params}>
|
|
102
|
+
{children}
|
|
103
|
+
</Layout>
|
|
104
|
+
),
|
|
105
|
+
pageElement
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Route registry (order matters - more specific first)
|
|
110
|
+
const routes: RouteDefinition[] = [
|
|
111
|
+
// Home
|
|
112
|
+
defineRoute('/', HomePage, [RootLayout, MarketingLayout]),
|
|
113
|
+
defineRoute('/index', HomePage, [RootLayout, MarketingLayout]),
|
|
114
|
+
|
|
115
|
+
// Auth pages (minimal layout - replaces root)
|
|
116
|
+
defineRoute('/auth/login', LoginPage, [MinimalLayout]),
|
|
117
|
+
defineRoute('/auth/register', RegisterPage, [MinimalLayout]),
|
|
118
|
+
|
|
119
|
+
// Protected pages (dashboard layout)
|
|
120
|
+
defineRoute('/dashboard', DashboardPage, [RootLayout, DashboardLayout]),
|
|
121
|
+
|
|
122
|
+
// Public pages
|
|
123
|
+
defineRoute('/users', UsersPage, [RootLayout, MarketingLayout]),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
console.log(
|
|
127
|
+
'[SSR] Available routes:',
|
|
128
|
+
routes.map((r) => r.pattern)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// H3 event type for Vinxi
|
|
132
|
+
interface H3Event {
|
|
133
|
+
node: {
|
|
134
|
+
req: { url?: string };
|
|
135
|
+
res: {
|
|
136
|
+
statusCode?: number;
|
|
137
|
+
setHeader: (name: string, value: string) => void;
|
|
138
|
+
end: (data?: unknown) => void;
|
|
139
|
+
write: (chunk: unknown) => boolean;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Match a pathname against registered routes
|
|
146
|
+
*/
|
|
147
|
+
function matchRoute(pathname: string): RouteMatch | null {
|
|
148
|
+
console.log('[SSR] Matching:', pathname);
|
|
149
|
+
|
|
150
|
+
for (const route of routes) {
|
|
151
|
+
const match = pathname.match(route.regex);
|
|
152
|
+
if (match) {
|
|
153
|
+
const params: Record<string, string> = {};
|
|
154
|
+
route.paramNames.forEach((name, index) => {
|
|
155
|
+
params[name] = match[index + 1];
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log('[SSR] Matched:', route.pattern, 'params:', params);
|
|
159
|
+
return { component: route.component, params, layouts: route.layouts };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse search params from URL
|
|
168
|
+
*/
|
|
169
|
+
function parseSearchParams(url: URL): Record<string, string | string[]> {
|
|
170
|
+
const searchParams: Record<string, string | string[]> = {};
|
|
171
|
+
url.searchParams.forEach((value, key) => {
|
|
172
|
+
const existing = searchParams[key];
|
|
173
|
+
if (existing) {
|
|
174
|
+
searchParams[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
175
|
+
} else {
|
|
176
|
+
searchParams[key] = value;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return searchParams;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* H3-compatible SSR handler for Vinxi
|
|
184
|
+
*/
|
|
185
|
+
export default async function ssrHandler(event: H3Event): Promise<void> {
|
|
186
|
+
const res = event.node.res;
|
|
187
|
+
const url = new URL(event.node.req.url || '/', 'http://localhost');
|
|
188
|
+
const pathname = url.pathname;
|
|
189
|
+
|
|
190
|
+
console.log('[SSR] Handling:', pathname);
|
|
191
|
+
|
|
192
|
+
const match = matchRoute(pathname);
|
|
193
|
+
|
|
194
|
+
if (!match) {
|
|
195
|
+
const pageProps: PageProps = {
|
|
196
|
+
params: {},
|
|
197
|
+
searchParams: parseSearchParams(url),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const notFoundElement = <NotFoundPage {...pageProps} />;
|
|
201
|
+
const html = wrapWithLayouts(notFoundElement, [RootLayout], {});
|
|
202
|
+
|
|
203
|
+
const passThrough = new PassThrough();
|
|
204
|
+
const { pipe } = renderToPipeableStream(html, {
|
|
205
|
+
onShellReady() {
|
|
206
|
+
res.statusCode = 404;
|
|
207
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
208
|
+
passThrough.on('data', (chunk) => res.write(chunk));
|
|
209
|
+
passThrough.on('end', () => res.end());
|
|
210
|
+
pipe(passThrough);
|
|
211
|
+
},
|
|
212
|
+
onShellError(error) {
|
|
213
|
+
console.error('[SSR] 404 Shell error:', error);
|
|
214
|
+
res.statusCode = 404;
|
|
215
|
+
res.setHeader('Content-Type', 'text/html');
|
|
216
|
+
res.end(
|
|
217
|
+
`<!DOCTYPE html><html><body><h1>404 - Page Not Found</h1><p>Path: ${pathname}</p></body></html>`
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { component: PageComponent, params, layouts } = match;
|
|
225
|
+
|
|
226
|
+
const pageProps: PageProps = {
|
|
227
|
+
params,
|
|
228
|
+
searchParams: parseSearchParams(url),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const pageElement = <PageComponent {...pageProps} />;
|
|
233
|
+
const html = wrapWithLayouts(pageElement, layouts, params);
|
|
234
|
+
|
|
235
|
+
const passThrough = new PassThrough();
|
|
236
|
+
const { pipe } = renderToPipeableStream(html, {
|
|
237
|
+
onShellReady() {
|
|
238
|
+
res.statusCode = 200;
|
|
239
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
240
|
+
passThrough.on('data', (chunk) => res.write(chunk));
|
|
241
|
+
passThrough.on('end', () => res.end());
|
|
242
|
+
pipe(passThrough);
|
|
243
|
+
},
|
|
244
|
+
onShellError(error) {
|
|
245
|
+
console.error('[SSR] Shell error:', error);
|
|
246
|
+
res.statusCode = 500;
|
|
247
|
+
res.setHeader('Content-Type', 'text/html');
|
|
248
|
+
res.end(`<h1>Render Error</h1><pre>${error}</pre>`);
|
|
249
|
+
},
|
|
250
|
+
onError(error) {
|
|
251
|
+
console.error('[SSR] Render error:', error);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('[SSR] Handler error:', error);
|
|
256
|
+
res.statusCode = 500;
|
|
257
|
+
res.setHeader('Content-Type', 'text/html');
|
|
258
|
+
res.end(
|
|
259
|
+
`<h1>Server Error</h1><pre>${error instanceof Error ? error.stack : String(error)}</pre>`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["./src/*"],
|
|
18
|
+
"@/app/*": ["./app/*"]
|
|
19
|
+
},
|
|
20
|
+
"types": ["node", "vite/client"]
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*", "src/**/*.d.ts", "app/**/*", "*.config.ts"],
|
|
23
|
+
"exclude": ["node_modules", "dist"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check-client-imports.sh
|
|
3
|
+
# Checks for server-only imports in client bundle files
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./scripts/check-client-imports.sh
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
NC='\033[0m' # No Color
|
|
12
|
+
|
|
13
|
+
# Patterns that should NOT appear in client files
|
|
14
|
+
FORBIDDEN_IMPORTS=(
|
|
15
|
+
"@veloxts/web/server"
|
|
16
|
+
"@veloxts/web/actions"
|
|
17
|
+
"@veloxts/core"
|
|
18
|
+
"@veloxts/orm"
|
|
19
|
+
"@veloxts/auth"
|
|
20
|
+
"@/api/database"
|
|
21
|
+
"@/api/procedures"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Client file patterns to check
|
|
25
|
+
CLIENT_FILES=(
|
|
26
|
+
"src/entry.client.tsx"
|
|
27
|
+
"src/entry.client.ts"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Find files with 'use client' directive
|
|
31
|
+
USE_CLIENT_FILES=$(grep -rl "^'use client'" app/ 2>/dev/null || true)
|
|
32
|
+
|
|
33
|
+
errors=0
|
|
34
|
+
|
|
35
|
+
echo "Checking for server imports in client files..."
|
|
36
|
+
echo ""
|
|
37
|
+
|
|
38
|
+
# Check explicit client entry files
|
|
39
|
+
for file in "${CLIENT_FILES[@]}"; do
|
|
40
|
+
if [ -f "$file" ]; then
|
|
41
|
+
for pattern in "${FORBIDDEN_IMPORTS[@]}"; do
|
|
42
|
+
if grep -q "from ['\"]${pattern}" "$file" 2>/dev/null; then
|
|
43
|
+
echo -e "${RED}ERROR:${NC} $file imports '$pattern'"
|
|
44
|
+
echo " Server-only imports cannot be used in client bundles."
|
|
45
|
+
((errors++))
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# Check files with 'use client' directive
|
|
52
|
+
for file in $USE_CLIENT_FILES; do
|
|
53
|
+
for pattern in "${FORBIDDEN_IMPORTS[@]}"; do
|
|
54
|
+
if grep -q "from ['\"]${pattern}" "$file" 2>/dev/null; then
|
|
55
|
+
echo -e "${RED}ERROR:${NC} $file imports '$pattern'"
|
|
56
|
+
echo " This file has 'use client' but imports server-only code."
|
|
57
|
+
((errors++))
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
if [ $errors -gt 0 ]; then
|
|
65
|
+
echo -e "${RED}Found $errors server import(s) in client files.${NC}"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Solutions:"
|
|
68
|
+
echo "1. Use @veloxts/web/client for browser-safe exports"
|
|
69
|
+
echo "2. Move server logic to files with 'use server' directive"
|
|
70
|
+
echo "3. Import server actions (not server modules) in client components"
|
|
71
|
+
exit 1
|
|
72
|
+
else
|
|
73
|
+
echo -e "${GREEN}No forbidden imports found in client files.${NC}"
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|