@zap-js/client 0.1.0 → 0.1.2

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.
@@ -3,6 +3,7 @@ import { join, resolve } from 'path';
3
3
  import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, rmSync, writeFileSync } from 'fs';
4
4
  import { cliLogger } from '../utils/logger.js';
5
5
  import { resolveBinary, getPlatformIdentifier } from '../utils/binary-resolver.js';
6
+ import { validateBuildStructure } from '../utils/build-validator.js';
6
7
  /**
7
8
  * Build for production
8
9
  */
@@ -17,6 +18,25 @@ export async function buildCommand(options) {
17
18
  }
18
19
  // Step 2: TypeScript type checking (optional but recommended)
19
20
  await typeCheck();
21
+ // Step 2.5: Validate build structure
22
+ cliLogger.spinner('validate', 'Validating build structure...');
23
+ const validation = validateBuildStructure(process.cwd());
24
+ if (!validation.valid) {
25
+ cliLogger.failSpinner('validate', 'Build validation failed');
26
+ for (const error of validation.errors) {
27
+ cliLogger.error(error);
28
+ }
29
+ cliLogger.newline();
30
+ cliLogger.error('Cannot use server-side imports in frontend code');
31
+ cliLogger.info('Server imports (@zap-js/server, @zap-js/client/node) should only be used in routes/api/ or routes/ws/');
32
+ throw new Error('Build validation failed');
33
+ }
34
+ if (validation.warnings.length > 0) {
35
+ for (const warning of validation.warnings) {
36
+ cliLogger.warn(warning);
37
+ }
38
+ }
39
+ cliLogger.succeedSpinner('validate', 'Build structure valid');
20
40
  // Clean output directory
21
41
  if (existsSync(outputDir)) {
22
42
  cliLogger.spinner('clean', 'Cleaning output directory...');
@@ -29,6 +49,8 @@ export async function buildCommand(options) {
29
49
  if (!options.skipFrontend) {
30
50
  staticDir = await buildFrontend(outputDir);
31
51
  }
52
+ // Step 3.5: Compile server routes separately
53
+ await compileRoutes(outputDir);
32
54
  // Step 4: Create bin directory and build Rust binary
33
55
  // This happens AFTER frontend build so Vite doesn't overwrite it
34
56
  mkdirSync(join(outputDir, 'bin'), { recursive: true });
@@ -168,17 +190,48 @@ async function buildFrontend(outputDir) {
168
190
  return null;
169
191
  }
170
192
  cliLogger.spinner('vite', 'Building frontend (Vite)...');
193
+ // Create temporary vite config that externalizes server packages
194
+ const tempConfigPath = join(process.cwd(), '.vite.config.temp.mjs');
195
+ const tempConfig = `import { defineConfig } from 'vite';
196
+ import react from '@vitejs/plugin-react';
197
+ import { fileURLToPath } from 'url';
198
+ import path from 'path';
199
+
200
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
201
+
202
+ export default defineConfig({
203
+ plugins: [react()],
204
+ build: {
205
+ rollupOptions: {
206
+ external: [
207
+ '@zap-js/server',
208
+ '@zap-js/client/node',
209
+ '@zap-js/client/server',
210
+ ],
211
+ }
212
+ },
213
+ resolve: {
214
+ alias: {
215
+ '@': path.resolve(__dirname, './src')
216
+ }
217
+ }
218
+ });
219
+ `;
171
220
  try {
221
+ // Write temporary config
222
+ writeFileSync(tempConfigPath, tempConfig);
172
223
  // Build to a temporary directory to avoid conflicts
173
224
  const tempDist = join(process.cwd(), '.dist-temp');
174
225
  // Clean temp directory if it exists
175
226
  if (existsSync(tempDist)) {
176
227
  rmSync(tempDist, { recursive: true, force: true });
177
228
  }
178
- execSync(`npx vite build --outDir ${tempDist}`, {
229
+ execSync(`npx vite build --config ${tempConfigPath} --outDir ${tempDist}`, {
179
230
  cwd: process.cwd(),
180
231
  stdio: 'pipe',
181
232
  });
233
+ // Clean up temp config
234
+ rmSync(tempConfigPath, { force: true });
182
235
  const staticDir = join(outputDir, 'static');
183
236
  if (existsSync(tempDist)) {
184
237
  copyDirectory(tempDist, staticDir);
@@ -193,11 +246,58 @@ async function buildFrontend(outputDir) {
193
246
  }
194
247
  }
195
248
  catch (error) {
249
+ // Clean up temp config on error
250
+ if (existsSync(tempConfigPath)) {
251
+ rmSync(tempConfigPath, { force: true });
252
+ }
196
253
  cliLogger.failSpinner('vite', 'Frontend build failed');
197
254
  cliLogger.warn('Continuing without frontend');
198
255
  return null;
199
256
  }
200
257
  }
258
+ async function compileRoutes(outputDir) {
259
+ const routesDir = join(process.cwd(), 'routes');
260
+ if (!existsSync(routesDir)) {
261
+ cliLogger.info('No routes directory, skipping route compilation');
262
+ return;
263
+ }
264
+ cliLogger.spinner('routes', 'Compiling server routes...');
265
+ const tempTsConfig = '.tsconfig.routes.json';
266
+ try {
267
+ // Create temporary tsconfig for routes only
268
+ const routesTsConfig = {
269
+ extends: './tsconfig.json',
270
+ compilerOptions: {
271
+ outDir: join(outputDir, 'routes'),
272
+ rootDir: './routes',
273
+ module: 'NodeNext',
274
+ moduleResolution: 'NodeNext',
275
+ noEmit: false,
276
+ declaration: false,
277
+ sourceMap: true,
278
+ },
279
+ include: ['routes/**/*.ts'],
280
+ exclude: ['routes/**/*.tsx', 'node_modules']
281
+ };
282
+ writeFileSync(tempTsConfig, JSON.stringify(routesTsConfig, null, 2));
283
+ execSync(`npx tsc --project ${tempTsConfig}`, {
284
+ cwd: process.cwd(),
285
+ stdio: 'pipe',
286
+ });
287
+ // Clean up temp config
288
+ rmSync(tempTsConfig, { force: true });
289
+ cliLogger.succeedSpinner('routes', 'Server routes compiled');
290
+ }
291
+ catch (error) {
292
+ // Clean up temp config on error
293
+ if (existsSync(tempTsConfig)) {
294
+ rmSync(tempTsConfig, { force: true });
295
+ }
296
+ cliLogger.failSpinner('routes', 'Route compilation failed');
297
+ // Don't throw - routes may not exist or may not need compilation
298
+ cliLogger.warn('Continuing without compiled routes');
299
+ }
300
+ }
201
301
  async function typeCheck() {
202
302
  // Check if tsconfig exists
203
303
  const tsconfigPath = join(process.cwd(), 'tsconfig.json');
@@ -216,6 +216,7 @@ export const POST = async ({ request }: { request: Request }) => {
216
216
  '@types/react': '^18.0.0',
217
217
  '@types/react-dom': '^18.0.0',
218
218
  '@zapjs/cli': '^0.1.0',
219
+ '@vitejs/plugin-react': '^4.0.0',
219
220
  'typescript': '^5.0.0',
220
221
  'vite': '^5.0.0',
221
222
  },
@@ -268,6 +269,23 @@ export default defineConfig({
268
269
  references: [{ path: './tsconfig.node.json' }],
269
270
  };
270
271
  // Write files
272
+ // Create vite.config.ts
273
+ const viteConfig = `import { defineConfig } from 'vite';
274
+ import react from '@vitejs/plugin-react';
275
+
276
+ export default defineConfig({
277
+ plugins: [react()],
278
+ build: {
279
+ rollupOptions: {
280
+ external: [
281
+ '@zap-js/server',
282
+ '@zap-js/client/node',
283
+ '@zap-js/client/server',
284
+ ]
285
+ }
286
+ }
287
+ });
288
+ `;
271
289
  writeFileSync(join(projectDir, 'server/src/main.rs'), mainRs);
272
290
  writeFileSync(join(projectDir, 'routes/__root.tsx'), rootTsx);
273
291
  writeFileSync(join(projectDir, 'routes/index.tsx'), indexRoute);
@@ -277,6 +295,7 @@ export default defineConfig({
277
295
  writeFileSync(join(projectDir, 'Cargo.toml'), cargoToml);
278
296
  writeFileSync(join(projectDir, 'zap.config.ts'), zapConfig);
279
297
  writeFileSync(join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
298
+ writeFileSync(join(projectDir, 'vite.config.ts'), viteConfig);
280
299
  // Create .gitignore
281
300
  const gitignore = `# Dependencies
282
301
  node_modules/
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Validates that frontend code doesn't import server-only packages
3
+ * Returns array of error messages (empty if valid)
4
+ */
5
+ export declare function validateNoServerImportsInFrontend(srcDir: string): string[];
6
+ /**
7
+ * Validates the entire project structure for build
8
+ */
9
+ export declare function validateBuildStructure(projectDir: string): {
10
+ valid: boolean;
11
+ errors: string[];
12
+ warnings: string[];
13
+ };
@@ -0,0 +1,97 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ const SERVER_ONLY_IMPORTS = [
4
+ '@zap-js/server',
5
+ '@zap-js/client/node',
6
+ '@zap-js/client/server',
7
+ ];
8
+ /**
9
+ * Validates that frontend code doesn't import server-only packages
10
+ * Returns array of error messages (empty if valid)
11
+ */
12
+ export function validateNoServerImportsInFrontend(srcDir) {
13
+ const errors = [];
14
+ function scanFile(filePath) {
15
+ // Only scan TypeScript/JavaScript files
16
+ if (!filePath.match(/\.(tsx?|jsx?)$/))
17
+ return;
18
+ // Skip API routes (these are server-side)
19
+ if (filePath.includes('/routes/api/'))
20
+ return;
21
+ if (filePath.includes('/routes/ws/'))
22
+ return;
23
+ // Skip node_modules
24
+ if (filePath.includes('node_modules'))
25
+ return;
26
+ try {
27
+ const content = readFileSync(filePath, 'utf-8');
28
+ for (const serverImport of SERVER_ONLY_IMPORTS) {
29
+ // Check for both single and double quotes
30
+ const patterns = [
31
+ `from '${serverImport}'`,
32
+ `from "${serverImport}"`,
33
+ `require('${serverImport}')`,
34
+ `require("${serverImport}")`,
35
+ ];
36
+ for (const pattern of patterns) {
37
+ if (content.includes(pattern)) {
38
+ errors.push(`${filePath}: Illegal server import '${serverImport}' in frontend code`);
39
+ break; // Only report once per file
40
+ }
41
+ }
42
+ }
43
+ }
44
+ catch (err) {
45
+ // Ignore files that can't be read
46
+ }
47
+ }
48
+ function scanDir(dir) {
49
+ if (!existsSync(dir))
50
+ return;
51
+ try {
52
+ const entries = readdirSync(dir, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const fullPath = join(dir, entry.name);
55
+ // Skip node_modules and hidden directories
56
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
57
+ continue;
58
+ }
59
+ if (entry.isDirectory()) {
60
+ scanDir(fullPath);
61
+ }
62
+ else {
63
+ scanFile(fullPath);
64
+ }
65
+ }
66
+ }
67
+ catch (err) {
68
+ // Ignore directories that can't be read
69
+ }
70
+ }
71
+ scanDir(srcDir);
72
+ return errors;
73
+ }
74
+ /**
75
+ * Validates the entire project structure for build
76
+ */
77
+ export function validateBuildStructure(projectDir) {
78
+ const errors = [];
79
+ const warnings = [];
80
+ // Check src directory for server imports
81
+ const srcDir = join(projectDir, 'src');
82
+ if (existsSync(srcDir)) {
83
+ const srcErrors = validateNoServerImportsInFrontend(srcDir);
84
+ errors.push(...srcErrors);
85
+ }
86
+ // Check routes directory for server imports (excluding api and ws routes)
87
+ const routesDir = join(projectDir, 'routes');
88
+ if (existsSync(routesDir)) {
89
+ const routesErrors = validateNoServerImportsInFrontend(routesDir);
90
+ errors.push(...routesErrors);
91
+ }
92
+ return {
93
+ valid: errors.length === 0,
94
+ errors,
95
+ warnings,
96
+ };
97
+ }
@@ -76,6 +76,11 @@ export declare class RouteScannerRunner extends EventEmitter {
76
76
  * Stop watching
77
77
  */
78
78
  stopWatching(): Promise<void>;
79
+ /**
80
+ * Scan compiled routes from a production build (dist/routes/)
81
+ * Used when running production builds
82
+ */
83
+ scanCompiledRoutes(distDir: string): Promise<RouteTree | null>;
79
84
  /**
80
85
  * Check if routes directory exists
81
86
  */
@@ -104,6 +104,30 @@ export class RouteScannerRunner extends EventEmitter {
104
104
  this.watcher = null;
105
105
  }
106
106
  }
107
+ /**
108
+ * Scan compiled routes from a production build (dist/routes/)
109
+ * Used when running production builds
110
+ */
111
+ async scanCompiledRoutes(distDir) {
112
+ const compiledRoutesDir = join(distDir, 'routes');
113
+ if (!existsSync(compiledRoutesDir)) {
114
+ return null;
115
+ }
116
+ try {
117
+ const router = await this.loadRouter();
118
+ if (!router) {
119
+ return null;
120
+ }
121
+ // Scan the compiled routes directory
122
+ const tree = router.scanRoutes(compiledRoutesDir);
123
+ this.emit('scan-complete', tree);
124
+ return tree;
125
+ }
126
+ catch (err) {
127
+ this.emit('error', err);
128
+ return null;
129
+ }
130
+ }
107
131
  /**
108
132
  * Check if routes directory exists
109
133
  */
@@ -3,13 +3,6 @@ import { tmpdir } from "os";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { ProcessManager } from "./process-manager.js";
5
5
  import { IpcServer } from "./ipc-client.js";
6
- // DEPRECATION WARNING - Show in development only
7
- if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
8
- console.warn('\x1b[33m[DEPRECATION]\x1b[0m Importing from @zap-js/client is deprecated.\n' +
9
- 'Use explicit imports:\n' +
10
- ' - Server/Node.js: import { Zap } from "@zap-js/client/node"\n' +
11
- ' - Browser/Client: import { router } from "@zap-js/client/browser"');
12
- }
13
6
  // Re-export type guards
14
7
  export { isInvokeHandlerMessage, isHandlerResponseMessage, isErrorMessage, isHealthCheckMessage, isHealthCheckResponseMessage, isRpcResponseMessage, isRpcErrorMessage, isAsyncIterable, } from "./types.js";
15
8
  // Re-export internal modules for dev-server usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-js/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "High-performance fullstack React framework - Client package",
5
5
  "homepage": "https://github.com/saint0x/zapjs",
6
6
  "repository": {
@@ -71,9 +71,9 @@
71
71
  "ws": "^8.16.0"
72
72
  },
73
73
  "optionalDependencies": {
74
- "@zap-js/darwin-arm64": "0.1.0",
75
- "@zap-js/darwin-x64": "0.1.0",
76
- "@zap-js/linux-x64": "0.1.0"
74
+ "@zap-js/darwin-arm64": "0.1.2",
75
+ "@zap-js/darwin-x64": "0.1.2",
76
+ "@zap-js/linux-x64": "0.1.2"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "react": "^18.0.0",