@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.
- package/dist/cli/commands/build.js +109 -1
- package/dist/cli/commands/new.js +19 -0
- package/dist/cli/utils/build-validator.d.ts +13 -0
- package/dist/cli/utils/build-validator.js +133 -0
- package/dist/dev-server/route-scanner.d.ts +5 -0
- package/dist/dev-server/route-scanner.js +24 -0
- package/package.json +4 -4
|
@@ -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');
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -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.
|
|
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.
|
|
75
|
-
"@zap-js/darwin-x64": "0.1.
|
|
76
|
-
"@zap-js/linux-x64": "0.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",
|