mirror of
https://gitverse.ru/kpa39l/telepost.git
synced 2026-09-29 01:50:08 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# TelePost Clone - MVP
|
||||
|
||||
## Описание
|
||||
|
||||
TelePost Clone - это приложение для управления публикациями в Telegram и других платформах. Проект реализует основной функционал планировщика постов с поддержкой различных интеграций.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │◄──►│ Backend │◄──►│ External APIs │
|
||||
│ (SvelteKit) │ │ (FastAPI) │ │ (Telegram, MAX) │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
┌──────────────────┐
|
||||
│ PostgreSQL │
|
||||
│ │
|
||||
│ Redis (queue) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Первый этап MVP
|
||||
|
||||
Первый этап включает в себя:
|
||||
|
||||
1. **Базовая аутентификация**
|
||||
- Аутентификация через Telegram Login Widget
|
||||
- Проверка аутентичности данных от Telegram
|
||||
- Генерация JWT-токенов
|
||||
|
||||
2. **Структура приложения**
|
||||
- Основная структура проекта (backend и frontend)
|
||||
- Модель пользователя в базе данных
|
||||
- Маршруты аутентификации
|
||||
- Система схем Pydantic
|
||||
|
||||
## Установка и запуск
|
||||
|
||||
### Требования
|
||||
|
||||
- Docker и Docker Compose
|
||||
- Node.js (для локальной разработки)
|
||||
|
||||
### Запуск с помощью Docker Compose
|
||||
|
||||
```bash
|
||||
# Установите переменные окружения
|
||||
cp .env.example .env
|
||||
# Отредактируйте .env файл и укажите свои значения
|
||||
|
||||
# Запустите приложение
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Приложение будет доступно по адресу:
|
||||
- Frontend: http://localhost:3000
|
||||
- Backend API: http://localhost:8000
|
||||
|
||||
### Локальная разработка
|
||||
|
||||
Для бэкенда:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
Для фронтенда:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Для корректной работы приложения необходимо задать следующие переменные окружения:
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN` - токен вашего Telegram бота
|
||||
- `TELEGRAM_BOT_NAME` - имя вашего Telegram бота
|
||||
- `SECRET_KEY` - секретный ключ для подписи JWT токенов
|
||||
|
||||
## Использование
|
||||
|
||||
1. Зарегистрируйте Telegram бота через @BotFather
|
||||
2. Укажите домен вашего приложения в настройках бота для авторизации
|
||||
3. Войдите в приложение через Telegram Login Widget
|
||||
4. Начните использовать возможности планировщика постов
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
telepost-clone/
|
||||
├── backend/ # FastAPI приложение
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API маршруты
|
||||
│ │ ├── models/ # ORM модели
|
||||
│ │ ├── schemas/ # Pydantic схемы
|
||||
│ │ ├── services/ # Бизнес-логика
|
||||
│ │ ├── database/ # Настройки базы данных
|
||||
│ │ ├── auth/ # Аутентификация
|
||||
│ │ ├── integrations/ # Интеграции с внешними сервисами
|
||||
│ │ └── utils/ # Утилиты
|
||||
│ ├── migrations/ # Миграции базы данных
|
||||
│ ├── tests/ # Тесты
|
||||
│ └── requirements.txt # Зависимости
|
||||
├── frontend/ # SvelteKit приложение
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # Страницы
|
||||
│ │ ├── components/ # Компоненты
|
||||
│ │ ├── lib/ # Библиотеки
|
||||
│ │ ├── stores/ # Svelte stores
|
||||
│ │ └── app.html # Базовый шаблон
|
||||
│ ├── static/ # Статические файлы
|
||||
│ └── package.json # Зависимости
|
||||
├── docs/ # Документация
|
||||
└── docker-compose.yml # Оркестрация
|
||||
```
|
||||
|
||||
## Дальнейшие этапы
|
||||
|
||||
2. **Этап 2**: Создание и редактирование постов
|
||||
3. **Этап 3**: Публикация в Telegram
|
||||
4. **Этап 4**: Добавление интеграции с внешней платформой
|
||||
5. **Этап 5**: Расширенные функции (планирование, история, черновики)
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# Архитектура приложения TelePost Clone
|
||||
|
||||
## 1. Общая архитектура системы
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │◄──►│ Backend │◄──►│ External APIs │
|
||||
│ (SvelteKit) │ │ (FastAPI) │ │ (Telegram, MAX) │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
┌──────────────────┐
|
||||
│ PostgreSQL │
|
||||
│ │
|
||||
│ Redis (queue) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## 2. Структура проекта
|
||||
|
||||
```
|
||||
telepost-clone/
|
||||
├── backend/ # FastAPI приложение
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API маршруты
|
||||
│ │ ├── models/ # ORM модели
|
||||
│ │ ├── schemas/ # Pydantic схемы
|
||||
│ │ ├── services/ # Бизнес-логика
|
||||
│ │ ├── database/ # Настройки базы данных
|
||||
│ │ ├── auth/ # Аутентификация
|
||||
│ │ ├── integrations/ # Интеграции с внешними сервисами
|
||||
│ │ └── utils/ # Утилиты
|
||||
│ ├── migrations/ # Миграции базы данных
|
||||
│ ├── tests/ # Тесты
|
||||
│ └── requirements.txt # Зависимости
|
||||
├── frontend/ # SvelteKit приложение
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # Страницы
|
||||
│ │ ├── components/ # Компоненты
|
||||
│ │ ├── lib/ # Библиотеки
|
||||
│ │ ├── stores/ # Svelte stores
|
||||
│ │ └── app.html # Базовый шаблон
|
||||
│ ├── static/ # Статические файлы
|
||||
│ └── package.json # Зависимости
|
||||
├── docs/ # Документация
|
||||
└── docker-compose.yml # Оркестрация
|
||||
```
|
||||
|
||||
## 3. Подробное описание компонентов
|
||||
|
||||
### 3.1. Бэкенд (FastAPI)
|
||||
|
||||
**Основные модули:**
|
||||
|
||||
- **Auth module**: Telegram Login Widget с проверкой аутентичности
|
||||
- **Posts module**: CRUD операции для постов, работа с черновиками
|
||||
- **Channels module**: Управление Telegram-каналами
|
||||
- **Integrations module**: Подключение и управление внешними платформами
|
||||
- **Publications module**: Планирование и выполнение публикаций
|
||||
- **Media module**: Загрузка и обработка медиафайлов
|
||||
|
||||
### 3.2. Фронтенд (SvelteKit)
|
||||
|
||||
**Основные страницы:**
|
||||
|
||||
- `/login` - Страница входа через Telegram
|
||||
- `/dashboard` - Главная панель управления
|
||||
- `/posts` - Список постов
|
||||
- `/posts/new` - Создание нового поста
|
||||
- `/posts/[id]/edit` - Редактирование поста
|
||||
- `/channels` - Управление каналами
|
||||
- `/integrations` - Подключение интеграций
|
||||
- `/history` - История публикаций
|
||||
|
||||
### 3.3. База данных (PostgreSQL)
|
||||
|
||||
**Основные таблицы:**
|
||||
|
||||
- `users`: Информация о пользователях
|
||||
- `channels`: Привязанные Telegram-каналы
|
||||
- `posts`: Созданные посты
|
||||
- `integrations`: Настроенные интеграции
|
||||
- `publications`: История публикаций
|
||||
- `media_files`: Загруженные медиафайлы
|
||||
|
||||
## 4. Потоки работы
|
||||
|
||||
### 4.1. Аутентификация
|
||||
```
|
||||
1. Пользователь переходит на /login
|
||||
2. Нажимает кнопку "Войти через Telegram"
|
||||
3. Telegram возвращает данные пользователя
|
||||
4. Приложение проверяет подлинность данных
|
||||
5. Создается/обновляется запись пользователя
|
||||
6. Генерируется JWT-токен
|
||||
7. Пользователь перенаправляется в панель управления
|
||||
```
|
||||
|
||||
### 4.2. Создание и публикация поста
|
||||
```
|
||||
1. Пользователь создает новый пост
|
||||
2. Пользователь выбирает каналы для публикации
|
||||
3. Пользователь может назначить время публикации
|
||||
4. Пост сохраняется в базе данных
|
||||
5. Если время публикации в будущем - задача помещается в очередь
|
||||
6. При наступлении времени публикации:
|
||||
- Формируется сообщение под формат каждого канала
|
||||
- Отправляется в соответствующие интеграции
|
||||
- Результат сохраняется в истории публикаций
|
||||
```
|
||||
|
||||
## 5. Технические особенности
|
||||
|
||||
### 5.1. Безопасность
|
||||
- Проверка аутентичности данных от Telegram
|
||||
- Валидация всех входных данных
|
||||
- Защита от XSS и CSRF атак
|
||||
- Хранение чувствительных данных в зашифрованном виде
|
||||
|
||||
### 5.2. Масштабируемость
|
||||
- Использование очередей задач для фоновых операций
|
||||
- Кэширование частых запросов
|
||||
- Оптимизация SQL-запросов
|
||||
|
||||
### 5.3. Надежность
|
||||
- Обработка ошибок и повторные попытки отправки
|
||||
- Логирование всех действий
|
||||
- Мониторинг статуса публикаций
|
||||
|
||||
## 6. Особенности реализации
|
||||
|
||||
### 6.1. Визуальный редактор
|
||||
Для редактирования постов будет использован современный WYSIWYG редактор, поддерживающий:
|
||||
- Форматирование текста
|
||||
- Вставку изображений и видео
|
||||
- Добавление опросов
|
||||
- Работу с альбомами
|
||||
|
||||
### 6.2. Поддержка различных типов контента
|
||||
- Текстовые посты
|
||||
- Медиафайлы (фото, видео, документы)
|
||||
- Альбомы
|
||||
- Опросы
|
||||
- Видеосообщения
|
||||
- Специальные элементы (реакции, закрепление)
|
||||
|
||||
### 6.3. Интеграции
|
||||
- Telegram (основная платформа)
|
||||
- MAX (как пример внешней интеграции)
|
||||
- Возможность легко добавлять новые интеграции
|
||||
|
||||
## 7. План реализации MVP
|
||||
|
||||
1. **Этап 1**: Базовая аутентификация и структура приложения
|
||||
2. **Этап 2**: Создание и редактирование постов
|
||||
3. **Этап 3**: Публикация в Telegram
|
||||
4. **Этап 4**: Добавление интеграции с внешней платформой
|
||||
5. **Этап 5**: Расширенные функции (планирование, история, черновики)
|
||||
@@ -0,0 +1,27 @@
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ....database.database import get_db
|
||||
from ....auth.telegram_auth import authenticate_user_telegram
|
||||
from ....schemas.token import Token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/telegram-login", response_model=Token)
|
||||
async def telegram_login(auth_data: Dict[str, Any], db: Session = Depends(get_db)):
|
||||
"""
|
||||
Authenticate user via Telegram Login Widget
|
||||
Expects data received from Telegram Login Widget
|
||||
"""
|
||||
result = authenticate_user_telegram(db, auth_data)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid Telegram login data",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ....database.database import get_db
|
||||
from ....auth.jwt import get_current_user
|
||||
from ....models.user import User
|
||||
from ....schemas.post import Post, PostCreate, PostUpdate
|
||||
from ....services.post_service import PostService
|
||||
|
||||
router = APIRouter(prefix="/posts", tags=["posts"])
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Post])
|
||||
def get_posts(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Получить список постов пользователя
|
||||
"""
|
||||
post_service = PostService(db)
|
||||
posts = post_service.get_posts(owner_id=current_user.id, skip=skip, limit=limit)
|
||||
return posts
|
||||
|
||||
|
||||
@router.post("/", response_model=Post)
|
||||
def create_post(
|
||||
post: PostCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Создать новый пост
|
||||
"""
|
||||
post_service = PostService(db)
|
||||
db_post = post_service.create_post(post_data=post, owner_id=current_user.id)
|
||||
return db_post
|
||||
|
||||
|
||||
@router.get("/{post_id}", response_model=Post)
|
||||
def get_post(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Получить конкретный пост
|
||||
"""
|
||||
post_service = PostService(db)
|
||||
db_post = post_service.get_post(post_id=post_id, owner_id=current_user.id)
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
return db_post
|
||||
|
||||
|
||||
@router.put("/{post_id}", response_model=Post)
|
||||
def update_post(
|
||||
post_id: int,
|
||||
post_update: PostUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Обновить пост
|
||||
"""
|
||||
post_service = PostService(db)
|
||||
db_post = post_service.update_post(
|
||||
post_id=post_id,
|
||||
post_data=post_update,
|
||||
owner_id=current_user.id
|
||||
)
|
||||
if not db_post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
return db_post
|
||||
|
||||
|
||||
@router.delete("/{post_id}")
|
||||
def delete_post(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Удалить пост
|
||||
"""
|
||||
post_service = PostService(db)
|
||||
success = post_service.delete_post(post_id=post_id, owner_id=current_user.id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
return {"message": "Post deleted successfully"}
|
||||
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
from .endpoints import auth
|
||||
from .endpoints import posts
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Include authentication routes
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||
|
||||
# Include posts routes
|
||||
api_router.include_router(posts.router)
|
||||
@@ -0,0 +1,61 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from fastapi import HTTPException, status, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database.config import settings
|
||||
from ..models.user import User
|
||||
from ..schemas.token import TokenData
|
||||
from ..database.database import get_db
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_token(token: str, credentials_exception):
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
return token_data
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token_data = verify_token(credentials.credentials, credentials_exception)
|
||||
|
||||
user = db.query(User).filter(User.username == token_data.username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
@@ -0,0 +1,117 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database.config import settings
|
||||
from ..models.user import User
|
||||
from ..schemas.user import UserCreate, UserInDB
|
||||
from .jwt import create_access_token
|
||||
|
||||
|
||||
def verify_telegram_login_data(auth_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Verify the authenticity of Telegram login data
|
||||
"""
|
||||
if not settings.TELEGRAM_BOT_TOKEN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Telegram bot token not configured"
|
||||
)
|
||||
|
||||
# Extract the hash
|
||||
received_hash = auth_data.pop('hash', None)
|
||||
|
||||
if not received_hash:
|
||||
return False
|
||||
|
||||
# Get the auth date
|
||||
auth_date = auth_data.pop('auth_date', None)
|
||||
if auth_date:
|
||||
# Check if the data is not too old (more than 1 day)
|
||||
auth_time = datetime.fromtimestamp(int(auth_date))
|
||||
if (datetime.now() - auth_time).days > 0:
|
||||
return False
|
||||
|
||||
# Sort the remaining parameters alphabetically by key
|
||||
data_check_arr = []
|
||||
for key, value in sorted(auth_data.items()):
|
||||
data_check_arr.append(f"{key}={value}")
|
||||
|
||||
data_check_string = '\n'.join(data_check_arr)
|
||||
|
||||
# Create secret key using SHA256 of bot token
|
||||
secret_key = hashlib.sha256(settings.TELEGRAM_BOT_TOKEN.encode()).digest()
|
||||
|
||||
# Calculate HMAC-SHA256 signature
|
||||
calculated_hash = hmac.new(
|
||||
secret_key,
|
||||
data_check_string.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Compare the calculated hash with the received hash
|
||||
return calculated_hash == received_hash
|
||||
|
||||
|
||||
def authenticate_user_telegram(db: Session, auth_data: Dict[str, Any]):
|
||||
"""
|
||||
Authenticate user via Telegram login data and return JWT token
|
||||
"""
|
||||
# Verify the authenticity of the data
|
||||
if not verify_telegram_login_data(auth_data.copy()): # Copy to avoid modifying original
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid Telegram login data"
|
||||
)
|
||||
|
||||
# Extract user data
|
||||
telegram_id = str(auth_data.get('id'))
|
||||
username = auth_data.get('username')
|
||||
first_name = auth_data.get('first_name')
|
||||
last_name = auth_data.get('last_name')
|
||||
photo_url = auth_data.get('photo_url')
|
||||
|
||||
# Try to find existing user by telegram_id
|
||||
user = db.query(User).filter(User.telegram_id == telegram_id).first()
|
||||
|
||||
if user:
|
||||
# Update user data if it has changed
|
||||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.avatar_url = photo_url
|
||||
user.is_verified = True
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
else:
|
||||
# Create new user
|
||||
user = User(
|
||||
telegram_id=telegram_id,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
avatar_url=photo_url,
|
||||
is_verified=True,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# Create JWT token
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username or user.telegram_id},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": UserInDB.from_orm(user) if hasattr(UserInDB, 'from_orm') else UserInDB.model_validate(user)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DATABASE_URL: str = "postgresql://user:password@localhost/telepost"
|
||||
REDIS_URL: str = "redis://localhost:6379"
|
||||
|
||||
SECRET_KEY: str = "your-secret-key-here"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
# Telegram Bot API settings
|
||||
TELEGRAM_BOT_TOKEN: Optional[str] = None
|
||||
TELEGRAM_BOT_NAME: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from .config import settings
|
||||
|
||||
# Create the database engine
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True, # Verify connections before use
|
||||
pool_recycle=300, # Recycle connections after 5 minutes
|
||||
)
|
||||
|
||||
# Create a configured "SessionLocal" class
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine
|
||||
)
|
||||
|
||||
# Create a Base class for declarative models
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
Dependency to get database session
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import create_engine
|
||||
from typing import Generator
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db() -> Generator:
|
||||
"""
|
||||
Dependency to get database session
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Engine and session will be configured in database/database.py
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False)
|
||||
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database.database import Base
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class Post(Base):
|
||||
__tablename__ = "posts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
scheduled_time = Column(DateTime, nullable=True)
|
||||
status = Column(String(50), default="draft") # draft, scheduled, published, cancelled
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
published_at = Column(DateTime(timezone=True), nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Relationship
|
||||
owner = relationship("User", back_populates="posts")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Post(id={self.id}, title='{self.title}', status='{self.status}')>"
|
||||
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from typing import TYPE_CHECKING
|
||||
from .base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
telegram_id = Column(String, unique=True, index=True, nullable=False)
|
||||
username = Column(String, unique=True, index=True, nullable=True)
|
||||
first_name = Column(String, nullable=True)
|
||||
last_name = Column(String, nullable=True)
|
||||
email = Column(String, unique=True, index=True, nullable=True)
|
||||
avatar_url = Column(String, nullable=True)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, telegram_id={self.telegram_id}, username={self.username})>"
|
||||
@@ -0,0 +1,44 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PostStatus(str, Enum):
|
||||
draft = "draft"
|
||||
scheduled = "scheduled"
|
||||
published = "published"
|
||||
cancelled = "cancelled"
|
||||
|
||||
|
||||
class PostBase(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
scheduled_time: Optional[datetime] = None
|
||||
status: PostStatus = PostStatus.draft
|
||||
|
||||
|
||||
class PostCreate(PostBase):
|
||||
pass
|
||||
|
||||
|
||||
class PostUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
scheduled_time: Optional[datetime] = None
|
||||
status: Optional[PostStatus] = None
|
||||
|
||||
|
||||
class PostInDB(PostBase):
|
||||
id: int
|
||||
owner_id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
published_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class Post(PostInDB):
|
||||
pass
|
||||
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
user_id: Optional[int] = None
|
||||
@@ -0,0 +1,48 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
telegram_id: str
|
||||
username: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_verified: Optional[bool] = False
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
username: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
class UserInDB(UserBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserPublic(BaseModel):
|
||||
id: int
|
||||
telegram_id: str
|
||||
username: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
is_verified: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,88 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from ..models.post import Post
|
||||
from ..schemas.post import PostCreate, PostUpdate
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class PostService:
|
||||
def __init__(self, db_session: Session):
|
||||
self.db = db_session
|
||||
|
||||
def get_post(self, post_id: int, owner_id: int) -> Optional[Post]:
|
||||
"""Получить пост по ID и владельцу"""
|
||||
return self.db.query(Post).filter(
|
||||
and_(Post.id == post_id, Post.owner_id == owner_id)
|
||||
).first()
|
||||
|
||||
def get_posts(self, owner_id: int, skip: int = 0, limit: int = 100) -> List[Post]:
|
||||
"""Получить список постов пользователя"""
|
||||
return self.db.query(Post).filter(Post.owner_id == owner_id)\
|
||||
.order_by(Post.created_at.desc())\
|
||||
.offset(skip)\
|
||||
.limit(limit)\
|
||||
.all()
|
||||
|
||||
def create_post(self, post_data: PostCreate, owner_id: int) -> Post:
|
||||
"""Создать новый пост"""
|
||||
db_post = Post(
|
||||
title=post_data.title,
|
||||
content=post_data.content,
|
||||
scheduled_time=post_data.scheduled_time,
|
||||
status=post_data.status,
|
||||
owner_id=owner_id
|
||||
)
|
||||
self.db.add(db_post)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_post)
|
||||
return db_post
|
||||
|
||||
def update_post(self, post_id: int, post_data: PostUpdate, owner_id: int) -> Optional[Post]:
|
||||
"""Обновить пост"""
|
||||
db_post = self.get_post(post_id, owner_id)
|
||||
if not db_post:
|
||||
return None
|
||||
|
||||
# Обновляем только те поля, которые предоставлены
|
||||
update_data = post_data.dict(exclude_unset=True)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(db_post, field, value)
|
||||
|
||||
# Обновляем время последнего изменения
|
||||
db_post.updated_at = datetime.utcnow()
|
||||
|
||||
# Если статус меняется на published и published_at еще не установлен, устанавливаем его
|
||||
if post_data.status == "published" and not db_post.published_at:
|
||||
db_post.published_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(db_post)
|
||||
return db_post
|
||||
|
||||
def delete_post(self, post_id: int, owner_id: int) -> bool:
|
||||
"""Удалить пост"""
|
||||
db_post = self.get_post(post_id, owner_id)
|
||||
if not db_post:
|
||||
return False
|
||||
|
||||
self.db.delete(db_post)
|
||||
self.db.commit()
|
||||
return True
|
||||
|
||||
def get_scheduled_posts(self) -> List[Post]:
|
||||
"""Получить посты, готовые к публикации (статус scheduled и время наступило)"""
|
||||
current_time = datetime.utcnow()
|
||||
return self.db.query(Post).filter(
|
||||
and_(
|
||||
Post.status == "scheduled",
|
||||
Post.scheduled_time <= current_time
|
||||
)
|
||||
).all()
|
||||
|
||||
def get_posts_by_status(self, owner_id: int, status: str) -> List[Post]:
|
||||
"""Получить посты по статусу"""
|
||||
return self.db.query(Post).filter(
|
||||
and_(Post.owner_id == owner_id, Post.status == status)
|
||||
).all()
|
||||
@@ -0,0 +1,48 @@
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.v1.routers import api_router
|
||||
from app.database.database import engine, Base, get_db
|
||||
from app.database.config import settings
|
||||
|
||||
# Create database tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="TelePost Clone API",
|
||||
description="API for TelePost Clone application",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, replace with specific origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include API routes
|
||||
app.include_router(api_router, prefix="/api/v1", tags=["api"])
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "Welcome to TelePost Clone API"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {"status": "healthy", "service": "TelePost Clone API"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True # Only for development
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.13.1
|
||||
psycopg2-binary==2.9.9
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.6
|
||||
requests==2.31.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
redis==5.0.1
|
||||
cachetools==5.3.2
|
||||
@@ -0,0 +1,74 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: telepost_postgres
|
||||
environment:
|
||||
POSTGRES_DB: telepost
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U user -d telepost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: telepost_redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: telepost_backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://user:password@postgres:5432/telepost
|
||||
REDIS_URL: redis://redis:6379
|
||||
SECRET_KEY: your-secret-key-here
|
||||
ALGORITHM: HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: 30
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||
TELEGRAM_BOT_NAME: ${TELEGRAM_BOT_NAME}
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: telepost_frontend
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
VITE_API_BASE_URL: http://localhost:8000
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_URL=http://localhost:8000/api/v1
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
|
||||
else
|
||||
exec node "$basedir/../acorn/bin/acorn" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\acorn\bin\acorn" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
else
|
||||
exec node "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../rimraf/bin.js" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rimraf\bin.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../rimraf/bin.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../rimraf/bin.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
else
|
||||
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../sorcery/bin/sorcery" "$@"
|
||||
else
|
||||
exec node "$basedir/../sorcery/bin/sorcery" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sorcery\bin\sorcery" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../sorcery/bin/sorcery" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../sorcery/bin/sorcery" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../sorcery/bin/sorcery" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../sorcery/bin/sorcery" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../svelte-check/bin/svelte-check" "$@"
|
||||
else
|
||||
exec node "$basedir/../svelte-check/bin/svelte-check" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\svelte-check\bin\svelte-check" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../svelte-check/bin/svelte-check" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../svelte-check/bin/svelte-check" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../svelte-check/bin/svelte-check" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../svelte-check/bin/svelte-check" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../@sveltejs/kit/svelte-kit.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../@sveltejs/kit/svelte-kit.js" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@sveltejs\kit\svelte-kit.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../@sveltejs/kit/svelte-kit.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../@sveltejs/kit/svelte-kit.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../@sveltejs/kit/svelte-kit.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../@sveltejs/kit/svelte-kit.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsc" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../vite/bin/vite.js" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+1578
File diff suppressed because it is too large
Load Diff
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
# @ampproject/remapping
|
||||
|
||||
> Remap sequential sourcemaps through transformations to point at the original source code
|
||||
|
||||
Remapping allows you to take the sourcemaps generated through transforming your code and "remap"
|
||||
them to the original source locations. Think "my minified code, transformed with babel and bundled
|
||||
with webpack", all pointing to the correct location in your original source code.
|
||||
|
||||
With remapping, none of your source code transformations need to be aware of the input's sourcemap,
|
||||
they only need to generate an output sourcemap. This greatly simplifies building custom
|
||||
transformations (think a find-and-replace).
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install @ampproject/remapping
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
function remapping(
|
||||
map: SourceMap | SourceMap[],
|
||||
loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined),
|
||||
options?: { excludeContent: boolean, decodedMappings: boolean }
|
||||
): SourceMap;
|
||||
|
||||
// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the
|
||||
// "source" location (where child sources are resolved relative to, or the location of original
|
||||
// source), and the ability to override the "content" of an original source for inclusion in the
|
||||
// output sourcemap.
|
||||
type LoaderContext = {
|
||||
readonly importer: string;
|
||||
readonly depth: number;
|
||||
source: string;
|
||||
content: string | null | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer
|
||||
in the sourcemap, the `loader` will be called with the resolved path. If the path itself represents
|
||||
a transformed file (it has a sourcmap associated with it), then the `loader` should return that
|
||||
sourcemap. If not, the path will be treated as an original, untransformed source code.
|
||||
|
||||
```js
|
||||
// Babel transformed "helloworld.js" into "transformed.js"
|
||||
const transformedMap = JSON.stringify({
|
||||
file: 'transformed.js',
|
||||
// 1st column of 2nd line of output file translates into the 1st source
|
||||
// file, line 3, column 2
|
||||
mappings: ';CAEE',
|
||||
sources: ['helloworld.js'],
|
||||
version: 3,
|
||||
});
|
||||
|
||||
// Uglify minified "transformed.js" into "transformed.min.js"
|
||||
const minifiedTransformedMap = JSON.stringify({
|
||||
file: 'transformed.min.js',
|
||||
// 0th column of 1st line of output file translates into the 1st source
|
||||
// file, line 2, column 1.
|
||||
mappings: 'AACC',
|
||||
names: [],
|
||||
sources: ['transformed.js'],
|
||||
version: 3,
|
||||
});
|
||||
|
||||
const remapped = remapping(
|
||||
minifiedTransformedMap,
|
||||
(file, ctx) => {
|
||||
|
||||
// The "transformed.js" file is an transformed file.
|
||||
if (file === 'transformed.js') {
|
||||
// The root importer is empty.
|
||||
console.assert(ctx.importer === '');
|
||||
// The depth in the sourcemap tree we're currently loading.
|
||||
// The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc.
|
||||
console.assert(ctx.depth === 1);
|
||||
|
||||
return transformedMap;
|
||||
}
|
||||
|
||||
// Loader will be called to load transformedMap's source file pointers as well.
|
||||
console.assert(file === 'helloworld.js');
|
||||
// `transformed.js`'s sourcemap points into `helloworld.js`.
|
||||
console.assert(ctx.importer === 'transformed.js');
|
||||
// This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`.
|
||||
console.assert(ctx.depth === 2);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
console.log(remapped);
|
||||
// {
|
||||
// file: 'transpiled.min.js',
|
||||
// mappings: 'AAEE',
|
||||
// sources: ['helloworld.js'],
|
||||
// version: 3,
|
||||
// };
|
||||
```
|
||||
|
||||
In this example, `loader` will be called twice:
|
||||
|
||||
1. `"transformed.js"`, the first source file pointer in the `minifiedTransformedMap`. We return the
|
||||
associated sourcemap for it (its a transformed file, after all) so that sourcemap locations can
|
||||
be traced through it into the source files it represents.
|
||||
2. `"helloworld.js"`, our original, unmodified source code. This file does not have a sourcemap, so
|
||||
we return `null`.
|
||||
|
||||
The `remapped` sourcemap now points from `transformed.min.js` into locations in `helloworld.js`. If
|
||||
you were to read the `mappings`, it says "0th column of the first line output line points to the 1st
|
||||
column of the 2nd line of the file `helloworld.js`".
|
||||
|
||||
### Multiple transformations of a file
|
||||
|
||||
As a convenience, if you have multiple single-source transformations of a file, you may pass an
|
||||
array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this
|
||||
changes the `importer` and `depth` of each call to our loader. So our above example could have been
|
||||
written as:
|
||||
|
||||
```js
|
||||
const remapped = remapping(
|
||||
[minifiedTransformedMap, transformedMap],
|
||||
() => null
|
||||
);
|
||||
|
||||
console.log(remapped);
|
||||
// {
|
||||
// file: 'transpiled.min.js',
|
||||
// mappings: 'AAEE',
|
||||
// sources: ['helloworld.js'],
|
||||
// version: 3,
|
||||
// };
|
||||
```
|
||||
|
||||
### Advanced control of the loading graph
|
||||
|
||||
#### `source`
|
||||
|
||||
The `source` property can overridden to any value to change the location of the current load. Eg,
|
||||
for an original source file, it allows us to change the location to the original source regardless
|
||||
of what the sourcemap source entry says. And for transformed files, it allows us to change the
|
||||
relative resolving location for child sources of the loaded sourcemap.
|
||||
|
||||
```js
|
||||
const remapped = remapping(
|
||||
minifiedTransformedMap,
|
||||
(file, ctx) => {
|
||||
|
||||
if (file === 'transformed.js') {
|
||||
// We pretend the transformed.js file actually exists in the 'src/' directory. When the nested
|
||||
// source files are loaded, they will now be relative to `src/`.
|
||||
ctx.source = 'src/transformed.js';
|
||||
return transformedMap;
|
||||
}
|
||||
|
||||
console.assert(file === 'src/helloworld.js');
|
||||
// We could futher change the source of this original file, eg, to be inside a nested directory
|
||||
// itself. This will be reflected in the remapped sourcemap.
|
||||
ctx.source = 'src/nested/transformed.js';
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
console.log(remapped);
|
||||
// {
|
||||
// …,
|
||||
// sources: ['src/nested/helloworld.js'],
|
||||
// };
|
||||
```
|
||||
|
||||
|
||||
#### `content`
|
||||
|
||||
The `content` property can be overridden when we encounter an original source file. Eg, this allows
|
||||
you to manually provide the source content of the original file regardless of whether the
|
||||
`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove
|
||||
the source content.
|
||||
|
||||
```js
|
||||
const remapped = remapping(
|
||||
minifiedTransformedMap,
|
||||
(file, ctx) => {
|
||||
|
||||
if (file === 'transformed.js') {
|
||||
// transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap
|
||||
// would not include any `sourcesContent` values.
|
||||
return transformedMap;
|
||||
}
|
||||
|
||||
console.assert(file === 'helloworld.js');
|
||||
// We can read the file to provide the source content.
|
||||
ctx.content = fs.readFileSync(file, 'utf8');
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
console.log(remapped);
|
||||
// {
|
||||
// …,
|
||||
// sourcesContent: [
|
||||
// 'console.log("Hello world!")',
|
||||
// ],
|
||||
// };
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
#### excludeContent
|
||||
|
||||
By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the
|
||||
`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce
|
||||
the size out the sourcemap.
|
||||
|
||||
#### decodedMappings
|
||||
|
||||
By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the
|
||||
`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of
|
||||
encoding into a VLQ string.
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
import { decodedMappings, traceSegment, TraceMap } from '@jridgewell/trace-mapping';
|
||||
import { GenMapping, maybeAddSegment, setSourceContent, setIgnore, toDecodedMap, toEncodedMap } from '@jridgewell/gen-mapping';
|
||||
|
||||
const SOURCELESS_MAPPING = /* #__PURE__ */ SegmentObject('', -1, -1, '', null, false);
|
||||
const EMPTY_SOURCES = [];
|
||||
function SegmentObject(source, line, column, name, content, ignore) {
|
||||
return { source, line, column, name, content, ignore };
|
||||
}
|
||||
function Source(map, sources, source, content, ignore) {
|
||||
return {
|
||||
map,
|
||||
sources,
|
||||
source,
|
||||
content,
|
||||
ignore,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
|
||||
* (which may themselves be SourceMapTrees).
|
||||
*/
|
||||
function MapSource(map, sources) {
|
||||
return Source(map, sources, '', null, false);
|
||||
}
|
||||
/**
|
||||
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
|
||||
* segment tracing ends at the `OriginalSource`.
|
||||
*/
|
||||
function OriginalSource(source, content, ignore) {
|
||||
return Source(null, EMPTY_SOURCES, source, content, ignore);
|
||||
}
|
||||
/**
|
||||
* traceMappings is only called on the root level SourceMapTree, and begins the process of
|
||||
* resolving each mapping in terms of the original source files.
|
||||
*/
|
||||
function traceMappings(tree) {
|
||||
// TODO: Eventually support sourceRoot, which has to be removed because the sources are already
|
||||
// fully resolved. We'll need to make sources relative to the sourceRoot before adding them.
|
||||
const gen = new GenMapping({ file: tree.map.file });
|
||||
const { sources: rootSources, map } = tree;
|
||||
const rootNames = map.names;
|
||||
const rootMappings = decodedMappings(map);
|
||||
for (let i = 0; i < rootMappings.length; i++) {
|
||||
const segments = rootMappings[i];
|
||||
for (let j = 0; j < segments.length; j++) {
|
||||
const segment = segments[j];
|
||||
const genCol = segment[0];
|
||||
let traced = SOURCELESS_MAPPING;
|
||||
// 1-length segments only move the current generated column, there's no source information
|
||||
// to gather from it.
|
||||
if (segment.length !== 1) {
|
||||
const source = rootSources[segment[1]];
|
||||
traced = originalPositionFor(source, segment[2], segment[3], segment.length === 5 ? rootNames[segment[4]] : '');
|
||||
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
|
||||
// respective segment into an original source.
|
||||
if (traced == null)
|
||||
continue;
|
||||
}
|
||||
const { column, line, name, content, source, ignore } = traced;
|
||||
maybeAddSegment(gen, i, genCol, source, line, column, name);
|
||||
if (source && content != null)
|
||||
setSourceContent(gen, source, content);
|
||||
if (ignore)
|
||||
setIgnore(gen, source, true);
|
||||
}
|
||||
}
|
||||
return gen;
|
||||
}
|
||||
/**
|
||||
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
|
||||
* child SourceMapTrees, until we find the original source map.
|
||||
*/
|
||||
function originalPositionFor(source, line, column, name) {
|
||||
if (!source.map) {
|
||||
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
|
||||
}
|
||||
const segment = traceSegment(source.map, line, column);
|
||||
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
|
||||
if (segment == null)
|
||||
return null;
|
||||
// 1-length segments only move the current generated column, there's no source information
|
||||
// to gather from it.
|
||||
if (segment.length === 1)
|
||||
return SOURCELESS_MAPPING;
|
||||
return originalPositionFor(source.sources[segment[1]], segment[2], segment[3], segment.length === 5 ? source.map.names[segment[4]] : name);
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (Array.isArray(value))
|
||||
return value;
|
||||
return [value];
|
||||
}
|
||||
/**
|
||||
* Recursively builds a tree structure out of sourcemap files, with each node
|
||||
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
|
||||
* `OriginalSource`s and `SourceMapTree`s.
|
||||
*
|
||||
* Every sourcemap is composed of a collection of source files and mappings
|
||||
* into locations of those source files. When we generate a `SourceMapTree` for
|
||||
* the sourcemap, we attempt to load each source file's own sourcemap. If it
|
||||
* does not have an associated sourcemap, it is considered an original,
|
||||
* unmodified source file.
|
||||
*/
|
||||
function buildSourceMapTree(input, loader) {
|
||||
const maps = asArray(input).map((m) => new TraceMap(m, ''));
|
||||
const map = maps.pop();
|
||||
for (let i = 0; i < maps.length; i++) {
|
||||
if (maps[i].sources.length > 1) {
|
||||
throw new Error(`Transformation map ${i} must have exactly one source file.\n` +
|
||||
'Did you specify these with the most recent transformation maps first?');
|
||||
}
|
||||
}
|
||||
let tree = build(map, loader, '', 0);
|
||||
for (let i = maps.length - 1; i >= 0; i--) {
|
||||
tree = MapSource(maps[i], [tree]);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
function build(map, loader, importer, importerDepth) {
|
||||
const { resolvedSources, sourcesContent, ignoreList } = map;
|
||||
const depth = importerDepth + 1;
|
||||
const children = resolvedSources.map((sourceFile, i) => {
|
||||
// The loading context gives the loader more information about why this file is being loaded
|
||||
// (eg, from which importer). It also allows the loader to override the location of the loaded
|
||||
// sourcemap/original source, or to override the content in the sourcesContent field if it's
|
||||
// an unmodified source file.
|
||||
const ctx = {
|
||||
importer,
|
||||
depth,
|
||||
source: sourceFile || '',
|
||||
content: undefined,
|
||||
ignore: undefined,
|
||||
};
|
||||
// Use the provided loader callback to retrieve the file's sourcemap.
|
||||
// TODO: We should eventually support async loading of sourcemap files.
|
||||
const sourceMap = loader(ctx.source, ctx);
|
||||
const { source, content, ignore } = ctx;
|
||||
// If there is a sourcemap, then we need to recurse into it to load its source files.
|
||||
if (sourceMap)
|
||||
return build(new TraceMap(sourceMap, source), loader, source, depth);
|
||||
// Else, it's an unmodified source file.
|
||||
// The contents of this unmodified source file can be overridden via the loader context,
|
||||
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
|
||||
// the importing sourcemap's `sourcesContent` field.
|
||||
const sourceContent = content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
|
||||
const ignored = ignore !== undefined ? ignore : ignoreList ? ignoreList.includes(i) : false;
|
||||
return OriginalSource(source, sourceContent, ignored);
|
||||
});
|
||||
return MapSource(map, children);
|
||||
}
|
||||
|
||||
/**
|
||||
* A SourceMap v3 compatible sourcemap, which only includes fields that were
|
||||
* provided to it.
|
||||
*/
|
||||
class SourceMap {
|
||||
constructor(map, options) {
|
||||
const out = options.decodedMappings ? toDecodedMap(map) : toEncodedMap(map);
|
||||
this.version = out.version; // SourceMap spec says this should be first.
|
||||
this.file = out.file;
|
||||
this.mappings = out.mappings;
|
||||
this.names = out.names;
|
||||
this.ignoreList = out.ignoreList;
|
||||
this.sourceRoot = out.sourceRoot;
|
||||
this.sources = out.sources;
|
||||
if (!options.excludeContent) {
|
||||
this.sourcesContent = out.sourcesContent;
|
||||
}
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traces through all the mappings in the root sourcemap, through the sources
|
||||
* (and their sourcemaps), all the way back to the original source location.
|
||||
*
|
||||
* `loader` will be called every time we encounter a source file. If it returns
|
||||
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
|
||||
* it returns a falsey value, that source file is treated as an original,
|
||||
* unmodified source file.
|
||||
*
|
||||
* Pass `excludeContent` to exclude any self-containing source file content
|
||||
* from the output sourcemap.
|
||||
*
|
||||
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
|
||||
* VLQ encoded) mappings.
|
||||
*/
|
||||
function remapping(input, loader, options) {
|
||||
const opts = typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
|
||||
const tree = buildSourceMapTree(input, loader);
|
||||
return new SourceMap(traceMappings(tree), opts);
|
||||
}
|
||||
|
||||
export { remapping as default };
|
||||
//# sourceMappingURL=remapping.mjs.map
|
||||
+1
File diff suppressed because one or more lines are too long
+202
@@ -0,0 +1,202 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@jridgewell/trace-mapping'), require('@jridgewell/gen-mapping')) :
|
||||
typeof define === 'function' && define.amd ? define(['@jridgewell/trace-mapping', '@jridgewell/gen-mapping'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.remapping = factory(global.traceMapping, global.genMapping));
|
||||
})(this, (function (traceMapping, genMapping) { 'use strict';
|
||||
|
||||
const SOURCELESS_MAPPING = /* #__PURE__ */ SegmentObject('', -1, -1, '', null, false);
|
||||
const EMPTY_SOURCES = [];
|
||||
function SegmentObject(source, line, column, name, content, ignore) {
|
||||
return { source, line, column, name, content, ignore };
|
||||
}
|
||||
function Source(map, sources, source, content, ignore) {
|
||||
return {
|
||||
map,
|
||||
sources,
|
||||
source,
|
||||
content,
|
||||
ignore,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
|
||||
* (which may themselves be SourceMapTrees).
|
||||
*/
|
||||
function MapSource(map, sources) {
|
||||
return Source(map, sources, '', null, false);
|
||||
}
|
||||
/**
|
||||
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
|
||||
* segment tracing ends at the `OriginalSource`.
|
||||
*/
|
||||
function OriginalSource(source, content, ignore) {
|
||||
return Source(null, EMPTY_SOURCES, source, content, ignore);
|
||||
}
|
||||
/**
|
||||
* traceMappings is only called on the root level SourceMapTree, and begins the process of
|
||||
* resolving each mapping in terms of the original source files.
|
||||
*/
|
||||
function traceMappings(tree) {
|
||||
// TODO: Eventually support sourceRoot, which has to be removed because the sources are already
|
||||
// fully resolved. We'll need to make sources relative to the sourceRoot before adding them.
|
||||
const gen = new genMapping.GenMapping({ file: tree.map.file });
|
||||
const { sources: rootSources, map } = tree;
|
||||
const rootNames = map.names;
|
||||
const rootMappings = traceMapping.decodedMappings(map);
|
||||
for (let i = 0; i < rootMappings.length; i++) {
|
||||
const segments = rootMappings[i];
|
||||
for (let j = 0; j < segments.length; j++) {
|
||||
const segment = segments[j];
|
||||
const genCol = segment[0];
|
||||
let traced = SOURCELESS_MAPPING;
|
||||
// 1-length segments only move the current generated column, there's no source information
|
||||
// to gather from it.
|
||||
if (segment.length !== 1) {
|
||||
const source = rootSources[segment[1]];
|
||||
traced = originalPositionFor(source, segment[2], segment[3], segment.length === 5 ? rootNames[segment[4]] : '');
|
||||
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
|
||||
// respective segment into an original source.
|
||||
if (traced == null)
|
||||
continue;
|
||||
}
|
||||
const { column, line, name, content, source, ignore } = traced;
|
||||
genMapping.maybeAddSegment(gen, i, genCol, source, line, column, name);
|
||||
if (source && content != null)
|
||||
genMapping.setSourceContent(gen, source, content);
|
||||
if (ignore)
|
||||
genMapping.setIgnore(gen, source, true);
|
||||
}
|
||||
}
|
||||
return gen;
|
||||
}
|
||||
/**
|
||||
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
|
||||
* child SourceMapTrees, until we find the original source map.
|
||||
*/
|
||||
function originalPositionFor(source, line, column, name) {
|
||||
if (!source.map) {
|
||||
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
|
||||
}
|
||||
const segment = traceMapping.traceSegment(source.map, line, column);
|
||||
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
|
||||
if (segment == null)
|
||||
return null;
|
||||
// 1-length segments only move the current generated column, there's no source information
|
||||
// to gather from it.
|
||||
if (segment.length === 1)
|
||||
return SOURCELESS_MAPPING;
|
||||
return originalPositionFor(source.sources[segment[1]], segment[2], segment[3], segment.length === 5 ? source.map.names[segment[4]] : name);
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (Array.isArray(value))
|
||||
return value;
|
||||
return [value];
|
||||
}
|
||||
/**
|
||||
* Recursively builds a tree structure out of sourcemap files, with each node
|
||||
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
|
||||
* `OriginalSource`s and `SourceMapTree`s.
|
||||
*
|
||||
* Every sourcemap is composed of a collection of source files and mappings
|
||||
* into locations of those source files. When we generate a `SourceMapTree` for
|
||||
* the sourcemap, we attempt to load each source file's own sourcemap. If it
|
||||
* does not have an associated sourcemap, it is considered an original,
|
||||
* unmodified source file.
|
||||
*/
|
||||
function buildSourceMapTree(input, loader) {
|
||||
const maps = asArray(input).map((m) => new traceMapping.TraceMap(m, ''));
|
||||
const map = maps.pop();
|
||||
for (let i = 0; i < maps.length; i++) {
|
||||
if (maps[i].sources.length > 1) {
|
||||
throw new Error(`Transformation map ${i} must have exactly one source file.\n` +
|
||||
'Did you specify these with the most recent transformation maps first?');
|
||||
}
|
||||
}
|
||||
let tree = build(map, loader, '', 0);
|
||||
for (let i = maps.length - 1; i >= 0; i--) {
|
||||
tree = MapSource(maps[i], [tree]);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
function build(map, loader, importer, importerDepth) {
|
||||
const { resolvedSources, sourcesContent, ignoreList } = map;
|
||||
const depth = importerDepth + 1;
|
||||
const children = resolvedSources.map((sourceFile, i) => {
|
||||
// The loading context gives the loader more information about why this file is being loaded
|
||||
// (eg, from which importer). It also allows the loader to override the location of the loaded
|
||||
// sourcemap/original source, or to override the content in the sourcesContent field if it's
|
||||
// an unmodified source file.
|
||||
const ctx = {
|
||||
importer,
|
||||
depth,
|
||||
source: sourceFile || '',
|
||||
content: undefined,
|
||||
ignore: undefined,
|
||||
};
|
||||
// Use the provided loader callback to retrieve the file's sourcemap.
|
||||
// TODO: We should eventually support async loading of sourcemap files.
|
||||
const sourceMap = loader(ctx.source, ctx);
|
||||
const { source, content, ignore } = ctx;
|
||||
// If there is a sourcemap, then we need to recurse into it to load its source files.
|
||||
if (sourceMap)
|
||||
return build(new traceMapping.TraceMap(sourceMap, source), loader, source, depth);
|
||||
// Else, it's an unmodified source file.
|
||||
// The contents of this unmodified source file can be overridden via the loader context,
|
||||
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
|
||||
// the importing sourcemap's `sourcesContent` field.
|
||||
const sourceContent = content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
|
||||
const ignored = ignore !== undefined ? ignore : ignoreList ? ignoreList.includes(i) : false;
|
||||
return OriginalSource(source, sourceContent, ignored);
|
||||
});
|
||||
return MapSource(map, children);
|
||||
}
|
||||
|
||||
/**
|
||||
* A SourceMap v3 compatible sourcemap, which only includes fields that were
|
||||
* provided to it.
|
||||
*/
|
||||
class SourceMap {
|
||||
constructor(map, options) {
|
||||
const out = options.decodedMappings ? genMapping.toDecodedMap(map) : genMapping.toEncodedMap(map);
|
||||
this.version = out.version; // SourceMap spec says this should be first.
|
||||
this.file = out.file;
|
||||
this.mappings = out.mappings;
|
||||
this.names = out.names;
|
||||
this.ignoreList = out.ignoreList;
|
||||
this.sourceRoot = out.sourceRoot;
|
||||
this.sources = out.sources;
|
||||
if (!options.excludeContent) {
|
||||
this.sourcesContent = out.sourcesContent;
|
||||
}
|
||||
}
|
||||
toString() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traces through all the mappings in the root sourcemap, through the sources
|
||||
* (and their sourcemaps), all the way back to the original source location.
|
||||
*
|
||||
* `loader` will be called every time we encounter a source file. If it returns
|
||||
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
|
||||
* it returns a falsey value, that source file is treated as an original,
|
||||
* unmodified source file.
|
||||
*
|
||||
* Pass `excludeContent` to exclude any self-containing source file content
|
||||
* from the output sourcemap.
|
||||
*
|
||||
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
|
||||
* VLQ encoded) mappings.
|
||||
*/
|
||||
function remapping(input, loader, options) {
|
||||
const opts = typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
|
||||
const tree = buildSourceMapTree(input, loader);
|
||||
return new SourceMap(traceMappings(tree), opts);
|
||||
}
|
||||
|
||||
return remapping;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=remapping.umd.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import type { MapSource as MapSourceType } from './source-map-tree';
|
||||
import type { SourceMapInput, SourceMapLoader } from './types';
|
||||
/**
|
||||
* Recursively builds a tree structure out of sourcemap files, with each node
|
||||
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
|
||||
* `OriginalSource`s and `SourceMapTree`s.
|
||||
*
|
||||
* Every sourcemap is composed of a collection of source files and mappings
|
||||
* into locations of those source files. When we generate a `SourceMapTree` for
|
||||
* the sourcemap, we attempt to load each source file's own sourcemap. If it
|
||||
* does not have an associated sourcemap, it is considered an original,
|
||||
* unmodified source file.
|
||||
*/
|
||||
export default function buildSourceMapTree(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader): MapSourceType;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import SourceMap from './source-map';
|
||||
import type { SourceMapInput, SourceMapLoader, Options } from './types';
|
||||
export type { SourceMapSegment, EncodedSourceMap, EncodedSourceMap as RawSourceMap, DecodedSourceMap, SourceMapInput, SourceMapLoader, LoaderContext, Options, } from './types';
|
||||
export type { SourceMap };
|
||||
/**
|
||||
* Traces through all the mappings in the root sourcemap, through the sources
|
||||
* (and their sourcemaps), all the way back to the original source location.
|
||||
*
|
||||
* `loader` will be called every time we encounter a source file. If it returns
|
||||
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
|
||||
* it returns a falsey value, that source file is treated as an original,
|
||||
* unmodified source file.
|
||||
*
|
||||
* Pass `excludeContent` to exclude any self-containing source file content
|
||||
* from the output sourcemap.
|
||||
*
|
||||
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
|
||||
* VLQ encoded) mappings.
|
||||
*/
|
||||
export default function remapping(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader, options?: boolean | Options): SourceMap;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { GenMapping } from '@jridgewell/gen-mapping';
|
||||
import type { TraceMap } from '@jridgewell/trace-mapping';
|
||||
export declare type SourceMapSegmentObject = {
|
||||
column: number;
|
||||
line: number;
|
||||
name: string;
|
||||
source: string;
|
||||
content: string | null;
|
||||
ignore: boolean;
|
||||
};
|
||||
export declare type OriginalSource = {
|
||||
map: null;
|
||||
sources: Sources[];
|
||||
source: string;
|
||||
content: string | null;
|
||||
ignore: boolean;
|
||||
};
|
||||
export declare type MapSource = {
|
||||
map: TraceMap;
|
||||
sources: Sources[];
|
||||
source: string;
|
||||
content: null;
|
||||
ignore: false;
|
||||
};
|
||||
export declare type Sources = OriginalSource | MapSource;
|
||||
/**
|
||||
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
|
||||
* (which may themselves be SourceMapTrees).
|
||||
*/
|
||||
export declare function MapSource(map: TraceMap, sources: Sources[]): MapSource;
|
||||
/**
|
||||
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
|
||||
* segment tracing ends at the `OriginalSource`.
|
||||
*/
|
||||
export declare function OriginalSource(source: string, content: string | null, ignore: boolean): OriginalSource;
|
||||
/**
|
||||
* traceMappings is only called on the root level SourceMapTree, and begins the process of
|
||||
* resolving each mapping in terms of the original source files.
|
||||
*/
|
||||
export declare function traceMappings(tree: MapSource): GenMapping;
|
||||
/**
|
||||
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
|
||||
* child SourceMapTrees, until we find the original source map.
|
||||
*/
|
||||
export declare function originalPositionFor(source: Sources, line: number, column: number, name: string): SourceMapSegmentObject | null;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import type { GenMapping } from '@jridgewell/gen-mapping';
|
||||
import type { DecodedSourceMap, EncodedSourceMap, Options } from './types';
|
||||
/**
|
||||
* A SourceMap v3 compatible sourcemap, which only includes fields that were
|
||||
* provided to it.
|
||||
*/
|
||||
export default class SourceMap {
|
||||
file?: string | null;
|
||||
mappings: EncodedSourceMap['mappings'] | DecodedSourceMap['mappings'];
|
||||
sourceRoot?: string;
|
||||
names: string[];
|
||||
sources: (string | null)[];
|
||||
sourcesContent?: (string | null)[];
|
||||
version: 3;
|
||||
ignoreList: number[] | undefined;
|
||||
constructor(map: GenMapping, options: Options);
|
||||
toString(): string;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { SourceMapInput } from '@jridgewell/trace-mapping';
|
||||
export type { SourceMapSegment, DecodedSourceMap, EncodedSourceMap, } from '@jridgewell/trace-mapping';
|
||||
export type { SourceMapInput };
|
||||
export declare type LoaderContext = {
|
||||
readonly importer: string;
|
||||
readonly depth: number;
|
||||
source: string;
|
||||
content: string | null | undefined;
|
||||
ignore: boolean | undefined;
|
||||
};
|
||||
export declare type SourceMapLoader = (file: string, ctx: LoaderContext) => SourceMapInput | null | undefined | void;
|
||||
export declare type Options = {
|
||||
excludeContent?: boolean;
|
||||
decodedMappings?: boolean;
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@ampproject/remapping",
|
||||
"version": "2.3.0",
|
||||
"description": "Remap sequential sourcemaps through transformations to point at the original source code",
|
||||
"keywords": [
|
||||
"source",
|
||||
"map",
|
||||
"remap"
|
||||
],
|
||||
"main": "dist/remapping.umd.js",
|
||||
"module": "dist/remapping.mjs",
|
||||
"types": "dist/types/remapping.d.ts",
|
||||
"exports": {
|
||||
".": [
|
||||
{
|
||||
"types": "./dist/types/remapping.d.ts",
|
||||
"browser": "./dist/remapping.umd.js",
|
||||
"require": "./dist/remapping.umd.js",
|
||||
"import": "./dist/remapping.mjs"
|
||||
},
|
||||
"./dist/remapping.umd.js"
|
||||
],
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "Justin Ridgewell <jridgewell@google.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ampproject/remapping.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s -n build:*",
|
||||
"build:rollup": "rollup -c rollup.config.js",
|
||||
"build:ts": "tsc --project tsconfig.build.json",
|
||||
"lint": "run-s -n lint:*",
|
||||
"lint:prettier": "npm run test:lint:prettier -- --write",
|
||||
"lint:ts": "npm run test:lint:ts -- --fix",
|
||||
"prebuild": "rm -rf dist",
|
||||
"prepublishOnly": "npm run preversion",
|
||||
"preversion": "run-s test build",
|
||||
"test": "run-s -n test:lint test:only",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
|
||||
"test:lint": "run-s -n test:lint:*",
|
||||
"test:lint:prettier": "prettier --check '{src,test}/**/*.ts'",
|
||||
"test:lint:ts": "eslint '{src,test}/**/*.ts'",
|
||||
"test:only": "jest --coverage",
|
||||
"test:watch": "jest --coverage --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "8.3.2",
|
||||
"@types/jest": "27.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.20.0",
|
||||
"@typescript-eslint/parser": "5.20.0",
|
||||
"eslint": "8.14.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"jest": "27.5.1",
|
||||
"jest-config": "27.5.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.6.2",
|
||||
"rollup": "2.70.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# esbuild
|
||||
|
||||
This is the Windows 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
BIN
Binary file not shown.
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@esbuild/win32-x64",
|
||||
"version": "0.18.20",
|
||||
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"repository": "https://github.com/evanw/esbuild",
|
||||
"license": "MIT",
|
||||
"preferUnplugged": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
# busboy
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/fastify/busboy/actions)
|
||||
[](https://coveralls.io/r/fastify/busboy?branch=master)
|
||||
[](https://standardjs.com/)
|
||||
[](https://github.com/fastify/.github/blob/main/SECURITY.md)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/@fastify/busboy)
|
||||
[](https://www.npmjs.com/package/@fastify/busboy)
|
||||
|
||||
</div>
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
A Node.js module for parsing incoming HTML form data.
|
||||
|
||||
This is an officially supported fork by [fastify](https://github.com/fastify/) organization of the amazing library [originally created](https://github.com/mscdex/busboy) by Brian White,
|
||||
aimed at addressing long-standing issues with it.
|
||||
|
||||
Benchmark (Mean time for 500 Kb payload, 2000 cycles, 1000 cycle warmup):
|
||||
|
||||
| Library | Version | Mean time in nanoseconds (less is better) |
|
||||
|-----------------------|---------|-------------------------------------------|
|
||||
| busboy | 0.3.1 | `340114` |
|
||||
| @fastify/busboy | 1.0.0 | `270984` |
|
||||
|
||||
[Changelog](https://github.com/fastify/busboy/blob/master/CHANGELOG.md) since busboy 0.31.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
* [Node.js](http://nodejs.org/) 10+
|
||||
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
npm i @fastify/busboy
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
* Parsing (multipart) with default options:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const { inspect } = require('node:util');
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
||||
console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`);
|
||||
file.on('data', data => {
|
||||
console.log(`File [${fieldname}] got ${data.length} bytes`);
|
||||
});
|
||||
file.on('end', () => {
|
||||
console.log(`File [${fieldname}] Finished`);
|
||||
});
|
||||
});
|
||||
busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
|
||||
console.log(`Field [${fieldname}]: value: ${inspect(val)}`);
|
||||
});
|
||||
busboy.on('finish', () => {
|
||||
console.log('Done parsing form!');
|
||||
res.writeHead(303, { Connection: 'close', Location: '/' });
|
||||
res.end();
|
||||
});
|
||||
req.pipe(busboy);
|
||||
} else if (req.method === 'GET') {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end(`<html><head></head><body>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="textfield"><br>
|
||||
<input type="file" name="filefield"><br>
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body></html>`);
|
||||
}
|
||||
}).listen(8000, () => {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
|
||||
// Example output, using http://nodejs.org/images/ryan-speaker.jpg as the file:
|
||||
//
|
||||
// Listening for requests
|
||||
// File [filefield]: filename: ryan-speaker.jpg, encoding: binary
|
||||
// File [filefield] got 11971 bytes
|
||||
// Field [textfield]: value: 'testing! :-)'
|
||||
// File [filefield] Finished
|
||||
// Done parsing form!
|
||||
```
|
||||
|
||||
* Save all incoming files to disk:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||
var saveTo = path.join(os.tmpdir(), path.basename(fieldname));
|
||||
file.pipe(fs.createWriteStream(saveTo));
|
||||
});
|
||||
busboy.on('finish', function() {
|
||||
res.writeHead(200, { 'Connection': 'close' });
|
||||
res.end("That's all folks!");
|
||||
});
|
||||
return req.pipe(busboy);
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}).listen(8000, function() {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
```
|
||||
|
||||
* Parsing (urlencoded) with default options:
|
||||
|
||||
```javascript
|
||||
const http = require('node:http');
|
||||
const { inspect } = require('node:util');
|
||||
|
||||
const Busboy = require('busboy');
|
||||
|
||||
http.createServer(function(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||
console.log('File [' + fieldname + ']: filename: ' + filename);
|
||||
file.on('data', function(data) {
|
||||
console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
|
||||
});
|
||||
file.on('end', function() {
|
||||
console.log('File [' + fieldname + '] Finished');
|
||||
});
|
||||
});
|
||||
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
|
||||
console.log('Field [' + fieldname + ']: value: ' + inspect(val));
|
||||
});
|
||||
busboy.on('finish', function() {
|
||||
console.log('Done parsing form!');
|
||||
res.writeHead(303, { Connection: 'close', Location: '/' });
|
||||
res.end();
|
||||
});
|
||||
req.pipe(busboy);
|
||||
} else if (req.method === 'GET') {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('<html><head></head><body>\
|
||||
<form method="POST">\
|
||||
<input type="text" name="textfield"><br />\
|
||||
<select name="selectfield">\
|
||||
<option value="1">1</option>\
|
||||
<option value="10">10</option>\
|
||||
<option value="100">100</option>\
|
||||
<option value="9001">9001</option>\
|
||||
</select><br />\
|
||||
<input type="checkbox" name="checkfield">Node.js rules!<br />\
|
||||
<input type="submit">\
|
||||
</form>\
|
||||
</body></html>');
|
||||
}
|
||||
}).listen(8000, function() {
|
||||
console.log('Listening for requests');
|
||||
});
|
||||
|
||||
// Example output:
|
||||
//
|
||||
// Listening for requests
|
||||
// Field [textfield]: value: 'testing! :-)'
|
||||
// Field [selectfield]: value: '9001'
|
||||
// Field [checkfield]: value: 'on'
|
||||
// Done parsing form!
|
||||
```
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
_Busboy_ is a _Writable_ stream
|
||||
|
||||
Busboy (special) events
|
||||
-----------------------
|
||||
|
||||
* **file**(< _string_ >fieldname, < _ReadableStream_ >stream, < _string_ >filename, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new file form field found. `transferEncoding` contains the 'Content-Transfer-Encoding' value for the file stream. `mimeType` contains the 'Content-Type' value for the file stream.
|
||||
* Note: if you listen for this event, you should always handle the `stream` no matter if you care about the file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically and safely discarded (these discarded files do still count towards `files` and `parts` limits).
|
||||
* If a configured file size limit was reached, `stream` will both have a boolean property `truncated` (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
|
||||
* The property `bytesRead` informs about the number of bytes that have been read so far.
|
||||
|
||||
* **field**(< _string_ >fieldname, < _string_ >value, < _boolean_ >fieldnameTruncated, < _boolean_ >valueTruncated, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new non-file field found.
|
||||
|
||||
* **partsLimit**() - Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
|
||||
|
||||
* **filesLimit**() - Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
|
||||
|
||||
* **fieldsLimit**() - Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
|
||||
|
||||
|
||||
Busboy methods
|
||||
--------------
|
||||
|
||||
* **(constructor)**(< _object_ >config) - Creates and returns a new Busboy instance.
|
||||
|
||||
* The constructor takes the following valid `config` settings:
|
||||
|
||||
* **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers.
|
||||
|
||||
* **autoDestroy** - _boolean_ - Whether this stream should automatically call .destroy() on itself after ending. (Default: false).
|
||||
|
||||
* **highWaterMark** - _integer_ - highWaterMark to use for this Busboy instance (Default: WritableStream default).
|
||||
|
||||
* **fileHwm** - _integer_ - highWaterMark to use for file streams (Default: ReadableStream default).
|
||||
|
||||
* **defCharset** - _string_ - Default character set to use when one isn't defined (Default: 'utf8').
|
||||
|
||||
* **preservePath** - _boolean_ - If paths in the multipart 'filename' field shall be preserved. (Default: false).
|
||||
|
||||
* **isPartAFile** - __function__ - Use this function to override the default file detection functionality. It has following parameters:
|
||||
|
||||
* fieldName - __string__ The name of the field.
|
||||
|
||||
* contentType - __string__ The content-type of the part, e.g. `text/plain`, `image/jpeg`, `application/octet-stream`
|
||||
|
||||
* fileName - __string__ The name of a file supplied by the part.
|
||||
|
||||
(Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`)
|
||||
|
||||
* **limits** - _object_ - Various limits on incoming data. Valid properties are:
|
||||
|
||||
* **fieldNameSize** - _integer_ - Max field name size (in bytes) (Default: 100 bytes).
|
||||
|
||||
* **fieldSize** - _integer_ - Max field value size (in bytes) (Default: 1 MiB, which is 1024 x 1024 bytes).
|
||||
|
||||
* **fields** - _integer_ - Max number of non-file fields (Default: Infinity).
|
||||
|
||||
* **fileSize** - _integer_ - For multipart forms, the max file size (in bytes) (Default: Infinity).
|
||||
|
||||
* **files** - _integer_ - For multipart forms, the max number of file fields (Default: Infinity).
|
||||
|
||||
* **parts** - _integer_ - For multipart forms, the max number of parts (fields + files) (Default: Infinity).
|
||||
|
||||
* **headerPairs** - _integer_ - For multipart forms, the max number of header key=>value pairs to parse **Default:** 2000
|
||||
|
||||
* **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920.
|
||||
|
||||
* The constructor can throw errors:
|
||||
|
||||
* **Busboy expected an options-Object.** - Busboy expected an Object as first parameters.
|
||||
|
||||
* **Busboy expected an options-Object with headers-attribute.** - The first parameter is lacking of a headers-attribute.
|
||||
|
||||
* **Limit $limit is not a valid number** - Busboy expected the desired limit to be of type number. Busboy throws this Error to prevent a potential security issue by falling silently back to the Busboy-defaults. Potential source for this Error can be the direct use of environment variables without transforming them to the type number.
|
||||
|
||||
* **Unsupported Content-Type.** - The `Content-Type` isn't one Busboy can parse.
|
||||
|
||||
* **Missing Content-Type-header.** - The provided headers don't include `Content-Type` at all.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
'use strict'
|
||||
|
||||
const WritableStream = require('node:stream').Writable
|
||||
const inherits = require('node:util').inherits
|
||||
|
||||
const StreamSearch = require('../../streamsearch/sbmh')
|
||||
|
||||
const PartStream = require('./PartStream')
|
||||
const HeaderParser = require('./HeaderParser')
|
||||
|
||||
const DASH = 45
|
||||
const B_ONEDASH = Buffer.from('-')
|
||||
const B_CRLF = Buffer.from('\r\n')
|
||||
const EMPTY_FN = function () {}
|
||||
|
||||
function Dicer (cfg) {
|
||||
if (!(this instanceof Dicer)) { return new Dicer(cfg) }
|
||||
WritableStream.call(this, cfg)
|
||||
|
||||
if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') }
|
||||
|
||||
if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined }
|
||||
|
||||
this._headerFirst = cfg.headerFirst
|
||||
|
||||
this._dashes = 0
|
||||
this._parts = 0
|
||||
this._finished = false
|
||||
this._realFinish = false
|
||||
this._isPreamble = true
|
||||
this._justMatched = false
|
||||
this._firstWrite = true
|
||||
this._inHeader = true
|
||||
this._part = undefined
|
||||
this._cb = undefined
|
||||
this._ignoreData = false
|
||||
this._partOpts = { highWaterMark: cfg.partHwm }
|
||||
this._pause = false
|
||||
|
||||
const self = this
|
||||
this._hparser = new HeaderParser(cfg)
|
||||
this._hparser.on('header', function (header) {
|
||||
self._inHeader = false
|
||||
self._part.emit('header', header)
|
||||
})
|
||||
}
|
||||
inherits(Dicer, WritableStream)
|
||||
|
||||
Dicer.prototype.emit = function (ev) {
|
||||
if (ev === 'finish' && !this._realFinish) {
|
||||
if (!this._finished) {
|
||||
const self = this
|
||||
process.nextTick(function () {
|
||||
self.emit('error', new Error('Unexpected end of multipart data'))
|
||||
if (self._part && !self._ignoreData) {
|
||||
const type = (self._isPreamble ? 'Preamble' : 'Part')
|
||||
self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data'))
|
||||
self._part.push(null)
|
||||
process.nextTick(function () {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
})
|
||||
return
|
||||
}
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
})
|
||||
}
|
||||
} else { WritableStream.prototype.emit.apply(this, arguments) }
|
||||
}
|
||||
|
||||
Dicer.prototype._write = function (data, encoding, cb) {
|
||||
// ignore unexpected data (e.g. extra trailer data after finished)
|
||||
if (!this._hparser && !this._bparser) { return cb() }
|
||||
|
||||
if (this._headerFirst && this._isPreamble) {
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts)
|
||||
if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() }
|
||||
}
|
||||
const r = this._hparser.push(data)
|
||||
if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() }
|
||||
}
|
||||
|
||||
// allows for "easier" testing
|
||||
if (this._firstWrite) {
|
||||
this._bparser.push(B_CRLF)
|
||||
this._firstWrite = false
|
||||
}
|
||||
|
||||
this._bparser.push(data)
|
||||
|
||||
if (this._pause) { this._cb = cb } else { cb() }
|
||||
}
|
||||
|
||||
Dicer.prototype.reset = function () {
|
||||
this._part = undefined
|
||||
this._bparser = undefined
|
||||
this._hparser = undefined
|
||||
}
|
||||
|
||||
Dicer.prototype.setBoundary = function (boundary) {
|
||||
const self = this
|
||||
this._bparser = new StreamSearch('\r\n--' + boundary)
|
||||
this._bparser.on('info', function (isMatch, data, start, end) {
|
||||
self._oninfo(isMatch, data, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
Dicer.prototype._ignore = function () {
|
||||
if (this._part && !this._ignoreData) {
|
||||
this._ignoreData = true
|
||||
this._part.on('error', EMPTY_FN)
|
||||
// we must perform some kind of read on the stream even though we are
|
||||
// ignoring the data, otherwise node's Readable stream will not emit 'end'
|
||||
// after pushing null to the stream
|
||||
this._part.resume()
|
||||
}
|
||||
}
|
||||
|
||||
Dicer.prototype._oninfo = function (isMatch, data, start, end) {
|
||||
let buf; const self = this; let i = 0; let r; let shouldWriteMore = true
|
||||
|
||||
if (!this._part && this._justMatched && data) {
|
||||
while (this._dashes < 2 && (start + i) < end) {
|
||||
if (data[start + i] === DASH) {
|
||||
++i
|
||||
++this._dashes
|
||||
} else {
|
||||
if (this._dashes) { buf = B_ONEDASH }
|
||||
this._dashes = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this._dashes === 2) {
|
||||
if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) }
|
||||
this.reset()
|
||||
this._finished = true
|
||||
// no more parts will be added
|
||||
if (self._parts === 0) {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
}
|
||||
}
|
||||
if (this._dashes) { return }
|
||||
}
|
||||
if (this._justMatched) { this._justMatched = false }
|
||||
if (!this._part) {
|
||||
this._part = new PartStream(this._partOpts)
|
||||
this._part._read = function (n) {
|
||||
self._unpause()
|
||||
}
|
||||
if (this._isPreamble && this.listenerCount('preamble') !== 0) {
|
||||
this.emit('preamble', this._part)
|
||||
} else if (this._isPreamble !== true && this.listenerCount('part') !== 0) {
|
||||
this.emit('part', this._part)
|
||||
} else {
|
||||
this._ignore()
|
||||
}
|
||||
if (!this._isPreamble) { this._inHeader = true }
|
||||
}
|
||||
if (data && start < end && !this._ignoreData) {
|
||||
if (this._isPreamble || !this._inHeader) {
|
||||
if (buf) { shouldWriteMore = this._part.push(buf) }
|
||||
shouldWriteMore = this._part.push(data.slice(start, end))
|
||||
if (!shouldWriteMore) { this._pause = true }
|
||||
} else if (!this._isPreamble && this._inHeader) {
|
||||
if (buf) { this._hparser.push(buf) }
|
||||
r = this._hparser.push(data.slice(start, end))
|
||||
if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) }
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
this._hparser.reset()
|
||||
if (this._isPreamble) { this._isPreamble = false } else {
|
||||
if (start !== end) {
|
||||
++this._parts
|
||||
this._part.on('end', function () {
|
||||
if (--self._parts === 0) {
|
||||
if (self._finished) {
|
||||
self._realFinish = true
|
||||
self.emit('finish')
|
||||
self._realFinish = false
|
||||
} else {
|
||||
self._unpause()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this._part.push(null)
|
||||
this._part = undefined
|
||||
this._ignoreData = false
|
||||
this._justMatched = true
|
||||
this._dashes = 0
|
||||
}
|
||||
}
|
||||
|
||||
Dicer.prototype._unpause = function () {
|
||||
if (!this._pause) { return }
|
||||
|
||||
this._pause = false
|
||||
if (this._cb) {
|
||||
const cb = this._cb
|
||||
this._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dicer
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
'use strict'
|
||||
|
||||
const EventEmitter = require('node:events').EventEmitter
|
||||
const inherits = require('node:util').inherits
|
||||
const getLimit = require('../../../lib/utils/getLimit')
|
||||
|
||||
const StreamSearch = require('../../streamsearch/sbmh')
|
||||
|
||||
const B_DCRLF = Buffer.from('\r\n\r\n')
|
||||
const RE_CRLF = /\r\n/g
|
||||
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex
|
||||
|
||||
function HeaderParser (cfg) {
|
||||
EventEmitter.call(this)
|
||||
|
||||
cfg = cfg || {}
|
||||
const self = this
|
||||
this.nread = 0
|
||||
this.maxed = false
|
||||
this.npairs = 0
|
||||
this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000)
|
||||
this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024)
|
||||
this.buffer = ''
|
||||
this.header = {}
|
||||
this.finished = false
|
||||
this.ss = new StreamSearch(B_DCRLF)
|
||||
this.ss.on('info', function (isMatch, data, start, end) {
|
||||
if (data && !self.maxed) {
|
||||
if (self.nread + end - start >= self.maxHeaderSize) {
|
||||
end = self.maxHeaderSize - self.nread + start
|
||||
self.nread = self.maxHeaderSize
|
||||
self.maxed = true
|
||||
} else { self.nread += (end - start) }
|
||||
|
||||
self.buffer += data.toString('binary', start, end)
|
||||
}
|
||||
if (isMatch) { self._finish() }
|
||||
})
|
||||
}
|
||||
inherits(HeaderParser, EventEmitter)
|
||||
|
||||
HeaderParser.prototype.push = function (data) {
|
||||
const r = this.ss.push(data)
|
||||
if (this.finished) { return r }
|
||||
}
|
||||
|
||||
HeaderParser.prototype.reset = function () {
|
||||
this.finished = false
|
||||
this.buffer = ''
|
||||
this.header = {}
|
||||
this.ss.reset()
|
||||
}
|
||||
|
||||
HeaderParser.prototype._finish = function () {
|
||||
if (this.buffer) { this._parseHeader() }
|
||||
this.ss.matches = this.ss.maxMatches
|
||||
const header = this.header
|
||||
this.header = {}
|
||||
this.buffer = ''
|
||||
this.finished = true
|
||||
this.nread = this.npairs = 0
|
||||
this.maxed = false
|
||||
this.emit('header', header)
|
||||
}
|
||||
|
||||
HeaderParser.prototype._parseHeader = function () {
|
||||
if (this.npairs === this.maxHeaderPairs) { return }
|
||||
|
||||
const lines = this.buffer.split(RE_CRLF)
|
||||
const len = lines.length
|
||||
let m, h
|
||||
|
||||
for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (lines[i].length === 0) { continue }
|
||||
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
|
||||
// folded header content
|
||||
// RFC2822 says to just remove the CRLF and not the whitespace following
|
||||
// it, so we follow the RFC and include the leading whitespace ...
|
||||
if (h) {
|
||||
this.header[h][this.header[h].length - 1] += lines[i]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const posColon = lines[i].indexOf(':')
|
||||
if (
|
||||
posColon === -1 ||
|
||||
posColon === 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
m = RE_HDR.exec(lines[i])
|
||||
h = m[1].toLowerCase()
|
||||
this.header[h] = this.header[h] || []
|
||||
this.header[h].push((m[2] || ''))
|
||||
if (++this.npairs === this.maxHeaderPairs) { break }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HeaderParser
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const inherits = require('node:util').inherits
|
||||
const ReadableStream = require('node:stream').Readable
|
||||
|
||||
function PartStream (opts) {
|
||||
ReadableStream.call(this, opts)
|
||||
}
|
||||
inherits(PartStream, ReadableStream)
|
||||
|
||||
PartStream.prototype._read = function (n) {}
|
||||
|
||||
module.exports = PartStream
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Type definitions for dicer 0.2
|
||||
// Project: https://github.com/mscdex/dicer
|
||||
// Definitions by: BendingBender <https://github.com/BendingBender>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 2.2
|
||||
/// <reference types="node" />
|
||||
|
||||
import stream = require("stream");
|
||||
|
||||
// tslint:disable:unified-signatures
|
||||
|
||||
/**
|
||||
* A very fast streaming multipart parser for node.js.
|
||||
* Dicer is a WritableStream
|
||||
*
|
||||
* Dicer (special) events:
|
||||
* - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended.
|
||||
* - on('part', (stream: PartStream)) - Emitted when a new part has been found.
|
||||
* - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored).
|
||||
* - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too).
|
||||
*/
|
||||
export class Dicer extends stream.Writable {
|
||||
/**
|
||||
* Creates and returns a new Dicer instance with the following valid config settings:
|
||||
*
|
||||
* @param config The configuration to use
|
||||
*/
|
||||
constructor(config: Dicer.Config);
|
||||
/**
|
||||
* Sets the boundary to use for parsing and performs some initialization needed for parsing.
|
||||
* You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header.
|
||||
*
|
||||
* @param boundary The boundary to use
|
||||
*/
|
||||
setBoundary(boundary: string): void;
|
||||
addListener(event: "finish", listener: () => void): this;
|
||||
addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
addListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
addListener(event: "drain", listener: () => void): this;
|
||||
addListener(event: "error", listener: (err: Error) => void): this;
|
||||
addListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
addListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
on(event: "finish", listener: () => void): this;
|
||||
on(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
on(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
on(event: "drain", listener: () => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
on(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: "finish", listener: () => void): this;
|
||||
once(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
once(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
once(event: "drain", listener: () => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
once(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
prependListener(event: "finish", listener: () => void): this;
|
||||
prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
prependListener(event: "close", listener: () => void): this;
|
||||
prependListener(event: "drain", listener: () => void): this;
|
||||
prependListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
||||
prependOnceListener(event: "finish", listener: () => void): this;
|
||||
prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
prependOnceListener(event: "close", listener: () => void): this;
|
||||
prependOnceListener(event: "drain", listener: () => void): this;
|
||||
prependOnceListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "finish", listener: () => void): this;
|
||||
removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
|
||||
removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
|
||||
removeListener(event: "trailer", listener: (data: Buffer) => void): this;
|
||||
removeListener(event: "close", listener: () => void): this;
|
||||
removeListener(event: "drain", listener: () => void): this;
|
||||
removeListener(event: "error", listener: (err: Error) => void): this;
|
||||
removeListener(event: "pipe", listener: (src: stream.Readable) => void): this;
|
||||
removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
|
||||
declare namespace Dicer {
|
||||
interface Config {
|
||||
/**
|
||||
* This is the boundary used to detect the beginning of a new part.
|
||||
*/
|
||||
boundary?: string | undefined;
|
||||
/**
|
||||
* If true, preamble header parsing will be performed first.
|
||||
*/
|
||||
headerFirst?: boolean | undefined;
|
||||
/**
|
||||
* The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http).
|
||||
*/
|
||||
maxHeaderPairs?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* PartStream is a _ReadableStream_
|
||||
*
|
||||
* PartStream (special) events:
|
||||
* - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values.
|
||||
*/
|
||||
interface PartStream extends stream.Readable {
|
||||
addListener(event: "header", listener: (header: object) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
addListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
addListener(event: "end", listener: () => void): this;
|
||||
addListener(event: "readable", listener: () => void): this;
|
||||
addListener(event: "error", listener: (err: Error) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
on(event: "header", listener: (header: object) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
on(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
on(event: "end", listener: () => void): this;
|
||||
on(event: "readable", listener: () => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: "header", listener: (header: object) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
once(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
once(event: "end", listener: () => void): this;
|
||||
once(event: "readable", listener: () => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
prependListener(event: "header", listener: (header: object) => void): this;
|
||||
prependListener(event: "close", listener: () => void): this;
|
||||
prependListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
prependListener(event: "end", listener: () => void): this;
|
||||
prependListener(event: "readable", listener: () => void): this;
|
||||
prependListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
||||
prependOnceListener(event: "header", listener: (header: object) => void): this;
|
||||
prependOnceListener(event: "close", listener: () => void): this;
|
||||
prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
prependOnceListener(event: "end", listener: () => void): this;
|
||||
prependOnceListener(event: "readable", listener: () => void): this;
|
||||
prependOnceListener(event: "error", listener: (err: Error) => void): this;
|
||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "header", listener: (header: object) => void): this;
|
||||
removeListener(event: "close", listener: () => void): this;
|
||||
removeListener(event: "data", listener: (chunk: Buffer | string) => void): this;
|
||||
removeListener(event: "end", listener: () => void): this;
|
||||
removeListener(event: "readable", listener: () => void): this;
|
||||
removeListener(event: "error", listener: (err: Error) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Copyright Brian White. All rights reserved.
|
||||
*
|
||||
* @see https://github.com/mscdex/streamsearch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*
|
||||
* Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation
|
||||
* by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool
|
||||
*/
|
||||
const EventEmitter = require('node:events').EventEmitter
|
||||
const inherits = require('node:util').inherits
|
||||
|
||||
function SBMH (needle) {
|
||||
if (typeof needle === 'string') {
|
||||
needle = Buffer.from(needle)
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(needle)) {
|
||||
throw new TypeError('The needle has to be a String or a Buffer.')
|
||||
}
|
||||
|
||||
const needleLength = needle.length
|
||||
|
||||
if (needleLength === 0) {
|
||||
throw new Error('The needle cannot be an empty String/Buffer.')
|
||||
}
|
||||
|
||||
if (needleLength > 256) {
|
||||
throw new Error('The needle cannot have a length bigger than 256.')
|
||||
}
|
||||
|
||||
this.maxMatches = Infinity
|
||||
this.matches = 0
|
||||
|
||||
this._occ = new Array(256)
|
||||
.fill(needleLength) // Initialize occurrence table.
|
||||
this._lookbehind_size = 0
|
||||
this._needle = needle
|
||||
this._bufpos = 0
|
||||
|
||||
this._lookbehind = Buffer.alloc(needleLength)
|
||||
|
||||
// Populate occurrence table with analysis of the needle,
|
||||
// ignoring last letter.
|
||||
for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var
|
||||
this._occ[needle[i]] = needleLength - 1 - i
|
||||
}
|
||||
}
|
||||
inherits(SBMH, EventEmitter)
|
||||
|
||||
SBMH.prototype.reset = function () {
|
||||
this._lookbehind_size = 0
|
||||
this.matches = 0
|
||||
this._bufpos = 0
|
||||
}
|
||||
|
||||
SBMH.prototype.push = function (chunk, pos) {
|
||||
if (!Buffer.isBuffer(chunk)) {
|
||||
chunk = Buffer.from(chunk, 'binary')
|
||||
}
|
||||
const chlen = chunk.length
|
||||
this._bufpos = pos || 0
|
||||
let r
|
||||
while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) }
|
||||
return r
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_feed = function (data) {
|
||||
const len = data.length
|
||||
const needle = this._needle
|
||||
const needleLength = needle.length
|
||||
const lastNeedleChar = needle[needleLength - 1]
|
||||
|
||||
// Positive: points to a position in `data`
|
||||
// pos == 3 points to data[3]
|
||||
// Negative: points to a position in the lookbehind buffer
|
||||
// pos == -2 points to lookbehind[lookbehind_size - 2]
|
||||
let pos = -this._lookbehind_size
|
||||
let ch
|
||||
|
||||
if (pos < 0) {
|
||||
// Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool
|
||||
// search with character lookup code that considers both the
|
||||
// lookbehind buffer and the current round's haystack data.
|
||||
//
|
||||
// Loop until
|
||||
// there is a match.
|
||||
// or until
|
||||
// we've moved past the position that requires the
|
||||
// lookbehind buffer. In this case we switch to the
|
||||
// optimized loop.
|
||||
// or until
|
||||
// the character to look at lies outside the haystack.
|
||||
while (pos < 0 && pos <= len - needleLength) {
|
||||
ch = this._sbmh_lookup_char(data, pos + needleLength - 1)
|
||||
|
||||
if (
|
||||
ch === lastNeedleChar &&
|
||||
this._sbmh_memcmp(data, pos, needleLength - 1)
|
||||
) {
|
||||
this._lookbehind_size = 0
|
||||
++this.matches
|
||||
this.emit('info', true)
|
||||
|
||||
return (this._bufpos = pos + needleLength)
|
||||
}
|
||||
pos += this._occ[ch]
|
||||
}
|
||||
|
||||
// No match.
|
||||
|
||||
if (pos < 0) {
|
||||
// There's too few data for Boyer-Moore-Horspool to run,
|
||||
// so let's use a different algorithm to skip as much as
|
||||
// we can.
|
||||
// Forward pos until
|
||||
// the trailing part of lookbehind + data
|
||||
// looks like the beginning of the needle
|
||||
// or until
|
||||
// pos == 0
|
||||
while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos }
|
||||
}
|
||||
|
||||
if (pos >= 0) {
|
||||
// Discard lookbehind buffer.
|
||||
this.emit('info', false, this._lookbehind, 0, this._lookbehind_size)
|
||||
this._lookbehind_size = 0
|
||||
} else {
|
||||
// Cut off part of the lookbehind buffer that has
|
||||
// been processed and append the entire haystack
|
||||
// into it.
|
||||
const bytesToCutOff = this._lookbehind_size + pos
|
||||
if (bytesToCutOff > 0) {
|
||||
// The cut off data is guaranteed not to contain the needle.
|
||||
this.emit('info', false, this._lookbehind, 0, bytesToCutOff)
|
||||
}
|
||||
|
||||
this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff,
|
||||
this._lookbehind_size - bytesToCutOff)
|
||||
this._lookbehind_size -= bytesToCutOff
|
||||
|
||||
data.copy(this._lookbehind, this._lookbehind_size)
|
||||
this._lookbehind_size += len
|
||||
|
||||
this._bufpos = len
|
||||
return len
|
||||
}
|
||||
}
|
||||
|
||||
pos += (pos >= 0) * this._bufpos
|
||||
|
||||
// Lookbehind buffer is now empty. We only need to check if the
|
||||
// needle is in the haystack.
|
||||
if (data.indexOf(needle, pos) !== -1) {
|
||||
pos = data.indexOf(needle, pos)
|
||||
++this.matches
|
||||
if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) }
|
||||
|
||||
return (this._bufpos = pos + needleLength)
|
||||
} else {
|
||||
pos = len - needleLength
|
||||
}
|
||||
|
||||
// There was no match. If there's trailing haystack data that we cannot
|
||||
// match yet using the Boyer-Moore-Horspool algorithm (because the trailing
|
||||
// data is less than the needle size) then match using a modified
|
||||
// algorithm that starts matching from the beginning instead of the end.
|
||||
// Whatever trailing data is left after running this algorithm is added to
|
||||
// the lookbehind buffer.
|
||||
while (
|
||||
pos < len &&
|
||||
(
|
||||
data[pos] !== needle[0] ||
|
||||
(
|
||||
(Buffer.compare(
|
||||
data.subarray(pos, pos + len - pos),
|
||||
needle.subarray(0, len - pos)
|
||||
) !== 0)
|
||||
)
|
||||
)
|
||||
) {
|
||||
++pos
|
||||
}
|
||||
if (pos < len) {
|
||||
data.copy(this._lookbehind, 0, pos, pos + (len - pos))
|
||||
this._lookbehind_size = len - pos
|
||||
}
|
||||
|
||||
// Everything until pos is guaranteed not to contain needle data.
|
||||
if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) }
|
||||
|
||||
this._bufpos = len
|
||||
return len
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_lookup_char = function (data, pos) {
|
||||
return (pos < 0)
|
||||
? this._lookbehind[this._lookbehind_size + pos]
|
||||
: data[pos]
|
||||
}
|
||||
|
||||
SBMH.prototype._sbmh_memcmp = function (data, pos, len) {
|
||||
for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = SBMH
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// Definitions by: Jacob Baskin <https://github.com/jacobbaskin>
|
||||
// BendingBender <https://github.com/BendingBender>
|
||||
// Igor Savin <https://github.com/kibertoad>
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as http from 'http';
|
||||
import { Readable, Writable } from 'stream';
|
||||
export { Dicer } from "../deps/dicer/lib/dicer";
|
||||
|
||||
export const Busboy: BusboyConstructor;
|
||||
export default Busboy;
|
||||
|
||||
export interface BusboyConfig {
|
||||
/**
|
||||
* These are the HTTP headers of the incoming request, which are used by individual parsers.
|
||||
*/
|
||||
headers: BusboyHeaders;
|
||||
/**
|
||||
* `highWaterMark` to use for this Busboy instance.
|
||||
* @default WritableStream default.
|
||||
*/
|
||||
highWaterMark?: number | undefined;
|
||||
/**
|
||||
* highWaterMark to use for file streams.
|
||||
* @default ReadableStream default.
|
||||
*/
|
||||
fileHwm?: number | undefined;
|
||||
/**
|
||||
* Default character set to use when one isn't defined.
|
||||
* @default 'utf8'
|
||||
*/
|
||||
defCharset?: string | undefined;
|
||||
/**
|
||||
* Detect if a Part is a file.
|
||||
*
|
||||
* By default a file is detected if contentType
|
||||
* is application/octet-stream or fileName is not
|
||||
* undefined.
|
||||
*
|
||||
* Modify this to handle e.g. Blobs.
|
||||
*/
|
||||
isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean;
|
||||
/**
|
||||
* If paths in the multipart 'filename' field shall be preserved.
|
||||
* @default false
|
||||
*/
|
||||
preservePath?: boolean | undefined;
|
||||
/**
|
||||
* Various limits on incoming data.
|
||||
*/
|
||||
limits?:
|
||||
| {
|
||||
/**
|
||||
* Max field name size (in bytes)
|
||||
* @default 100 bytes
|
||||
*/
|
||||
fieldNameSize?: number | undefined;
|
||||
/**
|
||||
* Max field value size (in bytes)
|
||||
* @default 1MB
|
||||
*/
|
||||
fieldSize?: number | undefined;
|
||||
/**
|
||||
* Max number of non-file fields
|
||||
* @default Infinity
|
||||
*/
|
||||
fields?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max file size (in bytes)
|
||||
* @default Infinity
|
||||
*/
|
||||
fileSize?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of file fields
|
||||
* @default Infinity
|
||||
*/
|
||||
files?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of parts (fields + files)
|
||||
* @default Infinity
|
||||
*/
|
||||
parts?: number | undefined;
|
||||
/**
|
||||
* For multipart forms, the max number of header key=>value pairs to parse
|
||||
* @default 2000
|
||||
*/
|
||||
headerPairs?: number | undefined;
|
||||
|
||||
/**
|
||||
* For multipart forms, the max size of a header part
|
||||
* @default 81920
|
||||
*/
|
||||
headerSize?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders;
|
||||
|
||||
export interface BusboyFileStream extends
|
||||
Readable {
|
||||
|
||||
truncated: boolean;
|
||||
|
||||
/**
|
||||
* The number of bytes that have been read so far.
|
||||
*/
|
||||
bytesRead: number;
|
||||
}
|
||||
|
||||
export interface Busboy extends Writable {
|
||||
addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
once(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
off(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
|
||||
prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
|
||||
|
||||
prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
|
||||
export interface BusboyEvents {
|
||||
/**
|
||||
* Emitted for each new file form field found.
|
||||
*
|
||||
* * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the
|
||||
* file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents),
|
||||
* otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any**
|
||||
* incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically
|
||||
* and safely discarded (these discarded files do still count towards `files` and `parts` limits).
|
||||
* * If a configured file size limit was reached, `stream` will both have a boolean property `truncated`
|
||||
* (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
|
||||
*
|
||||
* @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream.
|
||||
* @param listener.mimeType Contains the 'Content-Type' value for the file stream.
|
||||
*/
|
||||
file: (
|
||||
fieldname: string,
|
||||
stream: BusboyFileStream,
|
||||
filename: string,
|
||||
transferEncoding: string,
|
||||
mimeType: string,
|
||||
) => void;
|
||||
/**
|
||||
* Emitted for each new non-file field found.
|
||||
*/
|
||||
field: (
|
||||
fieldname: string,
|
||||
value: string,
|
||||
fieldnameTruncated: boolean,
|
||||
valueTruncated: boolean,
|
||||
transferEncoding: string,
|
||||
mimeType: string,
|
||||
) => void;
|
||||
finish: () => void;
|
||||
/**
|
||||
* Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
|
||||
*/
|
||||
partsLimit: () => void;
|
||||
/**
|
||||
* Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
|
||||
*/
|
||||
filesLimit: () => void;
|
||||
/**
|
||||
* Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
|
||||
*/
|
||||
fieldsLimit: () => void;
|
||||
error: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface BusboyConstructor {
|
||||
(options: BusboyConfig): Busboy;
|
||||
|
||||
new(options: BusboyConfig): Busboy;
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
'use strict'
|
||||
|
||||
const WritableStream = require('node:stream').Writable
|
||||
const { inherits } = require('node:util')
|
||||
const Dicer = require('../deps/dicer/lib/Dicer')
|
||||
|
||||
const MultipartParser = require('./types/multipart')
|
||||
const UrlencodedParser = require('./types/urlencoded')
|
||||
const parseParams = require('./utils/parseParams')
|
||||
|
||||
function Busboy (opts) {
|
||||
if (!(this instanceof Busboy)) { return new Busboy(opts) }
|
||||
|
||||
if (typeof opts !== 'object') {
|
||||
throw new TypeError('Busboy expected an options-Object.')
|
||||
}
|
||||
if (typeof opts.headers !== 'object') {
|
||||
throw new TypeError('Busboy expected an options-Object with headers-attribute.')
|
||||
}
|
||||
if (typeof opts.headers['content-type'] !== 'string') {
|
||||
throw new TypeError('Missing Content-Type-header.')
|
||||
}
|
||||
|
||||
const {
|
||||
headers,
|
||||
...streamOptions
|
||||
} = opts
|
||||
|
||||
this.opts = {
|
||||
autoDestroy: false,
|
||||
...streamOptions
|
||||
}
|
||||
WritableStream.call(this, this.opts)
|
||||
|
||||
this._done = false
|
||||
this._parser = this.getParserByHeaders(headers)
|
||||
this._finished = false
|
||||
}
|
||||
inherits(Busboy, WritableStream)
|
||||
|
||||
Busboy.prototype.emit = function (ev) {
|
||||
if (ev === 'finish') {
|
||||
if (!this._done) {
|
||||
this._parser?.end()
|
||||
return
|
||||
} else if (this._finished) {
|
||||
return
|
||||
}
|
||||
this._finished = true
|
||||
}
|
||||
WritableStream.prototype.emit.apply(this, arguments)
|
||||
}
|
||||
|
||||
Busboy.prototype.getParserByHeaders = function (headers) {
|
||||
const parsed = parseParams(headers['content-type'])
|
||||
|
||||
const cfg = {
|
||||
defCharset: this.opts.defCharset,
|
||||
fileHwm: this.opts.fileHwm,
|
||||
headers,
|
||||
highWaterMark: this.opts.highWaterMark,
|
||||
isPartAFile: this.opts.isPartAFile,
|
||||
limits: this.opts.limits,
|
||||
parsedConType: parsed,
|
||||
preservePath: this.opts.preservePath
|
||||
}
|
||||
|
||||
if (MultipartParser.detect.test(parsed[0])) {
|
||||
return new MultipartParser(this, cfg)
|
||||
}
|
||||
if (UrlencodedParser.detect.test(parsed[0])) {
|
||||
return new UrlencodedParser(this, cfg)
|
||||
}
|
||||
throw new Error('Unsupported Content-Type.')
|
||||
}
|
||||
|
||||
Busboy.prototype._write = function (chunk, encoding, cb) {
|
||||
this._parser.write(chunk, cb)
|
||||
}
|
||||
|
||||
module.exports = Busboy
|
||||
module.exports.default = Busboy
|
||||
module.exports.Busboy = Busboy
|
||||
|
||||
module.exports.Dicer = Dicer
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
'use strict'
|
||||
|
||||
// TODO:
|
||||
// * support 1 nested multipart level
|
||||
// (see second multipart example here:
|
||||
// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
|
||||
// * support limits.fieldNameSize
|
||||
// -- this will require modifications to utils.parseParams
|
||||
|
||||
const { Readable } = require('node:stream')
|
||||
const { inherits } = require('node:util')
|
||||
|
||||
const Dicer = require('../../deps/dicer/lib/Dicer')
|
||||
|
||||
const parseParams = require('../utils/parseParams')
|
||||
const decodeText = require('../utils/decodeText')
|
||||
const basename = require('../utils/basename')
|
||||
const getLimit = require('../utils/getLimit')
|
||||
|
||||
const RE_BOUNDARY = /^boundary$/i
|
||||
const RE_FIELD = /^form-data$/i
|
||||
const RE_CHARSET = /^charset$/i
|
||||
const RE_FILENAME = /^filename$/i
|
||||
const RE_NAME = /^name$/i
|
||||
|
||||
Multipart.detect = /^multipart\/form-data/i
|
||||
function Multipart (boy, cfg) {
|
||||
let i
|
||||
let len
|
||||
const self = this
|
||||
let boundary
|
||||
const limits = cfg.limits
|
||||
const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined))
|
||||
const parsedConType = cfg.parsedConType || []
|
||||
const defCharset = cfg.defCharset || 'utf8'
|
||||
const preservePath = cfg.preservePath
|
||||
const fileOpts = { highWaterMark: cfg.fileHwm }
|
||||
|
||||
for (i = 0, len = parsedConType.length; i < len; ++i) {
|
||||
if (Array.isArray(parsedConType[i]) &&
|
||||
RE_BOUNDARY.test(parsedConType[i][0])) {
|
||||
boundary = parsedConType[i][1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function checkFinished () {
|
||||
if (nends === 0 && finished && !boy._done) {
|
||||
finished = false
|
||||
self.end()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') }
|
||||
|
||||
const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
|
||||
const fileSizeLimit = getLimit(limits, 'fileSize', Infinity)
|
||||
const filesLimit = getLimit(limits, 'files', Infinity)
|
||||
const fieldsLimit = getLimit(limits, 'fields', Infinity)
|
||||
const partsLimit = getLimit(limits, 'parts', Infinity)
|
||||
const headerPairsLimit = getLimit(limits, 'headerPairs', 2000)
|
||||
const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024)
|
||||
|
||||
let nfiles = 0
|
||||
let nfields = 0
|
||||
let nends = 0
|
||||
let curFile
|
||||
let curField
|
||||
let finished = false
|
||||
|
||||
this._needDrain = false
|
||||
this._pause = false
|
||||
this._cb = undefined
|
||||
this._nparts = 0
|
||||
this._boy = boy
|
||||
|
||||
const parserCfg = {
|
||||
boundary,
|
||||
maxHeaderPairs: headerPairsLimit,
|
||||
maxHeaderSize: headerSizeLimit,
|
||||
partHwm: fileOpts.highWaterMark,
|
||||
highWaterMark: cfg.highWaterMark
|
||||
}
|
||||
|
||||
this.parser = new Dicer(parserCfg)
|
||||
this.parser.on('drain', function () {
|
||||
self._needDrain = false
|
||||
if (self._cb && !self._pause) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}).on('part', function onPart (part) {
|
||||
if (++self._nparts > partsLimit) {
|
||||
self.parser.removeListener('part', onPart)
|
||||
self.parser.on('part', skipPart)
|
||||
boy.hitPartsLimit = true
|
||||
boy.emit('partsLimit')
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
// hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
|
||||
// us emit 'end' early since we know the part has ended if we are already
|
||||
// seeing the next part
|
||||
if (curField) {
|
||||
const field = curField
|
||||
field.emit('end')
|
||||
field.removeAllListeners('end')
|
||||
}
|
||||
|
||||
part.on('header', function (header) {
|
||||
let contype
|
||||
let fieldname
|
||||
let parsed
|
||||
let charset
|
||||
let encoding
|
||||
let filename
|
||||
let nsize = 0
|
||||
|
||||
if (header['content-type']) {
|
||||
parsed = parseParams(header['content-type'][0])
|
||||
if (parsed[0]) {
|
||||
contype = parsed[0].toLowerCase()
|
||||
for (i = 0, len = parsed.length; i < len; ++i) {
|
||||
if (RE_CHARSET.test(parsed[i][0])) {
|
||||
charset = parsed[i][1].toLowerCase()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contype === undefined) { contype = 'text/plain' }
|
||||
if (charset === undefined) { charset = defCharset }
|
||||
|
||||
if (header['content-disposition']) {
|
||||
parsed = parseParams(header['content-disposition'][0])
|
||||
if (!RE_FIELD.test(parsed[0])) { return skipPart(part) }
|
||||
for (i = 0, len = parsed.length; i < len; ++i) {
|
||||
if (RE_NAME.test(parsed[i][0])) {
|
||||
fieldname = parsed[i][1]
|
||||
} else if (RE_FILENAME.test(parsed[i][0])) {
|
||||
filename = parsed[i][1]
|
||||
if (!preservePath) { filename = basename(filename) }
|
||||
}
|
||||
}
|
||||
} else { return skipPart(part) }
|
||||
|
||||
if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' }
|
||||
|
||||
let onData,
|
||||
onEnd
|
||||
|
||||
if (isPartAFile(fieldname, contype, filename)) {
|
||||
// file/binary field
|
||||
if (nfiles === filesLimit) {
|
||||
if (!boy.hitFilesLimit) {
|
||||
boy.hitFilesLimit = true
|
||||
boy.emit('filesLimit')
|
||||
}
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
++nfiles
|
||||
|
||||
if (boy.listenerCount('file') === 0) {
|
||||
self.parser._ignore()
|
||||
return
|
||||
}
|
||||
|
||||
++nends
|
||||
const file = new FileStream(fileOpts)
|
||||
curFile = file
|
||||
file.on('end', function () {
|
||||
--nends
|
||||
self._pause = false
|
||||
checkFinished()
|
||||
if (self._cb && !self._needDrain) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
})
|
||||
file._read = function (n) {
|
||||
if (!self._pause) { return }
|
||||
self._pause = false
|
||||
if (self._cb && !self._needDrain) {
|
||||
const cb = self._cb
|
||||
self._cb = undefined
|
||||
cb()
|
||||
}
|
||||
}
|
||||
boy.emit('file', fieldname, file, filename, encoding, contype)
|
||||
|
||||
onData = function (data) {
|
||||
if ((nsize += data.length) > fileSizeLimit) {
|
||||
const extralen = fileSizeLimit - nsize + data.length
|
||||
if (extralen > 0) { file.push(data.slice(0, extralen)) }
|
||||
file.truncated = true
|
||||
file.bytesRead = fileSizeLimit
|
||||
part.removeAllListeners('data')
|
||||
file.emit('limit')
|
||||
return
|
||||
} else if (!file.push(data)) { self._pause = true }
|
||||
|
||||
file.bytesRead = nsize
|
||||
}
|
||||
|
||||
onEnd = function () {
|
||||
curFile = undefined
|
||||
file.push(null)
|
||||
}
|
||||
} else {
|
||||
// non-file field
|
||||
if (nfields === fieldsLimit) {
|
||||
if (!boy.hitFieldsLimit) {
|
||||
boy.hitFieldsLimit = true
|
||||
boy.emit('fieldsLimit')
|
||||
}
|
||||
return skipPart(part)
|
||||
}
|
||||
|
||||
++nfields
|
||||
++nends
|
||||
let buffer = ''
|
||||
let truncated = false
|
||||
curField = part
|
||||
|
||||
onData = function (data) {
|
||||
if ((nsize += data.length) > fieldSizeLimit) {
|
||||
const extralen = (fieldSizeLimit - (nsize - data.length))
|
||||
buffer += data.toString('binary', 0, extralen)
|
||||
truncated = true
|
||||
part.removeAllListeners('data')
|
||||
} else { buffer += data.toString('binary') }
|
||||
}
|
||||
|
||||
onEnd = function () {
|
||||
curField = undefined
|
||||
if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) }
|
||||
boy.emit('field', fieldname, buffer, false, truncated, encoding, contype)
|
||||
--nends
|
||||
checkFinished()
|
||||
}
|
||||
}
|
||||
|
||||
/* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
|
||||
broken. Streams2/streams3 is a huge black box of confusion, but
|
||||
somehow overriding the sync state seems to fix things again (and still
|
||||
seems to work for previous node versions).
|
||||
*/
|
||||
part._readableState.sync = false
|
||||
|
||||
part.on('data', onData)
|
||||
part.on('end', onEnd)
|
||||
}).on('error', function (err) {
|
||||
if (curFile) { curFile.emit('error', err) }
|
||||
})
|
||||
}).on('error', function (err) {
|
||||
boy.emit('error', err)
|
||||
}).on('finish', function () {
|
||||
finished = true
|
||||
checkFinished()
|
||||
})
|
||||
}
|
||||
|
||||
Multipart.prototype.write = function (chunk, cb) {
|
||||
const r = this.parser.write(chunk)
|
||||
if (r && !this._pause) {
|
||||
cb()
|
||||
} else {
|
||||
this._needDrain = !r
|
||||
this._cb = cb
|
||||
}
|
||||
}
|
||||
|
||||
Multipart.prototype.end = function () {
|
||||
const self = this
|
||||
|
||||
if (self.parser.writable) {
|
||||
self.parser.end()
|
||||
} else if (!self._boy._done) {
|
||||
process.nextTick(function () {
|
||||
self._boy._done = true
|
||||
self._boy.emit('finish')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function skipPart (part) {
|
||||
part.resume()
|
||||
}
|
||||
|
||||
function FileStream (opts) {
|
||||
Readable.call(this, opts)
|
||||
|
||||
this.bytesRead = 0
|
||||
|
||||
this.truncated = false
|
||||
}
|
||||
|
||||
inherits(FileStream, Readable)
|
||||
|
||||
FileStream.prototype._read = function (n) {}
|
||||
|
||||
module.exports = Multipart
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
'use strict'
|
||||
|
||||
const Decoder = require('../utils/Decoder')
|
||||
const decodeText = require('../utils/decodeText')
|
||||
const getLimit = require('../utils/getLimit')
|
||||
|
||||
const RE_CHARSET = /^charset$/i
|
||||
|
||||
UrlEncoded.detect = /^application\/x-www-form-urlencoded/i
|
||||
function UrlEncoded (boy, cfg) {
|
||||
const limits = cfg.limits
|
||||
const parsedConType = cfg.parsedConType
|
||||
this.boy = boy
|
||||
|
||||
this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
|
||||
this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100)
|
||||
this.fieldsLimit = getLimit(limits, 'fields', Infinity)
|
||||
|
||||
let charset
|
||||
for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var
|
||||
if (Array.isArray(parsedConType[i]) &&
|
||||
RE_CHARSET.test(parsedConType[i][0])) {
|
||||
charset = parsedConType[i][1].toLowerCase()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (charset === undefined) { charset = cfg.defCharset || 'utf8' }
|
||||
|
||||
this.decoder = new Decoder()
|
||||
this.charset = charset
|
||||
this._fields = 0
|
||||
this._state = 'key'
|
||||
this._checkingBytes = true
|
||||
this._bytesKey = 0
|
||||
this._bytesVal = 0
|
||||
this._key = ''
|
||||
this._val = ''
|
||||
this._keyTrunc = false
|
||||
this._valTrunc = false
|
||||
this._hitLimit = false
|
||||
}
|
||||
|
||||
UrlEncoded.prototype.write = function (data, cb) {
|
||||
if (this._fields === this.fieldsLimit) {
|
||||
if (!this.boy.hitFieldsLimit) {
|
||||
this.boy.hitFieldsLimit = true
|
||||
this.boy.emit('fieldsLimit')
|
||||
}
|
||||
return cb()
|
||||
}
|
||||
|
||||
let idxeq; let idxamp; let i; let p = 0; const len = data.length
|
||||
|
||||
while (p < len) {
|
||||
if (this._state === 'key') {
|
||||
idxeq = idxamp = undefined
|
||||
for (i = p; i < len; ++i) {
|
||||
if (!this._checkingBytes) { ++p }
|
||||
if (data[i] === 0x3D/* = */) {
|
||||
idxeq = i
|
||||
break
|
||||
} else if (data[i] === 0x26/* & */) {
|
||||
idxamp = i
|
||||
break
|
||||
}
|
||||
if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) {
|
||||
this._hitLimit = true
|
||||
break
|
||||
} else if (this._checkingBytes) { ++this._bytesKey }
|
||||
}
|
||||
|
||||
if (idxeq !== undefined) {
|
||||
// key with assignment
|
||||
if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) }
|
||||
this._state = 'val'
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._val = ''
|
||||
this._bytesVal = 0
|
||||
this._valTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
p = idxeq + 1
|
||||
} else if (idxamp !== undefined) {
|
||||
// key with no assignment
|
||||
++this._fields
|
||||
let key; const keyTrunc = this._keyTrunc
|
||||
if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key }
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._key = ''
|
||||
this._bytesKey = 0
|
||||
this._keyTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
if (key.length) {
|
||||
this.boy.emit('field', decodeText(key, 'binary', this.charset),
|
||||
'',
|
||||
keyTrunc,
|
||||
false)
|
||||
}
|
||||
|
||||
p = idxamp + 1
|
||||
if (this._fields === this.fieldsLimit) { return cb() }
|
||||
} else if (this._hitLimit) {
|
||||
// we may not have hit the actual limit if there are encoded bytes...
|
||||
if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) }
|
||||
p = i
|
||||
if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) {
|
||||
// yep, we actually did hit the limit
|
||||
this._checkingBytes = false
|
||||
this._keyTrunc = true
|
||||
}
|
||||
} else {
|
||||
if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) }
|
||||
p = len
|
||||
}
|
||||
} else {
|
||||
idxamp = undefined
|
||||
for (i = p; i < len; ++i) {
|
||||
if (!this._checkingBytes) { ++p }
|
||||
if (data[i] === 0x26/* & */) {
|
||||
idxamp = i
|
||||
break
|
||||
}
|
||||
if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) {
|
||||
this._hitLimit = true
|
||||
break
|
||||
} else if (this._checkingBytes) { ++this._bytesVal }
|
||||
}
|
||||
|
||||
if (idxamp !== undefined) {
|
||||
++this._fields
|
||||
if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) }
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
decodeText(this._val, 'binary', this.charset),
|
||||
this._keyTrunc,
|
||||
this._valTrunc)
|
||||
this._state = 'key'
|
||||
|
||||
this._hitLimit = false
|
||||
this._checkingBytes = true
|
||||
this._key = ''
|
||||
this._bytesKey = 0
|
||||
this._keyTrunc = false
|
||||
this.decoder.reset()
|
||||
|
||||
p = idxamp + 1
|
||||
if (this._fields === this.fieldsLimit) { return cb() }
|
||||
} else if (this._hitLimit) {
|
||||
// we may not have hit the actual limit if there are encoded bytes...
|
||||
if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) }
|
||||
p = i
|
||||
if ((this._val === '' && this.fieldSizeLimit === 0) ||
|
||||
(this._bytesVal = this._val.length) === this.fieldSizeLimit) {
|
||||
// yep, we actually did hit the limit
|
||||
this._checkingBytes = false
|
||||
this._valTrunc = true
|
||||
}
|
||||
} else {
|
||||
if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) }
|
||||
p = len
|
||||
}
|
||||
}
|
||||
}
|
||||
cb()
|
||||
}
|
||||
|
||||
UrlEncoded.prototype.end = function () {
|
||||
if (this.boy._done) { return }
|
||||
|
||||
if (this._state === 'key' && this._key.length > 0) {
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
'',
|
||||
this._keyTrunc,
|
||||
false)
|
||||
} else if (this._state === 'val') {
|
||||
this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
|
||||
decodeText(this._val, 'binary', this.charset),
|
||||
this._keyTrunc,
|
||||
this._valTrunc)
|
||||
}
|
||||
this.boy._done = true
|
||||
this.boy.emit('finish')
|
||||
}
|
||||
|
||||
module.exports = UrlEncoded
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
'use strict'
|
||||
|
||||
const RE_PLUS = /\+/g
|
||||
|
||||
const HEX = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
|
||||
function Decoder () {
|
||||
this.buffer = undefined
|
||||
}
|
||||
Decoder.prototype.write = function (str) {
|
||||
// Replace '+' with ' ' before decoding
|
||||
str = str.replace(RE_PLUS, ' ')
|
||||
let res = ''
|
||||
let i = 0; let p = 0; const len = str.length
|
||||
for (; i < len; ++i) {
|
||||
if (this.buffer !== undefined) {
|
||||
if (!HEX[str.charCodeAt(i)]) {
|
||||
res += '%' + this.buffer
|
||||
this.buffer = undefined
|
||||
--i // retry character
|
||||
} else {
|
||||
this.buffer += str[i]
|
||||
++p
|
||||
if (this.buffer.length === 2) {
|
||||
res += String.fromCharCode(parseInt(this.buffer, 16))
|
||||
this.buffer = undefined
|
||||
}
|
||||
}
|
||||
} else if (str[i] === '%') {
|
||||
if (i > p) {
|
||||
res += str.substring(p, i)
|
||||
p = i
|
||||
}
|
||||
this.buffer = ''
|
||||
++p
|
||||
}
|
||||
}
|
||||
if (p < len && this.buffer === undefined) { res += str.substring(p) }
|
||||
return res
|
||||
}
|
||||
Decoder.prototype.reset = function () {
|
||||
this.buffer = undefined
|
||||
}
|
||||
|
||||
module.exports = Decoder
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function basename (path) {
|
||||
if (typeof path !== 'string') { return '' }
|
||||
for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var
|
||||
switch (path.charCodeAt(i)) {
|
||||
case 0x2F: // '/'
|
||||
case 0x5C: // '\'
|
||||
path = path.slice(i + 1)
|
||||
return (path === '..' || path === '.' ? '' : path)
|
||||
}
|
||||
}
|
||||
return (path === '..' || path === '.' ? '' : path)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
'use strict'
|
||||
|
||||
// Node has always utf-8
|
||||
const utf8Decoder = new TextDecoder('utf-8')
|
||||
const textDecoders = new Map([
|
||||
['utf-8', utf8Decoder],
|
||||
['utf8', utf8Decoder]
|
||||
])
|
||||
|
||||
function getDecoder (charset) {
|
||||
let lc
|
||||
while (true) {
|
||||
switch (charset) {
|
||||
case 'utf-8':
|
||||
case 'utf8':
|
||||
return decoders.utf8
|
||||
case 'latin1':
|
||||
case 'ascii': // TODO: Make these a separate, strict decoder?
|
||||
case 'us-ascii':
|
||||
case 'iso-8859-1':
|
||||
case 'iso8859-1':
|
||||
case 'iso88591':
|
||||
case 'iso_8859-1':
|
||||
case 'windows-1252':
|
||||
case 'iso_8859-1:1987':
|
||||
case 'cp1252':
|
||||
case 'x-cp1252':
|
||||
return decoders.latin1
|
||||
case 'utf16le':
|
||||
case 'utf-16le':
|
||||
case 'ucs2':
|
||||
case 'ucs-2':
|
||||
return decoders.utf16le
|
||||
case 'base64':
|
||||
return decoders.base64
|
||||
default:
|
||||
if (lc === undefined) {
|
||||
lc = true
|
||||
charset = charset.toLowerCase()
|
||||
continue
|
||||
}
|
||||
return decoders.other.bind(charset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decoders = {
|
||||
utf8: (data, sourceEncoding) => {
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, sourceEncoding)
|
||||
}
|
||||
return data.utf8Slice(0, data.length)
|
||||
},
|
||||
|
||||
latin1: (data, sourceEncoding) => {
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
return data
|
||||
}
|
||||
return data.latin1Slice(0, data.length)
|
||||
},
|
||||
|
||||
utf16le: (data, sourceEncoding) => {
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, sourceEncoding)
|
||||
}
|
||||
return data.ucs2Slice(0, data.length)
|
||||
},
|
||||
|
||||
base64: (data, sourceEncoding) => {
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, sourceEncoding)
|
||||
}
|
||||
return data.base64Slice(0, data.length)
|
||||
},
|
||||
|
||||
other: (data, sourceEncoding) => {
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, sourceEncoding)
|
||||
}
|
||||
|
||||
if (textDecoders.has(this.toString())) {
|
||||
try {
|
||||
return textDecoders.get(this).decode(data)
|
||||
} catch {}
|
||||
}
|
||||
return typeof data === 'string'
|
||||
? data
|
||||
: data.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function decodeText (text, sourceEncoding, destEncoding) {
|
||||
if (text) {
|
||||
return getDecoder(destEncoding)(text, sourceEncoding)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
module.exports = decodeText
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function getLimit (limits, name, defaultLimit) {
|
||||
if (
|
||||
!limits ||
|
||||
limits[name] === undefined ||
|
||||
limits[name] === null
|
||||
) { return defaultLimit }
|
||||
|
||||
if (
|
||||
typeof limits[name] !== 'number' ||
|
||||
isNaN(limits[name])
|
||||
) { throw new TypeError('Limit ' + name + ' is not a valid number') }
|
||||
|
||||
return limits[name]
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable object-property-newline */
|
||||
'use strict'
|
||||
|
||||
const decodeText = require('./decodeText')
|
||||
|
||||
const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g
|
||||
|
||||
const EncodedLookup = {
|
||||
'%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04',
|
||||
'%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09',
|
||||
'%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c',
|
||||
'%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e',
|
||||
'%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12',
|
||||
'%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17',
|
||||
'%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b',
|
||||
'%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d',
|
||||
'%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20',
|
||||
'%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25',
|
||||
'%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a',
|
||||
'%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c',
|
||||
'%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f',
|
||||
'%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33',
|
||||
'%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38',
|
||||
'%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b',
|
||||
'%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e',
|
||||
'%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41',
|
||||
'%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46',
|
||||
'%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a',
|
||||
'%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d',
|
||||
'%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f',
|
||||
'%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54',
|
||||
'%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59',
|
||||
'%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c',
|
||||
'%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e',
|
||||
'%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62',
|
||||
'%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67',
|
||||
'%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b',
|
||||
'%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d',
|
||||
'%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70',
|
||||
'%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75',
|
||||
'%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a',
|
||||
'%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c',
|
||||
'%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f',
|
||||
'%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83',
|
||||
'%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88',
|
||||
'%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b',
|
||||
'%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e',
|
||||
'%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91',
|
||||
'%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96',
|
||||
'%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a',
|
||||
'%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d',
|
||||
'%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f',
|
||||
'%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2',
|
||||
'%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4',
|
||||
'%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7',
|
||||
'%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9',
|
||||
'%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab',
|
||||
'%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac',
|
||||
'%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad',
|
||||
'%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae',
|
||||
'%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0',
|
||||
'%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2',
|
||||
'%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5',
|
||||
'%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7',
|
||||
'%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba',
|
||||
'%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb',
|
||||
'%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc',
|
||||
'%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd',
|
||||
'%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf',
|
||||
'%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0',
|
||||
'%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3',
|
||||
'%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5',
|
||||
'%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8',
|
||||
'%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca',
|
||||
'%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb',
|
||||
'%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc',
|
||||
'%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce',
|
||||
'%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf',
|
||||
'%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1',
|
||||
'%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3',
|
||||
'%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6',
|
||||
'%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8',
|
||||
'%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda',
|
||||
'%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb',
|
||||
'%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd',
|
||||
'%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde',
|
||||
'%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf',
|
||||
'%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1',
|
||||
'%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4',
|
||||
'%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6',
|
||||
'%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9',
|
||||
'%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea',
|
||||
'%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec',
|
||||
'%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed',
|
||||
'%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee',
|
||||
'%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef',
|
||||
'%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2',
|
||||
'%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4',
|
||||
'%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7',
|
||||
'%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9',
|
||||
'%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb',
|
||||
'%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc',
|
||||
'%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd',
|
||||
'%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe',
|
||||
'%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff'
|
||||
}
|
||||
|
||||
function encodedReplacer (match) {
|
||||
return EncodedLookup[match]
|
||||
}
|
||||
|
||||
const STATE_KEY = 0
|
||||
const STATE_VALUE = 1
|
||||
const STATE_CHARSET = 2
|
||||
const STATE_LANG = 3
|
||||
|
||||
function parseParams (str) {
|
||||
const res = []
|
||||
let state = STATE_KEY
|
||||
let charset = ''
|
||||
let inquote = false
|
||||
let escaping = false
|
||||
let p = 0
|
||||
let tmp = ''
|
||||
const len = str.length
|
||||
|
||||
for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
|
||||
const char = str[i]
|
||||
if (char === '\\' && inquote) {
|
||||
if (escaping) { escaping = false } else {
|
||||
escaping = true
|
||||
continue
|
||||
}
|
||||
} else if (char === '"') {
|
||||
if (!escaping) {
|
||||
if (inquote) {
|
||||
inquote = false
|
||||
state = STATE_KEY
|
||||
} else { inquote = true }
|
||||
continue
|
||||
} else { escaping = false }
|
||||
} else {
|
||||
if (escaping && inquote) { tmp += '\\' }
|
||||
escaping = false
|
||||
if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") {
|
||||
if (state === STATE_CHARSET) {
|
||||
state = STATE_LANG
|
||||
charset = tmp.substring(1)
|
||||
} else { state = STATE_VALUE }
|
||||
tmp = ''
|
||||
continue
|
||||
} else if (state === STATE_KEY &&
|
||||
(char === '*' || char === '=') &&
|
||||
res.length) {
|
||||
state = char === '*'
|
||||
? STATE_CHARSET
|
||||
: STATE_VALUE
|
||||
res[p] = [tmp, undefined]
|
||||
tmp = ''
|
||||
continue
|
||||
} else if (!inquote && char === ';') {
|
||||
state = STATE_KEY
|
||||
if (charset) {
|
||||
if (tmp.length) {
|
||||
tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
|
||||
'binary',
|
||||
charset)
|
||||
}
|
||||
charset = ''
|
||||
} else if (tmp.length) {
|
||||
tmp = decodeText(tmp, 'binary', 'utf8')
|
||||
}
|
||||
if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp }
|
||||
tmp = ''
|
||||
++p
|
||||
continue
|
||||
} else if (!inquote && (char === ' ' || char === '\t')) { continue }
|
||||
}
|
||||
tmp += char
|
||||
}
|
||||
if (charset && tmp.length) {
|
||||
tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
|
||||
'binary',
|
||||
charset)
|
||||
} else if (tmp) {
|
||||
tmp = decodeText(tmp, 'binary', 'utf8')
|
||||
}
|
||||
|
||||
if (res[p] === undefined) {
|
||||
if (tmp) { res[p] = tmp }
|
||||
} else { res[p][1] = tmp }
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
module.exports = parseParams
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@fastify/busboy",
|
||||
"version": "2.1.1",
|
||||
"private": false,
|
||||
"author": "Brian White <mscdex@mscdex.net>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Igor Savin",
|
||||
"email": "kibertoad@gmail.com",
|
||||
"url": "https://github.com/kibertoad"
|
||||
},
|
||||
{
|
||||
"name": "Aras Abbasi",
|
||||
"email": "aras.abbasi@gmail.com",
|
||||
"url": "https://github.com/uzlopak"
|
||||
}
|
||||
],
|
||||
"description": "A streaming parser for HTML form data for node.js",
|
||||
"main": "lib/main",
|
||||
"type": "commonjs",
|
||||
"types": "lib/main.d.ts",
|
||||
"scripts": {
|
||||
"bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify",
|
||||
"bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js",
|
||||
"coveralls": "nyc report --reporter=lcov",
|
||||
"lint": "npm run lint:standard",
|
||||
"lint:everything": "npm run lint && npm run test:types",
|
||||
"lint:fix": "standard --fix",
|
||||
"lint:standard": "standard --verbose | snazzy",
|
||||
"test:mocha": "tap",
|
||||
"test:types": "tsd",
|
||||
"test:coverage": "nyc npm run test",
|
||||
"test": "npm run test:mocha"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"busboy": "^1.0.0",
|
||||
"photofinish": "^1.8.0",
|
||||
"snazzy": "^9.0.0",
|
||||
"standard": "^17.0.0",
|
||||
"tap": "^16.3.8",
|
||||
"tinybench": "^2.5.1",
|
||||
"tsd": "^0.30.0",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"uploads",
|
||||
"forms",
|
||||
"multipart",
|
||||
"form-data"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/fastify/busboy.git"
|
||||
},
|
||||
"tsd": {
|
||||
"directory": "test/types",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": false,
|
||||
"module": "commonjs",
|
||||
"target": "ES2017"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"describe",
|
||||
"it"
|
||||
],
|
||||
"ignore": [
|
||||
"bench"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"lib/*",
|
||||
"deps/encoding/*",
|
||||
"deps/dicer/lib",
|
||||
"deps/streamsearch/",
|
||||
"deps/dicer/LICENSE"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user