@zap-js/client 0.1.1 → 0.1.3

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,33 @@ 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('Server imports found in restricted locations');
31
+ cliLogger.newline();
32
+ cliLogger.info('Allowed locations for server imports:');
33
+ cliLogger.info(' - routes/api/** (server-side routes)');
34
+ cliLogger.info(' - routes/ws/** (WebSocket routes)');
35
+ cliLogger.info(' - src/api/** (API clients)');
36
+ cliLogger.info(' - src/services/** (business logic)');
37
+ cliLogger.info(' - src/generated/** (generated code)');
38
+ cliLogger.newline();
39
+ cliLogger.info('Move server imports to allowed directories or remove them.');
40
+ throw new Error('Build validation failed');
41
+ }
42
+ if (validation.warnings.length > 0) {
43
+ for (const warning of validation.warnings) {
44
+ cliLogger.warn(warning);
45
+ }
46
+ }
47
+ cliLogger.succeedSpinner('validate', 'Build structure valid');
20
48
  // Clean output directory
21
49
  if (existsSync(outputDir)) {
22
50
  cliLogger.spinner('clean', 'Cleaning output directory...');
@@ -29,6 +57,8 @@ export async function buildCommand(options) {
29
57
  if (!options.skipFrontend) {
30
58
  staticDir = await buildFrontend(outputDir);
31
59
  }
60
+ // Step 3.5: Compile server routes separately
61
+ await compileRoutes(outputDir);
32
62
  // Step 4: Create bin directory and build Rust binary
33
63
  // This happens AFTER frontend build so Vite doesn't overwrite it
34
64
  mkdirSync(join(outputDir, 'bin'), { recursive: true });
@@ -168,17 +198,48 @@ async function buildFrontend(outputDir) {
168
198
  return null;
169
199
  }
170
200
  cliLogger.spinner('vite', 'Building frontend (Vite)...');
201
+ // Create temporary vite config that externalizes server packages
202
+ const tempConfigPath = join(process.cwd(), '.vite.config.temp.mjs');
203
+ const tempConfig = `import { defineConfig } from 'vite';
204
+ import react from '@vitejs/plugin-react';
205
+ import { fileURLToPath } from 'url';
206
+ import path from 'path';
207
+
208
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
209
+
210
+ export default defineConfig({
211
+ plugins: [react()],
212
+ build: {
213
+ rollupOptions: {
214
+ external: [
215
+ '@zap-js/server',
216
+ '@zap-js/client/node',
217
+ '@zap-js/client/server',
218
+ ],
219
+ }
220
+ },
221
+ resolve: {
222
+ alias: {
223
+ '@': path.resolve(__dirname, './src')
224
+ }
225
+ }
226
+ });
227
+ `;
171
228
  try {
229
+ // Write temporary config
230
+ writeFileSync(tempConfigPath, tempConfig);
172
231
  // Build to a temporary directory to avoid conflicts
173
232
  const tempDist = join(process.cwd(), '.dist-temp');
174
233
  // Clean temp directory if it exists
175
234
  if (existsSync(tempDist)) {
176
235
  rmSync(tempDist, { recursive: true, force: true });
177
236
  }
178
- execSync(`npx vite build --outDir ${tempDist}`, {
237
+ execSync(`npx vite build --config ${tempConfigPath} --outDir ${tempDist}`, {
179
238
  cwd: process.cwd(),
180
239
  stdio: 'pipe',
181
240
  });
241
+ // Clean up temp config
242
+ rmSync(tempConfigPath, { force: true });
182
243
  const staticDir = join(outputDir, 'static');
183
244
  if (existsSync(tempDist)) {
184
245
  copyDirectory(tempDist, staticDir);
@@ -193,11 +254,58 @@ async function buildFrontend(outputDir) {
193
254
  }
194
255
  }
195
256
  catch (error) {
257
+ // Clean up temp config on error
258
+ if (existsSync(tempConfigPath)) {
259
+ rmSync(tempConfigPath, { force: true });
260
+ }
196
261
  cliLogger.failSpinner('vite', 'Frontend build failed');
197
262
  cliLogger.warn('Continuing without frontend');
198
263
  return null;
199
264
  }
200
265
  }
266
+ async function compileRoutes(outputDir) {
267
+ const routesDir = join(process.cwd(), 'routes');
268
+ if (!existsSync(routesDir)) {
269
+ cliLogger.info('No routes directory, skipping route compilation');
270
+ return;
271
+ }
272
+ cliLogger.spinner('routes', 'Compiling server routes...');
273
+ const tempTsConfig = '.tsconfig.routes.json';
274
+ try {
275
+ // Create temporary tsconfig for routes only
276
+ const routesTsConfig = {
277
+ extends: './tsconfig.json',
278
+ compilerOptions: {
279
+ outDir: join(outputDir, 'routes'),
280
+ rootDir: './routes',
281
+ module: 'NodeNext',
282
+ moduleResolution: 'NodeNext',
283
+ noEmit: false,
284
+ declaration: false,
285
+ sourceMap: true,
286
+ },
287
+ include: ['routes/**/*.ts'],
288
+ exclude: ['routes/**/*.tsx', 'node_modules']
289
+ };
290
+ writeFileSync(tempTsConfig, JSON.stringify(routesTsConfig, null, 2));
291
+ execSync(`npx tsc --project ${tempTsConfig}`, {
292
+ cwd: process.cwd(),
293
+ stdio: 'pipe',
294
+ });
295
+ // Clean up temp config
296
+ rmSync(tempTsConfig, { force: true });
297
+ cliLogger.succeedSpinner('routes', 'Server routes compiled');
298
+ }
299
+ catch (error) {
300
+ // Clean up temp config on error
301
+ if (existsSync(tempTsConfig)) {
302
+ rmSync(tempTsConfig, { force: true });
303
+ }
304
+ cliLogger.failSpinner('routes', 'Route compilation failed');
305
+ // Don't throw - routes may not exist or may not need compilation
306
+ cliLogger.warn('Continuing without compiled routes');
307
+ }
308
+ }
201
309
  async function typeCheck() {
202
310
  // Check if tsconfig exists
203
311
  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,133 @@
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
+ // Path classification tiers
9
+ const ALWAYS_ALLOWED = ['/routes/api/', '/routes/ws/'];
10
+ const BUSINESS_LOGIC_ALLOWED = ['/src/api/', '/src/services/', '/src/generated/', '/src/lib/api/'];
11
+ const UI_LAYER_BLOCKED = ['/src/components/', '/src/pages/', '/src/ui/'];
12
+ const ALLOWED_FILE_PATTERNS = [/\/rpc-client\.(ts|js)$/, /\/api-client\.(ts|js)$/];
13
+ /**
14
+ * Validates that frontend code doesn't import server-only packages
15
+ * Returns array of error messages (empty if valid)
16
+ */
17
+ export function validateNoServerImportsInFrontend(srcDir) {
18
+ const errors = [];
19
+ function scanFile(filePath) {
20
+ // Only scan TypeScript/JavaScript files
21
+ if (!filePath.match(/\.(tsx?|jsx?)$/))
22
+ return;
23
+ // Skip node_modules
24
+ if (filePath.includes('node_modules'))
25
+ return;
26
+ // Tier 1: Check server-side paths first (early exit)
27
+ for (const allowed of ALWAYS_ALLOWED) {
28
+ if (filePath.includes(allowed))
29
+ return;
30
+ }
31
+ // Tier 2: Check business logic paths
32
+ for (const allowed of BUSINESS_LOGIC_ALLOWED) {
33
+ if (filePath.includes(allowed))
34
+ return;
35
+ }
36
+ // Tier 3: Check special file patterns
37
+ for (const pattern of ALLOWED_FILE_PATTERNS) {
38
+ if (pattern.test(filePath))
39
+ return;
40
+ }
41
+ // Now scan file for server imports
42
+ try {
43
+ const content = readFileSync(filePath, 'utf-8');
44
+ let serverImportFound = null;
45
+ for (const serverImport of SERVER_ONLY_IMPORTS) {
46
+ // Check for both single and double quotes
47
+ const patterns = [
48
+ `from '${serverImport}'`,
49
+ `from "${serverImport}"`,
50
+ `require('${serverImport}')`,
51
+ `require("${serverImport}")`,
52
+ ];
53
+ for (const pattern of patterns) {
54
+ if (content.includes(pattern)) {
55
+ serverImportFound = serverImport;
56
+ break;
57
+ }
58
+ }
59
+ if (serverImportFound)
60
+ break; // Only report once per file
61
+ }
62
+ // Tier 4: Classify and report errors
63
+ if (serverImportFound) {
64
+ // Check if in UI layer (explicit block)
65
+ let inUILayer = false;
66
+ for (const blocked of UI_LAYER_BLOCKED) {
67
+ if (filePath.includes(blocked)) {
68
+ inUILayer = true;
69
+ break;
70
+ }
71
+ }
72
+ if (inUILayer) {
73
+ errors.push(`${filePath}: Server import '${serverImportFound}' in UI layer`);
74
+ }
75
+ else {
76
+ errors.push(`${filePath}: Server import '${serverImportFound}' in unclassified path`);
77
+ }
78
+ }
79
+ }
80
+ catch (err) {
81
+ // Ignore files that can't be read
82
+ }
83
+ }
84
+ function scanDir(dir) {
85
+ if (!existsSync(dir))
86
+ return;
87
+ try {
88
+ const entries = readdirSync(dir, { withFileTypes: true });
89
+ for (const entry of entries) {
90
+ const fullPath = join(dir, entry.name);
91
+ // Skip node_modules and hidden directories
92
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
93
+ continue;
94
+ }
95
+ if (entry.isDirectory()) {
96
+ scanDir(fullPath);
97
+ }
98
+ else {
99
+ scanFile(fullPath);
100
+ }
101
+ }
102
+ }
103
+ catch (err) {
104
+ // Ignore directories that can't be read
105
+ }
106
+ }
107
+ scanDir(srcDir);
108
+ return errors;
109
+ }
110
+ /**
111
+ * Validates the entire project structure for build
112
+ */
113
+ export function validateBuildStructure(projectDir) {
114
+ const errors = [];
115
+ const warnings = [];
116
+ // Check src directory for server imports
117
+ const srcDir = join(projectDir, 'src');
118
+ if (existsSync(srcDir)) {
119
+ const srcErrors = validateNoServerImportsInFrontend(srcDir);
120
+ errors.push(...srcErrors);
121
+ }
122
+ // Check routes directory for server imports (excluding api and ws routes)
123
+ const routesDir = join(projectDir, 'routes');
124
+ if (existsSync(routesDir)) {
125
+ const routesErrors = validateNoServerImportsInFrontend(routesDir);
126
+ errors.push(...routesErrors);
127
+ }
128
+ return {
129
+ valid: errors.length === 0,
130
+ errors,
131
+ warnings,
132
+ };
133
+ }
@@ -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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-js/client",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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.1",
75
- "@zap-js/darwin-x64": "0.1.1",
76
- "@zap-js/linux-x64": "0.1.1"
74
+ "@zap-js/darwin-arm64": "0.1.3",
75
+ "@zap-js/darwin-x64": "0.1.3",
76
+ "@zap-js/linux-x64": "0.1.3"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "react": "^18.0.0",