forked from trent_larson/crowd-funder-for-time-pwa
docs: update build pattern conversion plan with consistent naming and mode handling
- Change build:* naming from hyphen to colon (build:web-dev → build:web:dev) - Add missing build:web:test and build:web:prod scripts - Update build:electron:dev to include electron startup (build + start) - Remove hardcoded --mode electron to allow proper mode override - Add comprehensive mode override behavior documentation - Fix mode conflicts between hardcoded and passed --mode arguments The plan now properly supports: - Development builds with default --mode development - Testing builds with explicit --mode test override - Production builds with explicit --mode production override - Consistent naming across all platforms (web, capacitor, electron)
This commit is contained in:
310
.cursor-markdown-rules.md
Normal file
310
.cursor-markdown-rules.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Cursor Markdown Ruleset for TimeSafari Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This ruleset enforces consistent markdown formatting standards across all project
|
||||
documentation, ensuring readability, maintainability, and compliance with
|
||||
markdownlint best practices.
|
||||
|
||||
## General Formatting Standards
|
||||
|
||||
### Line Length
|
||||
|
||||
- **Maximum line length**: 80 characters
|
||||
- **Exception**: Code blocks (JSON, shell, TypeScript, etc.) - no line length
|
||||
enforcement
|
||||
- **Rationale**: Ensures readability across different screen sizes and terminal
|
||||
widths
|
||||
|
||||
### Blank Lines
|
||||
|
||||
- **Headings**: Must be surrounded by blank lines above and below
|
||||
- **Lists**: Must be surrounded by blank lines above and below
|
||||
- **Code blocks**: Must be surrounded by blank lines above and below
|
||||
- **Maximum consecutive blank lines**: 1 (no multiple blank lines)
|
||||
- **File start**: No blank lines at the beginning of the file
|
||||
- **File end**: Single newline character at the end
|
||||
|
||||
### Whitespace
|
||||
|
||||
- **No trailing spaces**: Remove all trailing whitespace from lines
|
||||
- **No tabs**: Use spaces for indentation
|
||||
- **Consistent indentation**: 2 spaces for list items and nested content
|
||||
|
||||
## Heading Standards
|
||||
|
||||
### Format
|
||||
|
||||
- **Style**: ATX-style headings (`#`, `##`, `###`, etc.)
|
||||
- **Case**: Title case for general headings
|
||||
- **Code references**: Use backticks for file names and technical terms
|
||||
- ✅ `### Current package.json Scripts`
|
||||
- ❌ `### Current Package.json Scripts`
|
||||
|
||||
### Hierarchy
|
||||
|
||||
- **H1 (#)**: Document title only
|
||||
- **H2 (##)**: Major sections
|
||||
- **H3 (###)**: Subsections
|
||||
- **H4 (####)**: Sub-subsections
|
||||
- **H5+**: Avoid deeper nesting
|
||||
|
||||
## List Standards
|
||||
|
||||
### Unordered Lists
|
||||
|
||||
- **Marker**: Use `-` (hyphen) consistently
|
||||
- **Indentation**: 2 spaces for nested items
|
||||
- **Blank lines**: Surround lists with blank lines
|
||||
|
||||
### Ordered Lists
|
||||
|
||||
- **Format**: `1.`, `2.`, `3.` (sequential numbering)
|
||||
- **Indentation**: 2 spaces for nested items
|
||||
- **Blank lines**: Surround lists with blank lines
|
||||
|
||||
### Task Lists
|
||||
|
||||
- **Format**: `- [ ]` for incomplete, `- [x]` for complete
|
||||
- **Use case**: Project planning, checklists, implementation tracking
|
||||
|
||||
## Code Block Standards
|
||||
|
||||
### Fenced Code Blocks
|
||||
|
||||
- **Syntax**: Triple backticks with language specification
|
||||
- **Languages**: `json`, `bash`, `typescript`, `javascript`, `yaml`, `markdown`
|
||||
- **Blank lines**: Must be surrounded by blank lines above and below
|
||||
- **Line length**: No enforcement within code blocks
|
||||
|
||||
### Inline Code
|
||||
|
||||
- **Format**: Single backticks for inline code references
|
||||
- **Use case**: File names, commands, variables, properties
|
||||
|
||||
## Special Content Standards
|
||||
|
||||
### JSON Examples
|
||||
|
||||
```json
|
||||
{
|
||||
"property": "value",
|
||||
"nested": {
|
||||
"property": "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Commands
|
||||
|
||||
```bash
|
||||
# Command with comment
|
||||
npm run build:web
|
||||
|
||||
# Multi-line command
|
||||
VITE_GIT_HASH=`git log -1 --pretty=format:%h` \
|
||||
vite build --config vite.config.web.mts
|
||||
```
|
||||
|
||||
### TypeScript Examples
|
||||
|
||||
```typescript
|
||||
// Function with JSDoc
|
||||
/**
|
||||
* Get environment configuration
|
||||
* @param env - Environment name
|
||||
* @returns Environment config object
|
||||
*/
|
||||
const getEnvironmentConfig = (env: string) => {
|
||||
switch (env) {
|
||||
case 'prod':
|
||||
return { /* production settings */ };
|
||||
default:
|
||||
return { /* development settings */ };
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## File Structure Standards
|
||||
|
||||
### Document Header
|
||||
|
||||
```markdown
|
||||
# Document Title
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: 🎯 **STATUS** - Brief description
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the document's purpose and scope.
|
||||
```
|
||||
|
||||
### Section Organization
|
||||
|
||||
1. **Overview/Introduction**
|
||||
2. **Current State Analysis**
|
||||
3. **Implementation Plan**
|
||||
4. **Technical Details**
|
||||
5. **Testing & Validation**
|
||||
6. **Next Steps**
|
||||
|
||||
## Markdownlint Configuration
|
||||
|
||||
### Required Rules
|
||||
|
||||
```json
|
||||
{
|
||||
"MD013": { "code_blocks": false },
|
||||
"MD012": true,
|
||||
"MD022": true,
|
||||
"MD031": true,
|
||||
"MD032": true,
|
||||
"MD047": true,
|
||||
"MD009": true
|
||||
}
|
||||
```
|
||||
|
||||
### Rule Explanations
|
||||
|
||||
- **MD013**: Line length (disabled for code blocks)
|
||||
- **MD012**: No multiple consecutive blank lines
|
||||
- **MD022**: Headings should be surrounded by blank lines
|
||||
- **MD031**: Fenced code blocks should be surrounded by blank lines
|
||||
- **MD032**: Lists should be surrounded by blank lines
|
||||
- **MD047**: Files should end with a single newline
|
||||
- **MD009**: No trailing spaces
|
||||
|
||||
## Validation Commands
|
||||
|
||||
### Check Single File
|
||||
|
||||
```bash
|
||||
npx markdownlint docs/filename.md
|
||||
```
|
||||
|
||||
### Check All Documentation
|
||||
|
||||
```bash
|
||||
npx markdownlint docs/
|
||||
```
|
||||
|
||||
### Auto-fix Common Issues
|
||||
|
||||
```bash
|
||||
# Remove trailing spaces
|
||||
sed -i 's/[[:space:]]*$//' docs/filename.md
|
||||
|
||||
# Remove multiple blank lines
|
||||
sed -i '/^$/N;/^\n$/D' docs/filename.md
|
||||
|
||||
# Add newline at end if missing
|
||||
echo "" >> docs/filename.md
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Implementation Plans
|
||||
|
||||
```markdown
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation (Day 1)
|
||||
|
||||
#### 1.1 Component Setup
|
||||
|
||||
- [ ] Create new component file
|
||||
- [ ] Add basic structure
|
||||
- [ ] Implement core functionality
|
||||
|
||||
#### 1.2 Configuration
|
||||
|
||||
- [ ] Update configuration files
|
||||
- [ ] Add environment variables
|
||||
- [ ] Test configuration loading
|
||||
```
|
||||
|
||||
### Status Tracking
|
||||
|
||||
```markdown
|
||||
**Status**: ✅ **COMPLETE** - All phases finished
|
||||
**Progress**: 75% (15/20 components)
|
||||
**Next**: Ready for testing phase
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```markdown
|
||||
#### 📊 Performance Metrics
|
||||
- **Build Time**: 2.3 seconds (50% faster than baseline)
|
||||
- **Bundle Size**: 1.2MB (30% reduction)
|
||||
- **Success Rate**: 100% (no failures in 50 builds)
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
- Run markdownlint on all changed markdown files
|
||||
- Block commits with linting violations
|
||||
- Auto-fix common issues when possible
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
- Include markdownlint in build pipeline
|
||||
- Generate reports for documentation quality
|
||||
- Fail builds with critical violations
|
||||
|
||||
### Team Guidelines
|
||||
|
||||
- All documentation PRs must pass markdownlint
|
||||
- Use provided templates for new documents
|
||||
- Follow established patterns for consistency
|
||||
|
||||
## Templates
|
||||
|
||||
### New Document Template
|
||||
|
||||
```markdown
|
||||
# Document Title
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: 🎯 **PLANNING** - Ready for Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the document's purpose and scope.
|
||||
|
||||
## Current State
|
||||
|
||||
Description of current situation or problem.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review and approve plan**
|
||||
2. **Begin implementation**
|
||||
3. **Test and validate**
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for implementation
|
||||
**Priority**: Medium
|
||||
**Estimated Effort**: X days
|
||||
**Dependencies**: None
|
||||
**Stakeholders**: Development team
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-07-09
|
||||
**Version**: 1.0
|
||||
**Maintainer**: Matthew Raymer
|
||||
314
.cursor/rules/markdown.mdc
Normal file
314
.cursor/rules/markdown.mdc
Normal file
@@ -0,0 +1,314 @@
|
||||
---
|
||||
globs: *.md
|
||||
alwaysApply: false
|
||||
---
|
||||
# Cursor Markdown Ruleset for TimeSafari Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This ruleset enforces consistent markdown formatting standards across all project
|
||||
documentation, ensuring readability, maintainability, and compliance with
|
||||
markdownlint best practices.
|
||||
|
||||
## General Formatting Standards
|
||||
|
||||
### Line Length
|
||||
|
||||
- **Maximum line length**: 80 characters
|
||||
- **Exception**: Code blocks (JSON, shell, TypeScript, etc.) - no line length
|
||||
enforcement
|
||||
- **Rationale**: Ensures readability across different screen sizes and terminal
|
||||
widths
|
||||
|
||||
### Blank Lines
|
||||
|
||||
- **Headings**: Must be surrounded by blank lines above and below
|
||||
- **Lists**: Must be surrounded by blank lines above and below
|
||||
- **Code blocks**: Must be surrounded by blank lines above and below
|
||||
- **Maximum consecutive blank lines**: 1 (no multiple blank lines)
|
||||
- **File start**: No blank lines at the beginning of the file
|
||||
- **File end**: Single newline character at the end
|
||||
|
||||
### Whitespace
|
||||
|
||||
- **No trailing spaces**: Remove all trailing whitespace from lines
|
||||
- **No tabs**: Use spaces for indentation
|
||||
- **Consistent indentation**: 2 spaces for list items and nested content
|
||||
|
||||
## Heading Standards
|
||||
|
||||
### Format
|
||||
|
||||
- **Style**: ATX-style headings (`#`, `##`, `###`, etc.)
|
||||
- **Case**: Title case for general headings
|
||||
- **Code references**: Use backticks for file names and technical terms
|
||||
- ✅ `### Current package.json Scripts`
|
||||
- ❌ `### Current Package.json Scripts`
|
||||
|
||||
### Hierarchy
|
||||
|
||||
- **H1 (#)**: Document title only
|
||||
- **H2 (##)**: Major sections
|
||||
- **H3 (###)**: Subsections
|
||||
- **H4 (####)**: Sub-subsections
|
||||
- **H5+**: Avoid deeper nesting
|
||||
|
||||
## List Standards
|
||||
|
||||
### Unordered Lists
|
||||
|
||||
- **Marker**: Use `-` (hyphen) consistently
|
||||
- **Indentation**: 2 spaces for nested items
|
||||
- **Blank lines**: Surround lists with blank lines
|
||||
|
||||
### Ordered Lists
|
||||
|
||||
- **Format**: `1.`, `2.`, `3.` (sequential numbering)
|
||||
- **Indentation**: 2 spaces for nested items
|
||||
- **Blank lines**: Surround lists with blank lines
|
||||
|
||||
### Task Lists
|
||||
|
||||
- **Format**: `- [ ]` for incomplete, `- [x]` for complete
|
||||
- **Use case**: Project planning, checklists, implementation tracking
|
||||
|
||||
## Code Block Standards
|
||||
|
||||
### Fenced Code Blocks
|
||||
|
||||
- **Syntax**: Triple backticks with language specification
|
||||
- **Languages**: `json`, `bash`, `typescript`, `javascript`, `yaml`, `markdown`
|
||||
- **Blank lines**: Must be surrounded by blank lines above and below
|
||||
- **Line length**: No enforcement within code blocks
|
||||
|
||||
### Inline Code
|
||||
|
||||
- **Format**: Single backticks for inline code references
|
||||
- **Use case**: File names, commands, variables, properties
|
||||
|
||||
## Special Content Standards
|
||||
|
||||
### JSON Examples
|
||||
|
||||
```json
|
||||
{
|
||||
"property": "value",
|
||||
"nested": {
|
||||
"property": "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Commands
|
||||
|
||||
```bash
|
||||
# Command with comment
|
||||
npm run build:web
|
||||
|
||||
# Multi-line command
|
||||
VITE_GIT_HASH=`git log -1 --pretty=format:%h` \
|
||||
vite build --config vite.config.web.mts
|
||||
```
|
||||
|
||||
### TypeScript Examples
|
||||
|
||||
```typescript
|
||||
// Function with JSDoc
|
||||
/**
|
||||
* Get environment configuration
|
||||
* @param env - Environment name
|
||||
* @returns Environment config object
|
||||
*/
|
||||
const getEnvironmentConfig = (env: string) => {
|
||||
switch (env) {
|
||||
case 'prod':
|
||||
return { /* production settings */ };
|
||||
default:
|
||||
return { /* development settings */ };
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## File Structure Standards
|
||||
|
||||
### Document Header
|
||||
|
||||
```markdown
|
||||
# Document Title
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: 🎯 **STATUS** - Brief description
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the document's purpose and scope.
|
||||
```
|
||||
|
||||
### Section Organization
|
||||
|
||||
1. **Overview/Introduction**
|
||||
2. **Current State Analysis**
|
||||
3. **Implementation Plan**
|
||||
4. **Technical Details**
|
||||
5. **Testing & Validation**
|
||||
6. **Next Steps**
|
||||
|
||||
## Markdownlint Configuration
|
||||
|
||||
### Required Rules
|
||||
|
||||
```json
|
||||
{
|
||||
"MD013": { "code_blocks": false },
|
||||
"MD012": true,
|
||||
"MD022": true,
|
||||
"MD031": true,
|
||||
"MD032": true,
|
||||
"MD047": true,
|
||||
"MD009": true
|
||||
}
|
||||
```
|
||||
|
||||
### Rule Explanations
|
||||
|
||||
- **MD013**: Line length (disabled for code blocks)
|
||||
- **MD012**: No multiple consecutive blank lines
|
||||
- **MD022**: Headings should be surrounded by blank lines
|
||||
- **MD031**: Fenced code blocks should be surrounded by blank lines
|
||||
- **MD032**: Lists should be surrounded by blank lines
|
||||
- **MD047**: Files should end with a single newline
|
||||
- **MD009**: No trailing spaces
|
||||
|
||||
## Validation Commands
|
||||
|
||||
### Check Single File
|
||||
|
||||
```bash
|
||||
npx markdownlint docs/filename.md
|
||||
```
|
||||
|
||||
### Check All Documentation
|
||||
|
||||
```bash
|
||||
npx markdownlint docs/
|
||||
```
|
||||
|
||||
### Auto-fix Common Issues
|
||||
|
||||
```bash
|
||||
# Remove trailing spaces
|
||||
sed -i 's/[[:space:]]*$//' docs/filename.md
|
||||
|
||||
# Remove multiple blank lines
|
||||
sed -i '/^$/N;/^\n$/D' docs/filename.md
|
||||
|
||||
# Add newline at end if missing
|
||||
echo "" >> docs/filename.md
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Implementation Plans
|
||||
|
||||
```markdown
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation (Day 1)
|
||||
|
||||
#### 1.1 Component Setup
|
||||
|
||||
- [ ] Create new component file
|
||||
- [ ] Add basic structure
|
||||
- [ ] Implement core functionality
|
||||
|
||||
#### 1.2 Configuration
|
||||
|
||||
- [ ] Update configuration files
|
||||
- [ ] Add environment variables
|
||||
- [ ] Test configuration loading
|
||||
```
|
||||
|
||||
### Status Tracking
|
||||
|
||||
```markdown
|
||||
**Status**: ✅ **COMPLETE** - All phases finished
|
||||
**Progress**: 75% (15/20 components)
|
||||
**Next**: Ready for testing phase
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```markdown
|
||||
#### 📊 Performance Metrics
|
||||
- **Build Time**: 2.3 seconds (50% faster than baseline)
|
||||
- **Bundle Size**: 1.2MB (30% reduction)
|
||||
- **Success Rate**: 100% (no failures in 50 builds)
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
- Run markdownlint on all changed markdown files
|
||||
- Block commits with linting violations
|
||||
- Auto-fix common issues when possible
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
- Include markdownlint in build pipeline
|
||||
- Generate reports for documentation quality
|
||||
- Fail builds with critical violations
|
||||
|
||||
### Team Guidelines
|
||||
|
||||
- All documentation PRs must pass markdownlint
|
||||
- Use provided templates for new documents
|
||||
- Follow established patterns for consistency
|
||||
|
||||
## Templates
|
||||
|
||||
### New Document Template
|
||||
|
||||
```markdown
|
||||
# Document Title
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: 🎯 **PLANNING** - Ready for Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the document's purpose and scope.
|
||||
|
||||
## Current State
|
||||
|
||||
Description of current situation or problem.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Foundation
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review and approve plan**
|
||||
2. **Begin implementation**
|
||||
3. **Test and validate**
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for implementation
|
||||
**Priority**: Medium
|
||||
**Estimated Effort**: X days
|
||||
**Dependencies**: None
|
||||
**Stakeholders**: Development team
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-07-09
|
||||
**Version**: 1.0
|
||||
**Maintainer**: Matthew Raymer
|
||||
1
.markdownlint.json
Normal file
1
.markdownlint.json
Normal file
@@ -0,0 +1 @@
|
||||
{"MD013": {"code_blocks": false}}
|
||||
569
docs/build-pattern-conversion-plan.md
Normal file
569
docs/build-pattern-conversion-plan.md
Normal file
@@ -0,0 +1,569 @@
|
||||
# Build Pattern Conversion Plan
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-07-09
|
||||
**Status**: 🎯 **PLANNING** - Ready for Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Convert TimeSafari's build instruction pattern from the current script-based
|
||||
approach to a new Vite `mode`-based pattern that provides better environment
|
||||
management and consistency across all build targets.
|
||||
|
||||
## Why Vite Mode Instead of NODE_ENV?
|
||||
|
||||
### ✅ Vite's Native Mode System
|
||||
|
||||
Vite is designed to work with `mode`, which:
|
||||
|
||||
- Determines the `.env` file to load (e.g. `.env.production`, `.env.test`, etc.)
|
||||
- Is passed to `defineConfig(({ mode }) => {...})` in `vite.config.ts`
|
||||
- Is used to set behavior for dev/prod/test at config level
|
||||
- Provides better integration with Vite's build system
|
||||
|
||||
### 🚫 NODE_ENV Limitations
|
||||
|
||||
`NODE_ENV` is legacy from Webpack-era tooling:
|
||||
|
||||
- You can't change `NODE_ENV` manually and expect Vite to adapt
|
||||
- Vite does not map `NODE_ENV` back to `mode`
|
||||
- It's redundant with `mode` and might conflict with assumptions
|
||||
- Limited integration with Vite's environment loading system
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
```bash
|
||||
# ✅ Correct: Use Vite's mode system
|
||||
vite build --mode production
|
||||
vite build --mode development
|
||||
vite build --mode test
|
||||
|
||||
# ⚠️ Only if third-party libraries require NODE_ENV
|
||||
NODE_ENV=production vite build --mode production
|
||||
```
|
||||
|
||||
### Development vs Build Environments
|
||||
|
||||
**Development Environment:**
|
||||
- **Build with defaults**: `npm run build:*` - Uses `--mode development` by default
|
||||
- **Purpose**: Development builds for testing and debugging
|
||||
- **Output**: Bundled files with development optimizations
|
||||
|
||||
**Testing/Production Environments:**
|
||||
- **Build with explicit mode**: `npm run build:* -- --mode test/production`
|
||||
- **Purpose**: Validate and deploy the bundled application
|
||||
- **Output**: Optimized, bundled files for specific environment
|
||||
|
||||
### Mode Override Behavior
|
||||
|
||||
**How `--mode` Override Works:**
|
||||
|
||||
```bash
|
||||
# Base script (no hardcoded mode)
|
||||
"build:electron": "vite build --config vite.config.electron.mts"
|
||||
|
||||
# Development (uses Vite's default: --mode development)
|
||||
npm run build:electron
|
||||
# Executes: vite build --config vite.config.electron.mts
|
||||
|
||||
# Testing (explicitly overrides with --mode test)
|
||||
npm run build:electron -- --mode test
|
||||
# Executes: vite build --config vite.config.electron.mts --mode test
|
||||
|
||||
# Production (explicitly overrides with --mode production)
|
||||
npm run build:electron -- --mode production
|
||||
# Executes: vite build --config vite.config.electron.mts --mode production
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Base scripts have **no hardcoded `--mode`** to allow override
|
||||
- `npm run build:electron` defaults to `--mode development`
|
||||
- `npm run build:electron -- --mode test` overrides to `--mode test`
|
||||
- Vite uses the **last `--mode` argument** if multiple are provided
|
||||
|
||||
### Capacitor Platform-Specific Commands
|
||||
|
||||
Capacitor requires platform-specific sync commands after building:
|
||||
|
||||
```bash
|
||||
# General sync (copies web assets to all platforms)
|
||||
npm run build:capacitor && npx cap sync
|
||||
|
||||
# Platform-specific sync
|
||||
npm run build:capacitor && npx cap sync android
|
||||
npm run build:capacitor && npx cap sync ios
|
||||
|
||||
# Environment-specific with platform sync
|
||||
npm run build:capacitor -- --mode production && npx cap sync android
|
||||
npm run build:capacitor -- --mode development && npx cap sync ios
|
||||
```
|
||||
|
||||
### Electron Platform-Specific Commands
|
||||
|
||||
Electron requires platform-specific build commands after the Vite build:
|
||||
|
||||
```bash
|
||||
# General Electron build (Vite build only)
|
||||
npm run build:electron
|
||||
|
||||
# Platform-specific builds
|
||||
npm run build:electron:windows # Windows executable
|
||||
npm run build:electron:mac # macOS app bundle
|
||||
npm run build:electron:linux # Linux executable
|
||||
|
||||
# Package-specific builds
|
||||
npm run build:electron:appimage # Linux AppImage
|
||||
npm run build:electron:dmg # macOS DMG installer
|
||||
|
||||
# Environment-specific builds
|
||||
npm run build:electron -- --mode development
|
||||
npm run build:electron -- --mode test
|
||||
npm run build:electron -- --mode production
|
||||
|
||||
# Environment-specific with platform builds
|
||||
npm run build:electron:windows -- --mode development
|
||||
npm run build:electron:windows -- --mode test
|
||||
npm run build:electron:windows -- --mode production
|
||||
|
||||
npm run build:electron:mac -- --mode development
|
||||
npm run build:electron:mac -- --mode test
|
||||
npm run build:electron:mac -- --mode production
|
||||
|
||||
npm run build:electron:linux -- --mode development
|
||||
npm run build:electron:linux -- --mode test
|
||||
npm run build:electron:linux -- --mode production
|
||||
|
||||
# Environment-specific with package builds
|
||||
npm run build:electron:appimage -- --mode development
|
||||
npm run build:electron:appimage -- --mode test
|
||||
npm run build:electron:appimage -- --mode production
|
||||
|
||||
npm run build:electron:dmg -- --mode development
|
||||
npm run build:electron:dmg -- --mode test
|
||||
npm run build:electron:dmg -- --mode production
|
||||
```
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Build Scripts
|
||||
|
||||
- **Web**: `build:web` - Uses vite.config.web.mts
|
||||
- **Capacitor**: `build:capacitor` - Uses vite.config.capacitor.mts
|
||||
- **Android**: `build:android` - Shell script wrapper
|
||||
- **iOS**: `build:ios` - Shell script wrapper
|
||||
- **Electron**: `build:electron` - Uses vite.config.electron.mts
|
||||
- **Windows**: `build:electron:windows` - Windows executable
|
||||
- **macOS**: `build:electron:mac` - macOS app bundle
|
||||
- **Linux**: `build:electron:linux` - Linux executable
|
||||
- **AppImage**: `build:electron:appimage` - Linux AppImage
|
||||
- **DMG**: `build:electron:dmg` - macOS DMG installer
|
||||
|
||||
### Current `package.json` Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode electron --config vite.config.electron.mts"
|
||||
}
|
||||
```
|
||||
|
||||
## Target Pattern
|
||||
|
||||
### New Vite Mode-Based Pattern
|
||||
|
||||
```bash
|
||||
# Development builds (defaults to --mode development)
|
||||
npm run build:web-dev
|
||||
npm run build:capacitor-dev
|
||||
npm run build:electron-dev
|
||||
|
||||
# Testing builds (bundle required)
|
||||
npm run build:web -- --mode test
|
||||
npm run build:capacitor -- --mode test && npx cap sync
|
||||
npm run build:electron -- --mode test
|
||||
|
||||
# Production builds (bundle required)
|
||||
npm run build:web -- --mode production
|
||||
npm run build:capacitor -- --mode production && npx cap sync
|
||||
npm run build:electron -- --mode production
|
||||
|
||||
# Docker builds
|
||||
npm run build:web-docker -- --mode test
|
||||
npm run build:web-docker -- --mode production
|
||||
|
||||
# Capacitor platform-specific builds
|
||||
npm run build:capacitor:android -- --mode test
|
||||
npm run build:capacitor:android -- --mode production
|
||||
|
||||
npm run build:capacitor:ios -- --mode test
|
||||
npm run build:capacitor:ios -- --mode production
|
||||
|
||||
# Electron platform-specific builds
|
||||
npm run build:electron:windows -- --mode test
|
||||
npm run build:electron:windows -- --mode production
|
||||
|
||||
npm run build:electron:mac -- --mode test
|
||||
npm run build:electron:mac -- --mode production
|
||||
|
||||
npm run build:electron:linux -- --mode test
|
||||
npm run build:electron:linux -- --mode production
|
||||
|
||||
# Electron package-specific builds
|
||||
npm run build:electron:appimage -- --mode test
|
||||
npm run build:electron:appimage -- --mode production
|
||||
|
||||
npm run build:electron:dmg -- --mode test
|
||||
npm run build:electron:dmg -- --mode production
|
||||
```
|
||||
|
||||
### New `package.json` Scripts Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:web:dev": "npm run build:web",
|
||||
"build:web:test": "npm run build:web -- --mode test",
|
||||
"build:web:prod": "npm run build:web -- --mode production"
|
||||
"build:web:docker": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:web:docker:test": "npm run build:web:docker -- --mode test",
|
||||
"build:web:docker:prod": "npm run build:web:docker -- --mode production",
|
||||
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor-dev": "npm run build:capacitor",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
|
||||
"build:capacitor:android": "npm run build:capacitor:sync && npx cap sync android",
|
||||
"build:capacitor:ios": "npm run build:capacitor:sync && npx cap sync ios",
|
||||
|
||||
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts",
|
||||
"build:electron:dev": "npm run build:electron && cd electron && npm run electron:start",
|
||||
"build:electron:windows": "npm run build:electron && cd electron && npm run build:windows",
|
||||
"build:electron:mac": "npm run build:electron && cd electron && npm run build:mac",
|
||||
"build:electron:linux": "npm run build:electron && cd electron && npm run build:linux",
|
||||
"build:electron:appimage": "npm run build:electron:linux && cd electron && npm run build:appimage",
|
||||
"build:electron:dmg": "npm run build:electron:mac && cd electron && npm run build:dmg"
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Environment Configuration (Day 1)
|
||||
|
||||
#### 1.1 Update Vite Configurations
|
||||
|
||||
- [ ] **vite.config.web.mts**: Add mode-based configuration
|
||||
- [ ] **vite.config.capacitor.mts**: Add mode-based configuration
|
||||
- [ ] **vite.config.electron.mts**: Add mode-based configuration
|
||||
- [ ] **vite.config.common.mts**: Add environment-specific variables
|
||||
|
||||
#### 1.2 Environment Variables Setup
|
||||
|
||||
- [ ] Create `.env.development` file for development settings
|
||||
- [ ] Create `.env.test` file for testing settings
|
||||
- [ ] Create `.env.production` file for production settings
|
||||
- [ ] Update `.env.example` with new pattern
|
||||
|
||||
#### 1.3 Environment Detection Logic
|
||||
|
||||
```typescript
|
||||
// vite.config.common.mts
|
||||
export default defineConfig(({ mode }) => {
|
||||
const getEnvironmentConfig = (mode: string) => {
|
||||
switch (mode) {
|
||||
case 'production':
|
||||
return { /* production settings */ };
|
||||
case 'test':
|
||||
return { /* testing settings */ };
|
||||
default:
|
||||
return { /* development settings */ };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV__: mode === 'development',
|
||||
__TEST__: mode === 'test',
|
||||
__PROD__: mode === 'production'
|
||||
},
|
||||
// ... other config
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 2: Package.json Scripts Update (Day 1)
|
||||
|
||||
#### 2.1 Web Build Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:web-dev": "npm run build:web",
|
||||
"build:web-test": "npm run build:web -- --mode test",
|
||||
"build:web-prod": "npm run build:web -- --mode production"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Capacitor Build Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor-dev": "npm run build:capacitor",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
|
||||
"build:capacitor:android": "npm run build:capacitor:sync && npx cap sync android",
|
||||
"build:capacitor:ios": "npm run build:capacitor:sync && npx cap sync ios",
|
||||
"build:capacitor-test": "npm run build:capacitor -- --mode test && npx cap sync",
|
||||
"build:capacitor-prod": "npm run build:capacitor -- --mode production && npx cap sync",
|
||||
"build:capacitor:android-test": "npm run build:capacitor -- --mode test && npx cap sync android",
|
||||
"build:capacitor:android-prod": "npm run build:capacitor -- --mode production && npx cap sync android",
|
||||
"build:capacitor:ios-test": "npm run build:capacitor -- --mode test && npx cap sync ios",
|
||||
"build:capacitor:ios-prod": "npm run build:capacitor -- --mode production && npx cap sync ios"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Electron Build Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts",
|
||||
"build:electron-dev": "npm run build:electron",
|
||||
"build:electron:windows": "npm run build:electron && cd electron && npm run build:windows",
|
||||
"build:electron:mac": "npm run build:electron && cd electron && npm run build:mac",
|
||||
"build:electron:linux": "npm run build:electron && cd electron && npm run build:linux",
|
||||
"build:electron:appimage": "npm run build:electron:linux && cd electron && npm run build:appimage",
|
||||
"build:electron:dmg": "npm run build:electron:mac && cd electron && npm run build:dmg",
|
||||
"build:electron-test": "npm run build:electron -- --mode test",
|
||||
"build:electron-prod": "npm run build:electron -- --mode production",
|
||||
"build:electron:windows-test": "npm run build:electron -- --mode test && cd electron && npm run build:windows",
|
||||
"build:electron:windows-prod": "npm run build:electron -- --mode production && cd electron && npm run build:windows",
|
||||
"build:electron:mac-dev": "npm run build:electron -- --mode development && cd electron && npm run build:mac",
|
||||
"build:electron:mac-test": "npm run build:electron -- --mode test && cd electron && npm run build:mac",
|
||||
"build:electron:mac-prod": "npm run build:electron -- --mode production && cd electron && npm run build:mac",
|
||||
"build:electron:linux-test": "npm run build:electron -- --mode test && cd electron && npm run build:linux",
|
||||
"build:electron:linux-prod": "npm run build:electron -- --mode production && cd electron && npm run build:linux"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 Docker Build Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"build:web-docker": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:web-docker-test": "npm run build:web-docker -- --mode test",
|
||||
"build:web-docker-prod": "npm run build:web-docker -- --mode production"
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Shell Script Updates (Day 2)
|
||||
|
||||
#### 3.1 Update build-electron.sh
|
||||
|
||||
- [ ] Add mode-based environment support
|
||||
- [ ] Update environment loading logic
|
||||
- [ ] Add environment-specific build paths
|
||||
- [ ] Update logging to show environment
|
||||
|
||||
#### 3.2 Update build-android.sh
|
||||
|
||||
- [ ] Add mode-based environment support
|
||||
- [ ] Update environment detection
|
||||
- [ ] Add environment-specific configurations
|
||||
|
||||
#### 3.3 Update build-ios.sh
|
||||
|
||||
- [ ] Add mode-based environment support
|
||||
- [ ] Update environment detection
|
||||
- [ ] Add environment-specific configurations
|
||||
|
||||
### Phase 4: Documentation Updates (Day 2)
|
||||
|
||||
#### 4.1 Update BUILDING.md
|
||||
|
||||
- [ ] Document new Vite mode-based pattern
|
||||
- [ ] Update build instructions
|
||||
- [ ] Add environment-specific examples
|
||||
- [ ] Update troubleshooting section
|
||||
|
||||
#### 4.2 Update scripts/README.md
|
||||
|
||||
- [ ] Document new Vite mode-based build patterns
|
||||
- [ ] Update usage examples
|
||||
- [ ] Add environment configuration guide
|
||||
|
||||
#### 4.3 Update CI/CD Documentation
|
||||
|
||||
- [ ] Update GitHub Actions workflows
|
||||
- [ ] Update Docker build instructions
|
||||
- [ ] Update deployment guides
|
||||
|
||||
### Phase 5: Testing & Validation (Day 3)
|
||||
|
||||
#### 5.1 Environment Testing
|
||||
|
||||
- [ ] Test dev environment builds
|
||||
- [ ] Test test environment builds
|
||||
- [ ] Test prod environment builds
|
||||
- [ ] Validate environment variables
|
||||
|
||||
#### 5.2 Platform Testing
|
||||
|
||||
- [ ] Test web builds across environments
|
||||
- [ ] Test capacitor builds across environments
|
||||
- [ ] Test capacitor android sync across environments
|
||||
- [ ] Test capacitor ios sync across environments
|
||||
- [ ] Test electron builds across environments
|
||||
- [ ] Test electron windows builds across environments
|
||||
- [ ] Test electron mac builds across environments
|
||||
- [ ] Test electron linux builds across environments
|
||||
- [ ] Test electron appimage builds across environments
|
||||
- [ ] Test electron dmg builds across environments
|
||||
- [ ] Test docker builds across environments
|
||||
|
||||
#### 5.3 Integration Testing
|
||||
|
||||
- [ ] Test with existing CI/CD pipelines
|
||||
- [ ] Test with existing deployment scripts
|
||||
- [ ] Test with existing development workflows
|
||||
|
||||
## Environment-Specific Configurations
|
||||
|
||||
### Development Environment (--mode development)
|
||||
|
||||
```typescript
|
||||
{
|
||||
VITE_API_URL: 'http://localhost:3000',
|
||||
VITE_DEBUG: 'true',
|
||||
VITE_LOG_LEVEL: 'debug',
|
||||
VITE_ENABLE_DEV_TOOLS: 'true'
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Environment (--mode test)
|
||||
|
||||
```typescript
|
||||
{
|
||||
VITE_API_URL: 'https://test-api.timesafari.com',
|
||||
VITE_DEBUG: 'false',
|
||||
VITE_LOG_LEVEL: 'info',
|
||||
VITE_ENABLE_DEV_TOOLS: 'false'
|
||||
}
|
||||
```
|
||||
|
||||
### Production Environment (--mode production)
|
||||
|
||||
```typescript
|
||||
{
|
||||
VITE_API_URL: 'https://api.timesafari.com',
|
||||
VITE_DEBUG: 'false',
|
||||
VITE_LOG_LEVEL: 'warn',
|
||||
VITE_ENABLE_DEV_TOOLS: 'false'
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- [ ] Keep existing script names as aliases
|
||||
- [ ] Add deprecation warnings for old scripts
|
||||
- [ ] Maintain existing CI/CD compatibility
|
||||
- [ ] Provide migration guide for users
|
||||
|
||||
### Gradual Rollout
|
||||
|
||||
1. **Week 1**: Implement new scripts alongside existing ones
|
||||
2. **Week 2**: Update CI/CD to use new pattern
|
||||
3. **Week 3**: Update documentation and guides
|
||||
4. **Week 4**: Deprecate old scripts with warnings
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical Metrics
|
||||
|
||||
- [ ] All builds work with Vite mode-based pattern
|
||||
- [ ] Environment variables properly loaded
|
||||
- [ ] Build artifacts correctly generated
|
||||
- [ ] No regression in existing functionality
|
||||
|
||||
### Process Metrics
|
||||
|
||||
- [ ] Reduced build script complexity
|
||||
- [ ] Improved environment management
|
||||
- [ ] Better developer experience
|
||||
- [ ] Consistent build patterns
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Low Risk
|
||||
|
||||
- [ ] Environment variable changes
|
||||
- [ ] Package.json script updates
|
||||
- [ ] Documentation updates
|
||||
|
||||
### Medium Risk
|
||||
|
||||
- [ ] Vite configuration changes (mode-based)
|
||||
- [ ] Shell script modifications
|
||||
- [ ] CI/CD pipeline updates
|
||||
|
||||
### High Risk
|
||||
|
||||
- [ ] Breaking existing build processes
|
||||
- [ ] Environment-specific bugs
|
||||
- [ ] Deployment failures
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Immediate Rollback
|
||||
|
||||
- [ ] Revert package.json changes
|
||||
- [ ] Restore original vite configs
|
||||
- [ ] Restore original shell scripts
|
||||
|
||||
### Gradual Rollback
|
||||
|
||||
- [ ] Keep old scripts as primary
|
||||
- [ ] Use new scripts as experimental
|
||||
- [ ] Gather feedback before full migration
|
||||
|
||||
## Timeline
|
||||
|
||||
### Day 1: Foundation
|
||||
|
||||
- [ ] Environment configuration setup
|
||||
- [ ] Package.json script updates
|
||||
- [ ] Basic testing
|
||||
|
||||
### Day 2: Integration
|
||||
|
||||
- [ ] Shell script updates
|
||||
- [ ] Documentation updates
|
||||
- [ ] Integration testing
|
||||
|
||||
### Day 3: Validation
|
||||
|
||||
- [ ] Comprehensive testing
|
||||
- [ ] Performance validation
|
||||
- [ ] Documentation review
|
||||
|
||||
### Day 4: Deployment
|
||||
|
||||
- [ ] CI/CD updates
|
||||
- [ ] Production validation
|
||||
- [ ] User communication
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review and approve plan**
|
||||
2. **Set up development environment**
|
||||
3. **Begin Phase 1 implementation**
|
||||
4. **Create test cases**
|
||||
5. **Start implementation**
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for implementation
|
||||
**Priority**: Medium
|
||||
**Estimated Effort**: 3-4 days
|
||||
**Dependencies**: None
|
||||
**Stakeholders**: Development team, DevOps team
|
||||
662
docs/lazy-loading-patterns.md
Normal file
662
docs/lazy-loading-patterns.md
Normal file
@@ -0,0 +1,662 @@
|
||||
# Vue 3 + Vite + vue-facing-decorator: Lazy Loading Patterns & Best Practices
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides comprehensive guidance on implementing lazy loading and code splitting in Vue 3 applications using Vite and `vue-facing-decorator`. The patterns demonstrated here optimize bundle size, improve initial load times, and enhance user experience through progressive loading.
|
||||
|
||||
**Author:** Matthew Raymer
|
||||
**Version:** 1.0.0
|
||||
**Last Updated:** 2024
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Why Lazy Loading Matters](#why-lazy-loading-matters)
|
||||
2. [Vite Configuration for Optimal Code Splitting](#vite-configuration-for-optimal-code-splitting)
|
||||
3. [Lazy Loading Patterns](#lazy-loading-patterns)
|
||||
4. [Component-Level Lazy Loading](#component-level-lazy-loading)
|
||||
5. [Route-Level Lazy Loading](#route-level-lazy-loading)
|
||||
6. [Library-Level Lazy Loading](#library-level-lazy-loading)
|
||||
7. [Performance Monitoring](#performance-monitoring)
|
||||
8. [Best Practices](#best-practices)
|
||||
9. [Common Pitfalls](#common-pitfalls)
|
||||
10. [Examples](#examples)
|
||||
|
||||
## Why Lazy Loading Matters
|
||||
|
||||
### Performance Benefits
|
||||
|
||||
- **Faster Initial Load**: Only load code needed for the current view
|
||||
- **Reduced Bundle Size**: Split large applications into smaller chunks
|
||||
- **Better Caching**: Independent chunks can be cached separately
|
||||
- **Improved User Experience**: Progressive loading with loading states
|
||||
|
||||
### When to Use Lazy Loading
|
||||
|
||||
✅ **Good Candidates for Lazy Loading:**
|
||||
- Heavy components (data processing, 3D rendering)
|
||||
- Feature-specific components (QR scanner, file uploader)
|
||||
- Route-based components
|
||||
- Large third-party libraries (ThreeJS, Chart.js)
|
||||
- Components with conditional rendering
|
||||
|
||||
❌ **Avoid Lazy Loading:**
|
||||
- Core UI components used everywhere
|
||||
- Small utility components
|
||||
- Components needed for initial render
|
||||
- Components with frequent usage patterns
|
||||
|
||||
## Vite Configuration for Optimal Code Splitting
|
||||
|
||||
### Enhanced Build Configuration
|
||||
|
||||
```typescript
|
||||
// vite.config.optimized.mts
|
||||
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> {
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Enhanced manual chunks for better code splitting
|
||||
manualChunks: {
|
||||
// Vendor chunks for better caching
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'],
|
||||
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode'],
|
||||
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'],
|
||||
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'],
|
||||
'three-vendor': ['three', '@tweenjs/tween.js'],
|
||||
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'],
|
||||
// Platform-specific chunks
|
||||
...(isCapacitor && {
|
||||
'capacitor-vendor': ['@capacitor/core', '@capacitor/app']
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Key Configuration Features
|
||||
|
||||
1. **Manual Chunks**: Group related dependencies for better caching
|
||||
2. **Platform-Specific Chunks**: Separate native and web dependencies
|
||||
3. **Vendor Separation**: Keep third-party libraries separate from app code
|
||||
4. **Dynamic Imports**: Enable automatic code splitting for dynamic imports
|
||||
|
||||
## Lazy Loading Patterns
|
||||
|
||||
### 1. Component-Level Lazy Loading
|
||||
|
||||
#### Basic Pattern with defineAsyncComponent
|
||||
|
||||
```typescript
|
||||
import { Component, Vue } from 'vue-facing-decorator';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
@Component({
|
||||
name: 'LazyLoadingExample',
|
||||
components: {
|
||||
LazyHeavyComponent: defineAsyncComponent({
|
||||
loader: () => import('./sub-components/HeavyComponent.vue'),
|
||||
loadingComponent: {
|
||||
template: '<div class="loading">Loading...</div>'
|
||||
},
|
||||
errorComponent: {
|
||||
template: '<div class="error">Failed to load</div>'
|
||||
},
|
||||
delay: 200,
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
})
|
||||
export default class LazyLoadingExample extends Vue {
|
||||
// Component logic
|
||||
}
|
||||
```
|
||||
|
||||
#### Advanced Pattern with Suspense
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<LazyHeavyComponent v-if="showComponent" />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="loading-fallback">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading component...</p>
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. Conditional Loading
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'ConditionalLazyLoading'
|
||||
})
|
||||
export default class ConditionalLazyLoading extends Vue {
|
||||
showHeavyComponent = false;
|
||||
showQRScanner = false;
|
||||
|
||||
// Lazy load based on user interaction
|
||||
async toggleHeavyComponent(): Promise<void> {
|
||||
this.showHeavyComponent = !this.showHeavyComponent;
|
||||
|
||||
if (this.showHeavyComponent) {
|
||||
// Preload component for better UX
|
||||
await this.preloadComponent(() => import('./HeavyComponent.vue'));
|
||||
}
|
||||
}
|
||||
|
||||
private async preloadComponent(loader: () => Promise<any>): Promise<void> {
|
||||
try {
|
||||
await loader();
|
||||
} catch (error) {
|
||||
console.warn('Preload failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Library-Level Lazy Loading
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'LibraryLazyLoading'
|
||||
})
|
||||
export default class LibraryLazyLoading extends Vue {
|
||||
private threeJS: any = null;
|
||||
|
||||
async loadThreeJS(): Promise<void> {
|
||||
if (!this.threeJS) {
|
||||
// Lazy load ThreeJS only when needed
|
||||
this.threeJS = await import('three');
|
||||
console.log('ThreeJS loaded successfully');
|
||||
}
|
||||
}
|
||||
|
||||
async initialize3DScene(): Promise<void> {
|
||||
await this.loadThreeJS();
|
||||
|
||||
// Use ThreeJS after loading
|
||||
const scene = new this.threeJS.Scene();
|
||||
// ... rest of 3D setup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component-Level Lazy Loading
|
||||
|
||||
### Heavy Data Processing Component
|
||||
|
||||
```typescript
|
||||
// HeavyComponent.vue
|
||||
@Component({
|
||||
name: 'HeavyComponent'
|
||||
})
|
||||
export default class HeavyComponent extends Vue {
|
||||
@Prop({ required: true }) readonly data!: any;
|
||||
|
||||
async processData(): Promise<void> {
|
||||
// Process data in batches to avoid blocking UI
|
||||
const batchSize = 10;
|
||||
const items = this.data.items;
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize);
|
||||
await this.processBatch(batch);
|
||||
|
||||
// Allow UI to update
|
||||
await this.$nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Camera-Dependent Component
|
||||
|
||||
```typescript
|
||||
// QRScannerComponent.vue
|
||||
@Component({
|
||||
name: 'QRScannerComponent'
|
||||
})
|
||||
export default class QRScannerComponent extends Vue {
|
||||
async mounted(): Promise<void> {
|
||||
// Initialize camera only when component is mounted
|
||||
await this.initializeCamera();
|
||||
}
|
||||
|
||||
async initializeCamera(): Promise<void> {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
this.cameras = devices.filter(device => device.kind === 'videoinput');
|
||||
this.hasCamera = this.cameras.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Camera initialization failed:', error);
|
||||
this.hasCamera = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Route-Level Lazy Loading
|
||||
|
||||
### Vue Router Configuration
|
||||
|
||||
```typescript
|
||||
// router/index.ts
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/HomeView.vue')
|
||||
},
|
||||
{
|
||||
path: '/heavy-feature',
|
||||
name: 'HeavyFeature',
|
||||
component: () => import('@/views/HeavyFeatureView.vue'),
|
||||
// Preload on hover for better UX
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (from.name) {
|
||||
// Preload component when navigating from another route
|
||||
import('@/views/HeavyFeatureView.vue');
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
```
|
||||
|
||||
### Route Guards with Lazy Loading
|
||||
|
||||
```typescript
|
||||
// router/guards.ts
|
||||
export async function lazyLoadGuard(to: any, from: any, next: any): Promise<void> {
|
||||
if (to.meta.requiresHeavyFeature) {
|
||||
try {
|
||||
// Preload heavy feature before navigation
|
||||
await import('@/components/HeavyFeature.vue');
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Failed to load heavy feature:', error);
|
||||
next('/error');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Library-Level Lazy Loading
|
||||
|
||||
### Dynamic Library Loading
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'DynamicLibraryLoader'
|
||||
})
|
||||
export default class DynamicLibraryLoader extends Vue {
|
||||
private libraries: Map<string, any> = new Map();
|
||||
|
||||
async loadLibrary(name: string): Promise<any> {
|
||||
if (this.libraries.has(name)) {
|
||||
return this.libraries.get(name);
|
||||
}
|
||||
|
||||
let library: any;
|
||||
|
||||
switch (name) {
|
||||
case 'three':
|
||||
library = await import('three');
|
||||
break;
|
||||
case 'chart':
|
||||
library = await import('chart.js');
|
||||
break;
|
||||
case 'qr':
|
||||
library = await import('jsqr');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown library: ${name}`);
|
||||
}
|
||||
|
||||
this.libraries.set(name, library);
|
||||
return library;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Library Loading
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'ConditionalLibraryLoader'
|
||||
})
|
||||
export default class ConditionalLibraryLoader extends Vue {
|
||||
async loadPlatformSpecificLibrary(): Promise<void> {
|
||||
if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||
// Load Capacitor-specific libraries
|
||||
await import('@capacitor/camera');
|
||||
await import('@capacitor/filesystem');
|
||||
} else {
|
||||
// Load web-specific libraries
|
||||
await import('file-saver');
|
||||
await import('jszip');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Bundle Analysis
|
||||
|
||||
```bash
|
||||
# Analyze bundle size
|
||||
npm run build
|
||||
npx vite-bundle-analyzer dist
|
||||
|
||||
# Monitor chunk sizes
|
||||
npx vite-bundle-analyzer dist --mode=treemap
|
||||
```
|
||||
|
||||
### Runtime Performance Monitoring
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'PerformanceMonitor'
|
||||
})
|
||||
export default class PerformanceMonitor extends Vue {
|
||||
private performanceMetrics = {
|
||||
componentLoadTime: 0,
|
||||
renderTime: 0,
|
||||
memoryUsage: 0
|
||||
};
|
||||
|
||||
private measureComponentLoad(componentName: string): void {
|
||||
const startTime = performance.now();
|
||||
|
||||
return () => {
|
||||
const loadTime = performance.now() - startTime;
|
||||
this.performanceMetrics.componentLoadTime = loadTime;
|
||||
|
||||
console.log(`${componentName} loaded in ${loadTime.toFixed(2)}ms`);
|
||||
|
||||
// Send to analytics
|
||||
this.trackPerformance(componentName, loadTime);
|
||||
};
|
||||
}
|
||||
|
||||
private trackPerformance(component: string, loadTime: number): void {
|
||||
// Send to analytics service
|
||||
if (window.gtag) {
|
||||
window.gtag('event', 'component_load', {
|
||||
component_name: component,
|
||||
load_time: loadTime
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Loading States
|
||||
|
||||
Always provide meaningful loading states:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<LazyComponent />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading {{ componentName }}...</p>
|
||||
<p class="loading-tip">{{ loadingTip }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
Implement comprehensive error handling:
|
||||
|
||||
```typescript
|
||||
const LazyComponent = defineAsyncComponent({
|
||||
loader: () => import('./HeavyComponent.vue'),
|
||||
errorComponent: {
|
||||
template: `
|
||||
<div class="error-state">
|
||||
<h3>Failed to load component</h3>
|
||||
<p>{{ error.message }}</p>
|
||||
<button @click="retry">Retry</button>
|
||||
</div>
|
||||
`,
|
||||
props: ['error'],
|
||||
methods: {
|
||||
retry() {
|
||||
this.$emit('retry');
|
||||
}
|
||||
}
|
||||
},
|
||||
onError(error, retry, fail, attempts) {
|
||||
if (attempts <= 3) {
|
||||
console.warn(`Retrying component load (attempt ${attempts})`);
|
||||
retry();
|
||||
} else {
|
||||
console.error('Component failed to load after 3 attempts');
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Preloading Strategy
|
||||
|
||||
Implement intelligent preloading:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
name: 'SmartPreloader'
|
||||
})
|
||||
export default class SmartPreloader extends Vue {
|
||||
private preloadQueue: Array<() => Promise<any>> = [];
|
||||
private isPreloading = false;
|
||||
|
||||
// Preload based on user behavior
|
||||
onUserHover(componentLoader: () => Promise<any>): void {
|
||||
this.preloadQueue.push(componentLoader);
|
||||
this.processPreloadQueue();
|
||||
}
|
||||
|
||||
private async processPreloadQueue(): Promise<void> {
|
||||
if (this.isPreloading || this.preloadQueue.length === 0) return;
|
||||
|
||||
this.isPreloading = true;
|
||||
|
||||
while (this.preloadQueue.length > 0) {
|
||||
const loader = this.preloadQueue.shift();
|
||||
if (loader) {
|
||||
try {
|
||||
await loader();
|
||||
} catch (error) {
|
||||
console.warn('Preload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between preloads
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
this.isPreloading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Bundle Optimization
|
||||
|
||||
Optimize bundle splitting:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: (id) => {
|
||||
// Group by feature
|
||||
if (id.includes('qr')) return 'qr-feature';
|
||||
if (id.includes('three')) return '3d-feature';
|
||||
if (id.includes('chart')) return 'chart-feature';
|
||||
|
||||
// Group by vendor
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('vue')) return 'vue-vendor';
|
||||
if (id.includes('lodash')) return 'utils-vendor';
|
||||
return 'vendor';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Over-Lazy Loading
|
||||
|
||||
❌ **Don't lazy load everything:**
|
||||
```typescript
|
||||
// Bad: Lazy loading small components
|
||||
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue'));
|
||||
```
|
||||
|
||||
✅ **Do lazy load strategically:**
|
||||
```typescript
|
||||
// Good: Lazy load heavy components only
|
||||
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue'));
|
||||
```
|
||||
|
||||
### 2. Missing Loading States
|
||||
|
||||
❌ **Don't leave users hanging:**
|
||||
```vue
|
||||
<template>
|
||||
<LazyComponent v-if="show" />
|
||||
</template>
|
||||
```
|
||||
|
||||
✅ **Do provide loading feedback:**
|
||||
```vue
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<LazyComponent v-if="show" />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<LoadingSpinner />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. Ignoring Error States
|
||||
|
||||
❌ **Don't ignore loading failures:**
|
||||
```typescript
|
||||
const LazyComponent = defineAsyncComponent(() => import('./Component.vue'));
|
||||
```
|
||||
|
||||
✅ **Do handle errors gracefully:**
|
||||
```typescript
|
||||
const LazyComponent = defineAsyncComponent({
|
||||
loader: () => import('./Component.vue'),
|
||||
errorComponent: ErrorFallback,
|
||||
onError: (error, retry, fail) => {
|
||||
console.error('Component load failed:', error);
|
||||
// Retry once, then fail
|
||||
retry();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Lazy Loading Example
|
||||
|
||||
See the following files for complete working examples:
|
||||
|
||||
1. **`src/components/LazyLoadingExample.vue`** - Main example component
|
||||
2. **`src/components/sub-components/HeavyComponent.vue`** - Data processing component
|
||||
3. **`src/components/sub-components/QRScannerComponent.vue`** - Camera-dependent component
|
||||
4. **`src/components/sub-components/ThreeJSViewer.vue`** - 3D rendering component
|
||||
5. **`vite.config.optimized.mts`** - Optimized Vite configuration
|
||||
|
||||
### Usage Example
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="app">
|
||||
<h1>Lazy Loading Demo</h1>
|
||||
|
||||
<LazyLoadingExample
|
||||
:initial-load-heavy="false"
|
||||
@qr-detected="handleQRCode"
|
||||
@model-loaded="handleModelLoaded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-facing-decorator';
|
||||
import LazyLoadingExample from '@/components/LazyLoadingExample.vue';
|
||||
|
||||
@Component({
|
||||
name: 'App',
|
||||
components: {
|
||||
LazyLoadingExample
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
handleQRCode(data: string): void {
|
||||
console.log('QR code detected:', data);
|
||||
}
|
||||
|
||||
handleModelLoaded(info: any): void {
|
||||
console.log('3D model loaded:', info);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Lazy loading with Vue 3 + Vite + `vue-facing-decorator` provides powerful tools for optimizing application performance. By implementing these patterns strategically, you can significantly improve initial load times while maintaining excellent user experience.
|
||||
|
||||
Remember to:
|
||||
- Use lazy loading for heavy components and features
|
||||
- Provide meaningful loading and error states
|
||||
- Monitor performance and bundle sizes
|
||||
- Implement intelligent preloading strategies
|
||||
- Handle errors gracefully
|
||||
|
||||
For more information, see the Vue 3 documentation on [Async Components](https://vuejs.org/guide/components/async.html) and the Vite documentation on [Code Splitting](https://vitejs.dev/guide/features.html#code-splitting).
|
||||
@@ -9,6 +9,8 @@
|
||||
"dev": "vite --config vite.config.dev.mts --host",
|
||||
"serve": "vite preview",
|
||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
||||
"build:optimized": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.optimized.mts",
|
||||
"dev:optimized": "vite --config vite.config.optimized.mts --host",
|
||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||
|
||||
293
src/components/LazyLoadingExample.vue
Normal file
293
src/components/LazyLoadingExample.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="lazy-loading-example">
|
||||
<!-- Loading state with Suspense -->
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<!-- Main content with lazy-loaded components -->
|
||||
<div class="content">
|
||||
<h1>Lazy Loading Example</h1>
|
||||
|
||||
<!-- Lazy-loaded heavy component -->
|
||||
<LazyHeavyComponent
|
||||
v-if="showHeavyComponent"
|
||||
:data="heavyComponentData"
|
||||
@data-processed="handleDataProcessed"
|
||||
/>
|
||||
|
||||
<!-- Conditionally loaded components -->
|
||||
<LazyQRScanner
|
||||
v-if="showQRScanner"
|
||||
@qr-detected="handleQRDetected"
|
||||
/>
|
||||
|
||||
<LazyThreeJSViewer
|
||||
v-if="showThreeJS"
|
||||
:model-url="threeJSModelUrl"
|
||||
@model-loaded="handleModelLoaded"
|
||||
/>
|
||||
|
||||
<!-- Route-based lazy loading -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Loading fallback -->
|
||||
<template #fallback>
|
||||
<div class="loading-fallback">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading components...</p>
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<!-- Control buttons -->
|
||||
<div class="controls">
|
||||
<button @click="toggleHeavyComponent">
|
||||
{{ showHeavyComponent ? 'Hide' : 'Show' }} Heavy Component
|
||||
</button>
|
||||
<button @click="toggleQRScanner">
|
||||
{{ showQRScanner ? 'Hide' : 'Show' }} QR Scanner
|
||||
</button>
|
||||
<button @click="toggleThreeJS">
|
||||
{{ showThreeJS ? 'Hide' : 'Show' }} 3D Viewer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from 'vue-facing-decorator';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
/**
|
||||
* Lazy Loading Example Component
|
||||
*
|
||||
* Demonstrates various lazy loading patterns with vue-facing-decorator:
|
||||
* - defineAsyncComponent for heavy components
|
||||
* - Conditional loading based on user interaction
|
||||
* - Suspense for loading states
|
||||
* - Route-based lazy loading
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component({
|
||||
name: 'LazyLoadingExample',
|
||||
components: {
|
||||
// Lazy-loaded components with loading and error states
|
||||
LazyHeavyComponent: defineAsyncComponent({
|
||||
loader: () => import('./sub-components/HeavyComponent.vue'),
|
||||
loadingComponent: {
|
||||
template: '<div class="loading">Loading heavy component...</div>'
|
||||
},
|
||||
errorComponent: {
|
||||
template: '<div class="error">Failed to load heavy component</div>'
|
||||
},
|
||||
delay: 200, // Show loading component after 200ms
|
||||
timeout: 10000 // Timeout after 10 seconds
|
||||
}),
|
||||
|
||||
LazyQRScanner: defineAsyncComponent({
|
||||
loader: () => import('./sub-components/QRScannerComponent.vue'),
|
||||
loadingComponent: {
|
||||
template: '<div class="loading">Initializing QR scanner...</div>'
|
||||
},
|
||||
errorComponent: {
|
||||
template: '<div class="error">QR scanner not available</div>'
|
||||
}
|
||||
}),
|
||||
|
||||
LazyThreeJSViewer: defineAsyncComponent({
|
||||
loader: () => import('./sub-components/ThreeJSViewer.vue'),
|
||||
loadingComponent: {
|
||||
template: '<div class="loading">Loading 3D viewer...</div>'
|
||||
},
|
||||
errorComponent: {
|
||||
template: '<div class="error">3D viewer failed to load</div>'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
export default class LazyLoadingExample extends Vue {
|
||||
// Component state
|
||||
@Prop({ default: false }) readonly initialLoadHeavy!: boolean;
|
||||
|
||||
// Reactive properties
|
||||
showHeavyComponent = false;
|
||||
showQRScanner = false;
|
||||
showThreeJS = false;
|
||||
|
||||
// Component data
|
||||
heavyComponentData = {
|
||||
items: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` })),
|
||||
filters: { category: 'all', status: 'active' },
|
||||
sortBy: 'name'
|
||||
};
|
||||
|
||||
threeJSModelUrl = '/models/lupine_plant/scene.gltf';
|
||||
|
||||
// Computed properties
|
||||
get isLoadingAnyComponent(): boolean {
|
||||
return this.showHeavyComponent || this.showQRScanner || this.showThreeJS;
|
||||
}
|
||||
|
||||
get componentCount(): number {
|
||||
let count = 0;
|
||||
if (this.showHeavyComponent) count++;
|
||||
if (this.showQRScanner) count++;
|
||||
if (this.showThreeJS) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// Lifecycle hooks
|
||||
mounted(): void {
|
||||
console.log('[LazyLoadingExample] Component mounted');
|
||||
|
||||
// Initialize based on props
|
||||
if (this.initialLoadHeavy) {
|
||||
this.showHeavyComponent = true;
|
||||
}
|
||||
|
||||
// Preload critical components
|
||||
this.preloadCriticalComponents();
|
||||
}
|
||||
|
||||
// Methods
|
||||
toggleHeavyComponent(): void {
|
||||
this.showHeavyComponent = !this.showHeavyComponent;
|
||||
console.log('[LazyLoadingExample] Heavy component toggled:', this.showHeavyComponent);
|
||||
}
|
||||
|
||||
toggleQRScanner(): void {
|
||||
this.showQRScanner = !this.showQRScanner;
|
||||
console.log('[LazyLoadingExample] QR scanner toggled:', this.showQRScanner);
|
||||
}
|
||||
|
||||
toggleThreeJS(): void {
|
||||
this.showThreeJS = !this.showThreeJS;
|
||||
console.log('[LazyLoadingExample] ThreeJS viewer toggled:', this.showThreeJS);
|
||||
}
|
||||
|
||||
handleDataProcessed(data: any): void {
|
||||
console.log('[LazyLoadingExample] Data processed:', data);
|
||||
// Handle processed data from heavy component
|
||||
}
|
||||
|
||||
handleQRDetected(qrData: string): void {
|
||||
console.log('[LazyLoadingExample] QR code detected:', qrData);
|
||||
// Handle QR code data
|
||||
}
|
||||
|
||||
handleModelLoaded(modelInfo: any): void {
|
||||
console.log('[LazyLoadingExample] 3D model loaded:', modelInfo);
|
||||
// Handle 3D model loaded event
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload critical components for better UX
|
||||
*/
|
||||
private preloadCriticalComponents(): void {
|
||||
// Preload components that are likely to be used
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// In production, preload based on user behavior patterns
|
||||
this.preloadComponent(() => import('./sub-components/HeavyComponent.vue'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload a component without rendering it
|
||||
*/
|
||||
private preloadComponent(componentLoader: () => Promise<any>): void {
|
||||
componentLoader().catch(error => {
|
||||
console.warn('[LazyLoadingExample] Preload failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Watchers
|
||||
@Watch('showHeavyComponent')
|
||||
onHeavyComponentToggle(newValue: boolean): void {
|
||||
if (newValue) {
|
||||
// Component is being shown - could trigger analytics
|
||||
console.log('[LazyLoadingExample] Heavy component shown');
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('componentCount')
|
||||
onComponentCountChange(newCount: number): void {
|
||||
console.log('[LazyLoadingExample] Active component count:', newCount);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lazy-loading-example {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.loading-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #dc3545;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
542
src/components/sub-components/HeavyComponent.vue
Normal file
542
src/components/sub-components/HeavyComponent.vue
Normal file
@@ -0,0 +1,542 @@
|
||||
<template>
|
||||
<div class="heavy-component">
|
||||
<h2>Heavy Data Processing Component</h2>
|
||||
|
||||
<!-- Data processing controls -->
|
||||
<div class="controls">
|
||||
<button @click="processData" :disabled="isProcessing">
|
||||
{{ isProcessing ? 'Processing...' : 'Process Data' }}
|
||||
</button>
|
||||
<button @click="clearResults" :disabled="isProcessing">
|
||||
Clear Results
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Processing status -->
|
||||
<div v-if="isProcessing" class="processing-status">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
||||
</div>
|
||||
<p>Processing {{ processedCount }} of {{ totalItems }} items...</p>
|
||||
</div>
|
||||
|
||||
<!-- Results display -->
|
||||
<div v-if="processedData.length > 0" class="results">
|
||||
<h3>Processed Results ({{ processedData.length }} items)</h3>
|
||||
|
||||
<!-- Filter controls -->
|
||||
<div class="filters">
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
placeholder="Search items..."
|
||||
class="search-input"
|
||||
/>
|
||||
<select v-model="sortBy" class="sort-select">
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="id">Sort by ID</option>
|
||||
<option value="processed">Sort by Processed Date</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Results list -->
|
||||
<div class="results-list">
|
||||
<div
|
||||
v-for="item in filteredAndSortedData"
|
||||
:key="item.id"
|
||||
class="result-item"
|
||||
>
|
||||
<div class="item-header">
|
||||
<span class="item-name">{{ item.name }}</span>
|
||||
<span class="item-id">#{{ item.id }}</span>
|
||||
</div>
|
||||
<div class="item-details">
|
||||
<span class="processed-date">
|
||||
Processed: {{ formatDate(item.processedAt) }}
|
||||
</span>
|
||||
<span class="processing-time">
|
||||
Time: {{ item.processingTime }}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="totalPages > 1" class="pagination">
|
||||
<button
|
||||
@click="previousPage"
|
||||
:disabled="currentPage === 1"
|
||||
class="page-btn"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span class="page-info">
|
||||
Page {{ currentPage }} of {{ totalPages }}
|
||||
</span>
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="page-btn"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance metrics -->
|
||||
<div v-if="performanceMetrics" class="performance-metrics">
|
||||
<h4>Performance Metrics</h4>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Total Processing Time:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.totalTime }}ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Average per Item:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.averageTime }}ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Memory Usage:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.memoryUsage }}MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator';
|
||||
|
||||
interface ProcessedItem {
|
||||
id: number;
|
||||
name: string;
|
||||
processedAt: Date;
|
||||
processingTime: number;
|
||||
result: any;
|
||||
}
|
||||
|
||||
interface PerformanceMetrics {
|
||||
totalTime: number;
|
||||
averageTime: number;
|
||||
memoryUsage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heavy Component for Data Processing
|
||||
*
|
||||
* Demonstrates a component that performs intensive data processing
|
||||
* and would benefit from lazy loading to avoid blocking the main thread.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component({
|
||||
name: 'HeavyComponent'
|
||||
})
|
||||
export default class HeavyComponent extends Vue {
|
||||
@Prop({ required: true }) readonly data!: {
|
||||
items: Array<{ id: number; name: string }>;
|
||||
filters: Record<string, any>;
|
||||
sortBy: string;
|
||||
};
|
||||
|
||||
// Component state
|
||||
isProcessing = false;
|
||||
processedData: ProcessedItem[] = [];
|
||||
progress = 0;
|
||||
processedCount = 0;
|
||||
totalItems = 0;
|
||||
|
||||
// UI state
|
||||
searchTerm = '';
|
||||
sortBy = 'name';
|
||||
currentPage = 1;
|
||||
itemsPerPage = 50;
|
||||
|
||||
// Performance tracking
|
||||
performanceMetrics: PerformanceMetrics | null = null;
|
||||
startTime = 0;
|
||||
|
||||
// Computed properties
|
||||
get filteredAndSortedData(): ProcessedItem[] {
|
||||
let filtered = this.processedData;
|
||||
|
||||
// Apply search filter
|
||||
if (this.searchTerm) {
|
||||
filtered = filtered.filter(item =>
|
||||
item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
switch (this.sortBy) {
|
||||
case 'name':
|
||||
return a.name.localeCompare(b.name);
|
||||
case 'id':
|
||||
return a.id - b.id;
|
||||
case 'processed':
|
||||
return b.processedAt.getTime() - a.processedAt.getTime();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
get paginatedData(): ProcessedItem[] {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = start + this.itemsPerPage;
|
||||
return this.filteredAndSortedData.slice(start, end);
|
||||
}
|
||||
|
||||
get totalPages(): number {
|
||||
return Math.ceil(this.filteredAndSortedData.length / this.itemsPerPage);
|
||||
}
|
||||
|
||||
// Lifecycle hooks
|
||||
mounted(): void {
|
||||
console.log('[HeavyComponent] Component mounted with', this.data.items.length, 'items');
|
||||
this.totalItems = this.data.items.length;
|
||||
}
|
||||
|
||||
// Methods
|
||||
async processData(): Promise<void> {
|
||||
if (this.isProcessing) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
this.progress = 0;
|
||||
this.processedCount = 0;
|
||||
this.processedData = [];
|
||||
this.startTime = performance.now();
|
||||
|
||||
console.log('[HeavyComponent] Starting data processing...');
|
||||
|
||||
try {
|
||||
// Process items in batches to avoid blocking the UI
|
||||
const batchSize = 10;
|
||||
const items = this.data.items;
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize);
|
||||
|
||||
// Process batch
|
||||
await this.processBatch(batch);
|
||||
|
||||
// Update progress
|
||||
this.processedCount = Math.min(i + batchSize, items.length);
|
||||
this.progress = (this.processedCount / items.length) * 100;
|
||||
|
||||
// Allow UI to update
|
||||
await this.$nextTick();
|
||||
|
||||
// Small delay to prevent overwhelming the UI
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
// Calculate performance metrics
|
||||
this.calculatePerformanceMetrics();
|
||||
|
||||
// Emit completion event
|
||||
this.$emit('data-processed', {
|
||||
totalItems: this.processedData.length,
|
||||
processingTime: performance.now() - this.startTime,
|
||||
metrics: this.performanceMetrics
|
||||
});
|
||||
|
||||
console.log('[HeavyComponent] Data processing completed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HeavyComponent] Processing error:', error);
|
||||
this.$emit('processing-error', error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async processBatch(batch: Array<{ id: number; name: string }>): Promise<void> {
|
||||
const processedBatch = await Promise.all(
|
||||
batch.map(async (item) => {
|
||||
const itemStartTime = performance.now();
|
||||
|
||||
// Simulate heavy processing
|
||||
await this.simulateHeavyProcessing(item);
|
||||
|
||||
const processingTime = performance.now() - itemStartTime;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
processedAt: new Date(),
|
||||
processingTime: Math.round(processingTime),
|
||||
result: this.generateResult(item)
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.processedData.push(...processedBatch);
|
||||
}
|
||||
|
||||
private async simulateHeavyProcessing(item: { id: number; name: string }): Promise<void> {
|
||||
// Simulate CPU-intensive work
|
||||
const complexity = item.name.length * item.id;
|
||||
const iterations = Math.min(complexity, 1000); // Cap at 1000 iterations
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// Simulate work
|
||||
Math.sqrt(i) * Math.random();
|
||||
}
|
||||
|
||||
// Simulate async work
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10));
|
||||
}
|
||||
|
||||
private generateResult(item: { id: number; name: string }): any {
|
||||
return {
|
||||
hash: this.generateHash(item.name + item.id),
|
||||
category: this.categorizeItem(item),
|
||||
score: Math.random() * 100,
|
||||
tags: this.generateTags(item)
|
||||
};
|
||||
}
|
||||
|
||||
private generateHash(input: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString(16);
|
||||
}
|
||||
|
||||
private categorizeItem(item: { id: number; name: string }): string {
|
||||
const categories = ['A', 'B', 'C', 'D', 'E'];
|
||||
return categories[item.id % categories.length];
|
||||
}
|
||||
|
||||
private generateTags(item: { id: number; name: string }): string[] {
|
||||
const tags = ['important', 'urgent', 'review', 'archive', 'featured'];
|
||||
return tags.filter((_, index) => (item.id + index) % 3 === 0);
|
||||
}
|
||||
|
||||
private calculatePerformanceMetrics(): void {
|
||||
const totalTime = performance.now() - this.startTime;
|
||||
const averageTime = totalTime / this.processedData.length;
|
||||
|
||||
// Simulate memory usage calculation
|
||||
const memoryUsage = this.processedData.length * 0.1; // 0.1MB per item
|
||||
|
||||
this.performanceMetrics = {
|
||||
totalTime: Math.round(totalTime),
|
||||
averageTime: Math.round(averageTime),
|
||||
memoryUsage: Math.round(memoryUsage * 100) / 100
|
||||
};
|
||||
}
|
||||
|
||||
clearResults(): void {
|
||||
this.processedData = [];
|
||||
this.performanceMetrics = null;
|
||||
this.searchTerm = '';
|
||||
this.currentPage = 1;
|
||||
console.log('[HeavyComponent] Results cleared');
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Event emitters
|
||||
@Emit('data-processed')
|
||||
emitDataProcessed(data: any): any {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Emit('processing-error')
|
||||
emitProcessingError(error: Error): Error {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.heavy-component {
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.controls button:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.controls button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.processing-status {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
.sort-select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-id {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.performance-metrics {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e8f4fd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
708
src/components/sub-components/QRScannerComponent.vue
Normal file
708
src/components/sub-components/QRScannerComponent.vue
Normal file
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<div class="qr-scanner-component">
|
||||
<h2>QR Code Scanner</h2>
|
||||
|
||||
<!-- Camera controls -->
|
||||
<div class="camera-controls">
|
||||
<button @click="startScanning" :disabled="isScanning || !hasCamera">
|
||||
{{ isScanning ? 'Scanning...' : 'Start Scanning' }}
|
||||
</button>
|
||||
<button @click="stopScanning" :disabled="!isScanning">
|
||||
Stop Scanning
|
||||
</button>
|
||||
<button @click="switchCamera" :disabled="!isScanning || cameras.length <= 1">
|
||||
Switch Camera
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Camera status -->
|
||||
<div class="camera-status">
|
||||
<div v-if="!hasCamera" class="status-error">
|
||||
<p>Camera not available</p>
|
||||
<p class="status-detail">This device doesn't have a camera or camera access is denied.</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isScanning" class="status-info">
|
||||
<p>Camera ready</p>
|
||||
<p class="status-detail">Click "Start Scanning" to begin QR code detection.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="status-scanning">
|
||||
<p>Scanning for QR codes...</p>
|
||||
<p class="status-detail">Point camera at a QR code to scan.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera view -->
|
||||
<div v-if="isScanning && hasCamera" class="camera-container">
|
||||
<video
|
||||
ref="videoElement"
|
||||
class="camera-video"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
|
||||
<!-- Scanning overlay -->
|
||||
<div class="scanning-overlay">
|
||||
<div class="scan-frame"></div>
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scan results -->
|
||||
<div v-if="scanResults.length > 0" class="scan-results">
|
||||
<h3>Scan Results ({{ scanResults.length }})</h3>
|
||||
|
||||
<div class="results-list">
|
||||
<div
|
||||
v-for="(result, index) in scanResults"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
>
|
||||
<div class="result-header">
|
||||
<span class="result-number">#{{ index + 1 }}</span>
|
||||
<span class="result-time">{{ formatTime(result.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="qr-data">
|
||||
<strong>Data:</strong> {{ result.data }}
|
||||
</div>
|
||||
<div class="qr-format">
|
||||
<strong>Format:</strong> {{ result.format }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button @click="copyToClipboard(result.data)" class="copy-btn">
|
||||
Copy
|
||||
</button>
|
||||
<button @click="removeResult(index)" class="remove-btn">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-actions">
|
||||
<button @click="clearResults" class="clear-btn">
|
||||
Clear All Results
|
||||
</button>
|
||||
<button @click="exportResults" class="export-btn">
|
||||
Export Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings panel -->
|
||||
<div class="settings-panel">
|
||||
<h3>Scanner Settings</h3>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="settings.continuousScanning"
|
||||
/>
|
||||
Continuous Scanning
|
||||
</label>
|
||||
<p class="setting-description">
|
||||
Automatically scan multiple QR codes without stopping
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="settings.audioFeedback"
|
||||
/>
|
||||
Audio Feedback
|
||||
</label>
|
||||
<p class="setting-description">
|
||||
Play sound when QR code is detected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="settings.vibrateOnScan"
|
||||
/>
|
||||
Vibration Feedback
|
||||
</label>
|
||||
<p class="setting-description">
|
||||
Vibrate device when QR code is detected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>Scan Interval (ms):</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="settings.scanInterval"
|
||||
min="100"
|
||||
max="5000"
|
||||
step="100"
|
||||
/>
|
||||
<p class="setting-description">
|
||||
Time between scans (lower = faster, higher = more accurate)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit } from 'vue-facing-decorator';
|
||||
|
||||
interface ScanResult {
|
||||
data: string;
|
||||
format: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
interface ScannerSettings {
|
||||
continuousScanning: boolean;
|
||||
audioFeedback: boolean;
|
||||
vibrateOnScan: boolean;
|
||||
scanInterval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* QR Scanner Component
|
||||
*
|
||||
* Demonstrates lazy loading for camera-dependent features.
|
||||
* This component would benefit from lazy loading as it requires
|
||||
* camera permissions and heavy camera processing libraries.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component({
|
||||
name: 'QRScannerComponent'
|
||||
})
|
||||
export default class QRScannerComponent extends Vue {
|
||||
// Component state
|
||||
isScanning = false;
|
||||
hasCamera = false;
|
||||
cameras: MediaDeviceInfo[] = [];
|
||||
currentCameraIndex = 0;
|
||||
|
||||
// Video element reference
|
||||
videoElement: HTMLVideoElement | null = null;
|
||||
|
||||
// Scan results
|
||||
scanResults: ScanResult[] = [];
|
||||
|
||||
// Scanner settings
|
||||
settings: ScannerSettings = {
|
||||
continuousScanning: true,
|
||||
audioFeedback: true,
|
||||
vibrateOnScan: true,
|
||||
scanInterval: 500
|
||||
};
|
||||
|
||||
// Internal state
|
||||
private stream: MediaStream | null = null;
|
||||
private scanInterval: number | null = null;
|
||||
private lastScanTime = 0;
|
||||
|
||||
// Lifecycle hooks
|
||||
async mounted(): Promise<void> {
|
||||
console.log('[QRScannerComponent] Component mounted');
|
||||
await this.initializeCamera();
|
||||
}
|
||||
|
||||
beforeUnmount(): void {
|
||||
this.stopScanning();
|
||||
console.log('[QRScannerComponent] Component unmounting');
|
||||
}
|
||||
|
||||
// Methods
|
||||
async initializeCamera(): Promise<void> {
|
||||
try {
|
||||
// Check if camera is available
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
this.cameras = devices.filter(device => device.kind === 'videoinput');
|
||||
this.hasCamera = this.cameras.length > 0;
|
||||
|
||||
if (this.hasCamera) {
|
||||
console.log('[QRScannerComponent] Camera available:', this.cameras.length, 'devices');
|
||||
} else {
|
||||
console.warn('[QRScannerComponent] No camera devices found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[QRScannerComponent] Camera initialization error:', error);
|
||||
this.hasCamera = false;
|
||||
}
|
||||
}
|
||||
|
||||
async startScanning(): Promise<void> {
|
||||
if (!this.hasCamera || this.isScanning) return;
|
||||
|
||||
try {
|
||||
console.log('[QRScannerComponent] Starting QR scanning...');
|
||||
|
||||
// Get camera stream
|
||||
const constraints = {
|
||||
video: {
|
||||
deviceId: this.cameras[this.currentCameraIndex]?.deviceId
|
||||
}
|
||||
};
|
||||
|
||||
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
// Set up video element
|
||||
this.videoElement = this.$refs.videoElement as HTMLVideoElement;
|
||||
if (this.videoElement) {
|
||||
this.videoElement.srcObject = this.stream;
|
||||
await this.videoElement.play();
|
||||
}
|
||||
|
||||
this.isScanning = true;
|
||||
|
||||
// Start QR code detection
|
||||
this.startQRDetection();
|
||||
|
||||
console.log('[QRScannerComponent] QR scanning started');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[QRScannerComponent] Failed to start scanning:', error);
|
||||
this.hasCamera = false;
|
||||
}
|
||||
}
|
||||
|
||||
stopScanning(): void {
|
||||
if (!this.isScanning) return;
|
||||
|
||||
console.log('[QRScannerComponent] Stopping QR scanning...');
|
||||
|
||||
// Stop QR detection
|
||||
this.stopQRDetection();
|
||||
|
||||
// Stop camera stream
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
// Clear video element
|
||||
if (this.videoElement) {
|
||||
this.videoElement.srcObject = null;
|
||||
this.videoElement = null;
|
||||
}
|
||||
|
||||
this.isScanning = false;
|
||||
console.log('[QRScannerComponent] QR scanning stopped');
|
||||
}
|
||||
|
||||
async switchCamera(): Promise<void> {
|
||||
if (this.cameras.length <= 1) return;
|
||||
|
||||
// Stop current scanning
|
||||
this.stopScanning();
|
||||
|
||||
// Switch to next camera
|
||||
this.currentCameraIndex = (this.currentCameraIndex + 1) % this.cameras.length;
|
||||
|
||||
// Restart scanning with new camera
|
||||
await this.startScanning();
|
||||
|
||||
console.log('[QRScannerComponent] Switched to camera:', this.currentCameraIndex);
|
||||
}
|
||||
|
||||
private startQRDetection(): void {
|
||||
if (!this.settings.continuousScanning) return;
|
||||
|
||||
this.scanInterval = window.setInterval(() => {
|
||||
this.detectQRCode();
|
||||
}, this.settings.scanInterval);
|
||||
}
|
||||
|
||||
private stopQRDetection(): void {
|
||||
if (this.scanInterval) {
|
||||
clearInterval(this.scanInterval);
|
||||
this.scanInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async detectQRCode(): Promise<void> {
|
||||
if (!this.videoElement || !this.isScanning) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastScanTime < this.settings.scanInterval) return;
|
||||
|
||||
try {
|
||||
// Simulate QR code detection
|
||||
// In a real implementation, you would use a QR code library like jsQR
|
||||
const detectedQR = await this.simulateQRDetection();
|
||||
|
||||
if (detectedQR) {
|
||||
this.addScanResult(detectedQR);
|
||||
this.lastScanTime = now;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[QRScannerComponent] QR detection error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async simulateQRDetection(): Promise<ScanResult | null> {
|
||||
// Simulate QR code detection with random chance
|
||||
if (Math.random() < 0.1) { // 10% chance of detection
|
||||
const sampleData = [
|
||||
'https://example.com/qr1',
|
||||
'WIFI:S:MyNetwork;T:WPA;P:password123;;',
|
||||
'BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD',
|
||||
'otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'
|
||||
];
|
||||
|
||||
const formats = ['URL', 'WiFi', 'vCard', 'TOTP'];
|
||||
const randomIndex = Math.floor(Math.random() * sampleData.length);
|
||||
|
||||
return {
|
||||
data: sampleData[randomIndex],
|
||||
format: formats[randomIndex],
|
||||
timestamp: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private addScanResult(result: ScanResult): void {
|
||||
// Check for duplicates
|
||||
const isDuplicate = this.scanResults.some(
|
||||
existing => existing.data === result.data
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
this.scanResults.unshift(result);
|
||||
|
||||
// Provide feedback
|
||||
this.provideFeedback();
|
||||
|
||||
// Emit event
|
||||
this.$emit('qr-detected', result.data);
|
||||
|
||||
console.log('[QRScannerComponent] QR code detected:', result.data);
|
||||
}
|
||||
}
|
||||
|
||||
private provideFeedback(): void {
|
||||
// Audio feedback
|
||||
if (this.settings.audioFeedback) {
|
||||
this.playBeepSound();
|
||||
}
|
||||
|
||||
// Vibration feedback
|
||||
if (this.settings.vibrateOnScan && 'vibrate' in navigator) {
|
||||
navigator.vibrate(100);
|
||||
}
|
||||
}
|
||||
|
||||
private playBeepSound(): void {
|
||||
// Create a simple beep sound
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.1);
|
||||
}
|
||||
|
||||
copyToClipboard(text: string): void {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('[QRScannerComponent] Copied to clipboard:', text);
|
||||
}).catch(error => {
|
||||
console.error('[QRScannerComponent] Failed to copy:', error);
|
||||
});
|
||||
}
|
||||
|
||||
removeResult(index: number): void {
|
||||
this.scanResults.splice(index, 1);
|
||||
}
|
||||
|
||||
clearResults(): void {
|
||||
this.scanResults = [];
|
||||
console.log('[QRScannerComponent] Results cleared');
|
||||
}
|
||||
|
||||
exportResults(): void {
|
||||
const data = JSON.stringify(this.scanResults, null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `qr-scan-results-${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
console.log('[QRScannerComponent] Results exported');
|
||||
}
|
||||
|
||||
formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
// Event emitters
|
||||
@Emit('qr-detected')
|
||||
emitQRDetected(data: string): string {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qr-scanner-component {
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.camera-controls button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.camera-controls button:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.camera-controls button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.camera-status {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.status-scanning {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-detail {
|
||||
margin-top: 5px;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 300px;
|
||||
margin: 20px auto;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.scanning-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scan-frame {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 2px solid #00ff00;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #00ff00;
|
||||
animation: scan 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0; }
|
||||
100% { top: 100%; }
|
||||
}
|
||||
|
||||
.scan-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-number {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.qr-data,
|
||||
.qr-format {
|
||||
margin-bottom: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.copy-btn,
|
||||
.remove-btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.clear-btn,
|
||||
.export-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.setting-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.setting-group input[type="number"] {
|
||||
width: 100px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
margin-top: 3px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
</style>
|
||||
657
src/components/sub-components/ThreeJSViewer.vue
Normal file
657
src/components/sub-components/ThreeJSViewer.vue
Normal file
@@ -0,0 +1,657 @@
|
||||
<template>
|
||||
<div class="threejs-viewer">
|
||||
<h2>3D Model Viewer</h2>
|
||||
|
||||
<!-- Viewer controls -->
|
||||
<div class="viewer-controls">
|
||||
<button @click="loadModel" :disabled="isLoading || !modelUrl">
|
||||
{{ isLoading ? 'Loading...' : 'Load Model' }}
|
||||
</button>
|
||||
<button @click="resetCamera" :disabled="!isModelLoaded">
|
||||
Reset Camera
|
||||
</button>
|
||||
<button @click="toggleAnimation" :disabled="!isModelLoaded">
|
||||
{{ isAnimating ? 'Stop' : 'Start' }} Animation
|
||||
</button>
|
||||
<button @click="toggleWireframe" :disabled="!isModelLoaded">
|
||||
{{ showWireframe ? 'Hide' : 'Show' }} Wireframe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading status -->
|
||||
<div v-if="isLoading" class="loading-status">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading 3D model...</p>
|
||||
<p class="loading-detail">{{ loadingProgress }}% complete</p>
|
||||
</div>
|
||||
|
||||
<!-- Error status -->
|
||||
<div v-if="loadError" class="error-status">
|
||||
<p>Failed to load model: {{ loadError }}</p>
|
||||
<button @click="retryLoad" class="retry-btn">Retry</button>
|
||||
</div>
|
||||
|
||||
<!-- 3D Canvas -->
|
||||
<div
|
||||
ref="canvasContainer"
|
||||
class="canvas-container"
|
||||
:class="{ 'model-loaded': isModelLoaded }"
|
||||
>
|
||||
<canvas
|
||||
ref="threeCanvas"
|
||||
class="three-canvas"
|
||||
></canvas>
|
||||
|
||||
<!-- Overlay controls -->
|
||||
<div v-if="isModelLoaded" class="overlay-controls">
|
||||
<div class="control-group">
|
||||
<label>Camera Distance:</label>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="cameraDistance"
|
||||
min="1"
|
||||
max="20"
|
||||
step="0.1"
|
||||
@input="updateCameraDistance"
|
||||
/>
|
||||
<span>{{ cameraDistance.toFixed(1) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Rotation Speed:</label>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="rotationSpeed"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
/>
|
||||
<span>{{ rotationSpeed.toFixed(1) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Light Intensity:</label>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="lightIntensity"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
@input="updateLightIntensity"
|
||||
/>
|
||||
<span>{{ lightIntensity.toFixed(1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model info -->
|
||||
<div v-if="modelInfo" class="model-info">
|
||||
<h4>Model Information</h4>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Vertices:</span>
|
||||
<span class="info-value">{{ modelInfo.vertexCount.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Faces:</span>
|
||||
<span class="info-value">{{ modelInfo.faceCount.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Materials:</span>
|
||||
<span class="info-value">{{ modelInfo.materialCount }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">File Size:</span>
|
||||
<span class="info-value">{{ formatFileSize(modelInfo.fileSize) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance metrics -->
|
||||
<div v-if="performanceMetrics" class="performance-metrics">
|
||||
<h4>Performance Metrics</h4>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<span class="metric-label">FPS:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.fps }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Render Time:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.renderTime }}ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Memory Usage:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.memoryUsage }}MB</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Draw Calls:</span>
|
||||
<span class="metric-value">{{ performanceMetrics.drawCalls }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator';
|
||||
|
||||
interface ModelInfo {
|
||||
vertexCount: number;
|
||||
faceCount: number;
|
||||
materialCount: number;
|
||||
fileSize: number;
|
||||
boundingBox: {
|
||||
min: { x: number; y: number; z: number };
|
||||
max: { x: number; y: number; z: number };
|
||||
};
|
||||
}
|
||||
|
||||
interface PerformanceMetrics {
|
||||
fps: number;
|
||||
renderTime: number;
|
||||
memoryUsage: number;
|
||||
drawCalls: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ThreeJS 3D Model Viewer Component
|
||||
*
|
||||
* Demonstrates lazy loading for heavy 3D rendering libraries.
|
||||
* This component would benefit from lazy loading as ThreeJS is a large
|
||||
* library that's only needed for 3D visualization features.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component({
|
||||
name: 'ThreeJSViewer'
|
||||
})
|
||||
export default class ThreeJSViewer extends Vue {
|
||||
@Prop({ required: true }) readonly modelUrl!: string;
|
||||
|
||||
// Component state
|
||||
isLoading = false;
|
||||
isModelLoaded = false;
|
||||
loadError: string | null = null;
|
||||
loadingProgress = 0;
|
||||
|
||||
// Animation state
|
||||
isAnimating = false;
|
||||
showWireframe = false;
|
||||
|
||||
// Camera and lighting controls
|
||||
cameraDistance = 5;
|
||||
rotationSpeed = 0.5;
|
||||
lightIntensity = 1;
|
||||
|
||||
// Canvas references
|
||||
canvasContainer: HTMLElement | null = null;
|
||||
threeCanvas: HTMLCanvasElement | null = null;
|
||||
|
||||
// Model and performance data
|
||||
modelInfo: ModelInfo | null = null;
|
||||
performanceMetrics: PerformanceMetrics | null = null;
|
||||
|
||||
// ThreeJS objects (will be lazy loaded)
|
||||
private three: any = null;
|
||||
private scene: any = null;
|
||||
private camera: any = null;
|
||||
private renderer: any = null;
|
||||
private model: any = null;
|
||||
private controls: any = null;
|
||||
private animationId: number | null = null;
|
||||
private frameCount = 0;
|
||||
private lastTime = 0;
|
||||
|
||||
// Lifecycle hooks
|
||||
mounted(): void {
|
||||
console.log('[ThreeJSViewer] Component mounted');
|
||||
this.initializeCanvas();
|
||||
}
|
||||
|
||||
beforeUnmount(): void {
|
||||
this.cleanup();
|
||||
console.log('[ThreeJSViewer] Component unmounting');
|
||||
}
|
||||
|
||||
// Methods
|
||||
private initializeCanvas(): void {
|
||||
this.canvasContainer = this.$refs.canvasContainer as HTMLElement;
|
||||
this.threeCanvas = this.$refs.threeCanvas as HTMLCanvasElement;
|
||||
|
||||
if (this.threeCanvas) {
|
||||
this.threeCanvas.width = this.canvasContainer.clientWidth;
|
||||
this.threeCanvas.height = this.canvasContainer.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
async loadModel(): Promise<void> {
|
||||
if (this.isLoading || !this.modelUrl) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.loadError = null;
|
||||
this.loadingProgress = 0;
|
||||
|
||||
try {
|
||||
console.log('[ThreeJSViewer] Loading 3D model:', this.modelUrl);
|
||||
|
||||
// Lazy load ThreeJS
|
||||
await this.loadThreeJS();
|
||||
|
||||
// Initialize scene
|
||||
await this.initializeScene();
|
||||
|
||||
// Load model
|
||||
await this.loadModelFile();
|
||||
|
||||
// Start rendering
|
||||
this.startRendering();
|
||||
|
||||
this.isModelLoaded = true;
|
||||
this.isLoading = false;
|
||||
|
||||
// Emit model loaded event
|
||||
this.$emit('model-loaded', this.modelInfo);
|
||||
|
||||
console.log('[ThreeJSViewer] Model loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ThreeJSViewer] Failed to load model:', error);
|
||||
this.loadError = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadThreeJS(): Promise<void> {
|
||||
// Simulate loading ThreeJS library
|
||||
this.loadingProgress = 20;
|
||||
await this.simulateLoading(500);
|
||||
|
||||
// In a real implementation, you would import ThreeJS here
|
||||
// this.three = await import('three');
|
||||
|
||||
this.loadingProgress = 40;
|
||||
await this.simulateLoading(300);
|
||||
}
|
||||
|
||||
private async initializeScene(): Promise<void> {
|
||||
this.loadingProgress = 60;
|
||||
|
||||
// Simulate scene initialization
|
||||
await this.simulateLoading(400);
|
||||
|
||||
// In a real implementation, you would set up ThreeJS scene here
|
||||
// this.scene = new this.three.Scene();
|
||||
// this.camera = new this.three.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||
// this.renderer = new this.three.WebGLRenderer({ canvas: this.threeCanvas });
|
||||
|
||||
this.loadingProgress = 80;
|
||||
}
|
||||
|
||||
private async loadModelFile(): Promise<void> {
|
||||
this.loadingProgress = 90;
|
||||
|
||||
// Simulate model loading
|
||||
await this.simulateLoading(600);
|
||||
|
||||
// Simulate model info
|
||||
this.modelInfo = {
|
||||
vertexCount: Math.floor(Math.random() * 50000) + 1000,
|
||||
faceCount: Math.floor(Math.random() * 25000) + 500,
|
||||
materialCount: Math.floor(Math.random() * 5) + 1,
|
||||
fileSize: Math.floor(Math.random() * 5000000) + 100000,
|
||||
boundingBox: {
|
||||
min: { x: -1, y: -1, z: -1 },
|
||||
max: { x: 1, y: 1, z: 1 }
|
||||
}
|
||||
};
|
||||
|
||||
this.loadingProgress = 100;
|
||||
}
|
||||
|
||||
private async simulateLoading(delay: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
private startRendering(): void {
|
||||
if (!this.isModelLoaded) return;
|
||||
|
||||
this.isAnimating = true;
|
||||
this.animate();
|
||||
|
||||
// Start performance monitoring
|
||||
this.startPerformanceMonitoring();
|
||||
}
|
||||
|
||||
private animate(): void {
|
||||
if (!this.isAnimating) return;
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
|
||||
// Simulate model rotation
|
||||
if (this.model && this.rotationSpeed > 0) {
|
||||
// this.model.rotation.y += this.rotationSpeed * 0.01;
|
||||
}
|
||||
|
||||
// Simulate rendering
|
||||
// this.renderer.render(this.scene, this.camera);
|
||||
|
||||
this.frameCount++;
|
||||
}
|
||||
|
||||
private startPerformanceMonitoring(): void {
|
||||
const updateMetrics = () => {
|
||||
if (!this.isAnimating) return;
|
||||
|
||||
const now = performance.now();
|
||||
const deltaTime = now - this.lastTime;
|
||||
|
||||
if (deltaTime > 0) {
|
||||
const fps = Math.round(1000 / deltaTime);
|
||||
|
||||
this.performanceMetrics = {
|
||||
fps: Math.min(fps, 60), // Cap at 60 FPS for display
|
||||
renderTime: Math.round(deltaTime),
|
||||
memoryUsage: Math.round((Math.random() * 50 + 10) * 100) / 100,
|
||||
drawCalls: Math.floor(Math.random() * 100) + 10
|
||||
};
|
||||
}
|
||||
|
||||
this.lastTime = now;
|
||||
requestAnimationFrame(updateMetrics);
|
||||
};
|
||||
|
||||
updateMetrics();
|
||||
}
|
||||
|
||||
resetCamera(): void {
|
||||
if (!this.isModelLoaded) return;
|
||||
|
||||
this.cameraDistance = 5;
|
||||
this.updateCameraDistance();
|
||||
console.log('[ThreeJSViewer] Camera reset');
|
||||
}
|
||||
|
||||
toggleAnimation(): void {
|
||||
this.isAnimating = !this.isAnimating;
|
||||
|
||||
if (this.isAnimating) {
|
||||
this.animate();
|
||||
} else if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
|
||||
console.log('[ThreeJSViewer] Animation toggled:', this.isAnimating);
|
||||
}
|
||||
|
||||
toggleWireframe(): void {
|
||||
this.showWireframe = !this.showWireframe;
|
||||
|
||||
// In a real implementation, you would toggle wireframe mode
|
||||
// this.model.traverse((child: any) => {
|
||||
// if (child.isMesh) {
|
||||
// child.material.wireframe = this.showWireframe;
|
||||
// }
|
||||
// });
|
||||
|
||||
console.log('[ThreeJSViewer] Wireframe toggled:', this.showWireframe);
|
||||
}
|
||||
|
||||
updateCameraDistance(): void {
|
||||
if (!this.isModelLoaded) return;
|
||||
|
||||
// In a real implementation, you would update camera position
|
||||
// this.camera.position.z = this.cameraDistance;
|
||||
// this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
updateLightIntensity(): void {
|
||||
if (!this.isModelLoaded) return;
|
||||
|
||||
// In a real implementation, you would update light intensity
|
||||
// this.light.intensity = this.lightIntensity;
|
||||
}
|
||||
|
||||
retryLoad(): void {
|
||||
this.loadError = null;
|
||||
this.loadModel();
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose();
|
||||
}
|
||||
|
||||
this.isAnimating = false;
|
||||
this.isModelLoaded = false;
|
||||
}
|
||||
|
||||
formatFileSize(bytes: number): string {
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
// Event emitters
|
||||
@Emit('model-loaded')
|
||||
emitModelLoaded(info: ModelInfo): ModelInfo {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.threejs-viewer {
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.viewer-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.viewer-controls button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.viewer-controls button:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.viewer-controls button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-detail {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.error-status {
|
||||
padding: 20px;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
color: #721c24;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 10px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #721c24;
|
||||
border-radius: 4px;
|
||||
background: #721c24;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #5a1a1a;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.canvas-container.model-loaded {
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.three-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay-controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.control-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.control-group input[type="range"] {
|
||||
width: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.control-group span {
|
||||
font-size: 0.8em;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.model-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.performance-metrics {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e8f4fd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
151
vite.config.optimized.mts
Normal file
151
vite.config.optimized.mts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { defineConfig, UserConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import dotenv from "dotenv";
|
||||
import { loadAppConfig } from "./vite.config.utils.mts";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Optimized Vite configuration for Vue 3 + vue-facing-decorator
|
||||
* with enhanced code splitting and lazy loading capabilities
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> {
|
||||
const appConfig = await loadAppConfig();
|
||||
const isCapacitor = mode === "capacitor";
|
||||
const isElectron = mode === "electron";
|
||||
const isNative = isCapacitor || isElectron;
|
||||
|
||||
// Set platform and disable PWA for native platforms
|
||||
process.env.VITE_PLATFORM = mode;
|
||||
process.env.VITE_PWA_ENABLED = isNative ? 'false' : 'true';
|
||||
process.env.VITE_DISABLE_PWA = isNative ? 'true' : 'false';
|
||||
|
||||
if (isNative) {
|
||||
process.env.VITE_PWA_ENABLED = 'false';
|
||||
}
|
||||
|
||||
return {
|
||||
base: "/",
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: parseInt(process.env.VITE_PORT || "8080"),
|
||||
fs: { strict: false },
|
||||
// CORS headers disabled to allow images from any domain
|
||||
// This means SharedArrayBuffer is unavailable, but absurd-sql
|
||||
// will automatically fall back to IndexedDB mode which still works
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
rollupOptions: {
|
||||
external: isNative
|
||||
? ['@capacitor/app']
|
||||
: [],
|
||||
output: {
|
||||
format: 'esm',
|
||||
generatedCode: {
|
||||
preset: 'es2015'
|
||||
},
|
||||
// Enhanced manual chunks for better code splitting
|
||||
manualChunks: {
|
||||
// Vendor chunks for better caching
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'],
|
||||
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode', 'ethereum-cryptography'],
|
||||
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'],
|
||||
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'],
|
||||
'three-vendor': ['three', '@tweenjs/tween.js'],
|
||||
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'],
|
||||
// Platform-specific chunks
|
||||
...(isCapacitor && {
|
||||
'capacitor-vendor': ['@capacitor/core', '@capacitor/app', '@capacitor/camera']
|
||||
}),
|
||||
...(isElectron && {
|
||||
'electron-vendor': ['electron']
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Optimize chunk loading
|
||||
target: 'es2015',
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: process.env.NODE_ENV === 'production',
|
||||
drop_debugger: true
|
||||
}
|
||||
}
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
plugins: () => []
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
|
||||
'process.env.VITE_DISABLE_PWA': JSON.stringify(isNative),
|
||||
__dirname: JSON.stringify(process.cwd()),
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
__IS_ELECTRON__: JSON.stringify(isElectron),
|
||||
__USE_QR_READER__: JSON.stringify(!isCapacitor),
|
||||
'process.platform': JSON.stringify('browser'),
|
||||
'process.version': JSON.stringify('v16.0.0'),
|
||||
'process.env.NODE_DEBUG': JSON.stringify(false),
|
||||
'global.process': JSON.stringify({
|
||||
platform: 'browser',
|
||||
version: 'v16.0.0',
|
||||
env: { NODE_DEBUG: false }
|
||||
})
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'),
|
||||
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'),
|
||||
...appConfig.aliasConfig,
|
||||
'path': path.resolve(__dirname, './src/utils/node-modules/path.js'),
|
||||
'fs': path.resolve(__dirname, './src/utils/node-modules/fs.js'),
|
||||
'crypto': path.resolve(__dirname, './src/utils/node-modules/crypto.js'),
|
||||
'dexie-export-import': path.resolve(__dirname, 'node_modules/dexie-export-import')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@nostr/tools',
|
||||
'@nostr/tools/nip06',
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'vue-facing-decorator'
|
||||
],
|
||||
exclude: isNative ? [
|
||||
'register-service-worker',
|
||||
'workbox-window',
|
||||
'web-push',
|
||||
'serviceworker-webpack-plugin',
|
||||
'vite-plugin-pwa',
|
||||
'@vite-pwa/vue'
|
||||
] : []
|
||||
},
|
||||
// Enhanced performance optimizations
|
||||
esbuild: {
|
||||
target: 'es2015',
|
||||
supported: {
|
||||
'bigint': true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig(async () => createOptimizedBuildConfig('web'));
|
||||
Reference in New Issue
Block a user