create-ifc-lite 1.6.0 → 1.7.0

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/index.js CHANGED
@@ -1,26 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
3
- import { join, dirname } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { execSync } from 'child_process';
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
2
+ /* This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
5
+ import { existsSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { createBasicTemplate } from './templates/basic.js';
8
+ import { createServerTemplate } from './templates/server.js';
9
+ import { createServerNativeTemplate } from './templates/server-native.js';
10
+ import { fixViewerTemplate } from './utils/config-fixers.js';
11
+ import { downloadViewer } from './utils/download.js';
7
12
  const TEMPLATES = {
8
13
  basic: 'basic',
9
14
  react: 'react',
10
15
  server: 'server',
11
16
  'server-native': 'server-native',
12
17
  };
13
- const REPO_URL = 'https://github.com/louistrue/ifc-lite';
14
- const VIEWER_PATH = 'apps/viewer';
15
- function getLatestVersion() {
16
- try {
17
- const result = execSync('npm view @ifc-lite/parser version', { stdio: 'pipe' });
18
- return `^${result.toString().trim()}`;
19
- }
20
- catch {
21
- return '^1.0.0'; // fallback
22
- }
23
- }
24
18
  function printUsage() {
25
19
  console.log(`
26
20
  create-ifc-lite - Create IFC-Lite projects instantly
@@ -45,153 +39,6 @@ function printUsage() {
45
39
  server-native Native binary server (no Docker required)
46
40
  `);
47
41
  }
48
- function runCommand(cmd, cwd) {
49
- try {
50
- execSync(cmd, { cwd, stdio: 'pipe' });
51
- return true;
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
- async function downloadViewer(targetDir, projectName) {
58
- // Try degit first (fastest)
59
- if (runCommand('npx --version')) {
60
- console.log(' Downloading viewer template...');
61
- try {
62
- execSync(`npx degit ${REPO_URL}/${VIEWER_PATH} "${targetDir}"`, {
63
- stdio: 'pipe',
64
- timeout: 60000
65
- });
66
- return true;
67
- }
68
- catch {
69
- // degit failed, try git sparse checkout
70
- }
71
- }
72
- // Fallback: git sparse checkout
73
- if (runCommand('git --version')) {
74
- console.log(' Downloading via git...');
75
- const tempDir = join(dirname(targetDir), `.temp-${Date.now()}`);
76
- try {
77
- execSync(`git clone --filter=blob:none --sparse "${REPO_URL}.git" "${tempDir}"`, {
78
- stdio: 'pipe',
79
- timeout: 120000
80
- });
81
- execSync(`git sparse-checkout set ${VIEWER_PATH}`, { cwd: tempDir, stdio: 'pipe' });
82
- // Move viewer to target
83
- const viewerSrc = join(tempDir, VIEWER_PATH);
84
- execSync(`mv "${viewerSrc}" "${targetDir}"`, { stdio: 'pipe' });
85
- rmSync(tempDir, { recursive: true, force: true });
86
- return true;
87
- }
88
- catch {
89
- rmSync(tempDir, { recursive: true, force: true });
90
- }
91
- }
92
- return false;
93
- }
94
- function fixPackageJson(targetDir, projectName) {
95
- const pkgPath = join(targetDir, 'package.json');
96
- if (!existsSync(pkgPath))
97
- return;
98
- let pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
99
- // Update name
100
- pkg.name = projectName;
101
- // Replace workspace protocol with latest npm version in all dependency fields
102
- const latestVersion = getLatestVersion();
103
- const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
104
- for (const field of depFields) {
105
- const deps = pkg[field];
106
- if (!deps)
107
- continue;
108
- for (const [name, version] of Object.entries(deps)) {
109
- if (typeof version === 'string' && version.includes('workspace:')) {
110
- deps[name] = latestVersion;
111
- }
112
- }
113
- }
114
- // Remove git directory if present
115
- const gitDir = join(targetDir, '.git');
116
- if (existsSync(gitDir)) {
117
- rmSync(gitDir, { recursive: true, force: true });
118
- }
119
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
120
- }
121
- function fixTsConfig(targetDir) {
122
- const tsconfigPath = join(targetDir, 'tsconfig.json');
123
- // Write standalone tsconfig without monorepo references
124
- const tsconfig = {
125
- compilerOptions: {
126
- target: 'ES2022',
127
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
128
- module: 'ESNext',
129
- moduleResolution: 'bundler',
130
- jsx: 'react-jsx',
131
- strict: true,
132
- esModuleInterop: true,
133
- skipLibCheck: true,
134
- noEmit: true,
135
- baseUrl: '.',
136
- paths: {
137
- '@/*': ['./src/*']
138
- }
139
- },
140
- include: ['src/**/*'],
141
- exclude: ['node_modules']
142
- };
143
- writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
144
- }
145
- function fixViteConfig(targetDir) {
146
- const viteConfigPath = join(targetDir, 'vite.config.ts');
147
- // Write standalone vite config with WASM support
148
- const viteConfig = `import { defineConfig } from 'vite';
149
- import react from '@vitejs/plugin-react';
150
- import path from 'path';
151
-
152
- export default defineConfig({
153
- plugins: [
154
- react(),
155
- {
156
- name: 'wasm-mime-type',
157
- configureServer(server) {
158
- server.middlewares.use((req, res, next) => {
159
- if (req.url?.endsWith('.wasm')) {
160
- res.setHeader('Content-Type', 'application/wasm');
161
- }
162
- next();
163
- });
164
- },
165
- },
166
- ],
167
- resolve: {
168
- alias: {
169
- '@': path.resolve(__dirname, './src'),
170
- },
171
- },
172
- server: {
173
- port: 3000,
174
- open: true,
175
- fs: {
176
- allow: ['..'],
177
- },
178
- },
179
- build: {
180
- target: 'esnext',
181
- },
182
- optimizeDeps: {
183
- exclude: ['@duckdb/duckdb-wasm', '@ifc-lite/wasm'],
184
- },
185
- assetsInclude: ['**/*.wasm'],
186
- });
187
- `;
188
- writeFileSync(viteConfigPath, viteConfig);
189
- }
190
- function fixViewerTemplate(targetDir, projectName) {
191
- fixPackageJson(targetDir, projectName);
192
- fixTsConfig(targetDir);
193
- fixViteConfig(targetDir);
194
- }
195
42
  async function main() {
196
43
  const args = process.argv.slice(2);
197
44
  if (args.includes('--help') || args.includes('-h')) {
@@ -270,1091 +117,4 @@ async function main() {
270
117
  }
271
118
  console.log();
272
119
  }
273
- function createBasicTemplate(targetDir, projectName) {
274
- const latestVersion = getLatestVersion();
275
- // package.json
276
- writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
277
- name: projectName,
278
- version: latestVersion.replace('^', ''),
279
- type: 'module',
280
- scripts: {
281
- parse: 'npx tsx src/index.ts',
282
- build: 'tsc',
283
- },
284
- dependencies: {
285
- '@ifc-lite/parser': latestVersion,
286
- },
287
- devDependencies: {
288
- typescript: '^5.3.0',
289
- tsx: '^4.0.0',
290
- },
291
- }, null, 2));
292
- // tsconfig.json
293
- writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
294
- compilerOptions: {
295
- target: 'ES2022',
296
- module: 'ESNext',
297
- moduleResolution: 'bundler',
298
- strict: true,
299
- esModuleInterop: true,
300
- skipLibCheck: true,
301
- outDir: 'dist',
302
- },
303
- include: ['src'],
304
- }, null, 2));
305
- // src/index.ts
306
- mkdirSync(join(targetDir, 'src'));
307
- writeFileSync(join(targetDir, 'src', 'index.ts'), `import { IfcParser } from '@ifc-lite/parser';
308
- import { readFileSync } from 'fs';
309
-
310
- // Example: Parse an IFC file
311
- const ifcPath = process.argv[2];
312
-
313
- if (!ifcPath) {
314
- console.log('Usage: npm run parse <path-to-ifc-file>');
315
- console.log('');
316
- console.log('Example:');
317
- console.log(' npm run parse ./model.ifc');
318
- process.exit(1);
319
- }
320
-
321
- const buffer = readFileSync(ifcPath);
322
- const parser = new IfcParser();
323
-
324
- console.log('Parsing IFC file...');
325
- parser.parse(buffer).then(result => {
326
- console.log('\\nFile parsed successfully!');
327
- console.log(\` Entities: \${result.entityCount}\`);
328
-
329
- // Count by type
330
- const typeCounts = new Map<string, number>();
331
- for (const [id, entity] of result.entities) {
332
- typeCounts.set(entity.type, (typeCounts.get(entity.type) || 0) + 1);
333
- }
334
-
335
- console.log('\\nTop entity types:');
336
- const sorted = [...typeCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
337
- for (const [type, count] of sorted) {
338
- console.log(\` \${type}: \${count}\`);
339
- }
340
- });
341
- `);
342
- // README
343
- writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
344
-
345
- IFC parser project using [IFC-Lite](https://github.com/louistrue/ifc-lite).
346
-
347
- ## Quick Start
348
-
349
- \`\`\`bash
350
- npm install
351
- npm run parse ./your-model.ifc
352
- \`\`\`
353
-
354
- ## Learn More
355
-
356
- - [IFC-Lite Documentation](https://louistrue.github.io/ifc-lite/)
357
- - [API Reference](https://louistrue.github.io/ifc-lite/api/)
358
- `);
359
- }
360
- function createServerTemplate(targetDir, projectName) {
361
- const latestVersion = getLatestVersion();
362
- // docker-compose.yml - Production configuration
363
- writeFileSync(join(targetDir, 'docker-compose.yml'), `# IFC-Lite Server - Production Configuration
364
- # Start with: docker compose up -d
365
-
366
- services:
367
- ifc-server:
368
- image: ghcr.io/louistrue/ifc-lite-server:latest
369
- container_name: ${projectName}-server
370
- ports:
371
- - "\${PORT:-3001}:8080"
372
- volumes:
373
- - ifc-cache:/app/cache
374
- environment:
375
- - RUST_LOG=\${RUST_LOG:-info}
376
- - MAX_FILE_SIZE_MB=\${MAX_FILE_SIZE_MB:-500}
377
- - REQUEST_TIMEOUT_SECS=\${REQUEST_TIMEOUT_SECS:-300}
378
- - WORKER_THREADS=\${WORKER_THREADS:-4}
379
- - INITIAL_BATCH_SIZE=\${INITIAL_BATCH_SIZE:-100}
380
- - MAX_BATCH_SIZE=\${MAX_BATCH_SIZE:-1000}
381
- - CACHE_MAX_AGE_DAYS=\${CACHE_MAX_AGE_DAYS:-7}
382
- healthcheck:
383
- test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"]
384
- interval: 30s
385
- timeout: 3s
386
- retries: 3
387
- start_period: 5s
388
- restart: unless-stopped
389
-
390
- volumes:
391
- ifc-cache:
392
- name: ${projectName}-cache
393
- `);
394
- // docker-compose.dev.yml - Development configuration with live logs
395
- writeFileSync(join(targetDir, 'docker-compose.dev.yml'), `# IFC-Lite Server - Development Configuration
396
- # Start with: docker compose -f docker-compose.dev.yml up
397
-
398
- services:
399
- ifc-server:
400
- image: ghcr.io/louistrue/ifc-lite-server:latest
401
- container_name: ${projectName}-server-dev
402
- ports:
403
- - "\${PORT:-3001}:8080"
404
- volumes:
405
- - ./cache:/app/cache # Local cache directory for inspection
406
- environment:
407
- - RUST_LOG=debug,tower_http=debug,ifc_lite_server=debug
408
- - MAX_FILE_SIZE_MB=\${MAX_FILE_SIZE_MB:-500}
409
- - REQUEST_TIMEOUT_SECS=\${REQUEST_TIMEOUT_SECS:-300}
410
- - WORKER_THREADS=\${WORKER_THREADS:-4}
411
- - INITIAL_BATCH_SIZE=\${INITIAL_BATCH_SIZE:-100}
412
- - MAX_BATCH_SIZE=\${MAX_BATCH_SIZE:-1000}
413
- - CACHE_MAX_AGE_DAYS=\${CACHE_MAX_AGE_DAYS:-30}
414
- # No restart in dev - easier to debug
415
- `);
416
- // .env.example - Documented environment variables
417
- writeFileSync(join(targetDir, '.env.example'), `# IFC-Lite Server Configuration
418
- # Copy this file to .env and modify as needed
419
-
420
- # =============================================================================
421
- # SERVER SETTINGS
422
- # =============================================================================
423
-
424
- # Port to expose the server on (maps to internal port 8080)
425
- PORT=3001
426
-
427
- # Log level: error, warn, info, debug, trace
428
- # Use "debug" for development, "info" for production
429
- RUST_LOG=info
430
-
431
- # =============================================================================
432
- # FILE PROCESSING
433
- # =============================================================================
434
-
435
- # Maximum IFC file size in megabytes
436
- # Larger files need more memory and processing time
437
- MAX_FILE_SIZE_MB=500
438
-
439
- # Request timeout in seconds
440
- # Increase for very large files (500MB+)
441
- REQUEST_TIMEOUT_SECS=300
442
-
443
- # Number of worker threads for parallel geometry processing
444
- # Default: number of CPU cores
445
- # Reduce if running alongside other services
446
- WORKER_THREADS=4
447
-
448
- # =============================================================================
449
- # STREAMING (Progressive Rendering)
450
- # =============================================================================
451
-
452
- # Initial batch size for fast first frame (first 3 batches)
453
- # Smaller = faster first render, but more HTTP overhead
454
- INITIAL_BATCH_SIZE=100
455
-
456
- # Maximum batch size for throughput (batches 11+)
457
- # Larger = better throughput, but longer waits between updates
458
- MAX_BATCH_SIZE=1000
459
-
460
- # =============================================================================
461
- # CACHING
462
- # =============================================================================
463
-
464
- # How long to keep cached results (in days)
465
- # Cached files are served instantly without reprocessing
466
- CACHE_MAX_AGE_DAYS=7
467
- `);
468
- // Copy .env.example to .env
469
- writeFileSync(join(targetDir, '.env'), `# IFC-Lite Server Configuration
470
- # See .env.example for all available options with documentation
471
-
472
- PORT=3001
473
- RUST_LOG=info
474
- MAX_FILE_SIZE_MB=500
475
- WORKER_THREADS=4
476
- `);
477
- // .gitignore
478
- writeFileSync(join(targetDir, '.gitignore'), `# Dependencies
479
- node_modules/
480
-
481
- # Build output
482
- dist/
483
-
484
- # Environment files (keep .env.example)
485
- .env
486
- .env.local
487
- .env.*.local
488
-
489
- # Cache directory (when using dev compose)
490
- cache/
491
-
492
- # IDE
493
- .idea/
494
- .vscode/
495
- *.swp
496
- *.swo
497
-
498
- # OS
499
- .DS_Store
500
- Thumbs.db
501
-
502
- # Logs
503
- *.log
504
- npm-debug.log*
505
- `);
506
- // .dockerignore
507
- writeFileSync(join(targetDir, '.dockerignore'), `node_modules
508
- dist
509
- .git
510
- .gitignore
511
- *.md
512
- .env
513
- .env.*
514
- cache
515
- `);
516
- // package.json
517
- writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
518
- name: projectName,
519
- version: '0.1.0',
520
- type: 'module',
521
- description: 'IFC processing server with TypeScript client',
522
- scripts: {
523
- 'example': 'npx tsx src/example.ts',
524
- 'example:stream': 'npx tsx src/example-stream.ts',
525
- 'server:start': 'docker compose up -d',
526
- 'server:stop': 'docker compose down',
527
- 'server:logs': 'docker compose logs -f',
528
- 'server:dev': 'docker compose -f docker-compose.dev.yml up',
529
- 'build': 'tsc',
530
- 'typecheck': 'tsc --noEmit',
531
- },
532
- dependencies: {
533
- '@ifc-lite/server-client': latestVersion,
534
- },
535
- devDependencies: {
536
- 'typescript': '^5.3.0',
537
- 'tsx': '^4.0.0',
538
- '@types/node': '^20.0.0',
539
- },
540
- optionalDependencies: {
541
- 'parquet-wasm': '^0.6.0',
542
- 'apache-arrow': '^17.0.0',
543
- },
544
- }, null, 2));
545
- // tsconfig.json
546
- writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
547
- compilerOptions: {
548
- target: 'ES2022',
549
- module: 'ESNext',
550
- moduleResolution: 'bundler',
551
- strict: true,
552
- esModuleInterop: true,
553
- skipLibCheck: true,
554
- outDir: 'dist',
555
- declaration: true,
556
- lib: ['ES2022'],
557
- },
558
- include: ['src'],
559
- exclude: ['node_modules', 'dist'],
560
- }, null, 2));
561
- // Create src directory
562
- mkdirSync(join(targetDir, 'src'));
563
- // src/example.ts - Basic client example
564
- writeFileSync(join(targetDir, 'src', 'example.ts'), `/**
565
- * IFC-Lite Server Client Example
566
- *
567
- * This example demonstrates how to use the IFC-Lite server to parse IFC files.
568
- * The server handles heavy geometry processing, caching, and streaming.
569
- *
570
- * Prerequisites:
571
- * 1. Start the server: docker compose up -d
572
- * 2. Run this example: npm run example
573
- */
574
-
575
- import { IfcServerClient } from '@ifc-lite/server-client';
576
- import { readFileSync, existsSync } from 'fs';
577
-
578
- // Server URL - matches docker-compose.yml port mapping
579
- const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3001';
580
-
581
- async function main() {
582
- // Initialize client
583
- const client = new IfcServerClient({
584
- baseUrl: SERVER_URL,
585
- timeout: 300000, // 5 minutes for large files
586
- });
587
-
588
- // Check server health
589
- console.log('Checking server health...');
590
- try {
591
- const health = await client.health();
592
- console.log(\`Server status: \${health.status}\`);
593
- console.log(\`Server version: \${health.version || 'unknown'}\`);
594
- } catch (error) {
595
- console.error('Failed to connect to server. Is it running?');
596
- console.error('Start it with: docker compose up -d');
597
- process.exit(1);
598
- }
599
-
600
- // Get IFC file path from command line or use default
601
- const ifcPath = process.argv[2];
602
-
603
- if (!ifcPath) {
604
- console.log(\`
605
- Usage: npm run example <path-to-ifc-file>
606
-
607
- Example:
608
- npm run example ./model.ifc
609
-
610
- The server will:
611
- 1. Check if the file is already cached (instant response)
612
- 2. If not cached, parse and process geometry
613
- 3. Cache the result for future requests
614
- 4. Return geometry data ready for rendering
615
- \`);
616
- return;
617
- }
618
-
619
- if (!existsSync(ifcPath)) {
620
- console.error(\`File not found: \${ifcPath}\`);
621
- process.exit(1);
622
- }
623
-
624
- // Read IFC file
625
- console.log(\`\\nParsing: \${ifcPath}\`);
626
- const buffer = readFileSync(ifcPath);
627
- console.log(\`File size: \${(buffer.length / 1024 / 1024).toFixed(2)} MB\`);
628
-
629
- // Parse with Parquet format (most efficient)
630
- console.log('\\nSending to server...');
631
- const startTime = performance.now();
632
-
633
- try {
634
- // Check if Parquet is available (optional dependency)
635
- const parquetAvailable = await client.isParquetSupported();
636
-
637
- if (parquetAvailable) {
638
- console.log('Using Parquet format (15x smaller than JSON)');
639
- const result = await client.parseParquet(buffer);
640
-
641
- const elapsed = performance.now() - startTime;
642
- console.log(\`\\nParsing complete in \${elapsed.toFixed(0)}ms\`);
643
- console.log(\` Cache key: \${result.cache_key.substring(0, 16)}...\`);
644
- console.log(\` Meshes: \${result.meshes.length}\`);
645
- console.log(\` Payload size: \${(result.parquet_stats.payload_size / 1024).toFixed(1)} KB\`);
646
- console.log(\` Decode time: \${result.parquet_stats.decode_time_ms}ms\`);
647
-
648
- if (result.stats) {
649
- console.log(\`\\nServer stats:\`);
650
- console.log(\` Parse time: \${result.stats.parse_time_ms}ms\`);
651
- console.log(\` Geometry time: \${result.stats.geometry_time_ms}ms\`);
652
- console.log(\` Total triangles: \${result.stats.total_triangles}\`);
653
- }
654
-
655
- // Show sample mesh data
656
- if (result.meshes.length > 0) {
657
- const mesh = result.meshes[0];
658
- console.log(\`\\nSample mesh:\`);
659
- console.log(\` Express ID: \${mesh.express_id}\`);
660
- console.log(\` Vertices: \${mesh.positions.length / 3}\`);
661
- console.log(\` Triangles: \${mesh.indices.length / 3}\`);
662
- console.log(\` Color: rgba(\${mesh.color.join(', ')})\`);
663
- }
664
- } else {
665
- // Fallback to JSON format
666
- console.log('Parquet not available, using JSON format');
667
- console.log('Install optional deps for smaller payloads: npm install parquet-wasm apache-arrow');
668
-
669
- const result = await client.parse(buffer);
670
-
671
- const elapsed = performance.now() - startTime;
672
- console.log(\`\\nParsing complete in \${elapsed.toFixed(0)}ms\`);
673
- console.log(\` Cache key: \${result.cache_key.substring(0, 16)}...\`);
674
- console.log(\` Meshes: \${result.meshes.length}\`);
675
- }
676
- } catch (error) {
677
- console.error('Parse failed:', error);
678
- process.exit(1);
679
- }
680
- }
681
-
682
- main().catch(console.error);
683
- `);
684
- // src/example-stream.ts - Streaming example for large files
685
- writeFileSync(join(targetDir, 'src', 'example-stream.ts'), `/**
686
- * IFC-Lite Server Streaming Example
687
- *
688
- * This example demonstrates streaming parsing for large IFC files.
689
- * Geometry batches are received progressively, enabling immediate rendering
690
- * while the server continues processing.
691
- *
692
- * Best for: Files > 50MB where you want progressive rendering
693
- *
694
- * Prerequisites:
695
- * 1. Start the server: docker compose up -d
696
- * 2. Install optional deps: npm install parquet-wasm apache-arrow
697
- * 3. Run: npm run example:stream <path-to-ifc>
698
- */
699
-
700
- import { IfcServerClient } from '@ifc-lite/server-client';
701
- import { readFileSync, existsSync } from 'fs';
702
-
703
- const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3001';
704
-
705
- async function main() {
706
- const client = new IfcServerClient({
707
- baseUrl: SERVER_URL,
708
- timeout: 600000, // 10 minutes for very large files
709
- });
710
-
711
- // Check server
712
- try {
713
- await client.health();
714
- console.log('Server connected');
715
- } catch {
716
- console.error('Server not available. Start with: docker compose up -d');
717
- process.exit(1);
718
- }
719
-
720
- const ifcPath = process.argv[2];
721
- if (!ifcPath || !existsSync(ifcPath)) {
722
- console.log('Usage: npm run example:stream <path-to-ifc-file>');
723
- process.exit(1);
724
- }
725
-
726
- const buffer = readFileSync(ifcPath);
727
- console.log(\`\\nStreaming: \${ifcPath} (\${(buffer.length / 1024 / 1024).toFixed(1)} MB)\`);
728
-
729
- // Check Parquet support
730
- const parquetAvailable = await client.isParquetSupported();
731
- if (!parquetAvailable) {
732
- console.error('Streaming requires parquet-wasm and apache-arrow.');
733
- console.error('Install with: npm install parquet-wasm apache-arrow');
734
- process.exit(1);
735
- }
736
-
737
- const startTime = performance.now();
738
- let totalMeshes = 0;
739
- let batchCount = 0;
740
-
741
- try {
742
- // Stream with batch callback
743
- const result = await client.parseParquetStream(buffer, (batch) => {
744
- batchCount++;
745
- totalMeshes += batch.meshes.length;
746
-
747
- // In a real app, you would render each batch immediately
748
- console.log(
749
- \` Batch #\${batch.batch_number}: +\${batch.meshes.length} meshes \` +
750
- \`(decode: \${batch.decode_time_ms.toFixed(0)}ms) - Total: \${totalMeshes}\`
751
- );
752
- });
753
-
754
- const elapsed = performance.now() - startTime;
755
- console.log(\`\\nStreaming complete!\`);
756
- console.log(\` Total time: \${elapsed.toFixed(0)}ms\`);
757
- console.log(\` Batches received: \${batchCount}\`);
758
- console.log(\` Total meshes: \${result.total_meshes}\`);
759
- console.log(\` Cache key: \${result.cache_key.substring(0, 16)}...\`);
760
-
761
- if (result.stats) {
762
- console.log(\`\\nServer processing:\`);
763
- console.log(\` Parse: \${result.stats.parse_time_ms}ms\`);
764
- console.log(\` Geometry: \${result.stats.geometry_time_ms}ms\`);
765
- console.log(\` Triangles: \${result.stats.total_triangles}\`);
766
- }
767
-
768
- // Optionally fetch full data model for properties panel
769
- console.log(\`\\nFetching data model for properties...\`);
770
- const dataModel = await client.fetchDataModel(result.cache_key);
771
- if (dataModel) {
772
- console.log(\` Data model size: \${(dataModel.byteLength / 1024).toFixed(1)} KB\`);
773
- }
774
-
775
- } catch (error) {
776
- console.error('Streaming failed:', error);
777
- process.exit(1);
778
- }
779
- }
780
-
781
- main().catch(console.error);
782
- `);
783
- // src/index.ts - Library re-export for custom integrations
784
- writeFileSync(join(targetDir, 'src', 'index.ts'), `/**
785
- * ${projectName} - IFC Processing Server Client
786
- *
787
- * Re-exports the IFC-Lite server client for custom integrations.
788
- * See example.ts and example-stream.ts for usage examples.
789
- */
790
-
791
- export { IfcServerClient } from '@ifc-lite/server-client';
792
- export type {
793
- ServerConfig,
794
- ParseResponse,
795
- ParquetParseResponse,
796
- StreamEvent,
797
- HealthResponse,
798
- MetadataResponse,
799
- } from '@ifc-lite/server-client';
800
-
801
- // Default server URL
802
- export const DEFAULT_SERVER_URL = 'http://localhost:3001';
803
-
804
- /**
805
- * Create a pre-configured client for the local Docker server.
806
- */
807
- export function createLocalClient(options?: { timeout?: number }) {
808
- const { IfcServerClient } = require('@ifc-lite/server-client');
809
- return new IfcServerClient({
810
- baseUrl: process.env.SERVER_URL || DEFAULT_SERVER_URL,
811
- timeout: options?.timeout ?? 300000,
812
- });
813
- }
814
- `);
815
- // README.md
816
- writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
817
-
818
- IFC processing server using [IFC-Lite](https://github.com/louistrue/ifc-lite).
819
-
820
- ## Quick Start
821
-
822
- \`\`\`bash
823
- # 1. Start the server
824
- docker compose up -d
825
-
826
- # 2. Install client dependencies
827
- npm install
828
-
829
- # 3. Run the example (replace with your IFC file)
830
- npm run example ./your-model.ifc
831
- \`\`\`
832
-
833
- ## Features
834
-
835
- | Feature | Description |
836
- |---------|-------------|
837
- | **Content-Addressable Cache** | Same file = instant response (no reprocessing) |
838
- | **Parquet Format** | 15x smaller payloads than JSON |
839
- | **Streaming** | Progressive geometry for large files |
840
- | **Parallel Processing** | Multi-threaded geometry processing |
841
-
842
- ## Architecture
843
-
844
- \`\`\`
845
- ┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
846
- │ Your App │────▶│ IFC-Lite Server │────▶│ Cache │
847
- │ (TypeScript) │◀────│ (Docker) │◀────│ (Volume) │
848
- └─────────────────┘ └──────────────────┘ └─────────────┘
849
- \`\`\`
850
-
851
- ## Scripts
852
-
853
- | Command | Description |
854
- |---------|-------------|
855
- | \`npm run example\` | Basic parsing example |
856
- | \`npm run example:stream\` | Streaming example for large files |
857
- | \`npm run server:start\` | Start server in background |
858
- | \`npm run server:stop\` | Stop server |
859
- | \`npm run server:logs\` | View server logs |
860
- | \`npm run server:dev\` | Start with debug logging |
861
-
862
- ## Configuration
863
-
864
- Copy \`.env.example\` to \`.env\` to customize:
865
-
866
- | Variable | Default | Description |
867
- |----------|---------|-------------|
868
- | \`PORT\` | 3001 | Server port |
869
- | \`MAX_FILE_SIZE_MB\` | 500 | Max upload size |
870
- | \`WORKER_THREADS\` | 4 | Parallel processing threads |
871
- | \`CACHE_MAX_AGE_DAYS\` | 7 | Cache retention |
872
-
873
- See \`.env.example\` for all options with documentation.
874
-
875
- ## API Endpoints
876
-
877
- | Endpoint | Description |
878
- |----------|-------------|
879
- | \`GET /api/v1/health\` | Health check |
880
- | \`POST /api/v1/parse\` | Full parse (JSON response) |
881
- | \`POST /api/v1/parse/parquet\` | Full parse (Parquet, 15x smaller) |
882
- | \`POST /api/v1/parse/parquet-stream\` | Streaming parse (SSE + Parquet) |
883
- | \`GET /api/v1/cache/check/:hash\` | Check if file is cached |
884
-
885
- ## Client Usage
886
-
887
- ### Basic Parse
888
-
889
- \`\`\`typescript
890
- import { IfcServerClient } from '@ifc-lite/server-client';
891
-
892
- const client = new IfcServerClient({
893
- baseUrl: 'http://localhost:3001'
894
- });
895
-
896
- const result = await client.parseParquet(ifcBuffer);
897
- console.log(\`Meshes: \${result.meshes.length}\`);
898
- \`\`\`
899
-
900
- ### Streaming (Large Files)
901
-
902
- \`\`\`typescript
903
- await client.parseParquetStream(ifcBuffer, (batch) => {
904
- // Render each batch immediately
905
- for (const mesh of batch.meshes) {
906
- scene.addMesh(mesh);
907
- }
908
- });
909
- \`\`\`
910
-
911
- ### Cache-First Pattern
912
-
913
- \`\`\`typescript
914
- // Client automatically:
915
- // 1. Computes file hash locally
916
- // 2. Checks server cache
917
- // 3. Skips upload if cached (instant response!)
918
- const result = await client.parseParquet(ifcBuffer);
919
- \`\`\`
920
-
921
- ## Production Deployment
922
-
923
- ### Railway / Render / Fly.io
924
-
925
- The Docker image works on any container platform:
926
-
927
- \`\`\`bash
928
- # Pull and run
929
- docker pull ghcr.io/louistrue/ifc-lite-server:latest
930
- docker run -p 8080:8080 -v ifc-cache:/app/cache ghcr.io/louistrue/ifc-lite-server
931
- \`\`\`
932
-
933
- ### Environment Variables
934
-
935
- Set these in your deployment platform:
936
-
937
- \`\`\`
938
- PORT=8080
939
- MAX_FILE_SIZE_MB=500
940
- WORKER_THREADS=4
941
- CACHE_MAX_AGE_DAYS=30
942
- RUST_LOG=info
943
- \`\`\`
944
-
945
- ## Learn More
946
-
947
- - [IFC-Lite Documentation](https://louistrue.github.io/ifc-lite/)
948
- - [Server API Reference](https://louistrue.github.io/ifc-lite/api/server/)
949
- - [GitHub Repository](https://github.com/louistrue/ifc-lite)
950
- `);
951
- console.log(' Created docker-compose.yml');
952
- console.log(' Created docker-compose.dev.yml');
953
- console.log(' Created .env.example');
954
- console.log(' Created .env');
955
- console.log(' Created package.json');
956
- console.log(' Created tsconfig.json');
957
- console.log(' Created src/example.ts');
958
- console.log(' Created src/example-stream.ts');
959
- console.log(' Created src/index.ts');
960
- console.log(' Created README.md');
961
- console.log(' Created .gitignore');
962
- console.log(' Created .dockerignore');
963
- }
964
- function createServerNativeTemplate(targetDir, projectName) {
965
- const latestVersion = getLatestVersion();
966
- // package.json
967
- writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
968
- name: projectName,
969
- version: '0.1.0',
970
- type: 'module',
971
- description: 'IFC processing server (native binary) with TypeScript client',
972
- scripts: {
973
- 'server:start': 'npx @ifc-lite/server-bin',
974
- 'server:download': 'npx @ifc-lite/server-bin download',
975
- 'server:info': 'npx @ifc-lite/server-bin info',
976
- 'example': 'npx tsx src/example.ts',
977
- 'example:stream': 'npx tsx src/example-stream.ts',
978
- 'build': 'tsc',
979
- 'typecheck': 'tsc --noEmit',
980
- },
981
- dependencies: {
982
- '@ifc-lite/server-bin': latestVersion,
983
- '@ifc-lite/server-client': latestVersion,
984
- },
985
- devDependencies: {
986
- 'typescript': '^5.3.0',
987
- 'tsx': '^4.0.0',
988
- '@types/node': '^20.0.0',
989
- },
990
- optionalDependencies: {
991
- 'parquet-wasm': '^0.6.0',
992
- 'apache-arrow': '^17.0.0',
993
- },
994
- }, null, 2));
995
- // tsconfig.json
996
- writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
997
- compilerOptions: {
998
- target: 'ES2022',
999
- module: 'ESNext',
1000
- moduleResolution: 'bundler',
1001
- strict: true,
1002
- esModuleInterop: true,
1003
- skipLibCheck: true,
1004
- outDir: 'dist',
1005
- declaration: true,
1006
- lib: ['ES2022'],
1007
- },
1008
- include: ['src'],
1009
- exclude: ['node_modules', 'dist'],
1010
- }, null, 2));
1011
- // .env.example
1012
- writeFileSync(join(targetDir, '.env.example'), `# IFC-Lite Server Configuration (Native Binary)
1013
- # These environment variables configure the server
1014
-
1015
- # =============================================================================
1016
- # SERVER SETTINGS
1017
- # =============================================================================
1018
-
1019
- # Server port
1020
- PORT=8080
1021
-
1022
- # Log level: error, warn, info, debug, trace
1023
- RUST_LOG=info
1024
-
1025
- # =============================================================================
1026
- # FILE PROCESSING
1027
- # =============================================================================
1028
-
1029
- # Maximum IFC file size in megabytes
1030
- MAX_FILE_SIZE_MB=500
1031
-
1032
- # Request timeout in seconds
1033
- REQUEST_TIMEOUT_SECS=300
1034
-
1035
- # Number of worker threads for parallel processing
1036
- # Default: number of CPU cores
1037
- WORKER_THREADS=4
1038
-
1039
- # =============================================================================
1040
- # STREAMING
1041
- # =============================================================================
1042
-
1043
- # Initial batch size for fast first frame
1044
- INITIAL_BATCH_SIZE=100
1045
-
1046
- # Maximum batch size for throughput
1047
- MAX_BATCH_SIZE=1000
1048
-
1049
- # =============================================================================
1050
- # CACHING
1051
- # =============================================================================
1052
-
1053
- # Cache directory (relative or absolute path)
1054
- CACHE_DIR=./.cache
1055
-
1056
- # Cache retention in days
1057
- CACHE_MAX_AGE_DAYS=7
1058
- `);
1059
- // .gitignore
1060
- writeFileSync(join(targetDir, '.gitignore'), `# Dependencies
1061
- node_modules/
1062
-
1063
- # Build output
1064
- dist/
1065
-
1066
- # Environment files
1067
- .env
1068
- .env.local
1069
- .env.*.local
1070
-
1071
- # Cache directory
1072
- .cache/
1073
-
1074
- # IDE
1075
- .idea/
1076
- .vscode/
1077
- *.swp
1078
- *.swo
1079
-
1080
- # OS
1081
- .DS_Store
1082
- Thumbs.db
1083
-
1084
- # Logs
1085
- *.log
1086
- npm-debug.log*
1087
- `);
1088
- // Create src directory
1089
- mkdirSync(join(targetDir, 'src'));
1090
- // src/example.ts
1091
- writeFileSync(join(targetDir, 'src', 'example.ts'), `/**
1092
- * IFC-Lite Native Server Example
1093
- *
1094
- * This example demonstrates using the IFC-Lite server with native binary.
1095
- * No Docker required - the binary is downloaded and run automatically.
1096
- *
1097
- * Usage:
1098
- * 1. Start the server: npm run server:start
1099
- * 2. In another terminal: npm run example ./your-model.ifc
1100
- */
1101
-
1102
- import { IfcServerClient } from '@ifc-lite/server-client';
1103
- import { readFileSync, existsSync } from 'fs';
1104
-
1105
- const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8080';
1106
-
1107
- async function main() {
1108
- const client = new IfcServerClient({
1109
- baseUrl: SERVER_URL,
1110
- timeout: 300000,
1111
- });
1112
-
1113
- // Check server
1114
- console.log('Checking server health...');
1115
- try {
1116
- const health = await client.health();
1117
- console.log(\`Server status: \${health.status}\`);
1118
- } catch (error) {
1119
- console.error('Failed to connect to server.');
1120
- console.error('Start it with: npm run server:start');
1121
- process.exit(1);
1122
- }
1123
-
1124
- const ifcPath = process.argv[2];
1125
- if (!ifcPath) {
1126
- console.log(\`
1127
- Usage: npm run example <path-to-ifc-file>
1128
-
1129
- Example:
1130
- npm run example ./model.ifc
1131
- \`);
1132
- return;
1133
- }
1134
-
1135
- if (!existsSync(ifcPath)) {
1136
- console.error(\`File not found: \${ifcPath}\`);
1137
- process.exit(1);
1138
- }
1139
-
1140
- const buffer = readFileSync(ifcPath);
1141
- console.log(\`\\nParsing: \${ifcPath}\`);
1142
- console.log(\`File size: \${(buffer.length / 1024 / 1024).toFixed(2)} MB\`);
1143
-
1144
- const startTime = performance.now();
1145
-
1146
- try {
1147
- const parquetAvailable = await client.isParquetSupported();
1148
-
1149
- if (parquetAvailable) {
1150
- console.log('Using Parquet format (15x smaller)');
1151
- const result = await client.parseParquet(buffer);
1152
- const elapsed = performance.now() - startTime;
1153
-
1154
- console.log(\`\\nComplete in \${elapsed.toFixed(0)}ms\`);
1155
- console.log(\` Meshes: \${result.meshes.length}\`);
1156
- console.log(\` Payload: \${(result.parquet_stats.payload_size / 1024).toFixed(1)} KB\`);
1157
-
1158
- if (result.stats) {
1159
- console.log(\` Triangles: \${result.stats.total_triangles}\`);
1160
- }
1161
- } else {
1162
- console.log('Using JSON format');
1163
- const result = await client.parse(buffer);
1164
- const elapsed = performance.now() - startTime;
1165
-
1166
- console.log(\`\\nComplete in \${elapsed.toFixed(0)}ms\`);
1167
- console.log(\` Meshes: \${result.meshes.length}\`);
1168
- }
1169
- } catch (error) {
1170
- console.error('Parse failed:', error);
1171
- process.exit(1);
1172
- }
1173
- }
1174
-
1175
- main().catch(console.error);
1176
- `);
1177
- // src/example-stream.ts
1178
- writeFileSync(join(targetDir, 'src', 'example-stream.ts'), `/**
1179
- * IFC-Lite Native Server Streaming Example
1180
- *
1181
- * For large files (>50MB) - geometry arrives in batches.
1182
- *
1183
- * Usage:
1184
- * 1. Start the server: npm run server:start
1185
- * 2. In another terminal: npm run example:stream ./large-model.ifc
1186
- */
1187
-
1188
- import { IfcServerClient } from '@ifc-lite/server-client';
1189
- import { readFileSync, existsSync } from 'fs';
1190
-
1191
- const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8080';
1192
-
1193
- async function main() {
1194
- const client = new IfcServerClient({
1195
- baseUrl: SERVER_URL,
1196
- timeout: 600000,
1197
- });
1198
-
1199
- try {
1200
- await client.health();
1201
- console.log('Server connected');
1202
- } catch {
1203
- console.error('Server not available. Start with: npm run server:start');
1204
- process.exit(1);
1205
- }
1206
-
1207
- const ifcPath = process.argv[2];
1208
- if (!ifcPath || !existsSync(ifcPath)) {
1209
- console.log('Usage: npm run example:stream <path-to-ifc-file>');
1210
- process.exit(1);
1211
- }
1212
-
1213
- const parquetAvailable = await client.isParquetSupported();
1214
- if (!parquetAvailable) {
1215
- console.error('Streaming requires parquet-wasm and apache-arrow.');
1216
- console.error('Install with: npm install parquet-wasm apache-arrow');
1217
- process.exit(1);
1218
- }
1219
-
1220
- const buffer = readFileSync(ifcPath);
1221
- console.log(\`\\nStreaming: \${ifcPath} (\${(buffer.length / 1024 / 1024).toFixed(1)} MB)\`);
1222
-
1223
- const startTime = performance.now();
1224
- let totalMeshes = 0;
1225
-
1226
- try {
1227
- const result = await client.parseParquetStream(buffer, (batch) => {
1228
- totalMeshes += batch.meshes.length;
1229
- console.log(\` Batch #\${batch.batch_number}: +\${batch.meshes.length} meshes (total: \${totalMeshes})\`);
1230
- });
1231
-
1232
- const elapsed = performance.now() - startTime;
1233
- console.log(\`\\nComplete in \${elapsed.toFixed(0)}ms\`);
1234
- console.log(\` Total meshes: \${result.total_meshes}\`);
1235
-
1236
- } catch (error) {
1237
- console.error('Streaming failed:', error);
1238
- process.exit(1);
1239
- }
1240
- }
1241
-
1242
- main().catch(console.error);
1243
- `);
1244
- // src/index.ts
1245
- writeFileSync(join(targetDir, 'src', 'index.ts'), `/**
1246
- * ${projectName} - IFC Processing Server (Native Binary)
1247
- *
1248
- * Re-exports for custom integrations.
1249
- */
1250
-
1251
- export { IfcServerClient } from '@ifc-lite/server-client';
1252
- export type {
1253
- ServerConfig,
1254
- ParseResponse,
1255
- ParquetParseResponse,
1256
- StreamEvent,
1257
- HealthResponse,
1258
- MetadataResponse,
1259
- } from '@ifc-lite/server-client';
1260
-
1261
- export const DEFAULT_SERVER_URL = 'http://localhost:8080';
1262
- `);
1263
- // README.md
1264
- writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
1265
-
1266
- IFC processing server using native binary - no Docker required.
1267
-
1268
- ## Quick Start
1269
-
1270
- \`\`\`bash
1271
- # Install dependencies (downloads server binary automatically)
1272
- npm install
1273
-
1274
- # Start the server
1275
- npm run server:start
1276
-
1277
- # In another terminal, run the example
1278
- npm run example ./your-model.ifc
1279
- \`\`\`
1280
-
1281
- ## Features
1282
-
1283
- | Feature | Description |
1284
- |---------|-------------|
1285
- | **No Docker Required** | Native binary runs directly |
1286
- | **Auto-Download** | Binary downloaded on first run |
1287
- | **Cross-Platform** | macOS, Linux, Windows support |
1288
- | **Content-Addressable Cache** | Same file = instant response |
1289
-
1290
- ## Scripts
1291
-
1292
- | Command | Description |
1293
- |---------|-------------|
1294
- | \`npm run server:start\` | Start the IFC-Lite server |
1295
- | \`npm run server:download\` | Download binary without starting |
1296
- | \`npm run server:info\` | Show platform and binary info |
1297
- | \`npm run example\` | Basic parsing example |
1298
- | \`npm run example:stream\` | Streaming example for large files |
1299
-
1300
- ## Configuration
1301
-
1302
- Set environment variables to configure the server:
1303
-
1304
- \`\`\`bash
1305
- # Custom port
1306
- PORT=3001 npm run server:start
1307
-
1308
- # Debug logging
1309
- RUST_LOG=debug npm run server:start
1310
-
1311
- # Multiple options
1312
- PORT=3001 WORKER_THREADS=8 npm run server:start
1313
- \`\`\`
1314
-
1315
- See \`.env.example\` for all options.
1316
-
1317
- ## API Endpoints
1318
-
1319
- | Endpoint | Description |
1320
- |----------|-------------|
1321
- | \`GET /api/v1/health\` | Health check |
1322
- | \`POST /api/v1/parse\` | Full parse (JSON) |
1323
- | \`POST /api/v1/parse/parquet\` | Full parse (Parquet, 15x smaller) |
1324
- | \`POST /api/v1/parse/parquet-stream\` | Streaming parse |
1325
- | \`GET /api/v1/cache/check/:hash\` | Check cache |
1326
-
1327
- ## Supported Platforms
1328
-
1329
- | Platform | Architecture | Status |
1330
- |----------|--------------|--------|
1331
- | macOS | Intel (x64) | ✅ |
1332
- | macOS | Apple Silicon (arm64) | ✅ |
1333
- | Linux | x64 | ✅ |
1334
- | Linux | arm64 | ✅ |
1335
- | Windows | x64 | ✅ |
1336
-
1337
- ## Alternatives
1338
-
1339
- If native binaries don't work for your platform:
1340
-
1341
- \`\`\`bash
1342
- # Use Docker instead
1343
- npx create-ifc-lite my-app --template server
1344
- \`\`\`
1345
-
1346
- ## Learn More
1347
-
1348
- - [IFC-Lite Documentation](https://louistrue.github.io/ifc-lite/)
1349
- - [GitHub Repository](https://github.com/louistrue/ifc-lite)
1350
- `);
1351
- console.log(' Created package.json');
1352
- console.log(' Created tsconfig.json');
1353
- console.log(' Created .env.example');
1354
- console.log(' Created .gitignore');
1355
- console.log(' Created src/example.ts');
1356
- console.log(' Created src/example-stream.ts');
1357
- console.log(' Created src/index.ts');
1358
- console.log(' Created README.md');
1359
- }
1360
120
  main().catch(console.error);