Merge pull request #1 from Mia-Wu_wnc/python-migration
python migration
This commit is contained in:
+15
-5
@@ -18,7 +18,7 @@ dist-ssr
|
|||||||
*.local
|
*.local
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.template
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@@ -31,7 +31,17 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
# Server-specific
|
# Node/JS
|
||||||
server/config.json.bak
|
dashboard/node_modules/
|
||||||
server/*.pid
|
dashboard/dist/
|
||||||
server/test
|
dashboard/.env
|
||||||
|
|
||||||
|
# Python
|
||||||
|
server/.venv/
|
||||||
|
server/__pycache__/
|
||||||
|
server/dashboard.db*
|
||||||
|
server/*.pyc
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
test/
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# CLAUDE.md — Project Context for AI Assistants
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
A real-time web dashboard that tracks WiFi test execution progress. It compares a **target tests directory** (`.ini` files defining every test that must run) against a **results directory** (folders created when each test completes). The dashboard shows completion rates, elapsed time, estimated time remaining, and a filterable table of every test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
### Backend (Python)
|
||||||
|
```powershell
|
||||||
|
cd server
|
||||||
|
.\.venv\Scripts\Activate.ps1 # activate venv
|
||||||
|
python app.py # starts Flask on port 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (dev)
|
||||||
|
```powershell
|
||||||
|
cd dashboard
|
||||||
|
npm run dev # Vite dev server; proxies /api → localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (production build)
|
||||||
|
```powershell
|
||||||
|
cd dashboard
|
||||||
|
npm run build # outputs to dashboard/dist/
|
||||||
|
# Flask serves dist/ automatically when app.py is running
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Frontend | React 18 + Vite, TailwindCSS v4 (`@tailwindcss/vite`), TanStack React Query |
|
||||||
|
| Backend | Python 3, Flask, flask-cors |
|
||||||
|
| Database | SQLite via Python stdlib `sqlite3` (WAL mode, single persistent connection + RLock) |
|
||||||
|
| File watching | `watchdog` Python library |
|
||||||
|
| Real-time | Server-Sent Events (SSE) — one-way push from backend to browser |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
### Backend (`server/`)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `app.py` | Flask app, all route handlers, `bootstrap()`, `__main__` |
|
||||||
|
| `db_py.py` | SQLite schema init, all query helpers (`upsert_test`, `mark_completed`, `reset_by_file_id_and_device`, `get_config`, `set_config`, etc.) |
|
||||||
|
| `scanner.py` | `full_scan()`, `scan_targets()`, `scan_results()`, `process_result_dir()`, `parse_deleted_result_dir_name()` |
|
||||||
|
| `parser.py` | `parse_target_filename()`, `parse_result_filename()`, `parse_timestamp()`, `parse_elapsed_time()`, `parse_tput_rssi()` |
|
||||||
|
| `watcher.py` | Two watchdog `Observer` instances — `_TargetHandler` and `_ResultsHandler`. `start_watching()` / `stop_watching()` |
|
||||||
|
| `sse_py.py` | Per-client `Queue` registry, `stream_events()` generator, `broadcast(data)` |
|
||||||
|
|
||||||
|
### Frontend (`dashboard/src/`)
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `lib/api.js` | Thin `fetch` wrapper; all API calls go through `apiFetch('/path')` |
|
||||||
|
| `hooks/useStats.js` | Fetches `/api/stats`; **owns the SSE `EventSource`**; invalidates `['stats']` and `['tests']` on `update` events |
|
||||||
|
| `hooks/useTests.js` | Fetches `/api/tests` |
|
||||||
|
| `hooks/useConfig.js` | Fetches/saves `/api/config` |
|
||||||
|
| `components/ConfigModal.jsx` | Settings modal: directory paths (always editable) + avg time overrides |
|
||||||
|
| `components/DirectoryBrowser.jsx` | Server-side folder picker using `/api/browse` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
|
||||||
|
### How tests are identified
|
||||||
|
|
||||||
|
- **Target files** live in subdirectories of the target directory. Format: `TC_WIFI_<tags>.ini`. Files named `GLOBAL.ini` are skipped.
|
||||||
|
- **`test_id`**: segment matching `R\d+[A-Z0-9]+` (e.g., `R2COERXAX014`)
|
||||||
|
- **Result directories**: top-level subdirectories of the results directory. A test is **completed** when a result directory name contains the same `test_id`.
|
||||||
|
- **Result folder name example**: `COE_CGW453_R2COERXAX014_TPT3E_RSSI70_STA56_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL`
|
||||||
|
|
||||||
|
### Tags parsed from target filename
|
||||||
|
|
||||||
|
`interference`, `device`, `test_point`, `rssi`, `station`, `band`, `channel`, `bandwidth`, `direction`
|
||||||
|
|
||||||
|
`rotation` is parsed from the **parent folder name** (segment matching `ROT\d+`).
|
||||||
|
|
||||||
|
### Database tables
|
||||||
|
|
||||||
|
**`tests`** — one row per target `.ini` file:
|
||||||
|
- `id` (TEXT PK) — `parent_dir/base_filename`
|
||||||
|
- `test_id`, `parent_dir`, `filename`, `interference`, `device`, `rotation`, `test_point`, `station`, `band`, `channel`, `bandwidth`, `rssi`, `direction`
|
||||||
|
- `completed` (0/1), `completed_at` (ISO timestamp), `duration_seconds` (REAL)
|
||||||
|
- `tput_results` (JSON array of `{station, tput, dlRssi, ulRssi}`)
|
||||||
|
|
||||||
|
**`config`** — key/value store:
|
||||||
|
- `target_dir`, `results_dir`
|
||||||
|
- `avg_time_coe`, `avg_time_p2p`, `avg_time_p3p` (seconds as string; NULL = use calculated average)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API routes
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/tests` | All tests; supports query filters (`completed`, `interference`, `device`, `rotation`, `testPoint`, `station`, `band`, `channel`, `bandwidth`, `rssi`, `direction`) |
|
||||||
|
| GET | `/api/stats` | Aggregated stats: overall, per-device, timing with estimates |
|
||||||
|
| GET | `/api/events` | SSE stream; sends `data: {"type":"update"}\n\n` on any directory change |
|
||||||
|
| GET | `/api/config` | Current config values |
|
||||||
|
| POST | `/api/config` | Update config; triggers full rescan + rewatcher if dirs changed |
|
||||||
|
| POST | `/api/config/rescan` | Force full rescan without changing config |
|
||||||
|
| GET | `/api/browse?path=...` | List subdirectories at a server path (omit `path` for drive roots) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Watcher architecture
|
||||||
|
|
||||||
|
Two `watchdog.Observer` instances run in daemon threads:
|
||||||
|
|
||||||
|
1. **`_TargetHandler`** — watches `target_dir` recursively. Any add/delete/move of a `TC_WIFI_*.ini` file schedules a **debounced full scan** (1 second timer, cancels and restarts on rapid changes).
|
||||||
|
|
||||||
|
2. **`_ResultsHandler`** — watches `results_dir` recursively. Handles events surgically:
|
||||||
|
- Directory created → `process_result_dir()` + broadcast
|
||||||
|
- Directory deleted → `reset_by_file_id_and_device()` + broadcast
|
||||||
|
- Directory moved in/out/renamed → appropriate reset/process + broadcast
|
||||||
|
- File created/deleted inside a result dir → `process_result_dir()` + broadcast
|
||||||
|
|
||||||
|
### Critical Windows quirk
|
||||||
|
When a directory is deleted (including Recycle Bin), `watchdog` calls `os.path.isdir()` at event-processing time. The directory is already gone, so it returns `False`, making `event.is_directory = False` even for directory events. **All handlers check `_is_direct_child_dir(path)` directly** — never gate on `event.is_directory` for delete/move cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SSE implementation notes
|
||||||
|
|
||||||
|
- `sse_py.py`: each connected client gets a `queue.Queue`. `broadcast()` calls `put_nowait(payload)` on all queues. `stream_events()` is a generator yielding `f"data: {payload}\n\n"` (real newlines — `\n` not `\\n`).
|
||||||
|
- `useStats.js`: creates `new EventSource('/api/events')` once on mount. On `{"type":"update"}` message, invalidates React Query keys `['stats']` and `['tests']`.
|
||||||
|
- Vite dev proxy forwards `/api/events` to Flask, keeping the SSE connection alive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known issues / gotchas
|
||||||
|
|
||||||
|
- `config.json` in `server/` is a legacy file used for a one-time migration to SQLite on first run. It is no longer needed once `dashboard.db` exists.
|
||||||
|
- The backend entrypoint is `app.py`, not `main.py`.
|
||||||
|
- Flask dev server (`app.run`) is used directly — no gunicorn/waitress configured yet.
|
||||||
|
- Port is `3001` (configurable via `PORT` env var).
|
||||||
+77
-60
@@ -1,5 +1,7 @@
|
|||||||
# Implementation Plan — Test Dashboard
|
# Implementation Plan — Test Dashboard
|
||||||
|
|
||||||
|
> **Status as of May 2026**: fully implemented and running. Backend migrated from the original Node.js plan to Python (Flask). See current tech stack and structure below.
|
||||||
|
|
||||||
## 1. Overview
|
## 1. Overview
|
||||||
|
|
||||||
A web dashboard that monitors test execution progress by comparing a **target tests directory** (what should run) against a **results directory** (what has run). The backend runs on one machine with access to both directories; the frontend is accessible from any device on the network.
|
A web dashboard that monitors test execution progress by comparing a **target tests directory** (what should run) against a **results directory** (what has run). The backend runs on one machine with access to both directories; the frontend is accessible from any device on the network.
|
||||||
@@ -8,55 +10,59 @@ A web dashboard that monitors test execution progress by comparing a **target te
|
|||||||
|
|
||||||
## 2. Tech Stack
|
## 2. Tech Stack
|
||||||
|
|
||||||
| Layer | Technology | Rationale |
|
| Layer | Technology | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Frontend | React + Vite (existing) | Already scaffolded |
|
| Frontend | React + Vite | Dev server proxies `/api` → `localhost:3001` |
|
||||||
| Backend | Node.js + Express | Lightweight API + static file serving |
|
| Backend | Python 3 + Flask | Replaced original Node.js/Express plan |
|
||||||
| Real-time | Server-Sent Events (SSE) | Simpler than WebSocket for one-way push |
|
| Real-time | Server-Sent Events (SSE) | One-way push from backend to browser |
|
||||||
| Directory watching | chokidar | Cross-platform file system watcher |
|
| Directory watching | watchdog | Python filesystem watcher (Windows-compatible) |
|
||||||
| Database | SQLite (via `better-sqlite3`) | Embedded, no separate process, fast reads |
|
| Database | SQLite (via `sqlite3` stdlib) | WAL mode; single persistent connection with RLock |
|
||||||
| Frontend state | React Query (TanStack Query) | Cache, refetch, and SSE invalidation |
|
| Frontend state | TanStack React Query | Cache + SSE-driven invalidation |
|
||||||
| UI | TailwindCSS + shadcn/ui | Rapid, consistent component styling |
|
| UI | TailwindCSS v4 | Via `@tailwindcss/vite` plugin |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Project Structure
|
## 3. Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
Projects/
|
test_house_dashboard/
|
||||||
├── dashboard/ ← React frontend (existing)
|
├── DESIGNPLAN.md
|
||||||
│ ├── src/
|
├── IMPLEMENTATION_PLAN.md
|
||||||
│ │ ├── components/
|
├── CLAUDE.md ← project context for AI assistants
|
||||||
│ │ │ ├── StatCard.jsx – metric display card
|
|
||||||
│ │ │ ├── CompletionBar.jsx – progress bar with percentage
|
|
||||||
│ │ │ ├── TestTable.jsx – filterable test list
|
|
||||||
│ │ │ ├── FilterPanel.jsx – tag filter controls
|
|
||||||
│ │ │ └── TimeDisplay.jsx – elapsed / estimated time
|
|
||||||
│ │ ├── hooks/
|
|
||||||
│ │ │ ├── useTests.js – fetch + SSE subscription
|
|
||||||
│ │ │ └── useStats.js – derived stats from test data
|
|
||||||
│ │ ├── lib/
|
|
||||||
│ │ │ └── api.js – axios/fetch base client
|
|
||||||
│ │ ├── App.jsx
|
|
||||||
│ │ └── main.jsx
|
|
||||||
│ ├── package.json
|
|
||||||
│ └── vite.config.js – proxy /api → backend port
|
|
||||||
│
|
│
|
||||||
└── server/ ← NEW: Node.js backend
|
├── dashboard/ ← React frontend
|
||||||
├── index.js – Express app entry point
|
│ ├── vite.config.js – proxy /api → localhost:3001
|
||||||
├── db.js – SQLite schema + query helpers
|
│ ├── index.html
|
||||||
├── watcher.js – chokidar setup + change handlers
|
│ ├── package.json
|
||||||
├── parser.js – filename/foldername → tags
|
│ └── src/
|
||||||
├── scanner.js – full directory scan on startup
|
│ ├── App.jsx – root layout, SSE wiring via useStats
|
||||||
├── sse.js – SSE client registry + broadcast
|
│ ├── main.jsx
|
||||||
├── routes/
|
│ ├── components/
|
||||||
│ ├── tests.js – GET /api/tests, GET /api/tests/:id
|
│ │ ├── StatCard.jsx – metric card (label / value / sub)
|
||||||
│ ├── stats.js – GET /api/stats
|
│ │ ├── CompletionBar.jsx – progress bar
|
||||||
│ ├── config.js – GET/POST /api/config (directory paths)
|
│ │ ├── TestTable.jsx – filterable test list table
|
||||||
│ ├── browse.js – GET /api/browse (server-side directory browser)
|
│ │ ├── FilterPanel.jsx – dropdown tag filters
|
||||||
│ └── events.js – GET /api/events (SSE stream)
|
│ │ ├── TimeDisplay.jsx – elapsed / estimated time display
|
||||||
├── package.json
|
│ │ ├── StatusBadge.jsx – completed / pending indicator
|
||||||
└── .env – TARGET_DIR, RESULTS_DIR, PORT=3001
|
│ │ ├── ConfigModal.jsx – settings modal (dirs + avg time overrides)
|
||||||
|
│ │ └── DirectoryBrowser.jsx – server-side folder picker (uses /api/browse)
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ ├── useStats.js – fetches stats; owns the SSE EventSource
|
||||||
|
│ │ ├── useTests.js – fetches test list
|
||||||
|
│ │ └── useConfig.js – fetches/saves config
|
||||||
|
│ └── lib/
|
||||||
|
│ └── api.js – thin fetch wrapper; BASE = '/api'
|
||||||
|
│
|
||||||
|
└── server/ ← Python backend
|
||||||
|
├── app.py – Flask app, all routes, bootstrap(), __main__
|
||||||
|
├── db_py.py – SQLite schema, query helpers, config CRUD
|
||||||
|
├── scanner.py – full_scan(), scan_targets(), scan_results(), process_result_dir()
|
||||||
|
├── parser.py – parse_target_filename(), parse_result_filename(), etc.
|
||||||
|
├── watcher.py – watchdog observers for target + results dirs
|
||||||
|
├── sse_py.py – SSE queue registry + broadcast()
|
||||||
|
├── requirements.txt
|
||||||
|
├── dashboard.db – SQLite database (auto-created)
|
||||||
|
└── .venv/ – Python virtual environment
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -299,12 +305,16 @@ Allows the frontend to navigate the server's local filesystem so users can pick
|
|||||||
|
|
||||||
## 7. Real-time Updates
|
## 7. Real-time Updates
|
||||||
|
|
||||||
- The backend registers an SSE endpoint at `GET /api/events`
|
- Backend registers an SSE endpoint at `GET /api/events` (`sse_py.py`)
|
||||||
- chokidar watches both the target and results directories for `add`, `unlink`, and `change` events
|
- `watchdog` watches both target and results directories via two separate `Observer` instances (`watcher.py`)
|
||||||
- On any change, the backend re-scans the affected path, updates SQLite, and broadcasts an SSE event: `data: {"type":"update"}`
|
- On any change, the backend updates SQLite and calls `broadcast({"type": "update"})`, which pushes `data: {"type":"update"}\n\n` to all connected SSE clients
|
||||||
- The React frontend subscribes to the SSE stream; on receiving an `update` event it invalidates and refetches stats and test list via React Query
|
- `useStats.js` owns the `EventSource('/api/events')` connection; on `update` it calls `queryClient.invalidateQueries` for both `['stats']` and `['tests']`
|
||||||
|
|
||||||
**`unlink` handling for result directories**: when chokidar detects that a result directory has been deleted, the corresponding test row is reset — `completed = 0`, `completed_at = NULL`, `duration_seconds = NULL`. This ensures the dashboard always reflects the actual state of the filesystem.
|
**Windows watchdog quirk — `is_directory` unreliable on delete**: when a directory is deleted, watchdog calls `os.path.isdir()` to set `event.is_directory`, but the directory is already gone by then, so it returns `False`. All delete/move handlers therefore check `_is_direct_child_dir(path)` directly instead of relying on `event.is_directory`.
|
||||||
|
|
||||||
|
**Result directory deleted**: `reset_by_file_id_and_device(test_id, device)` resets `completed = 0`, `completed_at = NULL`, `duration_seconds = NULL`. broadcast fires unconditionally regardless of whether the folder name was parseable.
|
||||||
|
|
||||||
|
**Recycle Bin delete on Windows**: fires a `MovedEvent` (`src` = result dir, `dest` = `$RECYCLE.BIN\...`). Handled by `on_moved` via `_is_direct_child_dir` on the source path.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -370,34 +380,41 @@ All config values are POSTed to `POST /api/config` as key/value pairs and persis
|
|||||||
|
|
||||||
## 9. Vite Proxy Configuration
|
## 9. Vite Proxy Configuration
|
||||||
|
|
||||||
`vite.config.js` is updated to proxy all `/api` requests to the backend, so the React dev server and production build do not need CORS configuration:
|
`vite.config.js` proxies all `/api` requests to the Python backend:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// vite.config.js
|
|
||||||
server: {
|
server: {
|
||||||
|
host: true, // expose on LAN so other devices can reach the dev server
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': 'http://localhost:3001'
|
'/api': 'http://localhost:3001'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In production, Express serves the built React `dist/` as static files.
|
In production, Flask serves the built React `dist/` as static files via `send_from_directory`. Build with `npm run build` inside `dashboard/`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Implementation Phases
|
## 10. Implementation Status
|
||||||
|
|
||||||
### Phase 1 — Backend Foundation
|
### Completed
|
||||||
1. Initialize `server/package.json`; install `express`, `better-sqlite3`, `chokidar`, `dotenv`
|
- [x] Python Flask backend (`app.py`) with all API routes
|
||||||
2. Implement `db.js` — create schema, upsert helpers
|
- [x] SQLite schema, WAL mode, thread-safe helpers (`db_py.py`)
|
||||||
3. Implement `parser.js` — regex-based tag extraction from filenames and folder names
|
- [x] Tag parsing from target filenames and parent folder names (`parser.py`)
|
||||||
4. Implement `scanner.js` — walk target dir to build test list; walk results dir to mark completions
|
- [x] Full directory scan on startup / config change (`scanner.py`)
|
||||||
5. Implement `watcher.js` — chokidar watchers for both directories; call scanner on change
|
- [x] Watchdog filesystem watchers for both target and results dirs (`watcher.py`)
|
||||||
6. Implement `sse.js` — maintain SSE client set; broadcast on update
|
- [x] Windows `is_directory` timing bug fixed (check path directly)
|
||||||
7. Wire up Express routes and start server
|
- [x] Recycle Bin delete handled via `on_moved`
|
||||||
|
- [x] SSE broadcast with correct `\n\n` terminators (`sse_py.py`)
|
||||||
|
- [x] All frontend components and hooks
|
||||||
|
- [x] Config modal: directories always editable (lock removed)
|
||||||
|
- [x] Vite dev proxy + Flask static serving for production
|
||||||
|
- [x] Config migrated from `config.json` → SQLite on first run
|
||||||
|
|
||||||
### Phase 2 — Frontend Core
|
### Remaining / Future
|
||||||
1. Update `vite.config.js` with API proxy
|
- [ ] SSO / authentication (noted in design plan)
|
||||||
|
- [ ] Test detail view (click a row to see tput/RSSI breakdown)
|
||||||
|
- [ ] Production deployment docs (systemd / Task Scheduler service)
|
||||||
2. Install `@tanstack/react-query`, `axios`, `tailwindcss`, `lucide-react`
|
2. Install `@tanstack/react-query`, `axios`, `tailwindcss`, `lucide-react`
|
||||||
3. Build `api.js` — base fetch helpers + SSE subscription hook
|
3. Build `api.js` — base fetch helpers + SSE subscription hook
|
||||||
4. Build `useTests` and `useStats` hooks
|
4. Build `useTests` and `useStats` hooks
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ---- Build stage ----
|
||||||
|
FROM node:20 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Production stage ----
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built React app
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Replace nginx config
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
# Keep the /api prefix when forwarding (Flask routes are /api/*).
|
||||||
|
proxy_pass http://backend:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# SSE support (important for your EventSource)
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_buffering off;
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,6 @@ export default function ConfigModal({ onClose }) {
|
|||||||
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null
|
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null
|
||||||
const [saveError, setSaveError] = useState(null)
|
const [saveError, setSaveError] = useState(null)
|
||||||
|
|
||||||
// Directories are locked once both are saved — restart server to change them
|
|
||||||
const dirsLocked = !!(config?.target_dir && config?.results_dir)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
if (config) {
|
||||||
setForm({
|
setForm({
|
||||||
@@ -104,9 +101,6 @@ export default function ConfigModal({ onClose }) {
|
|||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-slate-300 text-xs uppercase tracking-widest">Directories</h3>
|
<h3 className="text-slate-300 text-xs uppercase tracking-widest">Directories</h3>
|
||||||
{dirsLocked && (
|
|
||||||
<span className="text-xs text-slate-500">🔒 Restart server to change directories</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{[
|
{[
|
||||||
@@ -116,15 +110,6 @@ export default function ConfigModal({ onClose }) {
|
|||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="text-slate-400 text-xs block mb-1">{label}</label>
|
<label className="text-slate-400 text-xs block mb-1">{label}</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{dirsLocked ? (
|
|
||||||
<div
|
|
||||||
title={form[key] ?? ''}
|
|
||||||
className="flex-1 border border-slate-700/50 bg-slate-800/40 text-slate-500 text-sm rounded-lg px-3 py-2 truncate cursor-default select-all font-mono"
|
|
||||||
>
|
|
||||||
{(form[key] ?? '').replace(/^(.+[/\\])([^/\\]+[/\\][^/\\]*)$/, '…$2')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form[key] ?? ''}
|
value={form[key] ?? ''}
|
||||||
@@ -138,8 +123,6 @@ export default function ConfigModal({ onClose }) {
|
|||||||
>
|
>
|
||||||
Browse
|
Browse
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = current ? current.replace(/\\/g, '/').split('/').filter(Boolean) : []
|
const breadcrumbs = current ? current.replace(/\\/g, '/').split('/').filter(Boolean) : []
|
||||||
|
const isUnixPath = !!current && current.startsWith('/')
|
||||||
|
|
||||||
|
function breadcrumbPathAt(index) {
|
||||||
|
const parts = breadcrumbs.slice(0, index + 1)
|
||||||
|
if (isUnixPath) {
|
||||||
|
return `/${parts.join('/')}`
|
||||||
|
}
|
||||||
|
return parts.join('\\') + (index === 0 ? '\\' : '')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||||
@@ -43,7 +52,7 @@ export default function DirectoryBrowser({ onSelect, onClose }) {
|
|||||||
<div className="px-4 py-2 border-b border-slate-800 flex items-center gap-1 text-xs text-slate-400 flex-wrap min-h-[36px]">
|
<div className="px-4 py-2 border-b border-slate-800 flex items-center gap-1 text-xs text-slate-400 flex-wrap min-h-[36px]">
|
||||||
<button onClick={() => navigate(null)} className="hover:text-slate-200">Drives</button>
|
<button onClick={() => navigate(null)} className="hover:text-slate-200">Drives</button>
|
||||||
{breadcrumbs.map((part, i) => {
|
{breadcrumbs.map((part, i) => {
|
||||||
const path = breadcrumbs.slice(0, i + 1).join('\\') + (i === 0 ? '\\' : '')
|
const path = breadcrumbPathAt(i)
|
||||||
return (
|
return (
|
||||||
<span key={i} className="flex items-center gap-1">
|
<span key={i} className="flex items-center gap-1">
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./server
|
||||||
|
container_name: dashboard-backend
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
environment:
|
||||||
|
- BROWSE_ROOTS=/host
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
# Host file browsing root inside container (adjust HOST_BROWSE_ROOT if needed)
|
||||||
|
- ${HOST_BROWSE_ROOT:-C:/Users}:/host
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./dashboard
|
||||||
|
container_name: dashboard-frontend
|
||||||
|
ports:
|
||||||
|
- "5173:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
dashboard.db*
|
||||||
+3
-4
@@ -1,6 +1,5 @@
|
|||||||
# Port the Express server listens on
|
# Flask backend port (default: 3001)
|
||||||
PORT=3001
|
PORT=3001
|
||||||
|
|
||||||
# Optional: seed directories on first run (can also be set via the UI settings)
|
# Note: Target and results directories are configured via the dashboard Settings UI
|
||||||
# TARGET_DIR=C:\path\to\target
|
# and stored in SQLite (dashboard.db), not in environment variables.
|
||||||
# RESULTS_DIR=C:\path\to\results
|
|
||||||
|
|||||||
+355
@@ -0,0 +1,355 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import Flask, Response, jsonify, request, send_from_directory
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from db_py import count_tests, del_config, get_all_tests, get_config, set_config
|
||||||
|
from scanner import full_scan
|
||||||
|
from sse_py import broadcast, stream_events
|
||||||
|
from watcher import start_watching
|
||||||
|
|
||||||
|
PORT = int(os.getenv("PORT", "3001"))
|
||||||
|
ALLOWED_KEYS = {
|
||||||
|
"target_dir",
|
||||||
|
"results_dir",
|
||||||
|
"avg_time_coe",
|
||||||
|
"avg_time_p2p",
|
||||||
|
"avg_time_p3p",
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DIST_DIR = BASE_DIR.parent / "dashboard" / "dist"
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder=str(DIST_DIR), static_url_path="")
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/tests")
|
||||||
|
def get_tests_route():
|
||||||
|
completed = request.args.get("completed")
|
||||||
|
interference = request.args.get("interference")
|
||||||
|
device = request.args.get("device")
|
||||||
|
rotation = request.args.get("rotation")
|
||||||
|
test_point = request.args.get("testPoint")
|
||||||
|
station = request.args.get("station")
|
||||||
|
band = request.args.get("band")
|
||||||
|
channel = request.args.get("channel")
|
||||||
|
bandwidth = request.args.get("bandwidth")
|
||||||
|
rssi = request.args.get("rssi")
|
||||||
|
direction = request.args.get("direction")
|
||||||
|
|
||||||
|
tests = get_all_tests()
|
||||||
|
|
||||||
|
if completed is not None:
|
||||||
|
completed_value = 1 if completed == "true" else 0
|
||||||
|
tests = [t for t in tests if t.get("completed") == completed_value]
|
||||||
|
if interference:
|
||||||
|
tests = [t for t in tests if t.get("interference") == interference]
|
||||||
|
if device:
|
||||||
|
tests = [t for t in tests if t.get("device") == device]
|
||||||
|
if rotation:
|
||||||
|
tests = [t for t in tests if t.get("rotation") == rotation]
|
||||||
|
if test_point:
|
||||||
|
tests = [t for t in tests if t.get("test_point") == test_point]
|
||||||
|
if station:
|
||||||
|
tests = [t for t in tests if t.get("station") == station]
|
||||||
|
if band:
|
||||||
|
tests = [t for t in tests if t.get("band") == band]
|
||||||
|
if channel:
|
||||||
|
tests = [t for t in tests if t.get("channel") == channel]
|
||||||
|
if bandwidth:
|
||||||
|
tests = [t for t in tests if t.get("bandwidth") == bandwidth]
|
||||||
|
if rssi:
|
||||||
|
tests = [t for t in tests if t.get("rssi") == rssi]
|
||||||
|
if direction:
|
||||||
|
tests = [t for t in tests if t.get("direction") == direction]
|
||||||
|
|
||||||
|
tests.sort(key=lambda item: ((item.get("interference") or ""), (item.get("test_id") or "")))
|
||||||
|
return jsonify(tests)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/stats")
|
||||||
|
def get_stats_route():
|
||||||
|
tests = get_all_tests()
|
||||||
|
|
||||||
|
total_completed = sum(1 for t in tests if t.get("completed"))
|
||||||
|
overall = {
|
||||||
|
"total": len(tests),
|
||||||
|
"completed": total_completed,
|
||||||
|
"completionRate": (total_completed / len(tests)) if tests else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
device_map = {}
|
||||||
|
for test in tests:
|
||||||
|
name = test.get("device")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name not in device_map:
|
||||||
|
device_map[name] = {"total": 0, "completed": 0}
|
||||||
|
device_map[name]["total"] += 1
|
||||||
|
if test.get("completed"):
|
||||||
|
device_map[name]["completed"] += 1
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
for name in sorted(device_map.keys()):
|
||||||
|
stats = device_map[name]
|
||||||
|
devices.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"total": stats["total"],
|
||||||
|
"completed": stats["completed"],
|
||||||
|
"completionRate": (stats["completed"] / stats["total"]) if stats["total"] else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed_seconds = 0
|
||||||
|
for test in tests:
|
||||||
|
duration = test.get("duration_seconds")
|
||||||
|
if test.get("completed") and duration is not None:
|
||||||
|
elapsed_seconds += duration
|
||||||
|
|
||||||
|
types = ["COE", "P2P", "P3P"]
|
||||||
|
by_type = {}
|
||||||
|
estimate_possible = True
|
||||||
|
estimated_remaining_seconds = 0
|
||||||
|
|
||||||
|
for test_type in types:
|
||||||
|
type_tests = [t for t in tests if t.get("interference") == test_type]
|
||||||
|
completed_tests = [t for t in type_tests if t.get("completed")]
|
||||||
|
with_duration = [t for t in completed_tests if t.get("duration_seconds") is not None]
|
||||||
|
remaining = len(type_tests) - len(completed_tests)
|
||||||
|
|
||||||
|
calc_avg = None
|
||||||
|
if with_duration:
|
||||||
|
calc_avg = sum(t.get("duration_seconds") for t in with_duration) / len(with_duration)
|
||||||
|
|
||||||
|
manual_value = get_config(f"avg_time_{test_type.lower()}")
|
||||||
|
|
||||||
|
avg_seconds = None
|
||||||
|
avg_source = None
|
||||||
|
|
||||||
|
if manual_value is not None:
|
||||||
|
avg_seconds = float(manual_value)
|
||||||
|
avg_source = "manual_override" if calc_avg is not None else "manual"
|
||||||
|
elif calc_avg is not None:
|
||||||
|
avg_seconds = calc_avg
|
||||||
|
avg_source = "calculated"
|
||||||
|
|
||||||
|
by_type[test_type] = {
|
||||||
|
"total": len(type_tests),
|
||||||
|
"completed": len(completed_tests),
|
||||||
|
"remaining": remaining,
|
||||||
|
"avgSeconds": avg_seconds,
|
||||||
|
"avgSource": avg_source,
|
||||||
|
}
|
||||||
|
|
||||||
|
if remaining > 0:
|
||||||
|
if avg_seconds is not None:
|
||||||
|
estimated_remaining_seconds += avg_seconds * remaining
|
||||||
|
else:
|
||||||
|
estimate_possible = False
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"overall": overall,
|
||||||
|
"devices": devices,
|
||||||
|
"timing": {
|
||||||
|
"elapsedSeconds": elapsed_seconds,
|
||||||
|
"estimatedRemainingSeconds": estimated_remaining_seconds if estimate_possible else None,
|
||||||
|
"byType": by_type,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/config")
|
||||||
|
def get_config_route():
|
||||||
|
config = {}
|
||||||
|
for key in ALLOWED_KEYS:
|
||||||
|
config[key] = get_config(key)
|
||||||
|
return jsonify(config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/config")
|
||||||
|
def set_config_route():
|
||||||
|
updates = request.get_json(silent=True)
|
||||||
|
if not isinstance(updates, dict):
|
||||||
|
return jsonify({"error": "Request body must be a JSON object"}), 400
|
||||||
|
|
||||||
|
dirs_changed = False
|
||||||
|
|
||||||
|
for key, value in updates.items():
|
||||||
|
if key not in ALLOWED_KEYS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if value in (None, ""):
|
||||||
|
del_config(key)
|
||||||
|
else:
|
||||||
|
set_config(key, str(value))
|
||||||
|
|
||||||
|
if key in {"target_dir", "results_dir"}:
|
||||||
|
dirs_changed = True
|
||||||
|
|
||||||
|
if dirs_changed:
|
||||||
|
target_dir = get_config("target_dir")
|
||||||
|
results_dir = get_config("results_dir")
|
||||||
|
try:
|
||||||
|
full_scan(target_dir, results_dir)
|
||||||
|
start_watching(target_dir, results_dir)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[config] fullScan error: {exc}")
|
||||||
|
return jsonify({"error": f"Scan failed: {exc}"}), 500
|
||||||
|
|
||||||
|
tests = get_all_tests()
|
||||||
|
completed = len([t for t in tests if t.get("completed")])
|
||||||
|
print(f"[config] Scan complete -> {len(tests)} tests found, {completed} completed")
|
||||||
|
broadcast({"type": "update"})
|
||||||
|
return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed})
|
||||||
|
|
||||||
|
return jsonify({"ok": True, "testCount": None, "completedCount": None})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/config/rescan")
|
||||||
|
def rescan_route():
|
||||||
|
target_dir = get_config("target_dir")
|
||||||
|
results_dir = get_config("results_dir")
|
||||||
|
if not target_dir or not results_dir:
|
||||||
|
return jsonify({"error": "Directories not configured"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_scan(target_dir, results_dir)
|
||||||
|
start_watching(target_dir, results_dir)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[config] rescan error: {exc}")
|
||||||
|
return jsonify({"error": f"Scan failed: {exc}"}), 500
|
||||||
|
|
||||||
|
tests = get_all_tests()
|
||||||
|
completed = len([t for t in tests if t.get("completed")])
|
||||||
|
print(f"[config] Rescan complete -> {len(tests)} tests, {completed} completed")
|
||||||
|
broadcast({"type": "update"})
|
||||||
|
return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/browse")
|
||||||
|
def browse_route():
|
||||||
|
def _configured_roots():
|
||||||
|
raw = os.getenv("BROWSE_ROOTS", "").strip()
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
roots = []
|
||||||
|
for part in raw.split(";"):
|
||||||
|
p = part.strip()
|
||||||
|
if not p:
|
||||||
|
continue
|
||||||
|
abs_p = os.path.abspath(p)
|
||||||
|
if os.path.isdir(abs_p):
|
||||||
|
roots.append(abs_p)
|
||||||
|
return roots
|
||||||
|
|
||||||
|
def _is_within_allowed_roots(path, roots):
|
||||||
|
if not roots:
|
||||||
|
return True
|
||||||
|
normalized = os.path.normcase(os.path.abspath(path))
|
||||||
|
for root in roots:
|
||||||
|
try:
|
||||||
|
if os.path.commonpath([normalized, root]) == root:
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
roots = _configured_roots()
|
||||||
|
req_path = request.args.get("path")
|
||||||
|
|
||||||
|
if not req_path:
|
||||||
|
if roots:
|
||||||
|
dirs = []
|
||||||
|
for root in roots:
|
||||||
|
display_name = os.path.basename(root.rstrip("/\\")) or root
|
||||||
|
dirs.append({"name": display_name, "path": root})
|
||||||
|
return jsonify({"path": None, "parent": None, "dirs": dirs})
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
dirs = []
|
||||||
|
for drive_idx in range(65, 91):
|
||||||
|
drive = f"{chr(drive_idx)}:\\"
|
||||||
|
if os.path.exists(drive):
|
||||||
|
dirs.append({"name": drive, "path": drive})
|
||||||
|
else:
|
||||||
|
dirs = [{"name": "/", "path": "/"}]
|
||||||
|
|
||||||
|
return jsonify({"path": None, "parent": None, "dirs": dirs})
|
||||||
|
|
||||||
|
if not os.path.exists(req_path):
|
||||||
|
return jsonify({"error": "Path does not exist"}), 400
|
||||||
|
if not os.path.isdir(req_path):
|
||||||
|
return jsonify({"error": "Path is not a directory"}), 400
|
||||||
|
if not _is_within_allowed_roots(req_path, roots):
|
||||||
|
return jsonify({"error": "Path is outside allowed browse roots"}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
dirs = []
|
||||||
|
for name in os.listdir(req_path):
|
||||||
|
child = os.path.join(req_path, name)
|
||||||
|
if os.path.isdir(child) and not name.startswith("."):
|
||||||
|
dirs.append({"name": name, "path": child})
|
||||||
|
|
||||||
|
dirs.sort(key=lambda item: item["name"].lower())
|
||||||
|
except OSError:
|
||||||
|
return jsonify({"error": "Cannot read directory"}), 403
|
||||||
|
|
||||||
|
parent = os.path.dirname(req_path)
|
||||||
|
at_root = os.path.normcase(parent) == os.path.normcase(req_path)
|
||||||
|
if roots and parent and not _is_within_allowed_roots(parent, roots):
|
||||||
|
parent = None
|
||||||
|
return jsonify({"path": req_path, "parent": None if at_root else parent, "dirs": dirs})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/events")
|
||||||
|
def events_route():
|
||||||
|
headers = {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
}
|
||||||
|
return Response(stream_events(), mimetype="text/event-stream", headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
@app.get("/<path:path>")
|
||||||
|
def static_or_spa(path=""):
|
||||||
|
if not DIST_DIR.exists():
|
||||||
|
return jsonify({"error": "Frontend dist not found"}), 404
|
||||||
|
|
||||||
|
if path and (DIST_DIR / path).is_file():
|
||||||
|
return send_from_directory(DIST_DIR, path)
|
||||||
|
return send_from_directory(DIST_DIR, "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap():
|
||||||
|
target_dir = get_config("target_dir")
|
||||||
|
results_dir = get_config("results_dir")
|
||||||
|
|
||||||
|
if target_dir and results_dir:
|
||||||
|
existing = count_tests()
|
||||||
|
if existing > 0:
|
||||||
|
print(f"[server] Resuming from DB -> {existing} tests already loaded.")
|
||||||
|
else:
|
||||||
|
print("[server] No cached data, scanning directories...")
|
||||||
|
full_scan(target_dir, results_dir)
|
||||||
|
tests = get_all_tests()
|
||||||
|
completed = len([t for t in tests if t.get("completed")])
|
||||||
|
print(f"[server] Scan complete -> {len(tests)} tests found, {completed} completed")
|
||||||
|
|
||||||
|
start_watching(target_dir, results_dir)
|
||||||
|
print("[server] Watching for changes.")
|
||||||
|
else:
|
||||||
|
print("[server] No directories configured -> open the dashboard settings to get started.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
bootstrap()
|
||||||
|
print(f"[server] Listening on http://0.0.0.0:{PORT}")
|
||||||
|
app.run(host="0.0.0.0", port=PORT, threaded=True)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"target_dir": "C:\\Users\\26005101\\Desktop\\CGW453\\CGW453",
|
|
||||||
"results_dir": "C:\\Users\\26005101\\Desktop\\MIA\\Test_results",
|
|
||||||
"avg_time_p3p": "6000"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
-147
@@ -1,147 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const Database = require('better-sqlite3');
|
|
||||||
|
|
||||||
const DB_PATH = path.join(__dirname, 'dashboard.db');
|
|
||||||
const CONFIG_JSON = path.join(__dirname, 'config.json');
|
|
||||||
|
|
||||||
const db = new Database(DB_PATH);
|
|
||||||
|
|
||||||
// Enable WAL for better concurrent read performance
|
|
||||||
db.pragma('journal_mode = WAL');
|
|
||||||
|
|
||||||
// ── Schema ───────────────────────────────────────────────────────────────────
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS config (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tests (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
test_id TEXT,
|
|
||||||
parent_dir TEXT,
|
|
||||||
filename TEXT,
|
|
||||||
interference TEXT,
|
|
||||||
device TEXT,
|
|
||||||
rotation TEXT,
|
|
||||||
test_point TEXT,
|
|
||||||
station TEXT,
|
|
||||||
band TEXT,
|
|
||||||
channel TEXT,
|
|
||||||
bandwidth TEXT,
|
|
||||||
rssi TEXT,
|
|
||||||
direction TEXT,
|
|
||||||
completed INTEGER NOT NULL DEFAULT 0,
|
|
||||||
completed_at TEXT,
|
|
||||||
duration_seconds REAL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tests_test_id ON tests (test_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tests_device ON tests (device);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Add new columns to existing databases (idempotent)
|
|
||||||
for (const colDef of ['tput_mbps REAL', 'dl_rssi_dbm REAL', 'ul_rssi_dbm REAL', 'tput_results TEXT']) {
|
|
||||||
try { db.exec(`ALTER TABLE tests ADD COLUMN ${colDef}`); } catch { /* already exists */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── One-time migration from config.json ──────────────────────────────────────
|
|
||||||
{
|
|
||||||
const alreadyMigrated = db.prepare("SELECT COUNT(*) AS n FROM config").get().n > 0;
|
|
||||||
if (!alreadyMigrated && fs.existsSync(CONFIG_JSON)) {
|
|
||||||
try {
|
|
||||||
const legacy = JSON.parse(fs.readFileSync(CONFIG_JSON, 'utf8'));
|
|
||||||
const insert = db.prepare('INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)');
|
|
||||||
const migrate = db.transaction((obj) => {
|
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
|
||||||
if (v != null) insert.run(k, String(v));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
migrate(legacy);
|
|
||||||
console.log('[db] Migrated config.json → SQLite');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[db] Could not migrate config.json:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Config ───────────────────────────────────────────────────────────────────
|
|
||||||
const _stmtGetConfig = db.prepare('SELECT value FROM config WHERE key = ?');
|
|
||||||
const _stmtSetConfig = db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)');
|
|
||||||
const _stmtDelConfig = db.prepare('DELETE FROM config WHERE key = ?');
|
|
||||||
|
|
||||||
function getConfig(key) { return _stmtGetConfig.get(key)?.value ?? null; }
|
|
||||||
function setConfig(key, value) { _stmtSetConfig.run(key, value); }
|
|
||||||
function delConfig(key) { _stmtDelConfig.run(key); }
|
|
||||||
|
|
||||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
||||||
const _stmtUpsertTest = db.prepare(`
|
|
||||||
INSERT INTO tests
|
|
||||||
(id, test_id, parent_dir, filename, interference, device, rotation,
|
|
||||||
test_point, station, band, channel, bandwidth, rssi, direction)
|
|
||||||
VALUES
|
|
||||||
(@id, @test_id, @parent_dir, @filename, @interference, @device, @rotation,
|
|
||||||
@test_point, @station, @band, @channel, @bandwidth, @rssi, @direction)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
test_id = excluded.test_id,
|
|
||||||
parent_dir = excluded.parent_dir,
|
|
||||||
filename = excluded.filename,
|
|
||||||
interference = excluded.interference,
|
|
||||||
device = excluded.device,
|
|
||||||
rotation = excluded.rotation,
|
|
||||||
test_point = excluded.test_point,
|
|
||||||
station = excluded.station,
|
|
||||||
band = excluded.band,
|
|
||||||
channel = excluded.channel,
|
|
||||||
bandwidth = excluded.bandwidth,
|
|
||||||
rssi = excluded.rssi,
|
|
||||||
direction = excluded.direction
|
|
||||||
-- completed / completed_at / duration_seconds intentionally preserved
|
|
||||||
`);
|
|
||||||
|
|
||||||
const _stmtMarkCompleted = db.prepare(`
|
|
||||||
UPDATE tests
|
|
||||||
SET completed = 1, completed_at = ?, duration_seconds = ?, tput_results = ?
|
|
||||||
WHERE test_id = ? AND (? IS NULL OR device = ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const _stmtResetTest = db.prepare(`
|
|
||||||
UPDATE tests
|
|
||||||
SET completed = 0, completed_at = NULL, duration_seconds = NULL, tput_results = NULL
|
|
||||||
WHERE test_id = ? AND (? IS NULL OR device = ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const _stmtGetStation = db.prepare('SELECT station FROM tests WHERE test_id = ? AND device = ? LIMIT 1');
|
|
||||||
|
|
||||||
const _stmtClearTests = db.prepare('DELETE FROM tests');
|
|
||||||
const _stmtGetAllTests = db.prepare('SELECT * FROM tests');
|
|
||||||
const _stmtCountTests = db.prepare('SELECT COUNT(*) AS n FROM tests');
|
|
||||||
|
|
||||||
function upsertTest(test) {
|
|
||||||
_stmtUpsertTest.run(test);
|
|
||||||
}
|
|
||||||
|
|
||||||
function markCompleted(test_id, device, completed_at, duration_seconds, tputResults = null) {
|
|
||||||
const json = tputResults && tputResults.length > 0 ? JSON.stringify(tputResults) : null;
|
|
||||||
_stmtMarkCompleted.run(completed_at, duration_seconds, json, test_id, device, device);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStationForTest(test_id, device) {
|
|
||||||
return _stmtGetStation.get(test_id, device)?.station ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetByFileIdAndDevice(test_id, device) {
|
|
||||||
_stmtResetTest.run(test_id, device, device);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTests() { _stmtClearTests.run(); }
|
|
||||||
function getAllTests() { return _stmtGetAllTests.all(); }
|
|
||||||
function countTests() { return _stmtCountTests.get().n; }
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getConfig, setConfig, delConfig,
|
|
||||||
upsertTest, markCompleted, resetByFileIdAndDevice, clearTests, getAllTests, countTests,
|
|
||||||
getStationForTest,
|
|
||||||
};
|
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DB_PATH = os.path.join(BASE_DIR, "dashboard.db")
|
||||||
|
CONFIG_JSON = os.path.join(BASE_DIR, "config.json")
|
||||||
|
|
||||||
|
_conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
_conn.row_factory = sqlite3.Row
|
||||||
|
_lock = threading.RLock()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _tx():
|
||||||
|
with _lock:
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
_conn.commit()
|
||||||
|
except Exception:
|
||||||
|
_conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _init_db():
|
||||||
|
with _tx():
|
||||||
|
_conn.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
_conn.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tests (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
test_id TEXT,
|
||||||
|
parent_dir TEXT,
|
||||||
|
filename TEXT,
|
||||||
|
interference TEXT,
|
||||||
|
device TEXT,
|
||||||
|
rotation TEXT,
|
||||||
|
test_point TEXT,
|
||||||
|
station TEXT,
|
||||||
|
band TEXT,
|
||||||
|
channel TEXT,
|
||||||
|
bandwidth TEXT,
|
||||||
|
rssi TEXT,
|
||||||
|
direction TEXT,
|
||||||
|
completed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
completed_at TEXT,
|
||||||
|
duration_seconds REAL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tests_test_id ON tests (test_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tests_device ON tests (device);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
for col_def in [
|
||||||
|
"tput_mbps REAL",
|
||||||
|
"dl_rssi_dbm REAL",
|
||||||
|
"ul_rssi_dbm REAL",
|
||||||
|
"tput_results TEXT",
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
_conn.execute(f"ALTER TABLE tests ADD COLUMN {col_def}")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(key):
|
||||||
|
with _lock:
|
||||||
|
row = _conn.execute("SELECT value FROM config WHERE key = ?", (key,)).fetchone()
|
||||||
|
return row["value"] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def set_config(key, value):
|
||||||
|
with _tx():
|
||||||
|
_conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def del_config(key):
|
||||||
|
with _tx():
|
||||||
|
_conn.execute("DELETE FROM config WHERE key = ?", (key,))
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_test(test):
|
||||||
|
with _tx():
|
||||||
|
_conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO tests
|
||||||
|
(id, test_id, parent_dir, filename, interference, device, rotation,
|
||||||
|
test_point, station, band, channel, bandwidth, rssi, direction)
|
||||||
|
VALUES
|
||||||
|
(:id, :test_id, :parent_dir, :filename, :interference, :device, :rotation,
|
||||||
|
:test_point, :station, :band, :channel, :bandwidth, :rssi, :direction)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
test_id = excluded.test_id,
|
||||||
|
parent_dir = excluded.parent_dir,
|
||||||
|
filename = excluded.filename,
|
||||||
|
interference = excluded.interference,
|
||||||
|
device = excluded.device,
|
||||||
|
rotation = excluded.rotation,
|
||||||
|
test_point = excluded.test_point,
|
||||||
|
station = excluded.station,
|
||||||
|
band = excluded.band,
|
||||||
|
channel = excluded.channel,
|
||||||
|
bandwidth = excluded.bandwidth,
|
||||||
|
rssi = excluded.rssi,
|
||||||
|
direction = excluded.direction
|
||||||
|
""",
|
||||||
|
test,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_completed(test_id, device, completed_at, duration_seconds, tput_results=None):
|
||||||
|
json_value = json.dumps(tput_results) if tput_results else None
|
||||||
|
with _tx():
|
||||||
|
_conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE tests
|
||||||
|
SET completed = 1,
|
||||||
|
completed_at = ?,
|
||||||
|
duration_seconds = ?,
|
||||||
|
tput_results = ?
|
||||||
|
WHERE test_id = ?
|
||||||
|
AND (? IS NULL OR device = ?)
|
||||||
|
""",
|
||||||
|
(completed_at, duration_seconds, json_value, test_id, device, device),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_station_for_test(test_id, device):
|
||||||
|
with _lock:
|
||||||
|
row = _conn.execute(
|
||||||
|
"SELECT station FROM tests WHERE test_id = ? AND device = ? LIMIT 1",
|
||||||
|
(test_id, device),
|
||||||
|
).fetchone()
|
||||||
|
return row["station"] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def reset_by_file_id_and_device(test_id, device):
|
||||||
|
with _tx():
|
||||||
|
_conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE tests
|
||||||
|
SET completed = 0,
|
||||||
|
completed_at = NULL,
|
||||||
|
duration_seconds = NULL,
|
||||||
|
tput_results = NULL
|
||||||
|
WHERE test_id = ?
|
||||||
|
AND (? IS NULL OR device = ?)
|
||||||
|
""",
|
||||||
|
(test_id, device, device),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_tests():
|
||||||
|
with _tx():
|
||||||
|
_conn.execute("DELETE FROM tests")
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_tests():
|
||||||
|
with _lock:
|
||||||
|
rows = _conn.execute("SELECT * FROM tests").fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def count_tests():
|
||||||
|
with _lock:
|
||||||
|
row = _conn.execute("SELECT COUNT(*) AS n FROM tests").fetchone()
|
||||||
|
return row["n"]
|
||||||
|
|
||||||
|
|
||||||
|
_init_db()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working dir
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy requirements first (for caching)
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy app code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose Flask port
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
# Environment vars
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Run app
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
require('dotenv').config();
|
|
||||||
const express = require('express');
|
|
||||||
const cors = require('cors');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const { getConfig, countTests } = require('./db');
|
|
||||||
const { fullScan } = require('./scanner');
|
|
||||||
const { startWatching } = require('./watcher')
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3001;
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// ── API routes ──────────────────────────────────────────────────────────────
|
|
||||||
app.use('/api/tests', require('./routes/tests'));
|
|
||||||
app.use('/api/stats', require('./routes/stats'));
|
|
||||||
app.use('/api/config', require('./routes/config'));
|
|
||||||
app.use('/api/browse', require('./routes/browse'));
|
|
||||||
app.use('/api/events', require('./routes/events'));
|
|
||||||
|
|
||||||
// ── Serve built React frontend in production ────────────────────────────────
|
|
||||||
const distPath = path.resolve(__dirname, '../dashboard/dist');
|
|
||||||
if (fs.existsSync(distPath)) {
|
|
||||||
app.use(express.static(distPath));
|
|
||||||
app.get('*', (req, res) => res.sendFile(path.join(distPath, 'index.html')));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Start ───────────────────────────────────────────────────────────────────
|
|
||||||
async function start() {
|
|
||||||
const targetDir = getConfig('target_dir');
|
|
||||||
const resultsDir = getConfig('results_dir');
|
|
||||||
|
|
||||||
if (targetDir && resultsDir) {
|
|
||||||
const existing = countTests();
|
|
||||||
if (existing > 0) {
|
|
||||||
console.log(`[server] Resuming from DB — ${existing} tests already loaded.`);
|
|
||||||
} else {
|
|
||||||
console.log('[server] No cached data, scanning directories...');
|
|
||||||
await fullScan(targetDir, resultsDir);
|
|
||||||
const { getAllTests } = require('./db');
|
|
||||||
const tests = getAllTests();
|
|
||||||
console.log(`[server] Scan complete — ${tests.length} tests found, ${tests.filter(t => t.completed).length} completed`);
|
|
||||||
}
|
|
||||||
startWatching(targetDir, resultsDir);
|
|
||||||
console.log('[server] Watching for changes.');
|
|
||||||
} else {
|
|
||||||
console.log('[server] No directories configured — open the dashboard settings to get started.');
|
|
||||||
}
|
|
||||||
|
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
|
||||||
console.log(`[server] Listening on http://0.0.0.0:${PORT}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start().catch(console.error);
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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/../mime/cli.js" "$@"
|
|
||||||
else
|
|
||||||
exec node "$basedir/../mime/cli.js" "$@"
|
|
||||||
fi
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
@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%\..\mime\cli.js" %*
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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/../mime/cli.js" $args
|
|
||||||
} else {
|
|
||||||
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
} else {
|
|
||||||
# Support pipeline input
|
|
||||||
if ($MyInvocation.ExpectingInput) {
|
|
||||||
$input | & "node$exe" "$basedir/../mime/cli.js" $args
|
|
||||||
} else {
|
|
||||||
& "node$exe" "$basedir/../mime/cli.js" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
}
|
|
||||||
exit $ret
|
|
||||||
-5349
File diff suppressed because it is too large
Load Diff
-243
@@ -1,243 +0,0 @@
|
|||||||
1.3.8 / 2022-02-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.34
|
|
||||||
- deps: mime-db@~1.51.0
|
|
||||||
* deps: negotiator@0.6.3
|
|
||||||
|
|
||||||
1.3.7 / 2019-04-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.6.2
|
|
||||||
- Fix sorting charset, encoding, and language with extra parameters
|
|
||||||
|
|
||||||
1.3.6 / 2019-04-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.24
|
|
||||||
- deps: mime-db@~1.40.0
|
|
||||||
|
|
||||||
1.3.5 / 2018-02-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.18
|
|
||||||
- deps: mime-db@~1.33.0
|
|
||||||
|
|
||||||
1.3.4 / 2017-08-22
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.16
|
|
||||||
- deps: mime-db@~1.29.0
|
|
||||||
|
|
||||||
1.3.3 / 2016-05-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.11
|
|
||||||
- deps: mime-db@~1.23.0
|
|
||||||
* deps: negotiator@0.6.1
|
|
||||||
- perf: improve `Accept` parsing speed
|
|
||||||
- perf: improve `Accept-Charset` parsing speed
|
|
||||||
- perf: improve `Accept-Encoding` parsing speed
|
|
||||||
- perf: improve `Accept-Language` parsing speed
|
|
||||||
|
|
||||||
1.3.2 / 2016-03-08
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.10
|
|
||||||
- Fix extension of `application/dash+xml`
|
|
||||||
- Update primary extension for `audio/mp4`
|
|
||||||
- deps: mime-db@~1.22.0
|
|
||||||
|
|
||||||
1.3.1 / 2016-01-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.9
|
|
||||||
- deps: mime-db@~1.21.0
|
|
||||||
|
|
||||||
1.3.0 / 2015-09-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.7
|
|
||||||
- deps: mime-db@~1.19.0
|
|
||||||
* deps: negotiator@0.6.0
|
|
||||||
- Fix including type extensions in parameters in `Accept` parsing
|
|
||||||
- Fix parsing `Accept` parameters with quoted equals
|
|
||||||
- Fix parsing `Accept` parameters with quoted semicolons
|
|
||||||
- Lazy-load modules from main entry point
|
|
||||||
- perf: delay type concatenation until needed
|
|
||||||
- perf: enable strict mode
|
|
||||||
- perf: hoist regular expressions
|
|
||||||
- perf: remove closures getting spec properties
|
|
||||||
- perf: remove a closure from media type parsing
|
|
||||||
- perf: remove property delete from media type parsing
|
|
||||||
|
|
||||||
1.2.13 / 2015-09-06
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.6
|
|
||||||
- deps: mime-db@~1.18.0
|
|
||||||
|
|
||||||
1.2.12 / 2015-07-30
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.4
|
|
||||||
- deps: mime-db@~1.16.0
|
|
||||||
|
|
||||||
1.2.11 / 2015-07-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.3
|
|
||||||
- deps: mime-db@~1.15.0
|
|
||||||
|
|
||||||
1.2.10 / 2015-07-01
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.2
|
|
||||||
- deps: mime-db@~1.14.0
|
|
||||||
|
|
||||||
1.2.9 / 2015-06-08
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.1
|
|
||||||
- perf: fix deopt during mapping
|
|
||||||
|
|
||||||
1.2.8 / 2015-06-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.1.0
|
|
||||||
- deps: mime-db@~1.13.0
|
|
||||||
* perf: avoid argument reassignment & argument slice
|
|
||||||
* perf: avoid negotiator recursive construction
|
|
||||||
* perf: enable strict mode
|
|
||||||
* perf: remove unnecessary bitwise operator
|
|
||||||
|
|
||||||
1.2.7 / 2015-05-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.5.3
|
|
||||||
- Fix media type parameter matching to be case-insensitive
|
|
||||||
|
|
||||||
1.2.6 / 2015-05-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.11
|
|
||||||
- deps: mime-db@~1.9.1
|
|
||||||
* deps: negotiator@0.5.2
|
|
||||||
- Fix comparing media types with quoted values
|
|
||||||
- Fix splitting media types with quoted commas
|
|
||||||
|
|
||||||
1.2.5 / 2015-03-13
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.10
|
|
||||||
- deps: mime-db@~1.8.0
|
|
||||||
|
|
||||||
1.2.4 / 2015-02-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Support Node.js 0.6
|
|
||||||
* deps: mime-types@~2.0.9
|
|
||||||
- deps: mime-db@~1.7.0
|
|
||||||
* deps: negotiator@0.5.1
|
|
||||||
- Fix preference sorting to be stable for long acceptable lists
|
|
||||||
|
|
||||||
1.2.3 / 2015-01-31
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.8
|
|
||||||
- deps: mime-db@~1.6.0
|
|
||||||
|
|
||||||
1.2.2 / 2014-12-30
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.7
|
|
||||||
- deps: mime-db@~1.5.0
|
|
||||||
|
|
||||||
1.2.1 / 2014-12-30
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.5
|
|
||||||
- deps: mime-db@~1.3.1
|
|
||||||
|
|
||||||
1.2.0 / 2014-12-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.5.0
|
|
||||||
- Fix list return order when large accepted list
|
|
||||||
- Fix missing identity encoding when q=0 exists
|
|
||||||
- Remove dynamic building of Negotiator class
|
|
||||||
|
|
||||||
1.1.4 / 2014-12-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.4
|
|
||||||
- deps: mime-db@~1.3.0
|
|
||||||
|
|
||||||
1.1.3 / 2014-11-09
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.3
|
|
||||||
- deps: mime-db@~1.2.0
|
|
||||||
|
|
||||||
1.1.2 / 2014-10-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.9
|
|
||||||
- Fix error when media type has invalid parameter
|
|
||||||
|
|
||||||
1.1.1 / 2014-09-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: mime-types@~2.0.2
|
|
||||||
- deps: mime-db@~1.1.0
|
|
||||||
* deps: negotiator@0.4.8
|
|
||||||
- Fix all negotiations to be case-insensitive
|
|
||||||
- Stable sort preferences of same quality according to client order
|
|
||||||
|
|
||||||
1.1.0 / 2014-09-02
|
|
||||||
==================
|
|
||||||
|
|
||||||
* update `mime-types`
|
|
||||||
|
|
||||||
1.0.7 / 2014-07-04
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix wrong type returned from `type` when match after unknown extension
|
|
||||||
|
|
||||||
1.0.6 / 2014-06-24
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.7
|
|
||||||
|
|
||||||
1.0.5 / 2014-06-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix crash when unknown extension given
|
|
||||||
|
|
||||||
1.0.4 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* use `mime-types`
|
|
||||||
|
|
||||||
1.0.3 / 2014-06-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: negotiator@0.4.6
|
|
||||||
- Order by specificity when quality is the same
|
|
||||||
|
|
||||||
1.0.2 / 2014-05-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix interpretation when header not in request
|
|
||||||
* deps: pin negotiator@0.4.5
|
|
||||||
|
|
||||||
1.0.1 / 2014-01-18
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Identity encoding isn't always acceptable
|
|
||||||
* deps: negotiator@~0.4.0
|
|
||||||
|
|
||||||
1.0.0 / 2013-12-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Genesis
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
|
||||||
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
|
||||||
|
|
||||||
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.
|
|
||||||
-140
@@ -1,140 +0,0 @@
|
|||||||
# accepts
|
|
||||||
|
|
||||||
[![NPM Version][npm-version-image]][npm-url]
|
|
||||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
|
||||||
[![Node.js Version][node-version-image]][node-version-url]
|
|
||||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
|
||||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
|
||||||
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
|
||||||
|
|
||||||
In addition to negotiator, it allows:
|
|
||||||
|
|
||||||
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
|
||||||
as well as `('text/html', 'application/json')`.
|
|
||||||
- Allows type shorthands such as `json`.
|
|
||||||
- Returns `false` when no types match
|
|
||||||
- Treats non-existent headers as `*`
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
|
||||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
|
||||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm install accepts
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```js
|
|
||||||
var accepts = require('accepts')
|
|
||||||
```
|
|
||||||
|
|
||||||
### accepts(req)
|
|
||||||
|
|
||||||
Create a new `Accepts` object for the given `req`.
|
|
||||||
|
|
||||||
#### .charset(charsets)
|
|
||||||
|
|
||||||
Return the first accepted charset. If nothing in `charsets` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .charsets()
|
|
||||||
|
|
||||||
Return the charsets that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .encoding(encodings)
|
|
||||||
|
|
||||||
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .encodings()
|
|
||||||
|
|
||||||
Return the encodings that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .language(languages)
|
|
||||||
|
|
||||||
Return the first accepted language. If nothing in `languages` is accepted,
|
|
||||||
then `false` is returned.
|
|
||||||
|
|
||||||
#### .languages()
|
|
||||||
|
|
||||||
Return the languages that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
#### .type(types)
|
|
||||||
|
|
||||||
Return the first accepted type (and it is returned as the same text as what
|
|
||||||
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
|
||||||
is returned.
|
|
||||||
|
|
||||||
The `types` array can contain full MIME types or file extensions. Any value
|
|
||||||
that is not a full MIME types is passed to `require('mime-types').lookup`.
|
|
||||||
|
|
||||||
#### .types()
|
|
||||||
|
|
||||||
Return the types that the request accepts, in the order of the client's
|
|
||||||
preference (most preferred first).
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Simple type negotiation
|
|
||||||
|
|
||||||
This simple example shows how to use `accepts` to return a different typed
|
|
||||||
respond body based on what the client wants to accept. The server lists it's
|
|
||||||
preferences in order and will get back the best match between the client and
|
|
||||||
server.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var accepts = require('accepts')
|
|
||||||
var http = require('http')
|
|
||||||
|
|
||||||
function app (req, res) {
|
|
||||||
var accept = accepts(req)
|
|
||||||
|
|
||||||
// the order of this list is significant; should be server preferred order
|
|
||||||
switch (accept.type(['json', 'html'])) {
|
|
||||||
case 'json':
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
|
||||||
res.write('{"hello":"world!"}')
|
|
||||||
break
|
|
||||||
case 'html':
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
|
||||||
res.write('<b>hello, world!</b>')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// the fallback is text/plain, so no need to specify it above
|
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
|
||||||
res.write('hello, world!')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
http.createServer(app).listen(3000)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can test this out with the cURL program:
|
|
||||||
```sh
|
|
||||||
curl -I -H'Accept: text/html' http://localhost:3000/
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
|
||||||
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
|
||||||
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
|
||||||
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
|
||||||
[node-version-image]: https://badgen.net/npm/node/accepts
|
|
||||||
[node-version-url]: https://nodejs.org/en/download
|
|
||||||
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
|
||||||
[npm-url]: https://npmjs.org/package/accepts
|
|
||||||
[npm-version-image]: https://badgen.net/npm/v/accepts
|
|
||||||
-238
@@ -1,238 +0,0 @@
|
|||||||
/*!
|
|
||||||
* accepts
|
|
||||||
* Copyright(c) 2014 Jonathan Ong
|
|
||||||
* Copyright(c) 2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Negotiator = require('negotiator')
|
|
||||||
var mime = require('mime-types')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = Accepts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Accepts object for the given req.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Accepts (req) {
|
|
||||||
if (!(this instanceof Accepts)) {
|
|
||||||
return new Accepts(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.headers = req.headers
|
|
||||||
this.negotiator = new Negotiator(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given `type(s)` is acceptable, returning
|
|
||||||
* the best match when true, otherwise `undefined`, in which
|
|
||||||
* case you should respond with 406 "Not Acceptable".
|
|
||||||
*
|
|
||||||
* The `type` value may be a single mime type string
|
|
||||||
* such as "application/json", the extension name
|
|
||||||
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
|
||||||
* or array is given the _best_ match, if any is returned.
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
*
|
|
||||||
* // Accept: text/html
|
|
||||||
* this.types('html');
|
|
||||||
* // => "html"
|
|
||||||
*
|
|
||||||
* // Accept: text/*, application/json
|
|
||||||
* this.types('html');
|
|
||||||
* // => "html"
|
|
||||||
* this.types('text/html');
|
|
||||||
* // => "text/html"
|
|
||||||
* this.types('json', 'text');
|
|
||||||
* // => "json"
|
|
||||||
* this.types('application/json');
|
|
||||||
* // => "application/json"
|
|
||||||
*
|
|
||||||
* // Accept: text/*, application/json
|
|
||||||
* this.types('image/png');
|
|
||||||
* this.types('png');
|
|
||||||
* // => undefined
|
|
||||||
*
|
|
||||||
* // Accept: text/*;q=.5, application/json
|
|
||||||
* this.types(['html', 'json']);
|
|
||||||
* this.types('html', 'json');
|
|
||||||
* // => "json"
|
|
||||||
*
|
|
||||||
* @param {String|Array} types...
|
|
||||||
* @return {String|Array|Boolean}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.type =
|
|
||||||
Accepts.prototype.types = function (types_) {
|
|
||||||
var types = types_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (types && !Array.isArray(types)) {
|
|
||||||
types = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < types.length; i++) {
|
|
||||||
types[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no types, return all requested types
|
|
||||||
if (!types || types.length === 0) {
|
|
||||||
return this.negotiator.mediaTypes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no accept header, return first given type
|
|
||||||
if (!this.headers.accept) {
|
|
||||||
return types[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
var mimes = types.map(extToMime)
|
|
||||||
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
|
||||||
var first = accepts[0]
|
|
||||||
|
|
||||||
return first
|
|
||||||
? types[mimes.indexOf(first)]
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted encodings or best fit based on `encodings`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Encoding: gzip, deflate`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['gzip', 'deflate']
|
|
||||||
*
|
|
||||||
* @param {String|Array} encodings...
|
|
||||||
* @return {String|Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.encoding =
|
|
||||||
Accepts.prototype.encodings = function (encodings_) {
|
|
||||||
var encodings = encodings_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (encodings && !Array.isArray(encodings)) {
|
|
||||||
encodings = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < encodings.length; i++) {
|
|
||||||
encodings[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no encodings, return all requested encodings
|
|
||||||
if (!encodings || encodings.length === 0) {
|
|
||||||
return this.negotiator.encodings()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.encodings(encodings)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted charsets or best fit based on `charsets`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['utf-8', 'utf-7', 'iso-8859-1']
|
|
||||||
*
|
|
||||||
* @param {String|Array} charsets...
|
|
||||||
* @return {String|Array}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.charset =
|
|
||||||
Accepts.prototype.charsets = function (charsets_) {
|
|
||||||
var charsets = charsets_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (charsets && !Array.isArray(charsets)) {
|
|
||||||
charsets = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < charsets.length; i++) {
|
|
||||||
charsets[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no charsets, return all requested charsets
|
|
||||||
if (!charsets || charsets.length === 0) {
|
|
||||||
return this.negotiator.charsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.charsets(charsets)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return accepted languages or best fit based on `langs`.
|
|
||||||
*
|
|
||||||
* Given `Accept-Language: en;q=0.8, es, pt`
|
|
||||||
* an array sorted by quality is returned:
|
|
||||||
*
|
|
||||||
* ['es', 'pt', 'en']
|
|
||||||
*
|
|
||||||
* @param {String|Array} langs...
|
|
||||||
* @return {Array|String}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Accepts.prototype.lang =
|
|
||||||
Accepts.prototype.langs =
|
|
||||||
Accepts.prototype.language =
|
|
||||||
Accepts.prototype.languages = function (languages_) {
|
|
||||||
var languages = languages_
|
|
||||||
|
|
||||||
// support flattened arguments
|
|
||||||
if (languages && !Array.isArray(languages)) {
|
|
||||||
languages = new Array(arguments.length)
|
|
||||||
for (var i = 0; i < languages.length; i++) {
|
|
||||||
languages[i] = arguments[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no languages, return all requested languages
|
|
||||||
if (!languages || languages.length === 0) {
|
|
||||||
return this.negotiator.languages()
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.negotiator.languages(languages)[0] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert extnames to mime.
|
|
||||||
*
|
|
||||||
* @param {String} type
|
|
||||||
* @return {String}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function extToMime (type) {
|
|
||||||
return type.indexOf('/') === -1
|
|
||||||
? mime.lookup(type)
|
|
||||||
: type
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if mime is valid.
|
|
||||||
*
|
|
||||||
* @param {String} type
|
|
||||||
* @return {String}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function validMime (type) {
|
|
||||||
return typeof type === 'string'
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "accepts",
|
|
||||||
"description": "Higher-level content negotiation",
|
|
||||||
"version": "1.3.8",
|
|
||||||
"contributors": [
|
|
||||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
||||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": "jshttp/accepts",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"deep-equal": "1.0.1",
|
|
||||||
"eslint": "7.32.0",
|
|
||||||
"eslint-config-standard": "14.1.1",
|
|
||||||
"eslint-plugin-import": "2.25.4",
|
|
||||||
"eslint-plugin-markdown": "2.2.1",
|
|
||||||
"eslint-plugin-node": "11.1.0",
|
|
||||||
"eslint-plugin-promise": "4.3.1",
|
|
||||||
"eslint-plugin-standard": "4.1.0",
|
|
||||||
"mocha": "9.2.0",
|
|
||||||
"nyc": "15.1.0"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"LICENSE",
|
|
||||||
"HISTORY.md",
|
|
||||||
"index.js"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "mocha --reporter spec --check-leaks --bail test/",
|
|
||||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
|
||||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"content",
|
|
||||||
"negotiation",
|
|
||||||
"accept",
|
|
||||||
"accepts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)
|
|
||||||
|
|
||||||
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.
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
# Array Flatten
|
|
||||||
|
|
||||||
[![NPM version][npm-image]][npm-url]
|
|
||||||
[![NPM downloads][downloads-image]][downloads-url]
|
|
||||||
[![Build status][travis-image]][travis-url]
|
|
||||||
[![Test coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
> Flatten an array of nested arrays into a single flat array. Accepts an optional depth.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install array-flatten --save
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var flatten = require('array-flatten')
|
|
||||||
|
|
||||||
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9])
|
|
||||||
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
||||||
|
|
||||||
flatten([1, [2, [3, [4, [5], 6], 7], 8], 9], 2)
|
|
||||||
//=> [1, 2, 3, [4, [5], 6], 7, 8, 9]
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
flatten(arguments) //=> [1, 2, 3]
|
|
||||||
})(1, [2, 3])
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
[npm-image]: https://img.shields.io/npm/v/array-flatten.svg?style=flat
|
|
||||||
[npm-url]: https://npmjs.org/package/array-flatten
|
|
||||||
[downloads-image]: https://img.shields.io/npm/dm/array-flatten.svg?style=flat
|
|
||||||
[downloads-url]: https://npmjs.org/package/array-flatten
|
|
||||||
[travis-image]: https://img.shields.io/travis/blakeembrey/array-flatten.svg?style=flat
|
|
||||||
[travis-url]: https://travis-ci.org/blakeembrey/array-flatten
|
|
||||||
[coveralls-image]: https://img.shields.io/coveralls/blakeembrey/array-flatten.svg?style=flat
|
|
||||||
[coveralls-url]: https://coveralls.io/r/blakeembrey/array-flatten?branch=master
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose `arrayFlatten`.
|
|
||||||
*/
|
|
||||||
module.exports = arrayFlatten
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive flatten function with depth.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Array} result
|
|
||||||
* @param {Number} depth
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function flattenWithDepth (array, result, depth) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var value = array[i]
|
|
||||||
|
|
||||||
if (depth > 0 && Array.isArray(value)) {
|
|
||||||
flattenWithDepth(value, result, depth - 1)
|
|
||||||
} else {
|
|
||||||
result.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive flatten function. Omitting depth is slightly faster.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Array} result
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function flattenForever (array, result) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var value = array[i]
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
flattenForever(value, result)
|
|
||||||
} else {
|
|
||||||
result.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flatten an array, with the ability to define a depth.
|
|
||||||
*
|
|
||||||
* @param {Array} array
|
|
||||||
* @param {Number} depth
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
|
||||||
function arrayFlatten (array, depth) {
|
|
||||||
if (depth == null) {
|
|
||||||
return flattenForever(array, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
return flattenWithDepth(array, [], depth)
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "array-flatten",
|
|
||||||
"version": "1.1.1",
|
|
||||||
"description": "Flatten an array of nested arrays into a single flat array",
|
|
||||||
"main": "array-flatten.js",
|
|
||||||
"files": [
|
|
||||||
"array-flatten.js",
|
|
||||||
"LICENSE"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "istanbul cover _mocha -- -R spec"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/blakeembrey/array-flatten.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"array",
|
|
||||||
"flatten",
|
|
||||||
"arguments",
|
|
||||||
"depth"
|
|
||||||
],
|
|
||||||
"author": {
|
|
||||||
"name": "Blake Embrey",
|
|
||||||
"email": "hello@blakeembrey.com",
|
|
||||||
"url": "http://blakeembrey.me"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/blakeembrey/array-flatten/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/blakeembrey/array-flatten",
|
|
||||||
"devDependencies": {
|
|
||||||
"istanbul": "^0.3.13",
|
|
||||||
"mocha": "^2.2.4",
|
|
||||||
"pre-commit": "^1.0.7",
|
|
||||||
"standard": "^3.7.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-686
@@ -1,686 +0,0 @@
|
|||||||
1.20.5 / 2026-04-24
|
|
||||||
===================
|
|
||||||
* refactor(json): simplify strict mode error string construction
|
|
||||||
* fix: extended urlencoded parsing of arrays with >100 elements (#716)
|
|
||||||
* deps: qs@~6.15.1
|
|
||||||
|
|
||||||
1.20.4 / 2025-12-01
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@~6.14.0
|
|
||||||
* deps: use tilde notation for dependencies
|
|
||||||
* deps: http-errors@~2.0.1
|
|
||||||
* deps: raw-body@~2.5.3
|
|
||||||
|
|
||||||
1.20.3 / 2024-09-10
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@6.13.0
|
|
||||||
* add `depth` option to customize the depth level in the parser
|
|
||||||
* IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`)
|
|
||||||
|
|
||||||
1.20.2 / 2023-02-21
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix strict json error message on Node.js 19+
|
|
||||||
* deps: content-type@~1.0.5
|
|
||||||
- perf: skip value escaping when unnecessary
|
|
||||||
* deps: raw-body@2.5.2
|
|
||||||
|
|
||||||
1.20.1 / 2022-10-06
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@6.11.0
|
|
||||||
* perf: remove unnecessary object clone
|
|
||||||
|
|
||||||
1.20.0 / 2022-04-02
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix error message for json parse whitespace in `strict`
|
|
||||||
* Fix internal error when inflated body exceeds limit
|
|
||||||
* Prevent loss of async hooks context
|
|
||||||
* Prevent hanging when request already read
|
|
||||||
* deps: depd@2.0.0
|
|
||||||
- Replace internal `eval` usage with `Function` constructor
|
|
||||||
- Use instance methods on `process` to check for listeners
|
|
||||||
* deps: http-errors@2.0.0
|
|
||||||
- deps: depd@2.0.0
|
|
||||||
- deps: statuses@2.0.1
|
|
||||||
* deps: on-finished@2.4.1
|
|
||||||
* deps: qs@6.10.3
|
|
||||||
* deps: raw-body@2.5.1
|
|
||||||
- deps: http-errors@2.0.0
|
|
||||||
|
|
||||||
1.19.2 / 2022-02-15
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@3.1.2
|
|
||||||
* deps: qs@6.9.7
|
|
||||||
* Fix handling of `__proto__` keys
|
|
||||||
* deps: raw-body@2.4.3
|
|
||||||
- deps: bytes@3.1.2
|
|
||||||
|
|
||||||
1.19.1 / 2021-12-10
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@3.1.1
|
|
||||||
* deps: http-errors@1.8.1
|
|
||||||
- deps: inherits@2.0.4
|
|
||||||
- deps: toidentifier@1.0.1
|
|
||||||
- deps: setprototypeof@1.2.0
|
|
||||||
* deps: qs@6.9.6
|
|
||||||
* deps: raw-body@2.4.2
|
|
||||||
- deps: bytes@3.1.1
|
|
||||||
- deps: http-errors@1.8.1
|
|
||||||
* deps: safe-buffer@5.2.1
|
|
||||||
* deps: type-is@~1.6.18
|
|
||||||
|
|
||||||
1.19.0 / 2019-04-25
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@3.1.0
|
|
||||||
- Add petabyte (`pb`) support
|
|
||||||
* deps: http-errors@1.7.2
|
|
||||||
- Set constructor name when possible
|
|
||||||
- deps: setprototypeof@1.1.1
|
|
||||||
- deps: statuses@'>= 1.5.0 < 2'
|
|
||||||
* deps: iconv-lite@0.4.24
|
|
||||||
- Added encoding MIK
|
|
||||||
* deps: qs@6.7.0
|
|
||||||
- Fix parsing array brackets after index
|
|
||||||
* deps: raw-body@2.4.0
|
|
||||||
- deps: bytes@3.1.0
|
|
||||||
- deps: http-errors@1.7.2
|
|
||||||
- deps: iconv-lite@0.4.24
|
|
||||||
* deps: type-is@~1.6.17
|
|
||||||
- deps: mime-types@~2.1.24
|
|
||||||
- perf: prevent internal `throw` on invalid type
|
|
||||||
|
|
||||||
1.18.3 / 2018-05-14
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix stack trace for strict json parse error
|
|
||||||
* deps: depd@~1.1.2
|
|
||||||
- perf: remove argument reassignment
|
|
||||||
* deps: http-errors@~1.6.3
|
|
||||||
- deps: depd@~1.1.2
|
|
||||||
- deps: setprototypeof@1.1.0
|
|
||||||
- deps: statuses@'>= 1.3.1 < 2'
|
|
||||||
* deps: iconv-lite@0.4.23
|
|
||||||
- Fix loading encoding with year appended
|
|
||||||
- Fix deprecation warnings on Node.js 10+
|
|
||||||
* deps: qs@6.5.2
|
|
||||||
* deps: raw-body@2.3.3
|
|
||||||
- deps: http-errors@1.6.3
|
|
||||||
- deps: iconv-lite@0.4.23
|
|
||||||
* deps: type-is@~1.6.16
|
|
||||||
- deps: mime-types@~2.1.18
|
|
||||||
|
|
||||||
1.18.2 / 2017-09-22
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@2.6.9
|
|
||||||
* perf: remove argument reassignment
|
|
||||||
|
|
||||||
1.18.1 / 2017-09-12
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: content-type@~1.0.4
|
|
||||||
- perf: remove argument reassignment
|
|
||||||
- perf: skip parameter parsing when no parameters
|
|
||||||
* deps: iconv-lite@0.4.19
|
|
||||||
- Fix ISO-8859-1 regression
|
|
||||||
- Update Windows-1255
|
|
||||||
* deps: qs@6.5.1
|
|
||||||
- Fix parsing & compacting very deep objects
|
|
||||||
* deps: raw-body@2.3.2
|
|
||||||
- deps: iconv-lite@0.4.19
|
|
||||||
|
|
||||||
1.18.0 / 2017-09-08
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix JSON strict violation error to match native parse error
|
|
||||||
* Include the `body` property on verify errors
|
|
||||||
* Include the `type` property on all generated errors
|
|
||||||
* Use `http-errors` to set status code on errors
|
|
||||||
* deps: bytes@3.0.0
|
|
||||||
* deps: debug@2.6.8
|
|
||||||
* deps: depd@~1.1.1
|
|
||||||
- Remove unnecessary `Buffer` loading
|
|
||||||
* deps: http-errors@~1.6.2
|
|
||||||
- deps: depd@1.1.1
|
|
||||||
* deps: iconv-lite@0.4.18
|
|
||||||
- Add support for React Native
|
|
||||||
- Add a warning if not loaded as utf-8
|
|
||||||
- Fix CESU-8 decoding in Node.js 8
|
|
||||||
- Improve speed of ISO-8859-1 encoding
|
|
||||||
* deps: qs@6.5.0
|
|
||||||
* deps: raw-body@2.3.1
|
|
||||||
- Use `http-errors` for standard emitted errors
|
|
||||||
- deps: bytes@3.0.0
|
|
||||||
- deps: iconv-lite@0.4.18
|
|
||||||
- perf: skip buffer decoding on overage chunk
|
|
||||||
* perf: prevent internal `throw` when missing charset
|
|
||||||
|
|
||||||
1.17.2 / 2017-05-17
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@2.6.7
|
|
||||||
- Fix `DEBUG_MAX_ARRAY_LENGTH`
|
|
||||||
- deps: ms@2.0.0
|
|
||||||
* deps: type-is@~1.6.15
|
|
||||||
- deps: mime-types@~2.1.15
|
|
||||||
|
|
||||||
1.17.1 / 2017-03-06
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@6.4.0
|
|
||||||
- Fix regression parsing keys starting with `[`
|
|
||||||
|
|
||||||
1.17.0 / 2017-03-01
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: http-errors@~1.6.1
|
|
||||||
- Make `message` property enumerable for `HttpError`s
|
|
||||||
- deps: setprototypeof@1.0.3
|
|
||||||
* deps: qs@6.3.1
|
|
||||||
- Fix compacting nested arrays
|
|
||||||
|
|
||||||
1.16.1 / 2017-02-10
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@2.6.1
|
|
||||||
- Fix deprecation messages in WebStorm and other editors
|
|
||||||
- Undeprecate `DEBUG_FD` set to `1` or `2`
|
|
||||||
|
|
||||||
1.16.0 / 2017-01-17
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@2.6.0
|
|
||||||
- Allow colors in workers
|
|
||||||
- Deprecated `DEBUG_FD` environment variable
|
|
||||||
- Fix error when running under React Native
|
|
||||||
- Use same color for same namespace
|
|
||||||
- deps: ms@0.7.2
|
|
||||||
* deps: http-errors@~1.5.1
|
|
||||||
- deps: inherits@2.0.3
|
|
||||||
- deps: setprototypeof@1.0.2
|
|
||||||
- deps: statuses@'>= 1.3.1 < 2'
|
|
||||||
* deps: iconv-lite@0.4.15
|
|
||||||
- Added encoding MS-31J
|
|
||||||
- Added encoding MS-932
|
|
||||||
- Added encoding MS-936
|
|
||||||
- Added encoding MS-949
|
|
||||||
- Added encoding MS-950
|
|
||||||
- Fix GBK/GB18030 handling of Euro character
|
|
||||||
* deps: qs@6.2.1
|
|
||||||
- Fix array parsing from skipping empty values
|
|
||||||
* deps: raw-body@~2.2.0
|
|
||||||
- deps: iconv-lite@0.4.15
|
|
||||||
* deps: type-is@~1.6.14
|
|
||||||
- deps: mime-types@~2.1.13
|
|
||||||
|
|
||||||
1.15.2 / 2016-06-19
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@2.4.0
|
|
||||||
* deps: content-type@~1.0.2
|
|
||||||
- perf: enable strict mode
|
|
||||||
* deps: http-errors@~1.5.0
|
|
||||||
- Use `setprototypeof` module to replace `__proto__` setting
|
|
||||||
- deps: statuses@'>= 1.3.0 < 2'
|
|
||||||
- perf: enable strict mode
|
|
||||||
* deps: qs@6.2.0
|
|
||||||
* deps: raw-body@~2.1.7
|
|
||||||
- deps: bytes@2.4.0
|
|
||||||
- perf: remove double-cleanup on happy path
|
|
||||||
* deps: type-is@~1.6.13
|
|
||||||
- deps: mime-types@~2.1.11
|
|
||||||
|
|
||||||
1.15.1 / 2016-05-05
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@2.3.0
|
|
||||||
- Drop partial bytes on all parsed units
|
|
||||||
- Fix parsing byte string that looks like hex
|
|
||||||
* deps: raw-body@~2.1.6
|
|
||||||
- deps: bytes@2.3.0
|
|
||||||
* deps: type-is@~1.6.12
|
|
||||||
- deps: mime-types@~2.1.10
|
|
||||||
|
|
||||||
1.15.0 / 2016-02-10
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: http-errors@~1.4.0
|
|
||||||
- Add `HttpError` export, for `err instanceof createError.HttpError`
|
|
||||||
- deps: inherits@2.0.1
|
|
||||||
- deps: statuses@'>= 1.2.1 < 2'
|
|
||||||
* deps: qs@6.1.0
|
|
||||||
* deps: type-is@~1.6.11
|
|
||||||
- deps: mime-types@~2.1.9
|
|
||||||
|
|
||||||
1.14.2 / 2015-12-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: bytes@2.2.0
|
|
||||||
* deps: iconv-lite@0.4.13
|
|
||||||
* deps: qs@5.2.0
|
|
||||||
* deps: raw-body@~2.1.5
|
|
||||||
- deps: bytes@2.2.0
|
|
||||||
- deps: iconv-lite@0.4.13
|
|
||||||
* deps: type-is@~1.6.10
|
|
||||||
- deps: mime-types@~2.1.8
|
|
||||||
|
|
||||||
1.14.1 / 2015-09-27
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix issue where invalid charset results in 400 when `verify` used
|
|
||||||
* deps: iconv-lite@0.4.12
|
|
||||||
- Fix CESU-8 decoding in Node.js 4.x
|
|
||||||
* deps: raw-body@~2.1.4
|
|
||||||
- Fix masking critical errors from `iconv-lite`
|
|
||||||
- deps: iconv-lite@0.4.12
|
|
||||||
* deps: type-is@~1.6.9
|
|
||||||
- deps: mime-types@~2.1.7
|
|
||||||
|
|
||||||
1.14.0 / 2015-09-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Fix JSON strict parse error to match syntax errors
|
|
||||||
* Provide static `require` analysis in `urlencoded` parser
|
|
||||||
* deps: depd@~1.1.0
|
|
||||||
- Support web browser loading
|
|
||||||
* deps: qs@5.1.0
|
|
||||||
* deps: raw-body@~2.1.3
|
|
||||||
- Fix sync callback when attaching data listener causes sync read
|
|
||||||
* deps: type-is@~1.6.8
|
|
||||||
- Fix type error when given invalid type to match against
|
|
||||||
- deps: mime-types@~2.1.6
|
|
||||||
|
|
||||||
1.13.3 / 2015-07-31
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: type-is@~1.6.6
|
|
||||||
- deps: mime-types@~2.1.4
|
|
||||||
|
|
||||||
1.13.2 / 2015-07-05
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: iconv-lite@0.4.11
|
|
||||||
* deps: qs@4.0.0
|
|
||||||
- Fix dropping parameters like `hasOwnProperty`
|
|
||||||
- Fix user-visible incompatibilities from 3.1.0
|
|
||||||
- Fix various parsing edge cases
|
|
||||||
* deps: raw-body@~2.1.2
|
|
||||||
- Fix error stack traces to skip `makeError`
|
|
||||||
- deps: iconv-lite@0.4.11
|
|
||||||
* deps: type-is@~1.6.4
|
|
||||||
- deps: mime-types@~2.1.2
|
|
||||||
- perf: enable strict mode
|
|
||||||
- perf: remove argument reassignment
|
|
||||||
|
|
||||||
1.13.1 / 2015-06-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@2.4.2
|
|
||||||
- Downgraded from 3.1.0 because of user-visible incompatibilities
|
|
||||||
|
|
||||||
1.13.0 / 2015-06-14
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Add `statusCode` property on `Error`s, in addition to `status`
|
|
||||||
* Change `type` default to `application/json` for JSON parser
|
|
||||||
* Change `type` default to `application/x-www-form-urlencoded` for urlencoded parser
|
|
||||||
* Provide static `require` analysis
|
|
||||||
* Use the `http-errors` module to generate errors
|
|
||||||
* deps: bytes@2.1.0
|
|
||||||
- Slight optimizations
|
|
||||||
* deps: iconv-lite@0.4.10
|
|
||||||
- The encoding UTF-16 without BOM now defaults to UTF-16LE when detection fails
|
|
||||||
- Leading BOM is now removed when decoding
|
|
||||||
* deps: on-finished@~2.3.0
|
|
||||||
- Add defined behavior for HTTP `CONNECT` requests
|
|
||||||
- Add defined behavior for HTTP `Upgrade` requests
|
|
||||||
- deps: ee-first@1.1.1
|
|
||||||
* deps: qs@3.1.0
|
|
||||||
- Fix dropping parameters like `hasOwnProperty`
|
|
||||||
- Fix various parsing edge cases
|
|
||||||
- Parsed object now has `null` prototype
|
|
||||||
* deps: raw-body@~2.1.1
|
|
||||||
- Use `unpipe` module for unpiping requests
|
|
||||||
- deps: iconv-lite@0.4.10
|
|
||||||
* deps: type-is@~1.6.3
|
|
||||||
- deps: mime-types@~2.1.1
|
|
||||||
- perf: reduce try block size
|
|
||||||
- perf: remove bitwise operations
|
|
||||||
* perf: enable strict mode
|
|
||||||
* perf: remove argument reassignment
|
|
||||||
* perf: remove delete call
|
|
||||||
|
|
||||||
1.12.4 / 2015-05-10
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@~2.2.0
|
|
||||||
* deps: qs@2.4.2
|
|
||||||
- Fix allowing parameters like `constructor`
|
|
||||||
* deps: on-finished@~2.2.1
|
|
||||||
* deps: raw-body@~2.0.1
|
|
||||||
- Fix a false-positive when unpiping in Node.js 0.8
|
|
||||||
- deps: bytes@2.0.1
|
|
||||||
* deps: type-is@~1.6.2
|
|
||||||
- deps: mime-types@~2.0.11
|
|
||||||
|
|
||||||
1.12.3 / 2015-04-15
|
|
||||||
===================
|
|
||||||
|
|
||||||
* Slight efficiency improvement when not debugging
|
|
||||||
* deps: depd@~1.0.1
|
|
||||||
* deps: iconv-lite@0.4.8
|
|
||||||
- Add encoding alias UNICODE-1-1-UTF-7
|
|
||||||
* deps: raw-body@1.3.4
|
|
||||||
- Fix hanging callback if request aborts during read
|
|
||||||
- deps: iconv-lite@0.4.8
|
|
||||||
|
|
||||||
1.12.2 / 2015-03-16
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: qs@2.4.1
|
|
||||||
- Fix error when parameter `hasOwnProperty` is present
|
|
||||||
|
|
||||||
1.12.1 / 2015-03-15
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: debug@~2.1.3
|
|
||||||
- Fix high intensity foreground color for bold
|
|
||||||
- deps: ms@0.7.0
|
|
||||||
* deps: type-is@~1.6.1
|
|
||||||
- deps: mime-types@~2.0.10
|
|
||||||
|
|
||||||
1.12.0 / 2015-02-13
|
|
||||||
===================
|
|
||||||
|
|
||||||
* add `debug` messages
|
|
||||||
* accept a function for the `type` option
|
|
||||||
* use `content-type` to parse `Content-Type` headers
|
|
||||||
* deps: iconv-lite@0.4.7
|
|
||||||
- Gracefully support enumerables on `Object.prototype`
|
|
||||||
* deps: raw-body@1.3.3
|
|
||||||
- deps: iconv-lite@0.4.7
|
|
||||||
* deps: type-is@~1.6.0
|
|
||||||
- fix argument reassignment
|
|
||||||
- fix false-positives in `hasBody` `Transfer-Encoding` check
|
|
||||||
- support wildcard for both type and subtype (`*/*`)
|
|
||||||
- deps: mime-types@~2.0.9
|
|
||||||
|
|
||||||
1.11.0 / 2015-01-30
|
|
||||||
===================
|
|
||||||
|
|
||||||
* make internal `extended: true` depth limit infinity
|
|
||||||
* deps: type-is@~1.5.6
|
|
||||||
- deps: mime-types@~2.0.8
|
|
||||||
|
|
||||||
1.10.2 / 2015-01-20
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: iconv-lite@0.4.6
|
|
||||||
- Fix rare aliases of single-byte encodings
|
|
||||||
* deps: raw-body@1.3.2
|
|
||||||
- deps: iconv-lite@0.4.6
|
|
||||||
|
|
||||||
1.10.1 / 2015-01-01
|
|
||||||
===================
|
|
||||||
|
|
||||||
* deps: on-finished@~2.2.0
|
|
||||||
* deps: type-is@~1.5.5
|
|
||||||
- deps: mime-types@~2.0.7
|
|
||||||
|
|
||||||
1.10.0 / 2014-12-02
|
|
||||||
===================
|
|
||||||
|
|
||||||
* make internal `extended: true` array limit dynamic
|
|
||||||
|
|
||||||
1.9.3 / 2014-11-21
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: iconv-lite@0.4.5
|
|
||||||
- Fix Windows-31J and X-SJIS encoding support
|
|
||||||
* deps: qs@2.3.3
|
|
||||||
- Fix `arrayLimit` behavior
|
|
||||||
* deps: raw-body@1.3.1
|
|
||||||
- deps: iconv-lite@0.4.5
|
|
||||||
* deps: type-is@~1.5.3
|
|
||||||
- deps: mime-types@~2.0.3
|
|
||||||
|
|
||||||
1.9.2 / 2014-10-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@2.3.2
|
|
||||||
- Fix parsing of mixed objects and values
|
|
||||||
|
|
||||||
1.9.1 / 2014-10-22
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: on-finished@~2.1.1
|
|
||||||
- Fix handling of pipelined requests
|
|
||||||
* deps: qs@2.3.0
|
|
||||||
- Fix parsing of mixed implicit and explicit arrays
|
|
||||||
* deps: type-is@~1.5.2
|
|
||||||
- deps: mime-types@~2.0.2
|
|
||||||
|
|
||||||
1.9.0 / 2014-09-24
|
|
||||||
==================
|
|
||||||
|
|
||||||
* include the charset in "unsupported charset" error message
|
|
||||||
* include the encoding in "unsupported content encoding" error message
|
|
||||||
* deps: depd@~1.0.0
|
|
||||||
|
|
||||||
1.8.4 / 2014-09-23
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix content encoding to be case-insensitive
|
|
||||||
|
|
||||||
1.8.3 / 2014-09-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@2.2.4
|
|
||||||
- Fix issue with object keys starting with numbers truncated
|
|
||||||
|
|
||||||
1.8.2 / 2014-09-15
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: depd@0.4.5
|
|
||||||
|
|
||||||
1.8.1 / 2014-09-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: media-typer@0.3.0
|
|
||||||
* deps: type-is@~1.5.1
|
|
||||||
|
|
||||||
1.8.0 / 2014-09-05
|
|
||||||
==================
|
|
||||||
|
|
||||||
* make empty-body-handling consistent between chunked requests
|
|
||||||
- empty `json` produces `{}`
|
|
||||||
- empty `raw` produces `new Buffer(0)`
|
|
||||||
- empty `text` produces `''`
|
|
||||||
- empty `urlencoded` produces `{}`
|
|
||||||
* deps: qs@2.2.3
|
|
||||||
- Fix issue where first empty value in array is discarded
|
|
||||||
* deps: type-is@~1.5.0
|
|
||||||
- fix `hasbody` to be true for `content-length: 0`
|
|
||||||
|
|
||||||
1.7.0 / 2014-09-01
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `parameterLimit` option to `urlencoded` parser
|
|
||||||
* change `urlencoded` extended array limit to 100
|
|
||||||
* respond with 413 when over `parameterLimit` in `urlencoded`
|
|
||||||
|
|
||||||
1.6.7 / 2014-08-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@2.2.2
|
|
||||||
- Remove unnecessary cloning
|
|
||||||
|
|
||||||
1.6.6 / 2014-08-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@2.2.0
|
|
||||||
- Array parsing fix
|
|
||||||
- Performance improvements
|
|
||||||
|
|
||||||
1.6.5 / 2014-08-16
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: on-finished@2.1.0
|
|
||||||
|
|
||||||
1.6.4 / 2014-08-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@1.2.2
|
|
||||||
|
|
||||||
1.6.3 / 2014-08-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@1.2.1
|
|
||||||
|
|
||||||
1.6.2 / 2014-08-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@1.2.0
|
|
||||||
- Fix parsing array of objects
|
|
||||||
|
|
||||||
1.6.1 / 2014-08-06
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@1.1.0
|
|
||||||
- Accept urlencoded square brackets
|
|
||||||
- Accept empty values in implicit array notation
|
|
||||||
|
|
||||||
1.6.0 / 2014-08-05
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: qs@1.0.2
|
|
||||||
- Complete rewrite
|
|
||||||
- Limits array length to 20
|
|
||||||
- Limits object depth to 5
|
|
||||||
- Limits parameters to 1,000
|
|
||||||
|
|
||||||
1.5.2 / 2014-07-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: depd@0.4.4
|
|
||||||
- Work-around v8 generating empty stack traces
|
|
||||||
|
|
||||||
1.5.1 / 2014-07-26
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: depd@0.4.3
|
|
||||||
- Fix exception when global `Error.stackTraceLimit` is too low
|
|
||||||
|
|
||||||
1.5.0 / 2014-07-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: depd@0.4.2
|
|
||||||
- Add `TRACE_DEPRECATION` environment variable
|
|
||||||
- Remove non-standard grey color from color output
|
|
||||||
- Support `--no-deprecation` argument
|
|
||||||
- Support `--trace-deprecation` argument
|
|
||||||
* deps: iconv-lite@0.4.4
|
|
||||||
- Added encoding UTF-7
|
|
||||||
* deps: raw-body@1.3.0
|
|
||||||
- deps: iconv-lite@0.4.4
|
|
||||||
- Added encoding UTF-7
|
|
||||||
- Fix `Cannot switch to old mode now` error on Node.js 0.10+
|
|
||||||
* deps: type-is@~1.3.2
|
|
||||||
|
|
||||||
1.4.3 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: type-is@1.3.1
|
|
||||||
- fix global variable leak
|
|
||||||
|
|
||||||
1.4.2 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: type-is@1.3.0
|
|
||||||
- improve type parsing
|
|
||||||
|
|
||||||
1.4.1 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix urlencoded extended deprecation message
|
|
||||||
|
|
||||||
1.4.0 / 2014-06-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `text` parser
|
|
||||||
* add `raw` parser
|
|
||||||
* check accepted charset in content-type (accepts utf-8)
|
|
||||||
* check accepted encoding in content-encoding (accepts identity)
|
|
||||||
* deprecate `bodyParser()` middleware; use `.json()` and `.urlencoded()` as needed
|
|
||||||
* deprecate `urlencoded()` without provided `extended` option
|
|
||||||
* lazy-load urlencoded parsers
|
|
||||||
* parsers split into files for reduced mem usage
|
|
||||||
* support gzip and deflate bodies
|
|
||||||
- set `inflate: false` to turn off
|
|
||||||
* deps: raw-body@1.2.2
|
|
||||||
- Support all encodings from `iconv-lite`
|
|
||||||
|
|
||||||
1.3.1 / 2014-06-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: type-is@1.2.1
|
|
||||||
- Switch dependency from mime to mime-types@1.0.0
|
|
||||||
|
|
||||||
1.3.0 / 2014-05-31
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `extended` option to urlencoded parser
|
|
||||||
|
|
||||||
1.2.2 / 2014-05-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: raw-body@1.1.6
|
|
||||||
- assert stream encoding on node.js 0.8
|
|
||||||
- assert stream encoding on node.js < 0.10.6
|
|
||||||
- deps: bytes@1
|
|
||||||
|
|
||||||
1.2.1 / 2014-05-26
|
|
||||||
==================
|
|
||||||
|
|
||||||
* invoke `next(err)` after request fully read
|
|
||||||
- prevents hung responses and socket hang ups
|
|
||||||
|
|
||||||
1.2.0 / 2014-05-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `verify` option
|
|
||||||
* deps: type-is@1.2.0
|
|
||||||
- support suffix matching
|
|
||||||
|
|
||||||
1.1.2 / 2014-05-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* improve json parser speed
|
|
||||||
|
|
||||||
1.1.1 / 2014-05-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix repeated limit parsing with every request
|
|
||||||
|
|
||||||
1.1.0 / 2014-05-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `type` option
|
|
||||||
* deps: pin for safety and consistency
|
|
||||||
|
|
||||||
1.0.2 / 2014-04-14
|
|
||||||
==================
|
|
||||||
|
|
||||||
* use `type-is` module
|
|
||||||
|
|
||||||
1.0.1 / 2014-03-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* lower default limits to 100kb
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
|
||||||
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
|
||||||
|
|
||||||
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.
|
|
||||||
-476
@@ -1,476 +0,0 @@
|
|||||||
# body-parser
|
|
||||||
|
|
||||||
[![NPM Version][npm-version-image]][npm-url]
|
|
||||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
|
||||||
[![Build Status][ci-image]][ci-url]
|
|
||||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
||||||
[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer]
|
|
||||||
|
|
||||||
Node.js body parsing middleware.
|
|
||||||
|
|
||||||
Parse incoming request bodies in a middleware before your handlers, available
|
|
||||||
under the `req.body` property.
|
|
||||||
|
|
||||||
**Note** As `req.body`'s shape is based on user-controlled input, all
|
|
||||||
properties and values in this object are untrusted and should be validated
|
|
||||||
before trusting. For example, `req.body.foo.toString()` may fail in multiple
|
|
||||||
ways, for example the `foo` property may not be there or may not be a string,
|
|
||||||
and `toString` may not be a function and instead a string or other user input.
|
|
||||||
|
|
||||||
[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/).
|
|
||||||
|
|
||||||
_This does not handle multipart bodies_, due to their complex and typically
|
|
||||||
large nature. For multipart bodies, you may be interested in the following
|
|
||||||
modules:
|
|
||||||
|
|
||||||
* [busboy](https://www.npmjs.org/package/busboy#readme) and
|
|
||||||
[connect-busboy](https://www.npmjs.org/package/connect-busboy#readme)
|
|
||||||
* [multiparty](https://www.npmjs.org/package/multiparty#readme) and
|
|
||||||
[connect-multiparty](https://www.npmjs.org/package/connect-multiparty#readme)
|
|
||||||
* [formidable](https://www.npmjs.org/package/formidable#readme)
|
|
||||||
* [multer](https://www.npmjs.org/package/multer#readme)
|
|
||||||
|
|
||||||
This module provides the following parsers:
|
|
||||||
|
|
||||||
* [JSON body parser](#bodyparserjsonoptions)
|
|
||||||
* [Raw body parser](#bodyparserrawoptions)
|
|
||||||
* [Text body parser](#bodyparsertextoptions)
|
|
||||||
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
|
|
||||||
|
|
||||||
Other body parsers you might be interested in:
|
|
||||||
|
|
||||||
- [body](https://www.npmjs.org/package/body#readme)
|
|
||||||
- [co-body](https://www.npmjs.org/package/co-body#readme)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm install body-parser
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```js
|
|
||||||
var bodyParser = require('body-parser')
|
|
||||||
```
|
|
||||||
|
|
||||||
The `bodyParser` object exposes various factories to create middlewares. All
|
|
||||||
middlewares will populate the `req.body` property with the parsed body when
|
|
||||||
the `Content-Type` request header matches the `type` option, or an empty
|
|
||||||
object (`{}`) if there was no body to parse, the `Content-Type` was not matched,
|
|
||||||
or an error occurred.
|
|
||||||
|
|
||||||
The various errors returned by this module are described in the
|
|
||||||
[errors section](#errors).
|
|
||||||
|
|
||||||
### bodyParser.json([options])
|
|
||||||
|
|
||||||
Returns middleware that only parses `json` and only looks at requests where
|
|
||||||
the `Content-Type` header matches the `type` option. This parser accepts any
|
|
||||||
Unicode encoding of the body and supports automatic inflation of `gzip` and
|
|
||||||
`deflate` encodings.
|
|
||||||
|
|
||||||
A new `body` object containing the parsed data is populated on the `request`
|
|
||||||
object after the middleware (i.e. `req.body`).
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
The `json` function takes an optional `options` object that may contain any of
|
|
||||||
the following keys:
|
|
||||||
|
|
||||||
##### inflate
|
|
||||||
|
|
||||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
|
||||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
|
||||||
|
|
||||||
##### limit
|
|
||||||
|
|
||||||
Controls the maximum request body size. If this is a number, then the value
|
|
||||||
specifies the number of bytes; if it is a string, the value is passed to the
|
|
||||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
|
||||||
to `'100kb'`.
|
|
||||||
|
|
||||||
##### reviver
|
|
||||||
|
|
||||||
The `reviver` option is passed directly to `JSON.parse` as the second
|
|
||||||
argument. You can find more information on this argument
|
|
||||||
[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter).
|
|
||||||
|
|
||||||
##### strict
|
|
||||||
|
|
||||||
When set to `true`, will only accept arrays and objects; when `false` will
|
|
||||||
accept anything `JSON.parse` accepts. Defaults to `true`.
|
|
||||||
|
|
||||||
##### type
|
|
||||||
|
|
||||||
The `type` option is used to determine what media type the middleware will
|
|
||||||
parse. This option can be a string, array of strings, or a function. If not a
|
|
||||||
function, `type` option is passed directly to the
|
|
||||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
|
||||||
be an extension name (like `json`), a mime type (like `application/json`), or
|
|
||||||
a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type`
|
|
||||||
option is called as `fn(req)` and the request is parsed if it returns a truthy
|
|
||||||
value. Defaults to `application/json`.
|
|
||||||
|
|
||||||
##### verify
|
|
||||||
|
|
||||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
|
||||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
|
||||||
encoding of the request. The parsing can be aborted by throwing an error.
|
|
||||||
|
|
||||||
### bodyParser.raw([options])
|
|
||||||
|
|
||||||
Returns middleware that parses all bodies as a `Buffer` and only looks at
|
|
||||||
requests where the `Content-Type` header matches the `type` option. This
|
|
||||||
parser supports automatic inflation of `gzip` and `deflate` encodings.
|
|
||||||
|
|
||||||
A new `body` object containing the parsed data is populated on the `request`
|
|
||||||
object after the middleware (i.e. `req.body`). This will be a `Buffer` object
|
|
||||||
of the body.
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
The `raw` function takes an optional `options` object that may contain any of
|
|
||||||
the following keys:
|
|
||||||
|
|
||||||
##### inflate
|
|
||||||
|
|
||||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
|
||||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
|
||||||
|
|
||||||
##### limit
|
|
||||||
|
|
||||||
Controls the maximum request body size. If this is a number, then the value
|
|
||||||
specifies the number of bytes; if it is a string, the value is passed to the
|
|
||||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
|
||||||
to `'100kb'`.
|
|
||||||
|
|
||||||
##### type
|
|
||||||
|
|
||||||
The `type` option is used to determine what media type the middleware will
|
|
||||||
parse. This option can be a string, array of strings, or a function.
|
|
||||||
If not a function, `type` option is passed directly to the
|
|
||||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this
|
|
||||||
can be an extension name (like `bin`), a mime type (like
|
|
||||||
`application/octet-stream`), or a mime type with a wildcard (like `*/*` or
|
|
||||||
`application/*`). If a function, the `type` option is called as `fn(req)`
|
|
||||||
and the request is parsed if it returns a truthy value. Defaults to
|
|
||||||
`application/octet-stream`.
|
|
||||||
|
|
||||||
##### verify
|
|
||||||
|
|
||||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
|
||||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
|
||||||
encoding of the request. The parsing can be aborted by throwing an error.
|
|
||||||
|
|
||||||
### bodyParser.text([options])
|
|
||||||
|
|
||||||
Returns middleware that parses all bodies as a string and only looks at
|
|
||||||
requests where the `Content-Type` header matches the `type` option. This
|
|
||||||
parser supports automatic inflation of `gzip` and `deflate` encodings.
|
|
||||||
|
|
||||||
A new `body` string containing the parsed data is populated on the `request`
|
|
||||||
object after the middleware (i.e. `req.body`). This will be a string of the
|
|
||||||
body.
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
The `text` function takes an optional `options` object that may contain any of
|
|
||||||
the following keys:
|
|
||||||
|
|
||||||
##### defaultCharset
|
|
||||||
|
|
||||||
Specify the default character set for the text content if the charset is not
|
|
||||||
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
|
||||||
|
|
||||||
##### inflate
|
|
||||||
|
|
||||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
|
||||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
|
||||||
|
|
||||||
##### limit
|
|
||||||
|
|
||||||
Controls the maximum request body size. If this is a number, then the value
|
|
||||||
specifies the number of bytes; if it is a string, the value is passed to the
|
|
||||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
|
||||||
to `'100kb'`.
|
|
||||||
|
|
||||||
##### type
|
|
||||||
|
|
||||||
The `type` option is used to determine what media type the middleware will
|
|
||||||
parse. This option can be a string, array of strings, or a function. If not
|
|
||||||
a function, `type` option is passed directly to the
|
|
||||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
|
||||||
be an extension name (like `txt`), a mime type (like `text/plain`), or a mime
|
|
||||||
type with a wildcard (like `*/*` or `text/*`). If a function, the `type`
|
|
||||||
option is called as `fn(req)` and the request is parsed if it returns a
|
|
||||||
truthy value. Defaults to `text/plain`.
|
|
||||||
|
|
||||||
##### verify
|
|
||||||
|
|
||||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
|
||||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
|
||||||
encoding of the request. The parsing can be aborted by throwing an error.
|
|
||||||
|
|
||||||
### bodyParser.urlencoded([options])
|
|
||||||
|
|
||||||
Returns middleware that only parses `urlencoded` bodies and only looks at
|
|
||||||
requests where the `Content-Type` header matches the `type` option. This
|
|
||||||
parser accepts only UTF-8 encoding of the body and supports automatic
|
|
||||||
inflation of `gzip` and `deflate` encodings.
|
|
||||||
|
|
||||||
A new `body` object containing the parsed data is populated on the `request`
|
|
||||||
object after the middleware (i.e. `req.body`). This object will contain
|
|
||||||
key-value pairs, where the value can be a string or array (when `extended` is
|
|
||||||
`false`), or any type (when `extended` is `true`).
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
The `urlencoded` function takes an optional `options` object that may contain
|
|
||||||
any of the following keys:
|
|
||||||
|
|
||||||
##### extended
|
|
||||||
|
|
||||||
The `extended` option allows to choose between parsing the URL-encoded data
|
|
||||||
with the `querystring` library (when `false`) or the `qs` library (when
|
|
||||||
`true`). The "extended" syntax allows for rich objects and arrays to be
|
|
||||||
encoded into the URL-encoded format, allowing for a JSON-like experience
|
|
||||||
with URL-encoded. For more information, please
|
|
||||||
[see the qs library](https://www.npmjs.org/package/qs#readme).
|
|
||||||
|
|
||||||
Defaults to `true`, but using the default has been deprecated. Please
|
|
||||||
research into the difference between `qs` and `querystring` and choose the
|
|
||||||
appropriate setting.
|
|
||||||
|
|
||||||
##### inflate
|
|
||||||
|
|
||||||
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
|
||||||
`false`, deflated bodies are rejected. Defaults to `true`.
|
|
||||||
|
|
||||||
##### limit
|
|
||||||
|
|
||||||
Controls the maximum request body size. If this is a number, then the value
|
|
||||||
specifies the number of bytes; if it is a string, the value is passed to the
|
|
||||||
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
|
||||||
to `'100kb'`.
|
|
||||||
|
|
||||||
##### parameterLimit
|
|
||||||
|
|
||||||
The `parameterLimit` option controls the maximum number of parameters that
|
|
||||||
are allowed in the URL-encoded data. If a request contains more parameters
|
|
||||||
than this value, a 413 will be returned to the client. Defaults to `1000`.
|
|
||||||
|
|
||||||
##### type
|
|
||||||
|
|
||||||
The `type` option is used to determine what media type the middleware will
|
|
||||||
parse. This option can be a string, array of strings, or a function. If not
|
|
||||||
a function, `type` option is passed directly to the
|
|
||||||
[type-is](https://www.npmjs.org/package/type-is#readme) library and this can
|
|
||||||
be an extension name (like `urlencoded`), a mime type (like
|
|
||||||
`application/x-www-form-urlencoded`), or a mime type with a wildcard (like
|
|
||||||
`*/x-www-form-urlencoded`). If a function, the `type` option is called as
|
|
||||||
`fn(req)` and the request is parsed if it returns a truthy value. Defaults
|
|
||||||
to `application/x-www-form-urlencoded`.
|
|
||||||
|
|
||||||
##### verify
|
|
||||||
|
|
||||||
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
|
||||||
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
|
||||||
encoding of the request. The parsing can be aborted by throwing an error.
|
|
||||||
|
|
||||||
#### depth
|
|
||||||
|
|
||||||
The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.
|
|
||||||
|
|
||||||
## Errors
|
|
||||||
|
|
||||||
The middlewares provided by this module create errors using the
|
|
||||||
[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors
|
|
||||||
will typically have a `status`/`statusCode` property that contains the suggested
|
|
||||||
HTTP response code, an `expose` property to determine if the `message` property
|
|
||||||
should be displayed to the client, a `type` property to determine the type of
|
|
||||||
error without matching against the `message`, and a `body` property containing
|
|
||||||
the read body, if available.
|
|
||||||
|
|
||||||
The following are the common errors created, though any error can come through
|
|
||||||
for various reasons.
|
|
||||||
|
|
||||||
### content encoding unsupported
|
|
||||||
|
|
||||||
This error will occur when the request had a `Content-Encoding` header that
|
|
||||||
contained an encoding but the "inflation" option was set to `false`. The
|
|
||||||
`status` property is set to `415`, the `type` property is set to
|
|
||||||
`'encoding.unsupported'`, and the `charset` property will be set to the
|
|
||||||
encoding that is unsupported.
|
|
||||||
|
|
||||||
### entity parse failed
|
|
||||||
|
|
||||||
This error will occur when the request contained an entity that could not be
|
|
||||||
parsed by the middleware. The `status` property is set to `400`, the `type`
|
|
||||||
property is set to `'entity.parse.failed'`, and the `body` property is set to
|
|
||||||
the entity value that failed parsing.
|
|
||||||
|
|
||||||
### entity verify failed
|
|
||||||
|
|
||||||
This error will occur when the request contained an entity that could not be
|
|
||||||
failed verification by the defined `verify` option. The `status` property is
|
|
||||||
set to `403`, the `type` property is set to `'entity.verify.failed'`, and the
|
|
||||||
`body` property is set to the entity value that failed verification.
|
|
||||||
|
|
||||||
### request aborted
|
|
||||||
|
|
||||||
This error will occur when the request is aborted by the client before reading
|
|
||||||
the body has finished. The `received` property will be set to the number of
|
|
||||||
bytes received before the request was aborted and the `expected` property is
|
|
||||||
set to the number of expected bytes. The `status` property is set to `400`
|
|
||||||
and `type` property is set to `'request.aborted'`.
|
|
||||||
|
|
||||||
### request entity too large
|
|
||||||
|
|
||||||
This error will occur when the request body's size is larger than the "limit"
|
|
||||||
option. The `limit` property will be set to the byte limit and the `length`
|
|
||||||
property will be set to the request body's length. The `status` property is
|
|
||||||
set to `413` and the `type` property is set to `'entity.too.large'`.
|
|
||||||
|
|
||||||
### request size did not match content length
|
|
||||||
|
|
||||||
This error will occur when the request's length did not match the length from
|
|
||||||
the `Content-Length` header. This typically occurs when the request is malformed,
|
|
||||||
typically when the `Content-Length` header was calculated based on characters
|
|
||||||
instead of bytes. The `status` property is set to `400` and the `type` property
|
|
||||||
is set to `'request.size.invalid'`.
|
|
||||||
|
|
||||||
### stream encoding should not be set
|
|
||||||
|
|
||||||
This error will occur when something called the `req.setEncoding` method prior
|
|
||||||
to this middleware. This module operates directly on bytes only and you cannot
|
|
||||||
call `req.setEncoding` when using this module. The `status` property is set to
|
|
||||||
`500` and the `type` property is set to `'stream.encoding.set'`.
|
|
||||||
|
|
||||||
### stream is not readable
|
|
||||||
|
|
||||||
This error will occur when the request is no longer readable when this middleware
|
|
||||||
attempts to read it. This typically means something other than a middleware from
|
|
||||||
this module read the request body already and the middleware was also configured to
|
|
||||||
read the same request. The `status` property is set to `500` and the `type`
|
|
||||||
property is set to `'stream.not.readable'`.
|
|
||||||
|
|
||||||
### too many parameters
|
|
||||||
|
|
||||||
This error will occur when the content of the request exceeds the configured
|
|
||||||
`parameterLimit` for the `urlencoded` parser. The `status` property is set to
|
|
||||||
`413` and the `type` property is set to `'parameters.too.many'`.
|
|
||||||
|
|
||||||
### unsupported charset "BOGUS"
|
|
||||||
|
|
||||||
This error will occur when the request had a charset parameter in the
|
|
||||||
`Content-Type` header, but the `iconv-lite` module does not support it OR the
|
|
||||||
parser does not support it. The charset is contained in the message as well
|
|
||||||
as in the `charset` property. The `status` property is set to `415`, the
|
|
||||||
`type` property is set to `'charset.unsupported'`, and the `charset` property
|
|
||||||
is set to the charset that is unsupported.
|
|
||||||
|
|
||||||
### unsupported content encoding "bogus"
|
|
||||||
|
|
||||||
This error will occur when the request had a `Content-Encoding` header that
|
|
||||||
contained an unsupported encoding. The encoding is contained in the message
|
|
||||||
as well as in the `encoding` property. The `status` property is set to `415`,
|
|
||||||
the `type` property is set to `'encoding.unsupported'`, and the `encoding`
|
|
||||||
property is set to the encoding that is unsupported.
|
|
||||||
|
|
||||||
### The input exceeded the depth
|
|
||||||
|
|
||||||
This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Express/Connect top-level generic
|
|
||||||
|
|
||||||
This example demonstrates adding a generic JSON and URL-encoded parser as a
|
|
||||||
top-level middleware, which will parse the bodies of all incoming requests.
|
|
||||||
This is the simplest setup.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var express = require('express')
|
|
||||||
var bodyParser = require('body-parser')
|
|
||||||
|
|
||||||
var app = express()
|
|
||||||
|
|
||||||
// parse application/x-www-form-urlencoded
|
|
||||||
app.use(bodyParser.urlencoded({ extended: false }))
|
|
||||||
|
|
||||||
// parse application/json
|
|
||||||
app.use(bodyParser.json())
|
|
||||||
|
|
||||||
app.use(function (req, res) {
|
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
|
||||||
res.write('you posted:\n')
|
|
||||||
res.end(JSON.stringify(req.body, null, 2))
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Express route-specific
|
|
||||||
|
|
||||||
This example demonstrates adding body parsers specifically to the routes that
|
|
||||||
need them. In general, this is the most recommended way to use body-parser with
|
|
||||||
Express.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var express = require('express')
|
|
||||||
var bodyParser = require('body-parser')
|
|
||||||
|
|
||||||
var app = express()
|
|
||||||
|
|
||||||
// create application/json parser
|
|
||||||
var jsonParser = bodyParser.json()
|
|
||||||
|
|
||||||
// create application/x-www-form-urlencoded parser
|
|
||||||
var urlencodedParser = bodyParser.urlencoded({ extended: false })
|
|
||||||
|
|
||||||
// POST /login gets urlencoded bodies
|
|
||||||
app.post('/login', urlencodedParser, function (req, res) {
|
|
||||||
res.send('welcome, ' + req.body.username)
|
|
||||||
})
|
|
||||||
|
|
||||||
// POST /api/users gets JSON bodies
|
|
||||||
app.post('/api/users', jsonParser, function (req, res) {
|
|
||||||
// create user in req.body
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Change accepted type for parsers
|
|
||||||
|
|
||||||
All the parsers accept a `type` option which allows you to change the
|
|
||||||
`Content-Type` that the middleware will parse.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var express = require('express')
|
|
||||||
var bodyParser = require('body-parser')
|
|
||||||
|
|
||||||
var app = express()
|
|
||||||
|
|
||||||
// parse various different custom JSON types as JSON
|
|
||||||
app.use(bodyParser.json({ type: 'application/*+json' }))
|
|
||||||
|
|
||||||
// parse some custom thing into a Buffer
|
|
||||||
app.use(bodyParser.raw({ type: 'application/vnd.custom-type' }))
|
|
||||||
|
|
||||||
// parse an HTML body into a string
|
|
||||||
app.use(bodyParser.text({ type: 'text/html' }))
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[ci-image]: https://badgen.net/github/checks/expressjs/body-parser/master?label=ci
|
|
||||||
[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml
|
|
||||||
[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/body-parser/master
|
|
||||||
[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master
|
|
||||||
[node-version-image]: https://badgen.net/npm/node/body-parser
|
|
||||||
[node-version-url]: https://nodejs.org/en/download
|
|
||||||
[npm-downloads-image]: https://badgen.net/npm/dm/body-parser
|
|
||||||
[npm-url]: https://npmjs.org/package/body-parser
|
|
||||||
[npm-version-image]: https://badgen.net/npm/v/body-parser
|
|
||||||
[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge
|
|
||||||
[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser
|
|
||||||
-156
@@ -1,156 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var deprecate = require('depd')('body-parser')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache of loaded parsers.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var parsers = Object.create(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef Parsers
|
|
||||||
* @type {function}
|
|
||||||
* @property {function} json
|
|
||||||
* @property {function} raw
|
|
||||||
* @property {function} text
|
|
||||||
* @property {function} urlencoded
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
* @type {Parsers}
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports = module.exports = deprecate.function(bodyParser,
|
|
||||||
'bodyParser: use individual json/urlencoded middlewares')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON parser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Object.defineProperty(exports, 'json', {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: createParserGetter('json')
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw parser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Object.defineProperty(exports, 'raw', {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: createParserGetter('raw')
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text parser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Object.defineProperty(exports, 'text', {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: createParserGetter('text')
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* URL-encoded parser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Object.defineProperty(exports, 'urlencoded', {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: createParserGetter('urlencoded')
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a middleware to parse json and urlencoded bodies.
|
|
||||||
*
|
|
||||||
* @param {object} [options]
|
|
||||||
* @return {function}
|
|
||||||
* @deprecated
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function bodyParser (options) {
|
|
||||||
// use default type for parsers
|
|
||||||
var opts = Object.create(options || null, {
|
|
||||||
type: {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
value: undefined,
|
|
||||||
writable: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var _urlencoded = exports.urlencoded(opts)
|
|
||||||
var _json = exports.json(opts)
|
|
||||||
|
|
||||||
return function bodyParser (req, res, next) {
|
|
||||||
_json(req, res, function (err) {
|
|
||||||
if (err) return next(err)
|
|
||||||
_urlencoded(req, res, next)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a getter for loading a parser.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function createParserGetter (name) {
|
|
||||||
return function get () {
|
|
||||||
return loadParser(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a parser module.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function loadParser (parserName) {
|
|
||||||
var parser = parsers[parserName]
|
|
||||||
|
|
||||||
if (parser !== undefined) {
|
|
||||||
return parser
|
|
||||||
}
|
|
||||||
|
|
||||||
// this uses a switch for static require analysis
|
|
||||||
switch (parserName) {
|
|
||||||
case 'json':
|
|
||||||
parser = require('./lib/types/json')
|
|
||||||
break
|
|
||||||
case 'raw':
|
|
||||||
parser = require('./lib/types/raw')
|
|
||||||
break
|
|
||||||
case 'text':
|
|
||||||
parser = require('./lib/types/text')
|
|
||||||
break
|
|
||||||
case 'urlencoded':
|
|
||||||
parser = require('./lib/types/urlencoded')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// store to prevent invoking require()
|
|
||||||
return (parsers[parserName] = parser)
|
|
||||||
}
|
|
||||||
-205
@@ -1,205 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var createError = require('http-errors')
|
|
||||||
var destroy = require('destroy')
|
|
||||||
var getBody = require('raw-body')
|
|
||||||
var iconv = require('iconv-lite')
|
|
||||||
var onFinished = require('on-finished')
|
|
||||||
var unpipe = require('unpipe')
|
|
||||||
var zlib = require('zlib')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = read
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a request into a buffer and parse.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @param {object} res
|
|
||||||
* @param {function} next
|
|
||||||
* @param {function} parse
|
|
||||||
* @param {function} debug
|
|
||||||
* @param {object} options
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function read (req, res, next, parse, debug, options) {
|
|
||||||
var length
|
|
||||||
var opts = options
|
|
||||||
var stream
|
|
||||||
|
|
||||||
// flag as parsed
|
|
||||||
req._body = true
|
|
||||||
|
|
||||||
// read options
|
|
||||||
var encoding = opts.encoding !== null
|
|
||||||
? opts.encoding
|
|
||||||
: null
|
|
||||||
var verify = opts.verify
|
|
||||||
|
|
||||||
try {
|
|
||||||
// get the content stream
|
|
||||||
stream = contentstream(req, debug, opts.inflate)
|
|
||||||
length = stream.length
|
|
||||||
stream.length = undefined
|
|
||||||
} catch (err) {
|
|
||||||
return next(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set raw-body options
|
|
||||||
opts.length = length
|
|
||||||
opts.encoding = verify
|
|
||||||
? null
|
|
||||||
: encoding
|
|
||||||
|
|
||||||
// assert charset is supported
|
|
||||||
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
|
|
||||||
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
|
||||||
charset: encoding.toLowerCase(),
|
|
||||||
type: 'charset.unsupported'
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// read body
|
|
||||||
debug('read body')
|
|
||||||
getBody(stream, opts, function (error, body) {
|
|
||||||
if (error) {
|
|
||||||
var _error
|
|
||||||
|
|
||||||
if (error.type === 'encoding.unsupported') {
|
|
||||||
// echo back charset
|
|
||||||
_error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
|
||||||
charset: encoding.toLowerCase(),
|
|
||||||
type: 'charset.unsupported'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// set status code on error
|
|
||||||
_error = createError(400, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpipe from stream and destroy
|
|
||||||
if (stream !== req) {
|
|
||||||
unpipe(req)
|
|
||||||
destroy(stream, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read off entire request
|
|
||||||
dump(req, function onfinished () {
|
|
||||||
next(createError(400, _error))
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify
|
|
||||||
if (verify) {
|
|
||||||
try {
|
|
||||||
debug('verify body')
|
|
||||||
verify(req, res, body, encoding)
|
|
||||||
} catch (err) {
|
|
||||||
next(createError(403, err, {
|
|
||||||
body: body,
|
|
||||||
type: err.type || 'entity.verify.failed'
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse
|
|
||||||
var str = body
|
|
||||||
try {
|
|
||||||
debug('parse body')
|
|
||||||
str = typeof body !== 'string' && encoding !== null
|
|
||||||
? iconv.decode(body, encoding)
|
|
||||||
: body
|
|
||||||
req.body = parse(str)
|
|
||||||
} catch (err) {
|
|
||||||
next(createError(400, err, {
|
|
||||||
body: str,
|
|
||||||
type: err.type || 'entity.parse.failed'
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content stream of the request.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @param {function} debug
|
|
||||||
* @param {boolean} [inflate=true]
|
|
||||||
* @return {object}
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function contentstream (req, debug, inflate) {
|
|
||||||
var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
|
|
||||||
var length = req.headers['content-length']
|
|
||||||
var stream
|
|
||||||
|
|
||||||
debug('content-encoding "%s"', encoding)
|
|
||||||
|
|
||||||
if (inflate === false && encoding !== 'identity') {
|
|
||||||
throw createError(415, 'content encoding unsupported', {
|
|
||||||
encoding: encoding,
|
|
||||||
type: 'encoding.unsupported'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (encoding) {
|
|
||||||
case 'deflate':
|
|
||||||
stream = zlib.createInflate()
|
|
||||||
debug('inflate body')
|
|
||||||
req.pipe(stream)
|
|
||||||
break
|
|
||||||
case 'gzip':
|
|
||||||
stream = zlib.createGunzip()
|
|
||||||
debug('gunzip body')
|
|
||||||
req.pipe(stream)
|
|
||||||
break
|
|
||||||
case 'identity':
|
|
||||||
stream = req
|
|
||||||
stream.length = length
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
|
|
||||||
encoding: encoding,
|
|
||||||
type: 'encoding.unsupported'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dump the contents of a request.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @param {function} callback
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function dump (req, callback) {
|
|
||||||
if (onFinished.isFinished(req)) {
|
|
||||||
callback(null)
|
|
||||||
} else {
|
|
||||||
onFinished(req, callback)
|
|
||||||
req.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-243
@@ -1,243 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014 Jonathan Ong
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var bytes = require('bytes')
|
|
||||||
var contentType = require('content-type')
|
|
||||||
var createError = require('http-errors')
|
|
||||||
var debug = require('debug')('body-parser:json')
|
|
||||||
var read = require('../read')
|
|
||||||
var typeis = require('type-is')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = json
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match the first non-space in a string.
|
|
||||||
*
|
|
||||||
* Allowed whitespace is defined in RFC 7159:
|
|
||||||
*
|
|
||||||
* ws = *(
|
|
||||||
* %x20 / ; Space
|
|
||||||
* %x09 / ; Horizontal tab
|
|
||||||
* %x0A / ; Line feed or New line
|
|
||||||
* %x0D ) ; Carriage return
|
|
||||||
*/
|
|
||||||
|
|
||||||
var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex
|
|
||||||
|
|
||||||
var JSON_SYNTAX_CHAR = '#'
|
|
||||||
var JSON_SYNTAX_REGEXP = /#+/g
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a middleware to parse JSON bodies.
|
|
||||||
*
|
|
||||||
* @param {object} [options]
|
|
||||||
* @return {function}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function json (options) {
|
|
||||||
var opts = options || {}
|
|
||||||
|
|
||||||
var limit = typeof opts.limit !== 'number'
|
|
||||||
? bytes.parse(opts.limit || '100kb')
|
|
||||||
: opts.limit
|
|
||||||
var inflate = opts.inflate !== false
|
|
||||||
var reviver = opts.reviver
|
|
||||||
var strict = opts.strict !== false
|
|
||||||
var type = opts.type || 'application/json'
|
|
||||||
var verify = opts.verify || false
|
|
||||||
|
|
||||||
if (verify !== false && typeof verify !== 'function') {
|
|
||||||
throw new TypeError('option verify must be function')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the appropriate type checking function
|
|
||||||
var shouldParse = typeof type !== 'function'
|
|
||||||
? typeChecker(type)
|
|
||||||
: type
|
|
||||||
|
|
||||||
function parse (body) {
|
|
||||||
if (body.length === 0) {
|
|
||||||
// special-case empty json body, as it's a common client-side mistake
|
|
||||||
// TODO: maybe make this configurable or part of "strict" option
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strict) {
|
|
||||||
var first = firstchar(body)
|
|
||||||
|
|
||||||
if (first !== '{' && first !== '[') {
|
|
||||||
debug('strict violation')
|
|
||||||
throw createStrictSyntaxError(body, first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
debug('parse json')
|
|
||||||
return JSON.parse(body, reviver)
|
|
||||||
} catch (e) {
|
|
||||||
throw normalizeJsonSyntaxError(e, {
|
|
||||||
message: e.message,
|
|
||||||
stack: e.stack
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return function jsonParser (req, res, next) {
|
|
||||||
if (req._body) {
|
|
||||||
debug('body already parsed')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = req.body || {}
|
|
||||||
|
|
||||||
// skip requests without bodies
|
|
||||||
if (!typeis.hasBody(req)) {
|
|
||||||
debug('skip empty body')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('content-type %j', req.headers['content-type'])
|
|
||||||
|
|
||||||
// determine if request should be parsed
|
|
||||||
if (!shouldParse(req)) {
|
|
||||||
debug('skip parsing')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert charset per RFC 7159 sec 8.1
|
|
||||||
var charset = getCharset(req) || 'utf-8'
|
|
||||||
if (charset.slice(0, 4) !== 'utf-') {
|
|
||||||
debug('invalid charset')
|
|
||||||
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
|
|
||||||
charset: charset,
|
|
||||||
type: 'charset.unsupported'
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read
|
|
||||||
read(req, res, next, parse, debug, {
|
|
||||||
encoding: charset,
|
|
||||||
inflate: inflate,
|
|
||||||
limit: limit,
|
|
||||||
verify: verify
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create strict violation syntax error matching native error.
|
|
||||||
*
|
|
||||||
* @param {string} str
|
|
||||||
* @param {string} char
|
|
||||||
* @return {Error}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function createStrictSyntaxError (str, char) {
|
|
||||||
var index = str.indexOf(char)
|
|
||||||
var partial = ''
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
partial = str.substring(0, index) + new Array(str.length - index + 1).join(JSON_SYNTAX_CHAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
|
|
||||||
} catch (e) {
|
|
||||||
return normalizeJsonSyntaxError(e, {
|
|
||||||
message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) {
|
|
||||||
return str.substring(index, index + placeholder.length)
|
|
||||||
}),
|
|
||||||
stack: e.stack
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first non-whitespace character in a string.
|
|
||||||
*
|
|
||||||
* @param {string} str
|
|
||||||
* @return {function}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function firstchar (str) {
|
|
||||||
var match = FIRST_CHAR_REGEXP.exec(str)
|
|
||||||
|
|
||||||
return match
|
|
||||||
? match[1]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the charset of a request.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getCharset (req) {
|
|
||||||
try {
|
|
||||||
return (contentType.parse(req).parameters.charset || '').toLowerCase()
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a SyntaxError for JSON.parse.
|
|
||||||
*
|
|
||||||
* @param {SyntaxError} error
|
|
||||||
* @param {object} obj
|
|
||||||
* @return {SyntaxError}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function normalizeJsonSyntaxError (error, obj) {
|
|
||||||
var keys = Object.getOwnPropertyNames(error)
|
|
||||||
|
|
||||||
for (var i = 0; i < keys.length; i++) {
|
|
||||||
var key = keys[i]
|
|
||||||
if (key !== 'stack' && key !== 'message') {
|
|
||||||
delete error[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace stack before message for Node.js 0.10 and below
|
|
||||||
error.stack = obj.stack.replace(error.message, obj.message)
|
|
||||||
error.message = obj.message
|
|
||||||
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the simple type checker.
|
|
||||||
*
|
|
||||||
* @param {string} type
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function typeChecker (type) {
|
|
||||||
return function checkType (req) {
|
|
||||||
return Boolean(typeis(req, type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var bytes = require('bytes')
|
|
||||||
var debug = require('debug')('body-parser:raw')
|
|
||||||
var read = require('../read')
|
|
||||||
var typeis = require('type-is')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = raw
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a middleware to parse raw bodies.
|
|
||||||
*
|
|
||||||
* @param {object} [options]
|
|
||||||
* @return {function}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function raw (options) {
|
|
||||||
var opts = options || {}
|
|
||||||
|
|
||||||
var inflate = opts.inflate !== false
|
|
||||||
var limit = typeof opts.limit !== 'number'
|
|
||||||
? bytes.parse(opts.limit || '100kb')
|
|
||||||
: opts.limit
|
|
||||||
var type = opts.type || 'application/octet-stream'
|
|
||||||
var verify = opts.verify || false
|
|
||||||
|
|
||||||
if (verify !== false && typeof verify !== 'function') {
|
|
||||||
throw new TypeError('option verify must be function')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the appropriate type checking function
|
|
||||||
var shouldParse = typeof type !== 'function'
|
|
||||||
? typeChecker(type)
|
|
||||||
: type
|
|
||||||
|
|
||||||
function parse (buf) {
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
return function rawParser (req, res, next) {
|
|
||||||
if (req._body) {
|
|
||||||
debug('body already parsed')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = req.body || {}
|
|
||||||
|
|
||||||
// skip requests without bodies
|
|
||||||
if (!typeis.hasBody(req)) {
|
|
||||||
debug('skip empty body')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('content-type %j', req.headers['content-type'])
|
|
||||||
|
|
||||||
// determine if request should be parsed
|
|
||||||
if (!shouldParse(req)) {
|
|
||||||
debug('skip parsing')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read
|
|
||||||
read(req, res, next, parse, debug, {
|
|
||||||
encoding: null,
|
|
||||||
inflate: inflate,
|
|
||||||
limit: limit,
|
|
||||||
verify: verify
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the simple type checker.
|
|
||||||
*
|
|
||||||
* @param {string} type
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function typeChecker (type) {
|
|
||||||
return function checkType (req) {
|
|
||||||
return Boolean(typeis(req, type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var bytes = require('bytes')
|
|
||||||
var contentType = require('content-type')
|
|
||||||
var debug = require('debug')('body-parser:text')
|
|
||||||
var read = require('../read')
|
|
||||||
var typeis = require('type-is')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = text
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a middleware to parse text bodies.
|
|
||||||
*
|
|
||||||
* @param {object} [options]
|
|
||||||
* @return {function}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function text (options) {
|
|
||||||
var opts = options || {}
|
|
||||||
|
|
||||||
var defaultCharset = opts.defaultCharset || 'utf-8'
|
|
||||||
var inflate = opts.inflate !== false
|
|
||||||
var limit = typeof opts.limit !== 'number'
|
|
||||||
? bytes.parse(opts.limit || '100kb')
|
|
||||||
: opts.limit
|
|
||||||
var type = opts.type || 'text/plain'
|
|
||||||
var verify = opts.verify || false
|
|
||||||
|
|
||||||
if (verify !== false && typeof verify !== 'function') {
|
|
||||||
throw new TypeError('option verify must be function')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the appropriate type checking function
|
|
||||||
var shouldParse = typeof type !== 'function'
|
|
||||||
? typeChecker(type)
|
|
||||||
: type
|
|
||||||
|
|
||||||
function parse (buf) {
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
return function textParser (req, res, next) {
|
|
||||||
if (req._body) {
|
|
||||||
debug('body already parsed')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = req.body || {}
|
|
||||||
|
|
||||||
// skip requests without bodies
|
|
||||||
if (!typeis.hasBody(req)) {
|
|
||||||
debug('skip empty body')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('content-type %j', req.headers['content-type'])
|
|
||||||
|
|
||||||
// determine if request should be parsed
|
|
||||||
if (!shouldParse(req)) {
|
|
||||||
debug('skip parsing')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get charset
|
|
||||||
var charset = getCharset(req) || defaultCharset
|
|
||||||
|
|
||||||
// read
|
|
||||||
read(req, res, next, parse, debug, {
|
|
||||||
encoding: charset,
|
|
||||||
inflate: inflate,
|
|
||||||
limit: limit,
|
|
||||||
verify: verify
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the charset of a request.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getCharset (req) {
|
|
||||||
try {
|
|
||||||
return (contentType.parse(req).parameters.charset || '').toLowerCase()
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the simple type checker.
|
|
||||||
*
|
|
||||||
* @param {string} type
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function typeChecker (type) {
|
|
||||||
return function checkType (req) {
|
|
||||||
return Boolean(typeis(req, type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-299
@@ -1,299 +0,0 @@
|
|||||||
/*!
|
|
||||||
* body-parser
|
|
||||||
* Copyright(c) 2014 Jonathan Ong
|
|
||||||
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var bytes = require('bytes')
|
|
||||||
var contentType = require('content-type')
|
|
||||||
var createError = require('http-errors')
|
|
||||||
var debug = require('debug')('body-parser:urlencoded')
|
|
||||||
var deprecate = require('depd')('body-parser')
|
|
||||||
var read = require('../read')
|
|
||||||
var typeis = require('type-is')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = urlencoded
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache of parser modules.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var parsers = Object.create(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a middleware to parse urlencoded bodies.
|
|
||||||
*
|
|
||||||
* @param {object} [options]
|
|
||||||
* @return {function}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function urlencoded (options) {
|
|
||||||
var opts = options || {}
|
|
||||||
|
|
||||||
// notice because option default will flip in next major
|
|
||||||
if (opts.extended === undefined) {
|
|
||||||
deprecate('undefined extended: provide extended option')
|
|
||||||
}
|
|
||||||
|
|
||||||
var extended = opts.extended !== false
|
|
||||||
var inflate = opts.inflate !== false
|
|
||||||
var limit = typeof opts.limit !== 'number'
|
|
||||||
? bytes.parse(opts.limit || '100kb')
|
|
||||||
: opts.limit
|
|
||||||
var type = opts.type || 'application/x-www-form-urlencoded'
|
|
||||||
var verify = opts.verify || false
|
|
||||||
|
|
||||||
if (verify !== false && typeof verify !== 'function') {
|
|
||||||
throw new TypeError('option verify must be function')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the appropriate query parser
|
|
||||||
var queryparse = extended
|
|
||||||
? extendedparser(opts)
|
|
||||||
: simpleparser(opts)
|
|
||||||
|
|
||||||
// create the appropriate type checking function
|
|
||||||
var shouldParse = typeof type !== 'function'
|
|
||||||
? typeChecker(type)
|
|
||||||
: type
|
|
||||||
|
|
||||||
function parse (body) {
|
|
||||||
return body.length
|
|
||||||
? queryparse(body)
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return function urlencodedParser (req, res, next) {
|
|
||||||
if (req._body) {
|
|
||||||
debug('body already parsed')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body = req.body || {}
|
|
||||||
|
|
||||||
// skip requests without bodies
|
|
||||||
if (!typeis.hasBody(req)) {
|
|
||||||
debug('skip empty body')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('content-type %j', req.headers['content-type'])
|
|
||||||
|
|
||||||
// determine if request should be parsed
|
|
||||||
if (!shouldParse(req)) {
|
|
||||||
debug('skip parsing')
|
|
||||||
next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert charset
|
|
||||||
var charset = getCharset(req) || 'utf-8'
|
|
||||||
if (charset !== 'utf-8') {
|
|
||||||
debug('invalid charset')
|
|
||||||
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
|
|
||||||
charset: charset,
|
|
||||||
type: 'charset.unsupported'
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read
|
|
||||||
read(req, res, next, parse, debug, {
|
|
||||||
debug: debug,
|
|
||||||
encoding: charset,
|
|
||||||
inflate: inflate,
|
|
||||||
limit: limit,
|
|
||||||
verify: verify
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the extended query parser.
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
|
|
||||||
function extendedparser (options) {
|
|
||||||
var parameterLimit = options.parameterLimit !== undefined
|
|
||||||
? options.parameterLimit
|
|
||||||
: 1000
|
|
||||||
var depth = options.depth !== undefined ? options.depth : 32
|
|
||||||
var parse = parser('qs')
|
|
||||||
|
|
||||||
if (isNaN(parameterLimit) || parameterLimit < 1) {
|
|
||||||
throw new TypeError('option parameterLimit must be a positive number')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(depth) || depth < 0) {
|
|
||||||
throw new TypeError('option depth must be a zero or a positive number')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFinite(parameterLimit)) {
|
|
||||||
parameterLimit = parameterLimit | 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return function queryparse (body) {
|
|
||||||
var paramCount = parameterCount(body, parameterLimit)
|
|
||||||
|
|
||||||
if (paramCount === undefined) {
|
|
||||||
debug('too many parameters')
|
|
||||||
throw createError(413, 'too many parameters', {
|
|
||||||
type: 'parameters.too.many'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var arrayLimit = Math.max(100, paramCount)
|
|
||||||
|
|
||||||
debug('parse extended urlencoding')
|
|
||||||
try {
|
|
||||||
return parse(body, {
|
|
||||||
allowPrototypes: true,
|
|
||||||
arrayLimit: arrayLimit,
|
|
||||||
depth: depth,
|
|
||||||
strictDepth: true,
|
|
||||||
parameterLimit: parameterLimit
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof RangeError) {
|
|
||||||
throw createError(400, 'The input exceeded the depth', {
|
|
||||||
type: 'querystring.parse.rangeError'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the charset of a request.
|
|
||||||
*
|
|
||||||
* @param {object} req
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getCharset (req) {
|
|
||||||
try {
|
|
||||||
return (contentType.parse(req).parameters.charset || '').toLowerCase()
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count the number of parameters, stopping once limit reached
|
|
||||||
*
|
|
||||||
* @param {string} body
|
|
||||||
* @param {number} limit
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parameterCount (body, limit) {
|
|
||||||
var count = 0
|
|
||||||
var index = -1
|
|
||||||
|
|
||||||
do {
|
|
||||||
count++
|
|
||||||
if (count > limit) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
index = body.indexOf('&', index + 1)
|
|
||||||
} while (index !== -1)
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get parser for module name dynamically.
|
|
||||||
*
|
|
||||||
* @param {string} name
|
|
||||||
* @return {function}
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parser (name) {
|
|
||||||
var mod = parsers[name]
|
|
||||||
|
|
||||||
if (mod !== undefined) {
|
|
||||||
return mod.parse
|
|
||||||
}
|
|
||||||
|
|
||||||
// this uses a switch for static require analysis
|
|
||||||
switch (name) {
|
|
||||||
case 'qs':
|
|
||||||
mod = require('qs')
|
|
||||||
break
|
|
||||||
case 'querystring':
|
|
||||||
mod = require('querystring')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// store to prevent invoking require()
|
|
||||||
parsers[name] = mod
|
|
||||||
|
|
||||||
return mod.parse
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the simple query parser.
|
|
||||||
*
|
|
||||||
* @param {object} options
|
|
||||||
*/
|
|
||||||
|
|
||||||
function simpleparser (options) {
|
|
||||||
var parameterLimit = options.parameterLimit !== undefined
|
|
||||||
? options.parameterLimit
|
|
||||||
: 1000
|
|
||||||
var parse = parser('querystring')
|
|
||||||
|
|
||||||
if (isNaN(parameterLimit) || parameterLimit < 1) {
|
|
||||||
throw new TypeError('option parameterLimit must be a positive number')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFinite(parameterLimit)) {
|
|
||||||
parameterLimit = parameterLimit | 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return function queryparse (body) {
|
|
||||||
var paramCount = parameterCount(body, parameterLimit)
|
|
||||||
|
|
||||||
if (paramCount === undefined) {
|
|
||||||
debug('too many parameters')
|
|
||||||
throw createError(413, 'too many parameters', {
|
|
||||||
type: 'parameters.too.many'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('parse urlencoding')
|
|
||||||
return parse(body, undefined, undefined, { maxKeys: parameterLimit })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the simple type checker.
|
|
||||||
*
|
|
||||||
* @param {string} type
|
|
||||||
* @return {function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function typeChecker (type) {
|
|
||||||
return function checkType (req) {
|
|
||||||
return Boolean(typeis(req, type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "body-parser",
|
|
||||||
"description": "Node.js body parsing middleware",
|
|
||||||
"version": "1.20.5",
|
|
||||||
"contributors": [
|
|
||||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
||||||
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": "expressjs/body-parser",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "~3.1.2",
|
|
||||||
"content-type": "~1.0.5",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "~1.2.0",
|
|
||||||
"http-errors": "~2.0.1",
|
|
||||||
"iconv-lite": "~0.4.24",
|
|
||||||
"on-finished": "~2.4.1",
|
|
||||||
"qs": "~6.15.1",
|
|
||||||
"raw-body": "~2.5.3",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"unpipe": "~1.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "8.34.0",
|
|
||||||
"eslint-config-standard": "14.1.1",
|
|
||||||
"eslint-plugin-import": "2.27.5",
|
|
||||||
"eslint-plugin-markdown": "3.0.0",
|
|
||||||
"eslint-plugin-node": "11.1.0",
|
|
||||||
"eslint-plugin-promise": "6.1.1",
|
|
||||||
"eslint-plugin-standard": "4.1.0",
|
|
||||||
"methods": "1.1.2",
|
|
||||||
"mocha": "10.2.0",
|
|
||||||
"nyc": "15.1.0",
|
|
||||||
"safe-buffer": "5.2.1",
|
|
||||||
"supertest": "6.3.3"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"lib/",
|
|
||||||
"LICENSE",
|
|
||||||
"HISTORY.md",
|
|
||||||
"index.js"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "mocha --require test/support/env --reporter spec --check-leaks --bail test/",
|
|
||||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
|
||||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-97
@@ -1,97 +0,0 @@
|
|||||||
3.1.2 / 2022-01-27
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix return value for un-parsable strings
|
|
||||||
|
|
||||||
3.1.1 / 2021-11-15
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix "thousandsSeparator" incorrecting formatting fractional part
|
|
||||||
|
|
||||||
3.1.0 / 2019-01-22
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Add petabyte (`pb`) support
|
|
||||||
|
|
||||||
3.0.0 / 2017-08-31
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Change "kB" to "KB" in format output
|
|
||||||
* Remove support for Node.js 0.6
|
|
||||||
* Remove support for ComponentJS
|
|
||||||
|
|
||||||
2.5.0 / 2017-03-24
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Add option "unit"
|
|
||||||
|
|
||||||
2.4.0 / 2016-06-01
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Add option "unitSeparator"
|
|
||||||
|
|
||||||
2.3.0 / 2016-02-15
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Drop partial bytes on all parsed units
|
|
||||||
* Fix non-finite numbers to `.format` to return `null`
|
|
||||||
* Fix parsing byte string that looks like hex
|
|
||||||
* perf: hoist regular expressions
|
|
||||||
|
|
||||||
2.2.0 / 2015-11-13
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add option "decimalPlaces"
|
|
||||||
* add option "fixedDecimals"
|
|
||||||
|
|
||||||
2.1.0 / 2015-05-21
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add `.format` export
|
|
||||||
* add `.parse` export
|
|
||||||
|
|
||||||
2.0.2 / 2015-05-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* remove map recreation
|
|
||||||
* remove unnecessary object construction
|
|
||||||
|
|
||||||
2.0.1 / 2015-05-07
|
|
||||||
==================
|
|
||||||
|
|
||||||
* fix browserify require
|
|
||||||
* remove node.extend dependency
|
|
||||||
|
|
||||||
2.0.0 / 2015-04-12
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add option "case"
|
|
||||||
* add option "thousandsSeparator"
|
|
||||||
* return "null" on invalid parse input
|
|
||||||
* support proper round-trip: bytes(bytes(num)) === num
|
|
||||||
* units no longer case sensitive when parsing
|
|
||||||
|
|
||||||
1.0.0 / 2014-05-05
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add negative support. fixes #6
|
|
||||||
|
|
||||||
0.3.0 / 2014-03-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* added terabyte support
|
|
||||||
|
|
||||||
0.2.1 / 2013-04-01
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add .component
|
|
||||||
|
|
||||||
0.2.0 / 2012-10-28
|
|
||||||
==================
|
|
||||||
|
|
||||||
* bytes(200).should.eql('200b')
|
|
||||||
|
|
||||||
0.1.0 / 2012-07-04
|
|
||||||
==================
|
|
||||||
|
|
||||||
* add bytes to string conversion [yields]
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
|
|
||||||
Copyright (c) 2015 Jed Watson <jed.watson@me.com>
|
|
||||||
|
|
||||||
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.
|
|
||||||
-152
@@ -1,152 +0,0 @@
|
|||||||
# Bytes utility
|
|
||||||
|
|
||||||
[![NPM Version][npm-image]][npm-url]
|
|
||||||
[![NPM Downloads][downloads-image]][downloads-url]
|
|
||||||
[![Build Status][ci-image]][ci-url]
|
|
||||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
This is a [Node.js](https://nodejs.org/en/) module available through the
|
|
||||||
[npm registry](https://www.npmjs.com/). Installation is done using the
|
|
||||||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ npm install bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```js
|
|
||||||
var bytes = require('bytes');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### bytes(number|string value, [options]): number|string|null
|
|
||||||
|
|
||||||
Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`.
|
|
||||||
|
|
||||||
**Arguments**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------|----------|--------------------|
|
|
||||||
| value | `number`|`string` | Number value to format or string value to parse |
|
|
||||||
| options | `Object` | Conversion options for `format` |
|
|
||||||
|
|
||||||
**Returns**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------|------------------|-------------------------------------------------|
|
|
||||||
| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. |
|
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
bytes(1024);
|
|
||||||
// output: '1KB'
|
|
||||||
|
|
||||||
bytes('1KB');
|
|
||||||
// output: 1024
|
|
||||||
```
|
|
||||||
|
|
||||||
#### bytes.format(number value, [options]): string|null
|
|
||||||
|
|
||||||
Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is
|
|
||||||
rounded.
|
|
||||||
|
|
||||||
**Arguments**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------|----------|--------------------|
|
|
||||||
| value | `number` | Value in bytes |
|
|
||||||
| options | `Object` | Conversion options |
|
|
||||||
|
|
||||||
**Options**
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|-------------------|--------|-----------------------------------------------------------------------------------------|
|
|
||||||
| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. |
|
|
||||||
| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` |
|
|
||||||
| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. |
|
|
||||||
| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). |
|
|
||||||
| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. |
|
|
||||||
|
|
||||||
**Returns**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------|------------------|-------------------------------------------------|
|
|
||||||
| results | `string`|`null` | Return null upon error. String value otherwise. |
|
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
bytes.format(1024);
|
|
||||||
// output: '1KB'
|
|
||||||
|
|
||||||
bytes.format(1000);
|
|
||||||
// output: '1000B'
|
|
||||||
|
|
||||||
bytes.format(1000, {thousandsSeparator: ' '});
|
|
||||||
// output: '1 000B'
|
|
||||||
|
|
||||||
bytes.format(1024 * 1.7, {decimalPlaces: 0});
|
|
||||||
// output: '2KB'
|
|
||||||
|
|
||||||
bytes.format(1024, {unitSeparator: ' '});
|
|
||||||
// output: '1 KB'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### bytes.parse(string|number value): number|null
|
|
||||||
|
|
||||||
Parse the string value into an integer in bytes. If no unit is given, or `value`
|
|
||||||
is a number, it is assumed the value is in bytes.
|
|
||||||
|
|
||||||
Supported units and abbreviations are as follows and are case-insensitive:
|
|
||||||
|
|
||||||
* `b` for bytes
|
|
||||||
* `kb` for kilobytes
|
|
||||||
* `mb` for megabytes
|
|
||||||
* `gb` for gigabytes
|
|
||||||
* `tb` for terabytes
|
|
||||||
* `pb` for petabytes
|
|
||||||
|
|
||||||
The units are in powers of two, not ten. This means 1kb = 1024b according to this parser.
|
|
||||||
|
|
||||||
**Arguments**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------------|--------|--------------------|
|
|
||||||
| value | `string`|`number` | String to parse, or number in bytes. |
|
|
||||||
|
|
||||||
**Returns**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|---------|-------------|-------------------------|
|
|
||||||
| results | `number`|`null` | Return null upon error. Value in bytes otherwise. |
|
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
bytes.parse('1KB');
|
|
||||||
// output: 1024
|
|
||||||
|
|
||||||
bytes.parse('1024');
|
|
||||||
// output: 1024
|
|
||||||
|
|
||||||
bytes.parse(1024);
|
|
||||||
// output: 1024
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci
|
|
||||||
[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci
|
|
||||||
[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master
|
|
||||||
[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master
|
|
||||||
[downloads-image]: https://badgen.net/npm/dm/bytes
|
|
||||||
[downloads-url]: https://npmjs.org/package/bytes
|
|
||||||
[npm-image]: https://badgen.net/npm/v/bytes
|
|
||||||
[npm-url]: https://npmjs.org/package/bytes
|
|
||||||
-170
@@ -1,170 +0,0 @@
|
|||||||
/*!
|
|
||||||
* bytes
|
|
||||||
* Copyright(c) 2012-2014 TJ Holowaychuk
|
|
||||||
* Copyright(c) 2015 Jed Watson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = bytes;
|
|
||||||
module.exports.format = format;
|
|
||||||
module.exports.parse = parse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module variables.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g;
|
|
||||||
|
|
||||||
var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/;
|
|
||||||
|
|
||||||
var map = {
|
|
||||||
b: 1,
|
|
||||||
kb: 1 << 10,
|
|
||||||
mb: 1 << 20,
|
|
||||||
gb: 1 << 30,
|
|
||||||
tb: Math.pow(1024, 4),
|
|
||||||
pb: Math.pow(1024, 5),
|
|
||||||
};
|
|
||||||
|
|
||||||
var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the given value in bytes into a string or parse to string to an integer in bytes.
|
|
||||||
*
|
|
||||||
* @param {string|number} value
|
|
||||||
* @param {{
|
|
||||||
* case: [string],
|
|
||||||
* decimalPlaces: [number]
|
|
||||||
* fixedDecimals: [boolean]
|
|
||||||
* thousandsSeparator: [string]
|
|
||||||
* unitSeparator: [string]
|
|
||||||
* }} [options] bytes options.
|
|
||||||
*
|
|
||||||
* @returns {string|number|null}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function bytes(value, options) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return format(value, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format the given value in bytes into a string.
|
|
||||||
*
|
|
||||||
* If the value is negative, it is kept as such. If it is a float,
|
|
||||||
* it is rounded.
|
|
||||||
*
|
|
||||||
* @param {number} value
|
|
||||||
* @param {object} [options]
|
|
||||||
* @param {number} [options.decimalPlaces=2]
|
|
||||||
* @param {number} [options.fixedDecimals=false]
|
|
||||||
* @param {string} [options.thousandsSeparator=]
|
|
||||||
* @param {string} [options.unit=]
|
|
||||||
* @param {string} [options.unitSeparator=]
|
|
||||||
*
|
|
||||||
* @returns {string|null}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function format(value, options) {
|
|
||||||
if (!Number.isFinite(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mag = Math.abs(value);
|
|
||||||
var thousandsSeparator = (options && options.thousandsSeparator) || '';
|
|
||||||
var unitSeparator = (options && options.unitSeparator) || '';
|
|
||||||
var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2;
|
|
||||||
var fixedDecimals = Boolean(options && options.fixedDecimals);
|
|
||||||
var unit = (options && options.unit) || '';
|
|
||||||
|
|
||||||
if (!unit || !map[unit.toLowerCase()]) {
|
|
||||||
if (mag >= map.pb) {
|
|
||||||
unit = 'PB';
|
|
||||||
} else if (mag >= map.tb) {
|
|
||||||
unit = 'TB';
|
|
||||||
} else if (mag >= map.gb) {
|
|
||||||
unit = 'GB';
|
|
||||||
} else if (mag >= map.mb) {
|
|
||||||
unit = 'MB';
|
|
||||||
} else if (mag >= map.kb) {
|
|
||||||
unit = 'KB';
|
|
||||||
} else {
|
|
||||||
unit = 'B';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = value / map[unit.toLowerCase()];
|
|
||||||
var str = val.toFixed(decimalPlaces);
|
|
||||||
|
|
||||||
if (!fixedDecimals) {
|
|
||||||
str = str.replace(formatDecimalsRegExp, '$1');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thousandsSeparator) {
|
|
||||||
str = str.split('.').map(function (s, i) {
|
|
||||||
return i === 0
|
|
||||||
? s.replace(formatThousandsRegExp, thousandsSeparator)
|
|
||||||
: s
|
|
||||||
}).join('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return str + unitSeparator + unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the string value into an integer in bytes.
|
|
||||||
*
|
|
||||||
* If no unit is given, it is assumed the value is in bytes.
|
|
||||||
*
|
|
||||||
* @param {number|string} val
|
|
||||||
*
|
|
||||||
* @returns {number|null}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parse(val) {
|
|
||||||
if (typeof val === 'number' && !isNaN(val)) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof val !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if the string passed is valid
|
|
||||||
var results = parseRegExp.exec(val);
|
|
||||||
var floatValue;
|
|
||||||
var unit = 'b';
|
|
||||||
|
|
||||||
if (!results) {
|
|
||||||
// Nothing could be extracted from the given string
|
|
||||||
floatValue = parseInt(val, 10);
|
|
||||||
unit = 'b'
|
|
||||||
} else {
|
|
||||||
// Retrieve the value and the unit
|
|
||||||
floatValue = parseFloat(results[1]);
|
|
||||||
unit = results[4].toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(floatValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor(map[unit] * floatValue);
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "bytes",
|
|
||||||
"description": "Utility to parse a string bytes to bytes and vice-versa",
|
|
||||||
"version": "3.1.2",
|
|
||||||
"author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
|
|
||||||
"contributors": [
|
|
||||||
"Jed Watson <jed.watson@me.com>",
|
|
||||||
"Théo FIDRY <theo.fidry@gmail.com>"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"byte",
|
|
||||||
"bytes",
|
|
||||||
"utility",
|
|
||||||
"parse",
|
|
||||||
"parser",
|
|
||||||
"convert",
|
|
||||||
"converter"
|
|
||||||
],
|
|
||||||
"repository": "visionmedia/bytes.js",
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "7.32.0",
|
|
||||||
"eslint-plugin-markdown": "2.2.1",
|
|
||||||
"mocha": "9.2.0",
|
|
||||||
"nyc": "15.1.0"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"History.md",
|
|
||||||
"LICENSE",
|
|
||||||
"Readme.md",
|
|
||||||
"index.js"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "mocha --check-leaks --reporter spec",
|
|
||||||
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
|
||||||
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
|
|
||||||
"extends": "@ljharb",
|
|
||||||
|
|
||||||
"rules": {
|
|
||||||
"func-name-matching": 0,
|
|
||||||
"id-length": 0,
|
|
||||||
"new-cap": [2, {
|
|
||||||
"capIsNewExceptions": [
|
|
||||||
"GetIntrinsic",
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
"no-extra-parens": 0,
|
|
||||||
"no-magic-numbers": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [ljharb]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: npm/call-bind-apply-helpers
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"all": true,
|
|
||||||
"check-coverage": false,
|
|
||||||
"reporter": ["text-summary", "text", "html", "json"],
|
|
||||||
"exclude": [
|
|
||||||
"coverage",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1)
|
|
||||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1)
|
|
||||||
|
|
||||||
## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8)
|
|
||||||
- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75)
|
|
||||||
- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940)
|
|
||||||
|
|
||||||
## v1.0.0 - 2024-12-05
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04)
|
|
||||||
- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f)
|
|
||||||
- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603)
|
|
||||||
- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930)
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 Jordan Harband
|
|
||||||
|
|
||||||
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.
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
# call-bind-apply-helpers <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
|
||||||
|
|
||||||
[![github actions][actions-image]][actions-url]
|
|
||||||
[![coverage][codecov-image]][codecov-url]
|
|
||||||
[![dependency status][deps-svg]][deps-url]
|
|
||||||
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
|
||||||
[![License][license-image]][license-url]
|
|
||||||
[![Downloads][downloads-image]][downloads-url]
|
|
||||||
|
|
||||||
[![npm badge][npm-badge-png]][package-url]
|
|
||||||
|
|
||||||
Helper functions around Function call/apply/bind, for use in `call-bind`.
|
|
||||||
|
|
||||||
The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`.
|
|
||||||
Please use `call-bind` unless you have a very good reason not to.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install --save call-bind-apply-helpers
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage/Examples
|
|
||||||
|
|
||||||
```js
|
|
||||||
const assert = require('assert');
|
|
||||||
const callBindBasic = require('call-bind-apply-helpers');
|
|
||||||
|
|
||||||
function f(a, b) {
|
|
||||||
assert.equal(this, 1);
|
|
||||||
assert.equal(a, 2);
|
|
||||||
assert.equal(b, 3);
|
|
||||||
assert.equal(arguments.length, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fBound = callBindBasic([f, 1]);
|
|
||||||
|
|
||||||
delete Function.prototype.call;
|
|
||||||
delete Function.prototype.bind;
|
|
||||||
|
|
||||||
fBound(2, 3);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Clone the repo, `npm install`, and run `npm test`
|
|
||||||
|
|
||||||
[package-url]: https://npmjs.org/package/call-bind-apply-helpers
|
|
||||||
[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg
|
|
||||||
[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg
|
|
||||||
[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers
|
|
||||||
[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg
|
|
||||||
[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies
|
|
||||||
[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true
|
|
||||||
[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg
|
|
||||||
[license-url]: LICENSE
|
|
||||||
[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg
|
|
||||||
[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers
|
|
||||||
[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg
|
|
||||||
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/
|
|
||||||
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers
|
|
||||||
[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export = Reflect.apply;
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var bind = require('function-bind');
|
|
||||||
|
|
||||||
var $apply = require('./functionApply');
|
|
||||||
var $call = require('./functionCall');
|
|
||||||
var $reflectApply = require('./reflectApply');
|
|
||||||
|
|
||||||
/** @type {import('./actualApply')} */
|
|
||||||
module.exports = $reflectApply || bind.call($call, $apply);
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
import actualApply from './actualApply';
|
|
||||||
|
|
||||||
type TupleSplitHead<T extends any[], N extends number> = T['length'] extends N
|
|
||||||
? T
|
|
||||||
: T extends [...infer R, any]
|
|
||||||
? TupleSplitHead<R, N>
|
|
||||||
: never
|
|
||||||
|
|
||||||
type TupleSplitTail<T, N extends number, O extends any[] = []> = O['length'] extends N
|
|
||||||
? T
|
|
||||||
: T extends [infer F, ...infer R]
|
|
||||||
? TupleSplitTail<[...R], N, [...O, F]>
|
|
||||||
: never
|
|
||||||
|
|
||||||
type TupleSplit<T extends any[], N extends number> = [TupleSplitHead<T, N>, TupleSplitTail<T, N>]
|
|
||||||
|
|
||||||
declare function applyBind(...args: TupleSplit<Parameters<typeof actualApply>, 2>[1]): ReturnType<typeof actualApply>;
|
|
||||||
|
|
||||||
export = applyBind;
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var bind = require('function-bind');
|
|
||||||
var $apply = require('./functionApply');
|
|
||||||
var actualApply = require('./actualApply');
|
|
||||||
|
|
||||||
/** @type {import('./applyBind')} */
|
|
||||||
module.exports = function applyBind() {
|
|
||||||
return actualApply(bind, $apply, arguments);
|
|
||||||
};
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export = Function.prototype.apply;
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('./functionApply')} */
|
|
||||||
module.exports = Function.prototype.apply;
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export = Function.prototype.call;
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('./functionCall')} */
|
|
||||||
module.exports = Function.prototype.call;
|
|
||||||
-64
@@ -1,64 +0,0 @@
|
|||||||
type RemoveFromTuple<
|
|
||||||
Tuple extends readonly unknown[],
|
|
||||||
RemoveCount extends number,
|
|
||||||
Index extends 1[] = []
|
|
||||||
> = Index["length"] extends RemoveCount
|
|
||||||
? Tuple
|
|
||||||
: Tuple extends [infer First, ...infer Rest]
|
|
||||||
? RemoveFromTuple<Rest, RemoveCount, [...Index, 1]>
|
|
||||||
: Tuple;
|
|
||||||
|
|
||||||
type ConcatTuples<
|
|
||||||
Prefix extends readonly unknown[],
|
|
||||||
Suffix extends readonly unknown[]
|
|
||||||
> = [...Prefix, ...Suffix];
|
|
||||||
|
|
||||||
type ExtractFunctionParams<T> = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R
|
|
||||||
? { thisArg: TThis; params: P; returnType: R }
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type BindFunction<
|
|
||||||
T extends (this: any, ...args: any[]) => any,
|
|
||||||
TThis,
|
|
||||||
TBoundArgs extends readonly unknown[],
|
|
||||||
ReceiverBound extends boolean
|
|
||||||
> = ExtractFunctionParams<T> extends {
|
|
||||||
thisArg: infer OrigThis;
|
|
||||||
params: infer P extends readonly unknown[];
|
|
||||||
returnType: infer R;
|
|
||||||
}
|
|
||||||
? ReceiverBound extends true
|
|
||||||
? (...args: RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>) => R extends [OrigThis, ...infer Rest]
|
|
||||||
? [TThis, ...Rest] // Replace `this` with `thisArg`
|
|
||||||
: R
|
|
||||||
: <U, RemainingArgs extends RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>>(
|
|
||||||
thisArg: U,
|
|
||||||
...args: RemainingArgs
|
|
||||||
) => R extends [OrigThis, ...infer Rest]
|
|
||||||
? [U, ...ConcatTuples<TBoundArgs, Rest>] // Preserve bound args in return type
|
|
||||||
: R
|
|
||||||
: never;
|
|
||||||
|
|
||||||
declare function callBind<
|
|
||||||
const T extends (this: any, ...args: any[]) => any,
|
|
||||||
Extracted extends ExtractFunctionParams<T>,
|
|
||||||
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[],
|
|
||||||
const TThis extends Extracted["thisArg"]
|
|
||||||
>(
|
|
||||||
args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs]
|
|
||||||
): BindFunction<T, TThis, TBoundArgs, true>;
|
|
||||||
|
|
||||||
declare function callBind<
|
|
||||||
const T extends (this: any, ...args: any[]) => any,
|
|
||||||
Extracted extends ExtractFunctionParams<T>,
|
|
||||||
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[]
|
|
||||||
>(
|
|
||||||
args: [fn: T, ...boundArgs: TBoundArgs]
|
|
||||||
): BindFunction<T, Extracted["thisArg"], TBoundArgs, false>;
|
|
||||||
|
|
||||||
declare function callBind<const TArgs extends readonly unknown[]>(
|
|
||||||
args: [fn: Exclude<TArgs[0], Function>, ...rest: TArgs]
|
|
||||||
): never;
|
|
||||||
|
|
||||||
// export as namespace callBind;
|
|
||||||
export = callBind;
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var bind = require('function-bind');
|
|
||||||
var $TypeError = require('es-errors/type');
|
|
||||||
|
|
||||||
var $call = require('./functionCall');
|
|
||||||
var $actualApply = require('./actualApply');
|
|
||||||
|
|
||||||
/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */
|
|
||||||
module.exports = function callBindBasic(args) {
|
|
||||||
if (args.length < 1 || typeof args[0] !== 'function') {
|
|
||||||
throw new $TypeError('a function is required');
|
|
||||||
}
|
|
||||||
return $actualApply(bind, $call, args);
|
|
||||||
};
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "call-bind-apply-helpers",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"description": "Helper functions around Function call/apply/bind, for use in `call-bind`",
|
|
||||||
"main": "index.js",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.js",
|
|
||||||
"./actualApply": "./actualApply.js",
|
|
||||||
"./applyBind": "./applyBind.js",
|
|
||||||
"./functionApply": "./functionApply.js",
|
|
||||||
"./functionCall": "./functionCall.js",
|
|
||||||
"./reflectApply": "./reflectApply.js",
|
|
||||||
"./package.json": "./package.json"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"prepack": "npmignore --auto --commentLines=auto",
|
|
||||||
"prepublish": "not-in-publish || npm run prepublishOnly",
|
|
||||||
"prepublishOnly": "safe-publish-latest",
|
|
||||||
"prelint": "evalmd README.md",
|
|
||||||
"lint": "eslint --ext=.js,.mjs .",
|
|
||||||
"postlint": "tsc -p . && attw -P",
|
|
||||||
"pretest": "npm run lint",
|
|
||||||
"tests-only": "nyc tape 'test/**/*.js'",
|
|
||||||
"test": "npm run tests-only",
|
|
||||||
"posttest": "npx npm@'>=10.2' audit --production",
|
|
||||||
"version": "auto-changelog && git add CHANGELOG.md",
|
|
||||||
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/ljharb/call-bind-apply-helpers.git"
|
|
||||||
},
|
|
||||||
"author": "Jordan Harband <ljharb@gmail.com>",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/ljharb/call-bind-apply-helpers/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@arethetypeswrong/cli": "^0.17.3",
|
|
||||||
"@ljharb/eslint-config": "^21.1.1",
|
|
||||||
"@ljharb/tsconfig": "^0.2.3",
|
|
||||||
"@types/for-each": "^0.3.3",
|
|
||||||
"@types/function-bind": "^1.1.10",
|
|
||||||
"@types/object-inspect": "^1.13.0",
|
|
||||||
"@types/tape": "^5.8.1",
|
|
||||||
"auto-changelog": "^2.5.0",
|
|
||||||
"encoding": "^0.1.13",
|
|
||||||
"es-value-fixtures": "^1.7.1",
|
|
||||||
"eslint": "=8.8.0",
|
|
||||||
"evalmd": "^0.0.19",
|
|
||||||
"for-each": "^0.3.5",
|
|
||||||
"has-strict-mode": "^1.1.0",
|
|
||||||
"in-publish": "^2.0.1",
|
|
||||||
"npmignore": "^0.3.1",
|
|
||||||
"nyc": "^10.3.2",
|
|
||||||
"object-inspect": "^1.13.4",
|
|
||||||
"safe-publish-latest": "^2.0.0",
|
|
||||||
"tape": "^5.9.0",
|
|
||||||
"typescript": "next"
|
|
||||||
},
|
|
||||||
"testling": {
|
|
||||||
"files": "test/index.js"
|
|
||||||
},
|
|
||||||
"auto-changelog": {
|
|
||||||
"output": "CHANGELOG.md",
|
|
||||||
"template": "keepachangelog",
|
|
||||||
"unreleased": false,
|
|
||||||
"commitLimit": false,
|
|
||||||
"backfillLimit": false,
|
|
||||||
"hideCredit": true
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"ignore": [
|
|
||||||
".github/workflows"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
declare const reflectApply: false | typeof Reflect.apply;
|
|
||||||
|
|
||||||
export = reflectApply;
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('./reflectApply')} */
|
|
||||||
module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply;
|
|
||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var callBind = require('../');
|
|
||||||
var hasStrictMode = require('has-strict-mode')();
|
|
||||||
var forEach = require('for-each');
|
|
||||||
var inspect = require('object-inspect');
|
|
||||||
var v = require('es-value-fixtures');
|
|
||||||
|
|
||||||
var test = require('tape');
|
|
||||||
|
|
||||||
test('callBindBasic', function (t) {
|
|
||||||
forEach(v.nonFunctions, function (nonFunction) {
|
|
||||||
t['throws'](
|
|
||||||
// @ts-expect-error
|
|
||||||
function () { callBind([nonFunction]); },
|
|
||||||
TypeError,
|
|
||||||
inspect(nonFunction) + ' is not a function'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
var sentinel = { sentinel: true };
|
|
||||||
/** @type {<T, A extends number, B extends number>(this: T, a: A, b: B) => [T | undefined, A, B]} */
|
|
||||||
var func = function (a, b) {
|
|
||||||
// eslint-disable-next-line no-invalid-this
|
|
||||||
return [!hasStrictMode && this === global ? undefined : this, a, b];
|
|
||||||
};
|
|
||||||
t.equal(func.length, 2, 'original function length is 2');
|
|
||||||
|
|
||||||
/** type {(thisArg: unknown, a: number, b: number) => [unknown, number, number]} */
|
|
||||||
var bound = callBind([func]);
|
|
||||||
/** type {((a: number, b: number) => [typeof sentinel, typeof a, typeof b])} */
|
|
||||||
var boundR = callBind([func, sentinel]);
|
|
||||||
/** type {((b: number) => [typeof sentinel, number, typeof b])} */
|
|
||||||
var boundArg = callBind([func, sentinel, /** @type {const} */ (1)]);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with no args');
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(bound(1, 2), [hasStrictMode ? 1 : Object(1), 2, undefined], 'bound func too few args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args');
|
|
||||||
|
|
||||||
t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args');
|
|
||||||
t.deepEqual(bound(1, 2, 3), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with right args');
|
|
||||||
t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args');
|
|
||||||
t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg');
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(bound(1, 2, 3, 4), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with too many args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args');
|
|
||||||
// @ts-expect-error
|
|
||||||
t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@ljharb/tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2021",
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"coverage",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
|
|
||||||
"extends": "@ljharb",
|
|
||||||
|
|
||||||
"rules": {
|
|
||||||
"new-cap": [2, {
|
|
||||||
"capIsNewExceptions": [
|
|
||||||
"GetIntrinsic",
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [ljharb]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: npm/call-bound
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"all": true,
|
|
||||||
"check-coverage": false,
|
|
||||||
"reporter": ["text-summary", "text", "html", "json"],
|
|
||||||
"exclude": [
|
|
||||||
"coverage",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [v1.0.4](https://github.com/ljharb/call-bound/compare/v1.0.3...v1.0.4) - 2025-03-03
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- [types] improve types [`e648922`](https://github.com/ljharb/call-bound/commit/e6489222a9e54f350fbf952ceabe51fd8b6027ff)
|
|
||||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`a42a5eb`](https://github.com/ljharb/call-bound/commit/a42a5ebe6c1b54fcdc7997c7dc64fdca9e936719)
|
|
||||||
- [Deps] update `call-bind-apply-helpers`, `get-intrinsic` [`f529eac`](https://github.com/ljharb/call-bound/commit/f529eac132404c17156bbc23ab2297a25d0f20b8)
|
|
||||||
|
|
||||||
## [v1.0.3](https://github.com/ljharb/call-bound/compare/v1.0.2...v1.0.3) - 2024-12-15
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- [Refactor] use `call-bind-apply-helpers` instead of `call-bind` [`5e0b134`](https://github.com/ljharb/call-bound/commit/5e0b13496df14fb7d05dae9412f088da8d3f75be)
|
|
||||||
- [Deps] update `get-intrinsic` [`41fc967`](https://github.com/ljharb/call-bound/commit/41fc96732a22c7b7e8f381f93ccc54bb6293be2e)
|
|
||||||
- [readme] fix example [`79a0137`](https://github.com/ljharb/call-bound/commit/79a0137723f7c6d09c9c05452bbf8d5efb5d6e49)
|
|
||||||
- [meta] add `sideEffects` flag [`08b07be`](https://github.com/ljharb/call-bound/commit/08b07be7f1c03f67dc6f3cdaf0906259771859f7)
|
|
||||||
|
|
||||||
## [v1.0.2](https://github.com/ljharb/call-bound/compare/v1.0.1...v1.0.2) - 2024-12-10
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `gopd` [`e6a5ffe`](https://github.com/ljharb/call-bound/commit/e6a5ffe849368fe4f74dfd6cdeca1b9baa39e8d5)
|
|
||||||
- [Deps] update `call-bind`, `get-intrinsic` [`2aeb5b5`](https://github.com/ljharb/call-bound/commit/2aeb5b521dc2b2683d1345c753ea1161de2d1c14)
|
|
||||||
- [types] improve return type [`1a0c9fe`](https://github.com/ljharb/call-bound/commit/1a0c9fe3114471e7ca1f57d104e2efe713bb4871)
|
|
||||||
|
|
||||||
## v1.0.1 - 2024-12-05
|
|
||||||
|
|
||||||
### Commits
|
|
||||||
|
|
||||||
- Initial implementation, tests, readme, types [`6d94121`](https://github.com/ljharb/call-bound/commit/6d94121a9243602e506334069f7a03189fe3363d)
|
|
||||||
- Initial commit [`0eae867`](https://github.com/ljharb/call-bound/commit/0eae867334ea025c33e6e91cdecfc9df96680cf9)
|
|
||||||
- npm init [`71b2479`](https://github.com/ljharb/call-bound/commit/71b2479c6723e0b7d91a6b663613067e98b7b275)
|
|
||||||
- Only apps should have lockfiles [`c3754a9`](https://github.com/ljharb/call-bound/commit/c3754a949b7f9132b47e2d18c1729889736741eb)
|
|
||||||
- [actions] skip `npm ls` in node < 10 [`74275a5`](https://github.com/ljharb/call-bound/commit/74275a5186b8caf6309b6b97472bdcb0df4683a8)
|
|
||||||
- [Dev Deps] add missing peer dep [`1354de8`](https://github.com/ljharb/call-bound/commit/1354de8679413e4ae9c523d85f76fa7a5e032d97)
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 Jordan Harband
|
|
||||||
|
|
||||||
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.
|
|
||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
# call-bound <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
|
||||||
|
|
||||||
[![github actions][actions-image]][actions-url]
|
|
||||||
[![coverage][codecov-image]][codecov-url]
|
|
||||||
[![dependency status][deps-svg]][deps-url]
|
|
||||||
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
|
||||||
[![License][license-image]][license-url]
|
|
||||||
[![Downloads][downloads-image]][downloads-url]
|
|
||||||
|
|
||||||
[![npm badge][npm-badge-png]][package-url]
|
|
||||||
|
|
||||||
Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install --save call-bound
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage/Examples
|
|
||||||
|
|
||||||
```js
|
|
||||||
const assert = require('assert');
|
|
||||||
const callBound = require('call-bound');
|
|
||||||
|
|
||||||
const slice = callBound('Array.prototype.slice');
|
|
||||||
|
|
||||||
delete Function.prototype.call;
|
|
||||||
delete Function.prototype.bind;
|
|
||||||
delete Array.prototype.slice;
|
|
||||||
|
|
||||||
assert.deepEqual(slice([1, 2, 3, 4], 1, -1), [2, 3]);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Clone the repo, `npm install`, and run `npm test`
|
|
||||||
|
|
||||||
[package-url]: https://npmjs.org/package/call-bound
|
|
||||||
[npm-version-svg]: https://versionbadg.es/ljharb/call-bound.svg
|
|
||||||
[deps-svg]: https://david-dm.org/ljharb/call-bound.svg
|
|
||||||
[deps-url]: https://david-dm.org/ljharb/call-bound
|
|
||||||
[dev-deps-svg]: https://david-dm.org/ljharb/call-bound/dev-status.svg
|
|
||||||
[dev-deps-url]: https://david-dm.org/ljharb/call-bound#info=devDependencies
|
|
||||||
[npm-badge-png]: https://nodei.co/npm/call-bound.png?downloads=true&stars=true
|
|
||||||
[license-image]: https://img.shields.io/npm/l/call-bound.svg
|
|
||||||
[license-url]: LICENSE
|
|
||||||
[downloads-image]: https://img.shields.io/npm/dm/call-bound.svg
|
|
||||||
[downloads-url]: https://npm-stat.com/charts.html?package=call-bound
|
|
||||||
[codecov-image]: https://codecov.io/gh/ljharb/call-bound/branch/main/graphs/badge.svg
|
|
||||||
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bound/
|
|
||||||
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bound
|
|
||||||
[actions-url]: https://github.com/ljharb/call-bound/actions
|
|
||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
type Intrinsic = typeof globalThis;
|
|
||||||
|
|
||||||
type IntrinsicName = keyof Intrinsic | `%${keyof Intrinsic}%`;
|
|
||||||
|
|
||||||
type IntrinsicPath = IntrinsicName | `${StripPercents<IntrinsicName>}.${string}` | `%${StripPercents<IntrinsicName>}.${string}%`;
|
|
||||||
|
|
||||||
type AllowMissing = boolean;
|
|
||||||
|
|
||||||
type StripPercents<T extends string> = T extends `%${infer U}%` ? U : T;
|
|
||||||
|
|
||||||
type BindMethodPrecise<F> =
|
|
||||||
F extends (this: infer This, ...args: infer Args) => infer R
|
|
||||||
? (obj: This, ...args: Args) => R
|
|
||||||
: F extends {
|
|
||||||
(this: infer This1, ...args: infer Args1): infer R1;
|
|
||||||
(this: infer This2, ...args: infer Args2): infer R2
|
|
||||||
}
|
|
||||||
? {
|
|
||||||
(obj: This1, ...args: Args1): R1;
|
|
||||||
(obj: This2, ...args: Args2): R2
|
|
||||||
}
|
|
||||||
: never
|
|
||||||
|
|
||||||
// Extract method type from a prototype
|
|
||||||
type GetPrototypeMethod<T extends keyof typeof globalThis, M extends string> =
|
|
||||||
(typeof globalThis)[T] extends { prototype: any }
|
|
||||||
? M extends keyof (typeof globalThis)[T]['prototype']
|
|
||||||
? (typeof globalThis)[T]['prototype'][M]
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
|
|
||||||
// Get static property/method
|
|
||||||
type GetStaticMember<T extends keyof typeof globalThis, P extends string> =
|
|
||||||
P extends keyof (typeof globalThis)[T] ? (typeof globalThis)[T][P] : never
|
|
||||||
|
|
||||||
// Type that maps string path to actual bound function or value with better precision
|
|
||||||
type BoundIntrinsic<S extends string> =
|
|
||||||
S extends `${infer Obj}.prototype.${infer Method}`
|
|
||||||
? Obj extends keyof typeof globalThis
|
|
||||||
? BindMethodPrecise<GetPrototypeMethod<Obj, Method & string>>
|
|
||||||
: unknown
|
|
||||||
: S extends `${infer Obj}.${infer Prop}`
|
|
||||||
? Obj extends keyof typeof globalThis
|
|
||||||
? GetStaticMember<Obj, Prop & string>
|
|
||||||
: unknown
|
|
||||||
: unknown
|
|
||||||
|
|
||||||
declare function arraySlice<T>(array: readonly T[], start?: number, end?: number): T[];
|
|
||||||
declare function arraySlice<T>(array: ArrayLike<T>, start?: number, end?: number): T[];
|
|
||||||
declare function arraySlice<T>(array: IArguments, start?: number, end?: number): T[];
|
|
||||||
|
|
||||||
// Special cases for methods that need explicit typing
|
|
||||||
interface SpecialCases {
|
|
||||||
'%Object.prototype.isPrototypeOf%': (thisArg: {}, obj: unknown) => boolean;
|
|
||||||
'%String.prototype.replace%': {
|
|
||||||
(str: string, searchValue: string | RegExp, replaceValue: string): string;
|
|
||||||
(str: string, searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string
|
|
||||||
};
|
|
||||||
'%Object.prototype.toString%': (obj: {}) => string;
|
|
||||||
'%Object.prototype.hasOwnProperty%': (obj: {}, v: PropertyKey) => boolean;
|
|
||||||
'%Array.prototype.slice%': typeof arraySlice;
|
|
||||||
'%Array.prototype.map%': <T, U>(array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any) => U[];
|
|
||||||
'%Array.prototype.filter%': <T>(array: readonly T[], predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any) => T[];
|
|
||||||
'%Array.prototype.indexOf%': <T>(array: readonly T[], searchElement: T, fromIndex?: number) => number;
|
|
||||||
'%Function.prototype.apply%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, args: A) => R;
|
|
||||||
'%Function.prototype.call%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => R;
|
|
||||||
'%Function.prototype.bind%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => (...remainingArgs: A) => R;
|
|
||||||
'%Promise.prototype.then%': {
|
|
||||||
<T, R>(promise: Promise<T>, onfulfilled: (value: T) => R | PromiseLike<R>): Promise<R>;
|
|
||||||
<T, R>(promise: Promise<T>, onfulfilled: ((value: T) => R | PromiseLike<R>) | undefined | null, onrejected: (reason: any) => R | PromiseLike<R>): Promise<R>;
|
|
||||||
};
|
|
||||||
'%RegExp.prototype.test%': (regexp: RegExp, str: string) => boolean;
|
|
||||||
'%RegExp.prototype.exec%': (regexp: RegExp, str: string) => RegExpExecArray | null;
|
|
||||||
'%Error.prototype.toString%': (error: Error) => string;
|
|
||||||
'%TypeError.prototype.toString%': (error: TypeError) => string;
|
|
||||||
'%String.prototype.split%': (
|
|
||||||
obj: unknown,
|
|
||||||
splitter: string | RegExp | {
|
|
||||||
[Symbol.split](string: string, limit?: number): string[];
|
|
||||||
},
|
|
||||||
limit?: number | undefined
|
|
||||||
) => string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a bound function for a prototype method, or a value for a static property.
|
|
||||||
*
|
|
||||||
* @param name - The name of the intrinsic (e.g. 'Array.prototype.slice')
|
|
||||||
* @param {AllowMissing} [allowMissing] - Whether to allow missing intrinsics (default: false)
|
|
||||||
*/
|
|
||||||
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: K, allowMissing?: AllowMissing): SpecialCases[`%${StripPercents<K>}%`];
|
|
||||||
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: S, allowMissing?: AllowMissing): BoundIntrinsic<S>;
|
|
||||||
|
|
||||||
export = callBound;
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var GetIntrinsic = require('get-intrinsic');
|
|
||||||
|
|
||||||
var callBindBasic = require('call-bind-apply-helpers');
|
|
||||||
|
|
||||||
/** @type {(thisArg: string, searchString: string, position?: number) => number} */
|
|
||||||
var $indexOf = callBindBasic([GetIntrinsic('%String.prototype.indexOf%')]);
|
|
||||||
|
|
||||||
/** @type {import('.')} */
|
|
||||||
module.exports = function callBoundIntrinsic(name, allowMissing) {
|
|
||||||
/* eslint no-extra-parens: 0 */
|
|
||||||
|
|
||||||
var intrinsic = /** @type {(this: unknown, ...args: unknown[]) => unknown} */ (GetIntrinsic(name, !!allowMissing));
|
|
||||||
if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {
|
|
||||||
return callBindBasic(/** @type {const} */ ([intrinsic]));
|
|
||||||
}
|
|
||||||
return intrinsic;
|
|
||||||
};
|
|
||||||
-99
@@ -1,99 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "call-bound",
|
|
||||||
"version": "1.0.4",
|
|
||||||
"description": "Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.",
|
|
||||||
"main": "index.js",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.js",
|
|
||||||
"./package.json": "./package.json"
|
|
||||||
},
|
|
||||||
"sideEffects": false,
|
|
||||||
"scripts": {
|
|
||||||
"prepack": "npmignore --auto --commentLines=auto",
|
|
||||||
"prepublish": "not-in-publish || npm run prepublishOnly",
|
|
||||||
"prepublishOnly": "safe-publish-latest",
|
|
||||||
"prelint": "evalmd README.md",
|
|
||||||
"lint": "eslint --ext=.js,.mjs .",
|
|
||||||
"postlint": "tsc -p . && attw -P",
|
|
||||||
"pretest": "npm run lint",
|
|
||||||
"tests-only": "nyc tape 'test/**/*.js'",
|
|
||||||
"test": "npm run tests-only",
|
|
||||||
"posttest": "npx npm@'>=10.2' audit --production",
|
|
||||||
"version": "auto-changelog && git add CHANGELOG.md",
|
|
||||||
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/ljharb/call-bound.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"javascript",
|
|
||||||
"ecmascript",
|
|
||||||
"es",
|
|
||||||
"js",
|
|
||||||
"callbind",
|
|
||||||
"callbound",
|
|
||||||
"call",
|
|
||||||
"bind",
|
|
||||||
"bound",
|
|
||||||
"call-bind",
|
|
||||||
"call-bound",
|
|
||||||
"function",
|
|
||||||
"es-abstract"
|
|
||||||
],
|
|
||||||
"author": "Jordan Harband <ljharb@gmail.com>",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/ljharb/call-bound/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/ljharb/call-bound#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
|
||||||
"get-intrinsic": "^1.3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@arethetypeswrong/cli": "^0.17.4",
|
|
||||||
"@ljharb/eslint-config": "^21.1.1",
|
|
||||||
"@ljharb/tsconfig": "^0.3.0",
|
|
||||||
"@types/call-bind": "^1.0.5",
|
|
||||||
"@types/get-intrinsic": "^1.2.3",
|
|
||||||
"@types/tape": "^5.8.1",
|
|
||||||
"auto-changelog": "^2.5.0",
|
|
||||||
"encoding": "^0.1.13",
|
|
||||||
"es-value-fixtures": "^1.7.1",
|
|
||||||
"eslint": "=8.8.0",
|
|
||||||
"evalmd": "^0.0.19",
|
|
||||||
"for-each": "^0.3.5",
|
|
||||||
"gopd": "^1.2.0",
|
|
||||||
"has-strict-mode": "^1.1.0",
|
|
||||||
"in-publish": "^2.0.1",
|
|
||||||
"npmignore": "^0.3.1",
|
|
||||||
"nyc": "^10.3.2",
|
|
||||||
"object-inspect": "^1.13.4",
|
|
||||||
"safe-publish-latest": "^2.0.0",
|
|
||||||
"tape": "^5.9.0",
|
|
||||||
"typescript": "next"
|
|
||||||
},
|
|
||||||
"testling": {
|
|
||||||
"files": "test/index.js"
|
|
||||||
},
|
|
||||||
"auto-changelog": {
|
|
||||||
"output": "CHANGELOG.md",
|
|
||||||
"template": "keepachangelog",
|
|
||||||
"unreleased": false,
|
|
||||||
"commitLimit": false,
|
|
||||||
"backfillLimit": false,
|
|
||||||
"hideCredit": true
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"ignore": [
|
|
||||||
".github/workflows"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var test = require('tape');
|
|
||||||
|
|
||||||
var callBound = require('../');
|
|
||||||
|
|
||||||
/** @template {true} T @template U @typedef {T extends U ? T : never} AssertType */
|
|
||||||
|
|
||||||
test('callBound', function (t) {
|
|
||||||
// static primitive
|
|
||||||
t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself');
|
|
||||||
t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself');
|
|
||||||
|
|
||||||
// static non-function object
|
|
||||||
t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself');
|
|
||||||
t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself');
|
|
||||||
t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself');
|
|
||||||
t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself');
|
|
||||||
|
|
||||||
// static function
|
|
||||||
t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself');
|
|
||||||
t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself');
|
|
||||||
|
|
||||||
// prototype primitive
|
|
||||||
t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself');
|
|
||||||
t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself');
|
|
||||||
|
|
||||||
var x = callBound('Object.prototype.toString');
|
|
||||||
var y = callBound('%Object.prototype.toString%');
|
|
||||||
|
|
||||||
// prototype function
|
|
||||||
t.notEqual(x, Object.prototype.toString, 'Object.prototype.toString does not yield itself');
|
|
||||||
t.notEqual(y, Object.prototype.toString, '%Object.prototype.toString% does not yield itself');
|
|
||||||
t.equal(x(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original');
|
|
||||||
t.equal(y(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original');
|
|
||||||
|
|
||||||
t['throws'](
|
|
||||||
// @ts-expect-error
|
|
||||||
function () { callBound('does not exist'); },
|
|
||||||
SyntaxError,
|
|
||||||
'nonexistent intrinsic throws'
|
|
||||||
);
|
|
||||||
t['throws'](
|
|
||||||
// @ts-expect-error
|
|
||||||
function () { callBound('does not exist', true); },
|
|
||||||
SyntaxError,
|
|
||||||
'allowMissing arg still throws for unknown intrinsic'
|
|
||||||
);
|
|
||||||
|
|
||||||
t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) {
|
|
||||||
st['throws'](
|
|
||||||
function () { callBound('WeakRef'); },
|
|
||||||
TypeError,
|
|
||||||
'real but absent intrinsic throws'
|
|
||||||
);
|
|
||||||
st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception');
|
|
||||||
st.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@ljharb/tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"lib": ["es2024"],
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"coverage",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2012 Paul Miller (https://paulmillr.com), Elan Shanker
|
|
||||||
|
|
||||||
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.
|
|
||||||
-305
@@ -1,305 +0,0 @@
|
|||||||
# Chokidar [](https://github.com/paulmillr/chokidar)
|
|
||||||
|
|
||||||
> Minimal and efficient cross-platform file watching library
|
|
||||||
|
|
||||||
## Why?
|
|
||||||
|
|
||||||
There are many reasons to prefer Chokidar to raw fs.watch / fs.watchFile in 2024:
|
|
||||||
|
|
||||||
- Events are properly reported
|
|
||||||
- macOS events report filenames
|
|
||||||
- events are not reported twice
|
|
||||||
- changes are reported as add / change / unlink instead of useless `rename`
|
|
||||||
- Atomic writes are supported, using `atomic` option
|
|
||||||
- Some file editors use them
|
|
||||||
- Chunked writes are supported, using `awaitWriteFinish` option
|
|
||||||
- Large files are commonly written in chunks
|
|
||||||
- File / dir filtering is supported
|
|
||||||
- Symbolic links are supported
|
|
||||||
- Recursive watching is always supported, instead of partial when using raw events
|
|
||||||
- Includes a way to limit recursion depth
|
|
||||||
|
|
||||||
Chokidar relies on the Node.js core `fs` module, but when using
|
|
||||||
`fs.watch` and `fs.watchFile` for watching, it normalizes the events it
|
|
||||||
receives, often checking for truth by getting file stats and/or dir contents.
|
|
||||||
The `fs.watch`-based implementation is the default, which
|
|
||||||
avoids polling and keeps CPU usage down. Be advised that chokidar will initiate
|
|
||||||
watchers recursively for everything within scope of the paths that have been
|
|
||||||
specified, so be judicious about not wasting system resources by watching much
|
|
||||||
more than needed. For some cases, `fs.watchFile`, which utilizes polling and uses more resources, is used.
|
|
||||||
|
|
||||||
Made for [Brunch](https://brunch.io/) in 2012,
|
|
||||||
it is now used in [~30 million repositories](https://www.npmjs.com/browse/depended/chokidar) and
|
|
||||||
has proven itself in production environments.
|
|
||||||
|
|
||||||
**Sep 2024 update:** v4 is out! It decreases dependency count from 13 to 1, removes
|
|
||||||
support for globs, adds support for ESM / Common.js modules, and bumps minimum node.js version from v8 to v14.
|
|
||||||
Check out [upgrading](#upgrading).
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Install with npm:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install chokidar
|
|
||||||
```
|
|
||||||
|
|
||||||
Use it in your code:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import chokidar from 'chokidar';
|
|
||||||
|
|
||||||
// One-liner for current directory
|
|
||||||
chokidar.watch('.').on('all', (event, path) => {
|
|
||||||
console.log(event, path);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Extended options
|
|
||||||
// ----------------
|
|
||||||
|
|
||||||
// Initialize watcher.
|
|
||||||
const watcher = chokidar.watch('file, dir, or array', {
|
|
||||||
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
|
|
||||||
persistent: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Something to use when events are received.
|
|
||||||
const log = console.log.bind(console);
|
|
||||||
// Add event listeners.
|
|
||||||
watcher
|
|
||||||
.on('add', path => log(`File ${path} has been added`))
|
|
||||||
.on('change', path => log(`File ${path} has been changed`))
|
|
||||||
.on('unlink', path => log(`File ${path} has been removed`));
|
|
||||||
|
|
||||||
// More possible events.
|
|
||||||
watcher
|
|
||||||
.on('addDir', path => log(`Directory ${path} has been added`))
|
|
||||||
.on('unlinkDir', path => log(`Directory ${path} has been removed`))
|
|
||||||
.on('error', error => log(`Watcher error: ${error}`))
|
|
||||||
.on('ready', () => log('Initial scan complete. Ready for changes'))
|
|
||||||
.on('raw', (event, path, details) => { // internal
|
|
||||||
log('Raw event info:', event, path, details);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 'add', 'addDir' and 'change' events also receive stat() results as second
|
|
||||||
// argument when available: https://nodejs.org/api/fs.html#fs_class_fs_stats
|
|
||||||
watcher.on('change', (path, stats) => {
|
|
||||||
if (stats) console.log(`File ${path} changed size to ${stats.size}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch new files.
|
|
||||||
watcher.add('new-file');
|
|
||||||
watcher.add(['new-file-2', 'new-file-3']);
|
|
||||||
|
|
||||||
// Get list of actual paths being watched on the filesystem
|
|
||||||
let watchedPaths = watcher.getWatched();
|
|
||||||
|
|
||||||
// Un-watch some files.
|
|
||||||
await watcher.unwatch('new-file');
|
|
||||||
|
|
||||||
// Stop watching. The method is async!
|
|
||||||
await watcher.close().then(() => console.log('closed'));
|
|
||||||
|
|
||||||
// Full list of options. See below for descriptions.
|
|
||||||
// Do not use this example!
|
|
||||||
chokidar.watch('file', {
|
|
||||||
persistent: true,
|
|
||||||
|
|
||||||
// ignore .txt files
|
|
||||||
ignored: (file) => file.endsWith('.txt'),
|
|
||||||
// watch only .txt files
|
|
||||||
// ignored: (file, _stats) => _stats?.isFile() && !file.endsWith('.txt'),
|
|
||||||
|
|
||||||
awaitWriteFinish: true, // emit single event when chunked writes are completed
|
|
||||||
atomic: true, // emit proper events when "atomic writes" (mv _tmp file) are used
|
|
||||||
|
|
||||||
// The options also allow specifying custom intervals in ms
|
|
||||||
// awaitWriteFinish: {
|
|
||||||
// stabilityThreshold: 2000,
|
|
||||||
// pollInterval: 100
|
|
||||||
// },
|
|
||||||
// atomic: 100,
|
|
||||||
|
|
||||||
interval: 100,
|
|
||||||
binaryInterval: 300,
|
|
||||||
|
|
||||||
cwd: '.',
|
|
||||||
depth: 99,
|
|
||||||
|
|
||||||
followSymlinks: true,
|
|
||||||
ignoreInitial: false,
|
|
||||||
ignorePermissionErrors: false,
|
|
||||||
usePolling: false,
|
|
||||||
alwaysStat: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
`chokidar.watch(paths, [options])`
|
|
||||||
|
|
||||||
* `paths` (string or array of strings). Paths to files, dirs to be watched
|
|
||||||
recursively.
|
|
||||||
* `options` (object) Options object as defined below:
|
|
||||||
|
|
||||||
#### Persistence
|
|
||||||
|
|
||||||
* `persistent` (default: `true`). Indicates whether the process
|
|
||||||
should continue to run as long as files are being watched.
|
|
||||||
|
|
||||||
#### Path filtering
|
|
||||||
|
|
||||||
* `ignored` function, regex, or path. Defines files/paths to be ignored.
|
|
||||||
The whole relative or absolute path is tested, not just filename. If a function with two arguments
|
|
||||||
is provided, it gets called twice per path - once with a single argument (the path), second
|
|
||||||
time with two arguments (the path and the
|
|
||||||
[`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats)
|
|
||||||
object of that path).
|
|
||||||
* `ignoreInitial` (default: `false`). If set to `false` then `add`/`addDir` events are also emitted for matching paths while
|
|
||||||
instantiating the watching as chokidar discovers these file paths (before the `ready` event).
|
|
||||||
* `followSymlinks` (default: `true`). When `false`, only the
|
|
||||||
symlinks themselves will be watched for changes instead of following
|
|
||||||
the link references and bubbling events through the link's path.
|
|
||||||
* `cwd` (no default). The base directory from which watch `paths` are to be
|
|
||||||
derived. Paths emitted with events will be relative to this.
|
|
||||||
|
|
||||||
#### Performance
|
|
||||||
|
|
||||||
* `usePolling` (default: `false`).
|
|
||||||
Whether to use fs.watchFile (backed by polling), or fs.watch. If polling
|
|
||||||
leads to high CPU utilization, consider setting this to `false`. It is
|
|
||||||
typically necessary to **set this to `true` to successfully watch files over
|
|
||||||
a network**, and it may be necessary to successfully watch files in other
|
|
||||||
non-standard situations. Setting to `true` explicitly on MacOS overrides the
|
|
||||||
`useFsEvents` default. You may also set the CHOKIDAR_USEPOLLING env variable
|
|
||||||
to true (1) or false (0) in order to override this option.
|
|
||||||
* _Polling-specific settings_ (effective when `usePolling: true`)
|
|
||||||
* `interval` (default: `100`). Interval of file system polling, in milliseconds. You may also
|
|
||||||
set the CHOKIDAR_INTERVAL env variable to override this option.
|
|
||||||
* `binaryInterval` (default: `300`). Interval of file system
|
|
||||||
polling for binary files.
|
|
||||||
([see list of binary extensions](https://github.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json))
|
|
||||||
* `alwaysStat` (default: `false`). If relying upon the
|
|
||||||
[`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats)
|
|
||||||
object that may get passed with `add`, `addDir`, and `change` events, set
|
|
||||||
this to `true` to ensure it is provided even in cases where it wasn't
|
|
||||||
already available from the underlying watch events.
|
|
||||||
* `depth` (default: `undefined`). If set, limits how many levels of
|
|
||||||
subdirectories will be traversed.
|
|
||||||
* `awaitWriteFinish` (default: `false`).
|
|
||||||
By default, the `add` event will fire when a file first appears on disk, before
|
|
||||||
the entire file has been written. Furthermore, in some cases some `change`
|
|
||||||
events will be emitted while the file is being written. In some cases,
|
|
||||||
especially when watching for large files there will be a need to wait for the
|
|
||||||
write operation to finish before responding to a file creation or modification.
|
|
||||||
Setting `awaitWriteFinish` to `true` (or a truthy value) will poll file size,
|
|
||||||
holding its `add` and `change` events until the size does not change for a
|
|
||||||
configurable amount of time. The appropriate duration setting is heavily
|
|
||||||
dependent on the OS and hardware. For accurate detection this parameter should
|
|
||||||
be relatively high, making file watching much less responsive.
|
|
||||||
Use with caution.
|
|
||||||
* *`options.awaitWriteFinish` can be set to an object in order to adjust
|
|
||||||
timing params:*
|
|
||||||
* `awaitWriteFinish.stabilityThreshold` (default: 2000). Amount of time in
|
|
||||||
milliseconds for a file size to remain constant before emitting its event.
|
|
||||||
* `awaitWriteFinish.pollInterval` (default: 100). File size polling interval, in milliseconds.
|
|
||||||
|
|
||||||
#### Errors
|
|
||||||
|
|
||||||
* `ignorePermissionErrors` (default: `false`). Indicates whether to watch files
|
|
||||||
that don't have read permissions if possible. If watching fails due to `EPERM`
|
|
||||||
or `EACCES` with this set to `true`, the errors will be suppressed silently.
|
|
||||||
* `atomic` (default: `true` if `useFsEvents` and `usePolling` are `false`).
|
|
||||||
Automatically filters out artifacts that occur when using editors that use
|
|
||||||
"atomic writes" instead of writing directly to the source file. If a file is
|
|
||||||
re-added within 100 ms of being deleted, Chokidar emits a `change` event
|
|
||||||
rather than `unlink` then `add`. If the default of 100 ms does not work well
|
|
||||||
for you, you can override it by setting `atomic` to a custom value, in
|
|
||||||
milliseconds.
|
|
||||||
|
|
||||||
### Methods & Events
|
|
||||||
|
|
||||||
`chokidar.watch()` produces an instance of `FSWatcher`. Methods of `FSWatcher`:
|
|
||||||
|
|
||||||
* `.add(path / paths)`: Add files, directories for tracking.
|
|
||||||
Takes an array of strings or just one string.
|
|
||||||
* `.on(event, callback)`: Listen for an FS event.
|
|
||||||
Available events: `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `ready`,
|
|
||||||
`raw`, `error`.
|
|
||||||
Additionally `all` is available which gets emitted with the underlying event
|
|
||||||
name and path for every event other than `ready`, `raw`, and `error`. `raw` is internal, use it carefully.
|
|
||||||
* `.unwatch(path / paths)`: Stop watching files or directories.
|
|
||||||
Takes an array of strings or just one string.
|
|
||||||
* `.close()`: **async** Removes all listeners from watched files. Asynchronous, returns Promise. Use with `await` to ensure bugs don't happen.
|
|
||||||
* `.getWatched()`: Returns an object representing all the paths on the file
|
|
||||||
system being watched by this `FSWatcher` instance. The object's keys are all the
|
|
||||||
directories (using absolute paths unless the `cwd` option was used), and the
|
|
||||||
values are arrays of the names of the items contained in each directory.
|
|
||||||
|
|
||||||
### CLI
|
|
||||||
|
|
||||||
Check out third party [chokidar-cli](https://github.com/open-cli-tools/chokidar-cli),
|
|
||||||
which allows to execute a command on each change, or get a stdio stream of change events.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Sometimes, Chokidar runs out of file handles, causing `EMFILE` and `ENOSP` errors:
|
|
||||||
|
|
||||||
* `bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell`
|
|
||||||
* `Error: watch /home/ ENOSPC`
|
|
||||||
|
|
||||||
There are two things that can cause it.
|
|
||||||
|
|
||||||
1. Exhausted file handles for generic fs operations
|
|
||||||
- Can be solved by using [graceful-fs](https://www.npmjs.com/package/graceful-fs),
|
|
||||||
which can monkey-patch native `fs` module used by chokidar: `let fs = require('fs'); let grfs = require('graceful-fs'); grfs.gracefulify(fs);`
|
|
||||||
- Can also be solved by tuning OS: `echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p`.
|
|
||||||
2. Exhausted file handles for `fs.watch`
|
|
||||||
- Can't seem to be solved by graceful-fs or OS tuning
|
|
||||||
- It's possible to start using `usePolling: true`, which will switch backend to resource-intensive `fs.watchFile`
|
|
||||||
|
|
||||||
All fsevents-related issues (`WARN optional dep failed`, `fsevents is not a constructor`) are solved by upgrading to v4+.
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
- **v4 (Sep 2024):** remove glob support and bundled fsevents. Decrease dependency count from 13 to 1. Rewrite in typescript. Bumps minimum node.js requirement to v14+
|
|
||||||
- **v3 (Apr 2019):** massive CPU & RAM consumption improvements; reduces deps / package size by a factor of 17x and bumps Node.js requirement to v8.16+.
|
|
||||||
- **v2 (Dec 2017):** globs are now posix-style-only. Tons of bugfixes.
|
|
||||||
- **v1 (Apr 2015):** glob support, symlink support, tons of bugfixes. Node 0.8+ is supported
|
|
||||||
- **v0.1 (Apr 2012):** Initial release, extracted from [Brunch](https://github.com/brunch/brunch/blob/9847a065aea300da99bd0753f90354cde9de1261/src/helpers.coffee#L66)
|
|
||||||
|
|
||||||
### Upgrading
|
|
||||||
|
|
||||||
If you've used globs before and want do replicate the functionality with v4:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// v3
|
|
||||||
chok.watch('**/*.js');
|
|
||||||
chok.watch("./directory/**/*");
|
|
||||||
|
|
||||||
// v4
|
|
||||||
chok.watch('.', {
|
|
||||||
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
|
|
||||||
});
|
|
||||||
chok.watch('./directory');
|
|
||||||
|
|
||||||
// other way
|
|
||||||
import { glob } from 'node:fs/promises';
|
|
||||||
const watcher = watch(await Array.fromAsync(glob('**/*.js')));
|
|
||||||
|
|
||||||
// unwatching
|
|
||||||
// v3
|
|
||||||
chok.unwatch('**/*.js');
|
|
||||||
// v4
|
|
||||||
chok.unwatch(await glob('**/*.js'));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Also
|
|
||||||
|
|
||||||
Why was chokidar named this way? What's the meaning behind it?
|
|
||||||
|
|
||||||
>Chowkidar is a transliteration of a Hindi word meaning 'watchman, gatekeeper', चौकीदार. This ultimately comes from Sanskrit _ चतुष्क_ (crossway, quadrangle, consisting-of-four). This word is also used in other languages like Urdu as (چوکیدار) which is widely used in Pakistan and India.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT (c) Paul Miller (<https://paulmillr.com>), see [LICENSE](LICENSE) file.
|
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
import type { WatchEventType, Stats, FSWatcher as NativeFsWatcher } from 'fs';
|
|
||||||
import type { FSWatcher, WatchHelper, Throttler } from './index.js';
|
|
||||||
import type { EntryInfo } from 'readdirp';
|
|
||||||
export type Path = string;
|
|
||||||
export declare const STR_DATA = "data";
|
|
||||||
export declare const STR_END = "end";
|
|
||||||
export declare const STR_CLOSE = "close";
|
|
||||||
export declare const EMPTY_FN: () => void;
|
|
||||||
export declare const IDENTITY_FN: (val: unknown) => unknown;
|
|
||||||
export declare const isWindows: boolean;
|
|
||||||
export declare const isMacos: boolean;
|
|
||||||
export declare const isLinux: boolean;
|
|
||||||
export declare const isFreeBSD: boolean;
|
|
||||||
export declare const isIBMi: boolean;
|
|
||||||
export declare const EVENTS: {
|
|
||||||
readonly ALL: "all";
|
|
||||||
readonly READY: "ready";
|
|
||||||
readonly ADD: "add";
|
|
||||||
readonly CHANGE: "change";
|
|
||||||
readonly ADD_DIR: "addDir";
|
|
||||||
readonly UNLINK: "unlink";
|
|
||||||
readonly UNLINK_DIR: "unlinkDir";
|
|
||||||
readonly RAW: "raw";
|
|
||||||
readonly ERROR: "error";
|
|
||||||
};
|
|
||||||
export type EventName = (typeof EVENTS)[keyof typeof EVENTS];
|
|
||||||
export type FsWatchContainer = {
|
|
||||||
listeners: (path: string) => void | Set<any>;
|
|
||||||
errHandlers: (err: unknown) => void | Set<any>;
|
|
||||||
rawEmitters: (ev: WatchEventType, path: string, opts: unknown) => void | Set<any>;
|
|
||||||
watcher: NativeFsWatcher;
|
|
||||||
watcherUnusable?: boolean;
|
|
||||||
};
|
|
||||||
export interface WatchHandlers {
|
|
||||||
listener: (path: string) => void;
|
|
||||||
errHandler: (err: unknown) => void;
|
|
||||||
rawEmitter: (ev: WatchEventType, path: string, opts: unknown) => void;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @mixin
|
|
||||||
*/
|
|
||||||
export declare class NodeFsHandler {
|
|
||||||
fsw: FSWatcher;
|
|
||||||
_boundHandleError: (error: unknown) => void;
|
|
||||||
constructor(fsW: FSWatcher);
|
|
||||||
/**
|
|
||||||
* Watch file for changes with fs_watchFile or fs_watch.
|
|
||||||
* @param path to file or dir
|
|
||||||
* @param listener on fs change
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_watchWithNodeFs(path: string, listener: (path: string, newStats?: any) => void | Promise<void>): (() => void) | undefined;
|
|
||||||
/**
|
|
||||||
* Watch a file and emit add event if warranted.
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_handleFile(file: Path, stats: Stats, initialAdd: boolean): (() => void) | undefined;
|
|
||||||
/**
|
|
||||||
* Handle symlinks encountered while reading a dir.
|
|
||||||
* @param entry returned by readdirp
|
|
||||||
* @param directory path of dir being read
|
|
||||||
* @param path of this item
|
|
||||||
* @param item basename of this item
|
|
||||||
* @returns true if no more processing is needed for this entry.
|
|
||||||
*/
|
|
||||||
_handleSymlink(entry: EntryInfo, directory: string, path: Path, item: string): Promise<boolean | undefined>;
|
|
||||||
_handleRead(directory: string, initialAdd: boolean, wh: WatchHelper, target: Path, dir: Path, depth: number, throttler: Throttler): Promise<unknown> | undefined;
|
|
||||||
/**
|
|
||||||
* Read directory to add / remove files from `@watched` list and re-read it on change.
|
|
||||||
* @param dir fs path
|
|
||||||
* @param stats
|
|
||||||
* @param initialAdd
|
|
||||||
* @param depth relative to user-supplied path
|
|
||||||
* @param target child path targeted for watch
|
|
||||||
* @param wh Common watch helpers for this path
|
|
||||||
* @param realpath
|
|
||||||
* @returns closer for the watcher instance.
|
|
||||||
*/
|
|
||||||
_handleDir(dir: string, stats: Stats, initialAdd: boolean, depth: number, target: string, wh: WatchHelper, realpath: string): Promise<(() => void) | undefined>;
|
|
||||||
/**
|
|
||||||
* Handle added file, directory, or glob pattern.
|
|
||||||
* Delegates call to _handleFile / _handleDir after checks.
|
|
||||||
* @param path to file or ir
|
|
||||||
* @param initialAdd was the file added at watch instantiation?
|
|
||||||
* @param priorWh depth relative to user-supplied path
|
|
||||||
* @param depth Child path actually targeted for watch
|
|
||||||
* @param target Child path actually targeted for watch
|
|
||||||
*/
|
|
||||||
_addToNodeFs(path: string, initialAdd: boolean, priorWh: WatchHelper | undefined, depth: number, target?: string): Promise<string | false | undefined>;
|
|
||||||
}
|
|
||||||
-629
@@ -1,629 +0,0 @@
|
|||||||
import { watchFile, unwatchFile, watch as fs_watch } from 'fs';
|
|
||||||
import { open, stat, lstat, realpath as fsrealpath } from 'fs/promises';
|
|
||||||
import * as sysPath from 'path';
|
|
||||||
import { type as osType } from 'os';
|
|
||||||
export const STR_DATA = 'data';
|
|
||||||
export const STR_END = 'end';
|
|
||||||
export const STR_CLOSE = 'close';
|
|
||||||
export const EMPTY_FN = () => { };
|
|
||||||
export const IDENTITY_FN = (val) => val;
|
|
||||||
const pl = process.platform;
|
|
||||||
export const isWindows = pl === 'win32';
|
|
||||||
export const isMacos = pl === 'darwin';
|
|
||||||
export const isLinux = pl === 'linux';
|
|
||||||
export const isFreeBSD = pl === 'freebsd';
|
|
||||||
export const isIBMi = osType() === 'OS400';
|
|
||||||
export const EVENTS = {
|
|
||||||
ALL: 'all',
|
|
||||||
READY: 'ready',
|
|
||||||
ADD: 'add',
|
|
||||||
CHANGE: 'change',
|
|
||||||
ADD_DIR: 'addDir',
|
|
||||||
UNLINK: 'unlink',
|
|
||||||
UNLINK_DIR: 'unlinkDir',
|
|
||||||
RAW: 'raw',
|
|
||||||
ERROR: 'error',
|
|
||||||
};
|
|
||||||
const EV = EVENTS;
|
|
||||||
const THROTTLE_MODE_WATCH = 'watch';
|
|
||||||
const statMethods = { lstat, stat };
|
|
||||||
const KEY_LISTENERS = 'listeners';
|
|
||||||
const KEY_ERR = 'errHandlers';
|
|
||||||
const KEY_RAW = 'rawEmitters';
|
|
||||||
const HANDLER_KEYS = [KEY_LISTENERS, KEY_ERR, KEY_RAW];
|
|
||||||
// prettier-ignore
|
|
||||||
const binaryExtensions = new Set([
|
|
||||||
'3dm', '3ds', '3g2', '3gp', '7z', 'a', 'aac', 'adp', 'afdesign', 'afphoto', 'afpub', 'ai',
|
|
||||||
'aif', 'aiff', 'alz', 'ape', 'apk', 'appimage', 'ar', 'arj', 'asf', 'au', 'avi',
|
|
||||||
'bak', 'baml', 'bh', 'bin', 'bk', 'bmp', 'btif', 'bz2', 'bzip2',
|
|
||||||
'cab', 'caf', 'cgm', 'class', 'cmx', 'cpio', 'cr2', 'cur', 'dat', 'dcm', 'deb', 'dex', 'djvu',
|
|
||||||
'dll', 'dmg', 'dng', 'doc', 'docm', 'docx', 'dot', 'dotm', 'dra', 'DS_Store', 'dsk', 'dts',
|
|
||||||
'dtshd', 'dvb', 'dwg', 'dxf',
|
|
||||||
'ecelp4800', 'ecelp7470', 'ecelp9600', 'egg', 'eol', 'eot', 'epub', 'exe',
|
|
||||||
'f4v', 'fbs', 'fh', 'fla', 'flac', 'flatpak', 'fli', 'flv', 'fpx', 'fst', 'fvt',
|
|
||||||
'g3', 'gh', 'gif', 'graffle', 'gz', 'gzip',
|
|
||||||
'h261', 'h263', 'h264', 'icns', 'ico', 'ief', 'img', 'ipa', 'iso',
|
|
||||||
'jar', 'jpeg', 'jpg', 'jpgv', 'jpm', 'jxr', 'key', 'ktx',
|
|
||||||
'lha', 'lib', 'lvp', 'lz', 'lzh', 'lzma', 'lzo',
|
|
||||||
'm3u', 'm4a', 'm4v', 'mar', 'mdi', 'mht', 'mid', 'midi', 'mj2', 'mka', 'mkv', 'mmr', 'mng',
|
|
||||||
'mobi', 'mov', 'movie', 'mp3',
|
|
||||||
'mp4', 'mp4a', 'mpeg', 'mpg', 'mpga', 'mxu',
|
|
||||||
'nef', 'npx', 'numbers', 'nupkg',
|
|
||||||
'o', 'odp', 'ods', 'odt', 'oga', 'ogg', 'ogv', 'otf', 'ott',
|
|
||||||
'pages', 'pbm', 'pcx', 'pdb', 'pdf', 'pea', 'pgm', 'pic', 'png', 'pnm', 'pot', 'potm',
|
|
||||||
'potx', 'ppa', 'ppam',
|
|
||||||
'ppm', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx', 'psd', 'pya', 'pyc', 'pyo', 'pyv',
|
|
||||||
'qt',
|
|
||||||
'rar', 'ras', 'raw', 'resources', 'rgb', 'rip', 'rlc', 'rmf', 'rmvb', 'rpm', 'rtf', 'rz',
|
|
||||||
's3m', 's7z', 'scpt', 'sgi', 'shar', 'snap', 'sil', 'sketch', 'slk', 'smv', 'snk', 'so',
|
|
||||||
'stl', 'suo', 'sub', 'swf',
|
|
||||||
'tar', 'tbz', 'tbz2', 'tga', 'tgz', 'thmx', 'tif', 'tiff', 'tlz', 'ttc', 'ttf', 'txz',
|
|
||||||
'udf', 'uvh', 'uvi', 'uvm', 'uvp', 'uvs', 'uvu',
|
|
||||||
'viv', 'vob',
|
|
||||||
'war', 'wav', 'wax', 'wbmp', 'wdp', 'weba', 'webm', 'webp', 'whl', 'wim', 'wm', 'wma',
|
|
||||||
'wmv', 'wmx', 'woff', 'woff2', 'wrm', 'wvx',
|
|
||||||
'xbm', 'xif', 'xla', 'xlam', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx', 'xm',
|
|
||||||
'xmind', 'xpi', 'xpm', 'xwd', 'xz',
|
|
||||||
'z', 'zip', 'zipx',
|
|
||||||
]);
|
|
||||||
const isBinaryPath = (filePath) => binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
|
|
||||||
// TODO: emit errors properly. Example: EMFILE on Macos.
|
|
||||||
const foreach = (val, fn) => {
|
|
||||||
if (val instanceof Set) {
|
|
||||||
val.forEach(fn);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fn(val);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const addAndConvert = (main, prop, item) => {
|
|
||||||
let container = main[prop];
|
|
||||||
if (!(container instanceof Set)) {
|
|
||||||
main[prop] = container = new Set([container]);
|
|
||||||
}
|
|
||||||
container.add(item);
|
|
||||||
};
|
|
||||||
const clearItem = (cont) => (key) => {
|
|
||||||
const set = cont[key];
|
|
||||||
if (set instanceof Set) {
|
|
||||||
set.clear();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete cont[key];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const delFromSet = (main, prop, item) => {
|
|
||||||
const container = main[prop];
|
|
||||||
if (container instanceof Set) {
|
|
||||||
container.delete(item);
|
|
||||||
}
|
|
||||||
else if (container === item) {
|
|
||||||
delete main[prop];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const isEmptySet = (val) => (val instanceof Set ? val.size === 0 : !val);
|
|
||||||
const FsWatchInstances = new Map();
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watch interface
|
|
||||||
* @param path to be watched
|
|
||||||
* @param options to be passed to fs_watch
|
|
||||||
* @param listener main event handler
|
|
||||||
* @param errHandler emits info about errors
|
|
||||||
* @param emitRaw emits raw event data
|
|
||||||
* @returns {NativeFsWatcher}
|
|
||||||
*/
|
|
||||||
function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
||||||
const handleEvent = (rawEvent, evPath) => {
|
|
||||||
listener(path);
|
|
||||||
emitRaw(rawEvent, evPath, { watchedPath: path });
|
|
||||||
// emit based on events occurring for files from a directory's watcher in
|
|
||||||
// case the file's watcher misses it (and rely on throttling to de-dupe)
|
|
||||||
if (evPath && path !== evPath) {
|
|
||||||
fsWatchBroadcast(sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
return fs_watch(path, {
|
|
||||||
persistent: options.persistent,
|
|
||||||
}, handleEvent);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
errHandler(error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Helper for passing fs_watch event data to a collection of listeners
|
|
||||||
* @param fullPath absolute path bound to fs_watch instance
|
|
||||||
*/
|
|
||||||
const fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
|
|
||||||
const cont = FsWatchInstances.get(fullPath);
|
|
||||||
if (!cont)
|
|
||||||
return;
|
|
||||||
foreach(cont[listenerType], (listener) => {
|
|
||||||
listener(val1, val2, val3);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watch interface or binds listeners
|
|
||||||
* to an existing one covering the same file system entry
|
|
||||||
* @param path
|
|
||||||
* @param fullPath absolute path
|
|
||||||
* @param options to be passed to fs_watch
|
|
||||||
* @param handlers container for event listener functions
|
|
||||||
*/
|
|
||||||
const setFsWatchListener = (path, fullPath, options, handlers) => {
|
|
||||||
const { listener, errHandler, rawEmitter } = handlers;
|
|
||||||
let cont = FsWatchInstances.get(fullPath);
|
|
||||||
let watcher;
|
|
||||||
if (!options.persistent) {
|
|
||||||
watcher = createFsWatchInstance(path, options, listener, errHandler, rawEmitter);
|
|
||||||
if (!watcher)
|
|
||||||
return;
|
|
||||||
return watcher.close.bind(watcher);
|
|
||||||
}
|
|
||||||
if (cont) {
|
|
||||||
addAndConvert(cont, KEY_LISTENERS, listener);
|
|
||||||
addAndConvert(cont, KEY_ERR, errHandler);
|
|
||||||
addAndConvert(cont, KEY_RAW, rawEmitter);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
watcher = createFsWatchInstance(path, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, // no need to use broadcast here
|
|
||||||
fsWatchBroadcast.bind(null, fullPath, KEY_RAW));
|
|
||||||
if (!watcher)
|
|
||||||
return;
|
|
||||||
watcher.on(EV.ERROR, async (error) => {
|
|
||||||
const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
|
|
||||||
if (cont)
|
|
||||||
cont.watcherUnusable = true; // documented since Node 10.4.1
|
|
||||||
// Workaround for https://github.com/joyent/node/issues/4337
|
|
||||||
if (isWindows && error.code === 'EPERM') {
|
|
||||||
try {
|
|
||||||
const fd = await open(path, 'r');
|
|
||||||
await fd.close();
|
|
||||||
broadcastErr(error);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
broadcastErr(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cont = {
|
|
||||||
listeners: listener,
|
|
||||||
errHandlers: errHandler,
|
|
||||||
rawEmitters: rawEmitter,
|
|
||||||
watcher,
|
|
||||||
};
|
|
||||||
FsWatchInstances.set(fullPath, cont);
|
|
||||||
}
|
|
||||||
// const index = cont.listeners.indexOf(listener);
|
|
||||||
// removes this instance's listeners and closes the underlying fs_watch
|
|
||||||
// instance if there are no more listeners left
|
|
||||||
return () => {
|
|
||||||
delFromSet(cont, KEY_LISTENERS, listener);
|
|
||||||
delFromSet(cont, KEY_ERR, errHandler);
|
|
||||||
delFromSet(cont, KEY_RAW, rawEmitter);
|
|
||||||
if (isEmptySet(cont.listeners)) {
|
|
||||||
// Check to protect against issue gh-730.
|
|
||||||
// if (cont.watcherUnusable) {
|
|
||||||
cont.watcher.close();
|
|
||||||
// }
|
|
||||||
FsWatchInstances.delete(fullPath);
|
|
||||||
HANDLER_KEYS.forEach(clearItem(cont));
|
|
||||||
// @ts-ignore
|
|
||||||
cont.watcher = undefined;
|
|
||||||
Object.freeze(cont);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// fs_watchFile helpers
|
|
||||||
// object to hold per-process fs_watchFile instances
|
|
||||||
// (may be shared across chokidar FSWatcher instances)
|
|
||||||
const FsWatchFileInstances = new Map();
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watchFile interface or binds listeners
|
|
||||||
* to an existing one covering the same file system entry
|
|
||||||
* @param path to be watched
|
|
||||||
* @param fullPath absolute path
|
|
||||||
* @param options options to be passed to fs_watchFile
|
|
||||||
* @param handlers container for event listener functions
|
|
||||||
* @returns closer
|
|
||||||
*/
|
|
||||||
const setFsWatchFileListener = (path, fullPath, options, handlers) => {
|
|
||||||
const { listener, rawEmitter } = handlers;
|
|
||||||
let cont = FsWatchFileInstances.get(fullPath);
|
|
||||||
// let listeners = new Set();
|
|
||||||
// let rawEmitters = new Set();
|
|
||||||
const copts = cont && cont.options;
|
|
||||||
if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
|
|
||||||
// "Upgrade" the watcher to persistence or a quicker interval.
|
|
||||||
// This creates some unlikely edge case issues if the user mixes
|
|
||||||
// settings in a very weird way, but solving for those cases
|
|
||||||
// doesn't seem worthwhile for the added complexity.
|
|
||||||
// listeners = cont.listeners;
|
|
||||||
// rawEmitters = cont.rawEmitters;
|
|
||||||
unwatchFile(fullPath);
|
|
||||||
cont = undefined;
|
|
||||||
}
|
|
||||||
if (cont) {
|
|
||||||
addAndConvert(cont, KEY_LISTENERS, listener);
|
|
||||||
addAndConvert(cont, KEY_RAW, rawEmitter);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO
|
|
||||||
// listeners.add(listener);
|
|
||||||
// rawEmitters.add(rawEmitter);
|
|
||||||
cont = {
|
|
||||||
listeners: listener,
|
|
||||||
rawEmitters: rawEmitter,
|
|
||||||
options,
|
|
||||||
watcher: watchFile(fullPath, options, (curr, prev) => {
|
|
||||||
foreach(cont.rawEmitters, (rawEmitter) => {
|
|
||||||
rawEmitter(EV.CHANGE, fullPath, { curr, prev });
|
|
||||||
});
|
|
||||||
const currmtime = curr.mtimeMs;
|
|
||||||
if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
|
|
||||||
foreach(cont.listeners, (listener) => listener(path, curr));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
FsWatchFileInstances.set(fullPath, cont);
|
|
||||||
}
|
|
||||||
// const index = cont.listeners.indexOf(listener);
|
|
||||||
// Removes this instance's listeners and closes the underlying fs_watchFile
|
|
||||||
// instance if there are no more listeners left.
|
|
||||||
return () => {
|
|
||||||
delFromSet(cont, KEY_LISTENERS, listener);
|
|
||||||
delFromSet(cont, KEY_RAW, rawEmitter);
|
|
||||||
if (isEmptySet(cont.listeners)) {
|
|
||||||
FsWatchFileInstances.delete(fullPath);
|
|
||||||
unwatchFile(fullPath);
|
|
||||||
cont.options = cont.watcher = undefined;
|
|
||||||
Object.freeze(cont);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @mixin
|
|
||||||
*/
|
|
||||||
export class NodeFsHandler {
|
|
||||||
constructor(fsW) {
|
|
||||||
this.fsw = fsW;
|
|
||||||
this._boundHandleError = (error) => fsW._handleError(error);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Watch file for changes with fs_watchFile or fs_watch.
|
|
||||||
* @param path to file or dir
|
|
||||||
* @param listener on fs change
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_watchWithNodeFs(path, listener) {
|
|
||||||
const opts = this.fsw.options;
|
|
||||||
const directory = sysPath.dirname(path);
|
|
||||||
const basename = sysPath.basename(path);
|
|
||||||
const parent = this.fsw._getWatchedDir(directory);
|
|
||||||
parent.add(basename);
|
|
||||||
const absolutePath = sysPath.resolve(path);
|
|
||||||
const options = {
|
|
||||||
persistent: opts.persistent,
|
|
||||||
};
|
|
||||||
if (!listener)
|
|
||||||
listener = EMPTY_FN;
|
|
||||||
let closer;
|
|
||||||
if (opts.usePolling) {
|
|
||||||
const enableBin = opts.interval !== opts.binaryInterval;
|
|
||||||
options.interval = enableBin && isBinaryPath(basename) ? opts.binaryInterval : opts.interval;
|
|
||||||
closer = setFsWatchFileListener(path, absolutePath, options, {
|
|
||||||
listener,
|
|
||||||
rawEmitter: this.fsw._emitRaw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
closer = setFsWatchListener(path, absolutePath, options, {
|
|
||||||
listener,
|
|
||||||
errHandler: this._boundHandleError,
|
|
||||||
rawEmitter: this.fsw._emitRaw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Watch a file and emit add event if warranted.
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_handleFile(file, stats, initialAdd) {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirname = sysPath.dirname(file);
|
|
||||||
const basename = sysPath.basename(file);
|
|
||||||
const parent = this.fsw._getWatchedDir(dirname);
|
|
||||||
// stats is always present
|
|
||||||
let prevStats = stats;
|
|
||||||
// if the file is already being watched, do nothing
|
|
||||||
if (parent.has(basename))
|
|
||||||
return;
|
|
||||||
const listener = async (path, newStats) => {
|
|
||||||
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
|
|
||||||
return;
|
|
||||||
if (!newStats || newStats.mtimeMs === 0) {
|
|
||||||
try {
|
|
||||||
const newStats = await stat(file);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// Check that change event was not fired because of changed only accessTime.
|
|
||||||
const at = newStats.atimeMs;
|
|
||||||
const mt = newStats.mtimeMs;
|
|
||||||
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
||||||
this.fsw._emit(EV.CHANGE, file, newStats);
|
|
||||||
}
|
|
||||||
if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats.ino) {
|
|
||||||
this.fsw._closeFile(path);
|
|
||||||
prevStats = newStats;
|
|
||||||
const closer = this._watchWithNodeFs(file, listener);
|
|
||||||
if (closer)
|
|
||||||
this.fsw._addPathCloser(path, closer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prevStats = newStats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Fix issues where mtime is null but file is still present
|
|
||||||
this.fsw._remove(dirname, basename);
|
|
||||||
}
|
|
||||||
// add is about to be emitted if file not already tracked in parent
|
|
||||||
}
|
|
||||||
else if (parent.has(basename)) {
|
|
||||||
// Check that change event was not fired because of changed only accessTime.
|
|
||||||
const at = newStats.atimeMs;
|
|
||||||
const mt = newStats.mtimeMs;
|
|
||||||
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
||||||
this.fsw._emit(EV.CHANGE, file, newStats);
|
|
||||||
}
|
|
||||||
prevStats = newStats;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// kick off the watcher
|
|
||||||
const closer = this._watchWithNodeFs(file, listener);
|
|
||||||
// emit an add event if we're supposed to
|
|
||||||
if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
|
|
||||||
if (!this.fsw._throttle(EV.ADD, file, 0))
|
|
||||||
return;
|
|
||||||
this.fsw._emit(EV.ADD, file, stats);
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle symlinks encountered while reading a dir.
|
|
||||||
* @param entry returned by readdirp
|
|
||||||
* @param directory path of dir being read
|
|
||||||
* @param path of this item
|
|
||||||
* @param item basename of this item
|
|
||||||
* @returns true if no more processing is needed for this entry.
|
|
||||||
*/
|
|
||||||
async _handleSymlink(entry, directory, path, item) {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const full = entry.fullPath;
|
|
||||||
const dir = this.fsw._getWatchedDir(directory);
|
|
||||||
if (!this.fsw.options.followSymlinks) {
|
|
||||||
// watch symlink directly (don't follow) and detect changes
|
|
||||||
this.fsw._incrReadyCount();
|
|
||||||
let linkPath;
|
|
||||||
try {
|
|
||||||
linkPath = await fsrealpath(path);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.fsw._emitReady();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
if (dir.has(item)) {
|
|
||||||
if (this.fsw._symlinkPaths.get(full) !== linkPath) {
|
|
||||||
this.fsw._symlinkPaths.set(full, linkPath);
|
|
||||||
this.fsw._emit(EV.CHANGE, path, entry.stats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dir.add(item);
|
|
||||||
this.fsw._symlinkPaths.set(full, linkPath);
|
|
||||||
this.fsw._emit(EV.ADD, path, entry.stats);
|
|
||||||
}
|
|
||||||
this.fsw._emitReady();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// don't follow the same symlink more than once
|
|
||||||
if (this.fsw._symlinkPaths.has(full)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this.fsw._symlinkPaths.set(full, true);
|
|
||||||
}
|
|
||||||
_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
|
|
||||||
// Normalize the directory name on Windows
|
|
||||||
directory = sysPath.join(directory, '');
|
|
||||||
throttler = this.fsw._throttle('readdir', directory, 1000);
|
|
||||||
if (!throttler)
|
|
||||||
return;
|
|
||||||
const previous = this.fsw._getWatchedDir(wh.path);
|
|
||||||
const current = new Set();
|
|
||||||
let stream = this.fsw._readdirp(directory, {
|
|
||||||
fileFilter: (entry) => wh.filterPath(entry),
|
|
||||||
directoryFilter: (entry) => wh.filterDir(entry),
|
|
||||||
});
|
|
||||||
if (!stream)
|
|
||||||
return;
|
|
||||||
stream
|
|
||||||
.on(STR_DATA, async (entry) => {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const item = entry.path;
|
|
||||||
let path = sysPath.join(directory, item);
|
|
||||||
current.add(item);
|
|
||||||
if (entry.stats.isSymbolicLink() &&
|
|
||||||
(await this._handleSymlink(entry, directory, path, item))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Files that present in current directory snapshot
|
|
||||||
// but absent in previous are added to watch list and
|
|
||||||
// emit `add` event.
|
|
||||||
if (item === target || (!target && !previous.has(item))) {
|
|
||||||
this.fsw._incrReadyCount();
|
|
||||||
// ensure relativeness of path is preserved in case of watcher reuse
|
|
||||||
path = sysPath.join(dir, sysPath.relative(dir, path));
|
|
||||||
this._addToNodeFs(path, initialAdd, wh, depth + 1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on(EV.ERROR, this._boundHandleError);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!stream)
|
|
||||||
return reject();
|
|
||||||
stream.once(STR_END, () => {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const wasThrottled = throttler ? throttler.clear() : false;
|
|
||||||
resolve(undefined);
|
|
||||||
// Files that absent in current directory snapshot
|
|
||||||
// but present in previous emit `remove` event
|
|
||||||
// and are removed from @watched[directory].
|
|
||||||
previous
|
|
||||||
.getChildren()
|
|
||||||
.filter((item) => {
|
|
||||||
return item !== directory && !current.has(item);
|
|
||||||
})
|
|
||||||
.forEach((item) => {
|
|
||||||
this.fsw._remove(directory, item);
|
|
||||||
});
|
|
||||||
stream = undefined;
|
|
||||||
// one more time for any missed in case changes came in extremely quickly
|
|
||||||
if (wasThrottled)
|
|
||||||
this._handleRead(directory, false, wh, target, dir, depth, throttler);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read directory to add / remove files from `@watched` list and re-read it on change.
|
|
||||||
* @param dir fs path
|
|
||||||
* @param stats
|
|
||||||
* @param initialAdd
|
|
||||||
* @param depth relative to user-supplied path
|
|
||||||
* @param target child path targeted for watch
|
|
||||||
* @param wh Common watch helpers for this path
|
|
||||||
* @param realpath
|
|
||||||
* @returns closer for the watcher instance.
|
|
||||||
*/
|
|
||||||
async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) {
|
|
||||||
const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
|
|
||||||
const tracked = parentDir.has(sysPath.basename(dir));
|
|
||||||
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
|
|
||||||
this.fsw._emit(EV.ADD_DIR, dir, stats);
|
|
||||||
}
|
|
||||||
// ensure dir is tracked (harmless if redundant)
|
|
||||||
parentDir.add(sysPath.basename(dir));
|
|
||||||
this.fsw._getWatchedDir(dir);
|
|
||||||
let throttler;
|
|
||||||
let closer;
|
|
||||||
const oDepth = this.fsw.options.depth;
|
|
||||||
if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) {
|
|
||||||
if (!target) {
|
|
||||||
await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
closer = this._watchWithNodeFs(dir, (dirPath, stats) => {
|
|
||||||
// if current directory is removed, do nothing
|
|
||||||
if (stats && stats.mtimeMs === 0)
|
|
||||||
return;
|
|
||||||
this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle added file, directory, or glob pattern.
|
|
||||||
* Delegates call to _handleFile / _handleDir after checks.
|
|
||||||
* @param path to file or ir
|
|
||||||
* @param initialAdd was the file added at watch instantiation?
|
|
||||||
* @param priorWh depth relative to user-supplied path
|
|
||||||
* @param depth Child path actually targeted for watch
|
|
||||||
* @param target Child path actually targeted for watch
|
|
||||||
*/
|
|
||||||
async _addToNodeFs(path, initialAdd, priorWh, depth, target) {
|
|
||||||
const ready = this.fsw._emitReady;
|
|
||||||
if (this.fsw._isIgnored(path) || this.fsw.closed) {
|
|
||||||
ready();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const wh = this.fsw._getWatchHelpers(path);
|
|
||||||
if (priorWh) {
|
|
||||||
wh.filterPath = (entry) => priorWh.filterPath(entry);
|
|
||||||
wh.filterDir = (entry) => priorWh.filterDir(entry);
|
|
||||||
}
|
|
||||||
// evaluate what is at the path we're being asked to watch
|
|
||||||
try {
|
|
||||||
const stats = await statMethods[wh.statMethod](wh.watchPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
if (this.fsw._isIgnored(wh.watchPath, stats)) {
|
|
||||||
ready();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const follow = this.fsw.options.followSymlinks;
|
|
||||||
let closer;
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
const absPath = sysPath.resolve(path);
|
|
||||||
const targetPath = follow ? await fsrealpath(path) : path;
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// preserve this symlink's target path
|
|
||||||
if (absPath !== targetPath && targetPath !== undefined) {
|
|
||||||
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (stats.isSymbolicLink()) {
|
|
||||||
const targetPath = follow ? await fsrealpath(path) : path;
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
const parent = sysPath.dirname(wh.watchPath);
|
|
||||||
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
||||||
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
||||||
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// preserve this symlink's target path
|
|
||||||
if (targetPath !== undefined) {
|
|
||||||
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
||||||
}
|
|
||||||
ready();
|
|
||||||
if (closer)
|
|
||||||
this.fsw._addPathCloser(path, closer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (this.fsw._handleError(error)) {
|
|
||||||
ready();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-215
@@ -1,215 +0,0 @@
|
|||||||
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
||||||
import { Stats } from 'fs';
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { ReaddirpStream, ReaddirpOptions, EntryInfo } from 'readdirp';
|
|
||||||
import { NodeFsHandler, EventName, Path, EVENTS as EV, WatchHandlers } from './handler.js';
|
|
||||||
type AWF = {
|
|
||||||
stabilityThreshold: number;
|
|
||||||
pollInterval: number;
|
|
||||||
};
|
|
||||||
type BasicOpts = {
|
|
||||||
persistent: boolean;
|
|
||||||
ignoreInitial: boolean;
|
|
||||||
followSymlinks: boolean;
|
|
||||||
cwd?: string;
|
|
||||||
usePolling: boolean;
|
|
||||||
interval: number;
|
|
||||||
binaryInterval: number;
|
|
||||||
alwaysStat?: boolean;
|
|
||||||
depth?: number;
|
|
||||||
ignorePermissionErrors: boolean;
|
|
||||||
atomic: boolean | number;
|
|
||||||
};
|
|
||||||
export type Throttler = {
|
|
||||||
timeoutObject: NodeJS.Timeout;
|
|
||||||
clear: () => void;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
export type ChokidarOptions = Partial<BasicOpts & {
|
|
||||||
ignored: Matcher | Matcher[];
|
|
||||||
awaitWriteFinish: boolean | Partial<AWF>;
|
|
||||||
}>;
|
|
||||||
export type FSWInstanceOptions = BasicOpts & {
|
|
||||||
ignored: Matcher[];
|
|
||||||
awaitWriteFinish: false | AWF;
|
|
||||||
};
|
|
||||||
export type ThrottleType = 'readdir' | 'watch' | 'add' | 'remove' | 'change';
|
|
||||||
export type EmitArgs = [path: Path, stats?: Stats];
|
|
||||||
export type EmitErrorArgs = [error: Error, stats?: Stats];
|
|
||||||
export type EmitArgsWithName = [event: EventName, ...EmitArgs];
|
|
||||||
export type MatchFunction = (val: string, stats?: Stats) => boolean;
|
|
||||||
export interface MatcherObject {
|
|
||||||
path: string;
|
|
||||||
recursive?: boolean;
|
|
||||||
}
|
|
||||||
export type Matcher = string | RegExp | MatchFunction | MatcherObject;
|
|
||||||
/**
|
|
||||||
* Directory entry.
|
|
||||||
*/
|
|
||||||
declare class DirEntry {
|
|
||||||
path: Path;
|
|
||||||
_removeWatcher: (dir: string, base: string) => void;
|
|
||||||
items: Set<Path>;
|
|
||||||
constructor(dir: Path, removeWatcher: (dir: string, base: string) => void);
|
|
||||||
add(item: string): void;
|
|
||||||
remove(item: string): Promise<void>;
|
|
||||||
has(item: string): boolean | undefined;
|
|
||||||
getChildren(): string[];
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
export declare class WatchHelper {
|
|
||||||
fsw: FSWatcher;
|
|
||||||
path: string;
|
|
||||||
watchPath: string;
|
|
||||||
fullWatchPath: string;
|
|
||||||
dirParts: string[][];
|
|
||||||
followSymlinks: boolean;
|
|
||||||
statMethod: 'stat' | 'lstat';
|
|
||||||
constructor(path: string, follow: boolean, fsw: FSWatcher);
|
|
||||||
entryPath(entry: EntryInfo): Path;
|
|
||||||
filterPath(entry: EntryInfo): boolean;
|
|
||||||
filterDir(entry: EntryInfo): boolean;
|
|
||||||
}
|
|
||||||
export interface FSWatcherKnownEventMap {
|
|
||||||
[EV.READY]: [];
|
|
||||||
[EV.RAW]: Parameters<WatchHandlers['rawEmitter']>;
|
|
||||||
[EV.ERROR]: Parameters<WatchHandlers['errHandler']>;
|
|
||||||
[EV.ALL]: [event: EventName, ...EmitArgs];
|
|
||||||
}
|
|
||||||
export type FSWatcherEventMap = FSWatcherKnownEventMap & {
|
|
||||||
[k in Exclude<EventName, keyof FSWatcherKnownEventMap>]: EmitArgs;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Watches files & directories for changes. Emitted events:
|
|
||||||
* `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
|
|
||||||
*
|
|
||||||
* new FSWatcher()
|
|
||||||
* .add(directories)
|
|
||||||
* .on('add', path => log('File', path, 'was added'))
|
|
||||||
*/
|
|
||||||
export declare class FSWatcher extends EventEmitter<FSWatcherEventMap> {
|
|
||||||
closed: boolean;
|
|
||||||
options: FSWInstanceOptions;
|
|
||||||
_closers: Map<string, Array<any>>;
|
|
||||||
_ignoredPaths: Set<Matcher>;
|
|
||||||
_throttled: Map<ThrottleType, Map<any, any>>;
|
|
||||||
_streams: Set<ReaddirpStream>;
|
|
||||||
_symlinkPaths: Map<Path, string | boolean>;
|
|
||||||
_watched: Map<string, DirEntry>;
|
|
||||||
_pendingWrites: Map<string, any>;
|
|
||||||
_pendingUnlinks: Map<string, EmitArgsWithName>;
|
|
||||||
_readyCount: number;
|
|
||||||
_emitReady: () => void;
|
|
||||||
_closePromise?: Promise<void>;
|
|
||||||
_userIgnored?: MatchFunction;
|
|
||||||
_readyEmitted: boolean;
|
|
||||||
_emitRaw: WatchHandlers['rawEmitter'];
|
|
||||||
_boundRemove: (dir: string, item: string) => void;
|
|
||||||
_nodeFsHandler: NodeFsHandler;
|
|
||||||
constructor(_opts?: ChokidarOptions);
|
|
||||||
_addIgnoredPath(matcher: Matcher): void;
|
|
||||||
_removeIgnoredPath(matcher: Matcher): void;
|
|
||||||
/**
|
|
||||||
* Adds paths to be watched on an existing FSWatcher instance.
|
|
||||||
* @param paths_ file or file list. Other arguments are unused
|
|
||||||
*/
|
|
||||||
add(paths_: Path | Path[], _origAdd?: string, _internal?: boolean): FSWatcher;
|
|
||||||
/**
|
|
||||||
* Close watchers or start ignoring events from specified paths.
|
|
||||||
*/
|
|
||||||
unwatch(paths_: Path | Path[]): FSWatcher;
|
|
||||||
/**
|
|
||||||
* Close watchers and remove all listeners from watched paths.
|
|
||||||
*/
|
|
||||||
close(): Promise<void>;
|
|
||||||
/**
|
|
||||||
* Expose list of watched paths
|
|
||||||
* @returns for chaining
|
|
||||||
*/
|
|
||||||
getWatched(): Record<string, string[]>;
|
|
||||||
emitWithAll(event: EventName, args: EmitArgs): void;
|
|
||||||
/**
|
|
||||||
* Normalize and emit events.
|
|
||||||
* Calling _emit DOES NOT MEAN emit() would be called!
|
|
||||||
* @param event Type of event
|
|
||||||
* @param path File or directory path
|
|
||||||
* @param stats arguments to be passed with event
|
|
||||||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_emit(event: EventName, path: Path, stats?: Stats): Promise<this | undefined>;
|
|
||||||
/**
|
|
||||||
* Common handler for errors
|
|
||||||
* @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_handleError(error: Error): Error | boolean;
|
|
||||||
/**
|
|
||||||
* Helper utility for throttling
|
|
||||||
* @param actionType type being throttled
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param timeout duration of time to suppress duplicate actions
|
|
||||||
* @returns tracking object or false if action should be suppressed
|
|
||||||
*/
|
|
||||||
_throttle(actionType: ThrottleType, path: Path, timeout: number): Throttler | false;
|
|
||||||
_incrReadyCount(): number;
|
|
||||||
/**
|
|
||||||
* Awaits write operation to finish.
|
|
||||||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
|
|
||||||
* @param event
|
|
||||||
* @param awfEmit Callback to be called when ready for event to be emitted.
|
|
||||||
*/
|
|
||||||
_awaitWriteFinish(path: Path, threshold: number, event: EventName, awfEmit: (err?: Error, stat?: Stats) => void): void;
|
|
||||||
/**
|
|
||||||
* Determines whether user has asked to ignore this path.
|
|
||||||
*/
|
|
||||||
_isIgnored(path: Path, stats?: Stats): boolean;
|
|
||||||
_isntIgnored(path: Path, stat?: Stats): boolean;
|
|
||||||
/**
|
|
||||||
* Provides a set of common helpers and properties relating to symlink handling.
|
|
||||||
* @param path file or directory pattern being watched
|
|
||||||
*/
|
|
||||||
_getWatchHelpers(path: Path): WatchHelper;
|
|
||||||
/**
|
|
||||||
* Provides directory tracking objects
|
|
||||||
* @param directory path of the directory
|
|
||||||
*/
|
|
||||||
_getWatchedDir(directory: string): DirEntry;
|
|
||||||
/**
|
|
||||||
* Check for read permissions: https://stackoverflow.com/a/11781404/1358405
|
|
||||||
*/
|
|
||||||
_hasReadPermissions(stats: Stats): boolean;
|
|
||||||
/**
|
|
||||||
* Handles emitting unlink events for
|
|
||||||
* files and directories, and via recursion, for
|
|
||||||
* files and directories within directories that are unlinked
|
|
||||||
* @param directory within which the following item is located
|
|
||||||
* @param item base path of item/directory
|
|
||||||
*/
|
|
||||||
_remove(directory: string, item: string, isDirectory?: boolean): void;
|
|
||||||
/**
|
|
||||||
* Closes all watchers for a path
|
|
||||||
*/
|
|
||||||
_closePath(path: Path): void;
|
|
||||||
/**
|
|
||||||
* Closes only file-specific watchers
|
|
||||||
*/
|
|
||||||
_closeFile(path: Path): void;
|
|
||||||
_addPathCloser(path: Path, closer: () => void): void;
|
|
||||||
_readdirp(root: Path, opts?: Partial<ReaddirpOptions>): ReaddirpStream | undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Instantiates watcher with paths to be tracked.
|
|
||||||
* @param paths file / directory paths
|
|
||||||
* @param options opts, such as `atomic`, `awaitWriteFinish`, `ignored`, and others
|
|
||||||
* @returns an instance of FSWatcher for chaining.
|
|
||||||
* @example
|
|
||||||
* const watcher = watch('.').on('all', (event, path) => { console.log(event, path); });
|
|
||||||
* watch('.', { atomic: true, awaitWriteFinish: true, ignored: (f, stats) => stats?.isFile() && !f.endsWith('.js') })
|
|
||||||
*/
|
|
||||||
export declare function watch(paths: string | string[], options?: ChokidarOptions): FSWatcher;
|
|
||||||
declare const _default: {
|
|
||||||
watch: typeof watch;
|
|
||||||
FSWatcher: typeof FSWatcher;
|
|
||||||
};
|
|
||||||
export default _default;
|
|
||||||
-798
@@ -1,798 +0,0 @@
|
|||||||
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
||||||
import { stat as statcb } from 'fs';
|
|
||||||
import { stat, readdir } from 'fs/promises';
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import * as sysPath from 'path';
|
|
||||||
import { readdirp } from 'readdirp';
|
|
||||||
import { NodeFsHandler, EVENTS as EV, isWindows, isIBMi, EMPTY_FN, STR_CLOSE, STR_END, } from './handler.js';
|
|
||||||
const SLASH = '/';
|
|
||||||
const SLASH_SLASH = '//';
|
|
||||||
const ONE_DOT = '.';
|
|
||||||
const TWO_DOTS = '..';
|
|
||||||
const STRING_TYPE = 'string';
|
|
||||||
const BACK_SLASH_RE = /\\/g;
|
|
||||||
const DOUBLE_SLASH_RE = /\/\//;
|
|
||||||
const DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
|
|
||||||
const REPLACER_RE = /^\.[/\\]/;
|
|
||||||
function arrify(item) {
|
|
||||||
return Array.isArray(item) ? item : [item];
|
|
||||||
}
|
|
||||||
const isMatcherObject = (matcher) => typeof matcher === 'object' && matcher !== null && !(matcher instanceof RegExp);
|
|
||||||
function createPattern(matcher) {
|
|
||||||
if (typeof matcher === 'function')
|
|
||||||
return matcher;
|
|
||||||
if (typeof matcher === 'string')
|
|
||||||
return (string) => matcher === string;
|
|
||||||
if (matcher instanceof RegExp)
|
|
||||||
return (string) => matcher.test(string);
|
|
||||||
if (typeof matcher === 'object' && matcher !== null) {
|
|
||||||
return (string) => {
|
|
||||||
if (matcher.path === string)
|
|
||||||
return true;
|
|
||||||
if (matcher.recursive) {
|
|
||||||
const relative = sysPath.relative(matcher.path, string);
|
|
||||||
if (!relative) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !relative.startsWith('..') && !sysPath.isAbsolute(relative);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return () => false;
|
|
||||||
}
|
|
||||||
function normalizePath(path) {
|
|
||||||
if (typeof path !== 'string')
|
|
||||||
throw new Error('string expected');
|
|
||||||
path = sysPath.normalize(path);
|
|
||||||
path = path.replace(/\\/g, '/');
|
|
||||||
let prepend = false;
|
|
||||||
if (path.startsWith('//'))
|
|
||||||
prepend = true;
|
|
||||||
const DOUBLE_SLASH_RE = /\/\//;
|
|
||||||
while (path.match(DOUBLE_SLASH_RE))
|
|
||||||
path = path.replace(DOUBLE_SLASH_RE, '/');
|
|
||||||
if (prepend)
|
|
||||||
path = '/' + path;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
function matchPatterns(patterns, testString, stats) {
|
|
||||||
const path = normalizePath(testString);
|
|
||||||
for (let index = 0; index < patterns.length; index++) {
|
|
||||||
const pattern = patterns[index];
|
|
||||||
if (pattern(path, stats)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function anymatch(matchers, testString) {
|
|
||||||
if (matchers == null) {
|
|
||||||
throw new TypeError('anymatch: specify first argument');
|
|
||||||
}
|
|
||||||
// Early cache for matchers.
|
|
||||||
const matchersArray = arrify(matchers);
|
|
||||||
const patterns = matchersArray.map((matcher) => createPattern(matcher));
|
|
||||||
if (testString == null) {
|
|
||||||
return (testString, stats) => {
|
|
||||||
return matchPatterns(patterns, testString, stats);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return matchPatterns(patterns, testString);
|
|
||||||
}
|
|
||||||
const unifyPaths = (paths_) => {
|
|
||||||
const paths = arrify(paths_).flat();
|
|
||||||
if (!paths.every((p) => typeof p === STRING_TYPE)) {
|
|
||||||
throw new TypeError(`Non-string provided as watch path: ${paths}`);
|
|
||||||
}
|
|
||||||
return paths.map(normalizePathToUnix);
|
|
||||||
};
|
|
||||||
// If SLASH_SLASH occurs at the beginning of path, it is not replaced
|
|
||||||
// because "//StoragePC/DrivePool/Movies" is a valid network path
|
|
||||||
const toUnix = (string) => {
|
|
||||||
let str = string.replace(BACK_SLASH_RE, SLASH);
|
|
||||||
let prepend = false;
|
|
||||||
if (str.startsWith(SLASH_SLASH)) {
|
|
||||||
prepend = true;
|
|
||||||
}
|
|
||||||
while (str.match(DOUBLE_SLASH_RE)) {
|
|
||||||
str = str.replace(DOUBLE_SLASH_RE, SLASH);
|
|
||||||
}
|
|
||||||
if (prepend) {
|
|
||||||
str = SLASH + str;
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
// Our version of upath.normalize
|
|
||||||
// TODO: this is not equal to path-normalize module - investigate why
|
|
||||||
const normalizePathToUnix = (path) => toUnix(sysPath.normalize(toUnix(path)));
|
|
||||||
// TODO: refactor
|
|
||||||
const normalizeIgnored = (cwd = '') => (path) => {
|
|
||||||
if (typeof path === 'string') {
|
|
||||||
return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getAbsolutePath = (path, cwd) => {
|
|
||||||
if (sysPath.isAbsolute(path)) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return sysPath.join(cwd, path);
|
|
||||||
};
|
|
||||||
const EMPTY_SET = Object.freeze(new Set());
|
|
||||||
/**
|
|
||||||
* Directory entry.
|
|
||||||
*/
|
|
||||||
class DirEntry {
|
|
||||||
constructor(dir, removeWatcher) {
|
|
||||||
this.path = dir;
|
|
||||||
this._removeWatcher = removeWatcher;
|
|
||||||
this.items = new Set();
|
|
||||||
}
|
|
||||||
add(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
if (item !== ONE_DOT && item !== TWO_DOTS)
|
|
||||||
items.add(item);
|
|
||||||
}
|
|
||||||
async remove(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
items.delete(item);
|
|
||||||
if (items.size > 0)
|
|
||||||
return;
|
|
||||||
const dir = this.path;
|
|
||||||
try {
|
|
||||||
await readdir(dir);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (this._removeWatcher) {
|
|
||||||
this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
return items.has(item);
|
|
||||||
}
|
|
||||||
getChildren() {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return [];
|
|
||||||
return [...items.values()];
|
|
||||||
}
|
|
||||||
dispose() {
|
|
||||||
this.items.clear();
|
|
||||||
this.path = '';
|
|
||||||
this._removeWatcher = EMPTY_FN;
|
|
||||||
this.items = EMPTY_SET;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const STAT_METHOD_F = 'stat';
|
|
||||||
const STAT_METHOD_L = 'lstat';
|
|
||||||
export class WatchHelper {
|
|
||||||
constructor(path, follow, fsw) {
|
|
||||||
this.fsw = fsw;
|
|
||||||
const watchPath = path;
|
|
||||||
this.path = path = path.replace(REPLACER_RE, '');
|
|
||||||
this.watchPath = watchPath;
|
|
||||||
this.fullWatchPath = sysPath.resolve(watchPath);
|
|
||||||
this.dirParts = [];
|
|
||||||
this.dirParts.forEach((parts) => {
|
|
||||||
if (parts.length > 1)
|
|
||||||
parts.pop();
|
|
||||||
});
|
|
||||||
this.followSymlinks = follow;
|
|
||||||
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
|
|
||||||
}
|
|
||||||
entryPath(entry) {
|
|
||||||
return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, entry.fullPath));
|
|
||||||
}
|
|
||||||
filterPath(entry) {
|
|
||||||
const { stats } = entry;
|
|
||||||
if (stats && stats.isSymbolicLink())
|
|
||||||
return this.filterDir(entry);
|
|
||||||
const resolvedPath = this.entryPath(entry);
|
|
||||||
// TODO: what if stats is undefined? remove !
|
|
||||||
return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
|
|
||||||
}
|
|
||||||
filterDir(entry) {
|
|
||||||
return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Watches files & directories for changes. Emitted events:
|
|
||||||
* `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
|
|
||||||
*
|
|
||||||
* new FSWatcher()
|
|
||||||
* .add(directories)
|
|
||||||
* .on('add', path => log('File', path, 'was added'))
|
|
||||||
*/
|
|
||||||
export class FSWatcher extends EventEmitter {
|
|
||||||
// Not indenting methods for history sake; for now.
|
|
||||||
constructor(_opts = {}) {
|
|
||||||
super();
|
|
||||||
this.closed = false;
|
|
||||||
this._closers = new Map();
|
|
||||||
this._ignoredPaths = new Set();
|
|
||||||
this._throttled = new Map();
|
|
||||||
this._streams = new Set();
|
|
||||||
this._symlinkPaths = new Map();
|
|
||||||
this._watched = new Map();
|
|
||||||
this._pendingWrites = new Map();
|
|
||||||
this._pendingUnlinks = new Map();
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyEmitted = false;
|
|
||||||
const awf = _opts.awaitWriteFinish;
|
|
||||||
const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
|
|
||||||
const opts = {
|
|
||||||
// Defaults
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: false,
|
|
||||||
ignorePermissionErrors: false,
|
|
||||||
interval: 100,
|
|
||||||
binaryInterval: 300,
|
|
||||||
followSymlinks: true,
|
|
||||||
usePolling: false,
|
|
||||||
// useAsync: false,
|
|
||||||
atomic: true, // NOTE: overwritten later (depends on usePolling)
|
|
||||||
..._opts,
|
|
||||||
// Change format
|
|
||||||
ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
|
|
||||||
awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === 'object' ? { ...DEF_AWF, ...awf } : false,
|
|
||||||
};
|
|
||||||
// Always default to polling on IBM i because fs.watch() is not available on IBM i.
|
|
||||||
if (isIBMi)
|
|
||||||
opts.usePolling = true;
|
|
||||||
// Editor atomic write normalization enabled by default with fs.watch
|
|
||||||
if (opts.atomic === undefined)
|
|
||||||
opts.atomic = !opts.usePolling;
|
|
||||||
// opts.atomic = typeof _opts.atomic === 'number' ? _opts.atomic : 100;
|
|
||||||
// Global override. Useful for developers, who need to force polling for all
|
|
||||||
// instances of chokidar, regardless of usage / dependency depth
|
|
||||||
const envPoll = process.env.CHOKIDAR_USEPOLLING;
|
|
||||||
if (envPoll !== undefined) {
|
|
||||||
const envLower = envPoll.toLowerCase();
|
|
||||||
if (envLower === 'false' || envLower === '0')
|
|
||||||
opts.usePolling = false;
|
|
||||||
else if (envLower === 'true' || envLower === '1')
|
|
||||||
opts.usePolling = true;
|
|
||||||
else
|
|
||||||
opts.usePolling = !!envLower;
|
|
||||||
}
|
|
||||||
const envInterval = process.env.CHOKIDAR_INTERVAL;
|
|
||||||
if (envInterval)
|
|
||||||
opts.interval = Number.parseInt(envInterval, 10);
|
|
||||||
// This is done to emit ready only once, but each 'add' will increase that?
|
|
||||||
let readyCalls = 0;
|
|
||||||
this._emitReady = () => {
|
|
||||||
readyCalls++;
|
|
||||||
if (readyCalls >= this._readyCount) {
|
|
||||||
this._emitReady = EMPTY_FN;
|
|
||||||
this._readyEmitted = true;
|
|
||||||
// use process.nextTick to allow time for listener to be bound
|
|
||||||
process.nextTick(() => this.emit(EV.READY));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this._emitRaw = (...args) => this.emit(EV.RAW, ...args);
|
|
||||||
this._boundRemove = this._remove.bind(this);
|
|
||||||
this.options = opts;
|
|
||||||
this._nodeFsHandler = new NodeFsHandler(this);
|
|
||||||
// You’re frozen when your heart’s not open.
|
|
||||||
Object.freeze(opts);
|
|
||||||
}
|
|
||||||
_addIgnoredPath(matcher) {
|
|
||||||
if (isMatcherObject(matcher)) {
|
|
||||||
// return early if we already have a deeply equal matcher object
|
|
||||||
for (const ignored of this._ignoredPaths) {
|
|
||||||
if (isMatcherObject(ignored) &&
|
|
||||||
ignored.path === matcher.path &&
|
|
||||||
ignored.recursive === matcher.recursive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._ignoredPaths.add(matcher);
|
|
||||||
}
|
|
||||||
_removeIgnoredPath(matcher) {
|
|
||||||
this._ignoredPaths.delete(matcher);
|
|
||||||
// now find any matcher objects with the matcher as path
|
|
||||||
if (typeof matcher === 'string') {
|
|
||||||
for (const ignored of this._ignoredPaths) {
|
|
||||||
// TODO (43081j): make this more efficient.
|
|
||||||
// probably just make a `this._ignoredDirectories` or some
|
|
||||||
// such thing.
|
|
||||||
if (isMatcherObject(ignored) && ignored.path === matcher) {
|
|
||||||
this._ignoredPaths.delete(ignored);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Public methods
|
|
||||||
/**
|
|
||||||
* Adds paths to be watched on an existing FSWatcher instance.
|
|
||||||
* @param paths_ file or file list. Other arguments are unused
|
|
||||||
*/
|
|
||||||
add(paths_, _origAdd, _internal) {
|
|
||||||
const { cwd } = this.options;
|
|
||||||
this.closed = false;
|
|
||||||
this._closePromise = undefined;
|
|
||||||
let paths = unifyPaths(paths_);
|
|
||||||
if (cwd) {
|
|
||||||
paths = paths.map((path) => {
|
|
||||||
const absPath = getAbsolutePath(path, cwd);
|
|
||||||
// Check `path` instead of `absPath` because the cwd portion can't be a glob
|
|
||||||
return absPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
paths.forEach((path) => {
|
|
||||||
this._removeIgnoredPath(path);
|
|
||||||
});
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
if (!this._readyCount)
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyCount += paths.length;
|
|
||||||
Promise.all(paths.map(async (path) => {
|
|
||||||
const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, undefined, 0, _origAdd);
|
|
||||||
if (res)
|
|
||||||
this._emitReady();
|
|
||||||
return res;
|
|
||||||
})).then((results) => {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
results.forEach((item) => {
|
|
||||||
if (item)
|
|
||||||
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Close watchers or start ignoring events from specified paths.
|
|
||||||
*/
|
|
||||||
unwatch(paths_) {
|
|
||||||
if (this.closed)
|
|
||||||
return this;
|
|
||||||
const paths = unifyPaths(paths_);
|
|
||||||
const { cwd } = this.options;
|
|
||||||
paths.forEach((path) => {
|
|
||||||
// convert to absolute path unless relative path already matches
|
|
||||||
if (!sysPath.isAbsolute(path) && !this._closers.has(path)) {
|
|
||||||
if (cwd)
|
|
||||||
path = sysPath.join(cwd, path);
|
|
||||||
path = sysPath.resolve(path);
|
|
||||||
}
|
|
||||||
this._closePath(path);
|
|
||||||
this._addIgnoredPath(path);
|
|
||||||
if (this._watched.has(path)) {
|
|
||||||
this._addIgnoredPath({
|
|
||||||
path,
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// reset the cached userIgnored anymatch fn
|
|
||||||
// to make ignoredPaths changes effective
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Close watchers and remove all listeners from watched paths.
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
if (this._closePromise) {
|
|
||||||
return this._closePromise;
|
|
||||||
}
|
|
||||||
this.closed = true;
|
|
||||||
// Memory management.
|
|
||||||
this.removeAllListeners();
|
|
||||||
const closers = [];
|
|
||||||
this._closers.forEach((closerList) => closerList.forEach((closer) => {
|
|
||||||
const promise = closer();
|
|
||||||
if (promise instanceof Promise)
|
|
||||||
closers.push(promise);
|
|
||||||
}));
|
|
||||||
this._streams.forEach((stream) => stream.destroy());
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyEmitted = false;
|
|
||||||
this._watched.forEach((dirent) => dirent.dispose());
|
|
||||||
this._closers.clear();
|
|
||||||
this._watched.clear();
|
|
||||||
this._streams.clear();
|
|
||||||
this._symlinkPaths.clear();
|
|
||||||
this._throttled.clear();
|
|
||||||
this._closePromise = closers.length
|
|
||||||
? Promise.all(closers).then(() => undefined)
|
|
||||||
: Promise.resolve();
|
|
||||||
return this._closePromise;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Expose list of watched paths
|
|
||||||
* @returns for chaining
|
|
||||||
*/
|
|
||||||
getWatched() {
|
|
||||||
const watchList = {};
|
|
||||||
this._watched.forEach((entry, dir) => {
|
|
||||||
const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir;
|
|
||||||
const index = key || ONE_DOT;
|
|
||||||
watchList[index] = entry.getChildren().sort();
|
|
||||||
});
|
|
||||||
return watchList;
|
|
||||||
}
|
|
||||||
emitWithAll(event, args) {
|
|
||||||
this.emit(event, ...args);
|
|
||||||
if (event !== EV.ERROR)
|
|
||||||
this.emit(EV.ALL, event, ...args);
|
|
||||||
}
|
|
||||||
// Common helpers
|
|
||||||
// --------------
|
|
||||||
/**
|
|
||||||
* Normalize and emit events.
|
|
||||||
* Calling _emit DOES NOT MEAN emit() would be called!
|
|
||||||
* @param event Type of event
|
|
||||||
* @param path File or directory path
|
|
||||||
* @param stats arguments to be passed with event
|
|
||||||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
async _emit(event, path, stats) {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
const opts = this.options;
|
|
||||||
if (isWindows)
|
|
||||||
path = sysPath.normalize(path);
|
|
||||||
if (opts.cwd)
|
|
||||||
path = sysPath.relative(opts.cwd, path);
|
|
||||||
const args = [path];
|
|
||||||
if (stats != null)
|
|
||||||
args.push(stats);
|
|
||||||
const awf = opts.awaitWriteFinish;
|
|
||||||
let pw;
|
|
||||||
if (awf && (pw = this._pendingWrites.get(path))) {
|
|
||||||
pw.lastChange = new Date();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (opts.atomic) {
|
|
||||||
if (event === EV.UNLINK) {
|
|
||||||
this._pendingUnlinks.set(path, [event, ...args]);
|
|
||||||
setTimeout(() => {
|
|
||||||
this._pendingUnlinks.forEach((entry, path) => {
|
|
||||||
this.emit(...entry);
|
|
||||||
this.emit(EV.ALL, ...entry);
|
|
||||||
this._pendingUnlinks.delete(path);
|
|
||||||
});
|
|
||||||
}, typeof opts.atomic === 'number' ? opts.atomic : 100);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (event === EV.ADD && this._pendingUnlinks.has(path)) {
|
|
||||||
event = EV.CHANGE;
|
|
||||||
this._pendingUnlinks.delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (awf && (event === EV.ADD || event === EV.CHANGE) && this._readyEmitted) {
|
|
||||||
const awfEmit = (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
event = EV.ERROR;
|
|
||||||
args[0] = err;
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
}
|
|
||||||
else if (stats) {
|
|
||||||
// if stats doesn't exist the file must have been deleted
|
|
||||||
if (args.length > 1) {
|
|
||||||
args[1] = stats;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
args.push(stats);
|
|
||||||
}
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (event === EV.CHANGE) {
|
|
||||||
const isThrottled = !this._throttle(EV.CHANGE, path, 50);
|
|
||||||
if (isThrottled)
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (opts.alwaysStat &&
|
|
||||||
stats === undefined &&
|
|
||||||
(event === EV.ADD || event === EV.ADD_DIR || event === EV.CHANGE)) {
|
|
||||||
const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path;
|
|
||||||
let stats;
|
|
||||||
try {
|
|
||||||
stats = await stat(fullPath);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
// Suppress event when fs_stat fails, to avoid sending undefined 'stat'
|
|
||||||
if (!stats || this.closed)
|
|
||||||
return;
|
|
||||||
args.push(stats);
|
|
||||||
}
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Common handler for errors
|
|
||||||
* @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_handleError(error) {
|
|
||||||
const code = error && error.code;
|
|
||||||
if (error &&
|
|
||||||
code !== 'ENOENT' &&
|
|
||||||
code !== 'ENOTDIR' &&
|
|
||||||
(!this.options.ignorePermissionErrors || (code !== 'EPERM' && code !== 'EACCES'))) {
|
|
||||||
this.emit(EV.ERROR, error);
|
|
||||||
}
|
|
||||||
return error || this.closed;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Helper utility for throttling
|
|
||||||
* @param actionType type being throttled
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param timeout duration of time to suppress duplicate actions
|
|
||||||
* @returns tracking object or false if action should be suppressed
|
|
||||||
*/
|
|
||||||
_throttle(actionType, path, timeout) {
|
|
||||||
if (!this._throttled.has(actionType)) {
|
|
||||||
this._throttled.set(actionType, new Map());
|
|
||||||
}
|
|
||||||
const action = this._throttled.get(actionType);
|
|
||||||
if (!action)
|
|
||||||
throw new Error('invalid throttle');
|
|
||||||
const actionPath = action.get(path);
|
|
||||||
if (actionPath) {
|
|
||||||
actionPath.count++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
let timeoutObject;
|
|
||||||
const clear = () => {
|
|
||||||
const item = action.get(path);
|
|
||||||
const count = item ? item.count : 0;
|
|
||||||
action.delete(path);
|
|
||||||
clearTimeout(timeoutObject);
|
|
||||||
if (item)
|
|
||||||
clearTimeout(item.timeoutObject);
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
timeoutObject = setTimeout(clear, timeout);
|
|
||||||
const thr = { timeoutObject, clear, count: 0 };
|
|
||||||
action.set(path, thr);
|
|
||||||
return thr;
|
|
||||||
}
|
|
||||||
_incrReadyCount() {
|
|
||||||
return this._readyCount++;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Awaits write operation to finish.
|
|
||||||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
|
|
||||||
* @param event
|
|
||||||
* @param awfEmit Callback to be called when ready for event to be emitted.
|
|
||||||
*/
|
|
||||||
_awaitWriteFinish(path, threshold, event, awfEmit) {
|
|
||||||
const awf = this.options.awaitWriteFinish;
|
|
||||||
if (typeof awf !== 'object')
|
|
||||||
return;
|
|
||||||
const pollInterval = awf.pollInterval;
|
|
||||||
let timeoutHandler;
|
|
||||||
let fullPath = path;
|
|
||||||
if (this.options.cwd && !sysPath.isAbsolute(path)) {
|
|
||||||
fullPath = sysPath.join(this.options.cwd, path);
|
|
||||||
}
|
|
||||||
const now = new Date();
|
|
||||||
const writes = this._pendingWrites;
|
|
||||||
function awaitWriteFinishFn(prevStat) {
|
|
||||||
statcb(fullPath, (err, curStat) => {
|
|
||||||
if (err || !writes.has(path)) {
|
|
||||||
if (err && err.code !== 'ENOENT')
|
|
||||||
awfEmit(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = Number(new Date());
|
|
||||||
if (prevStat && curStat.size !== prevStat.size) {
|
|
||||||
writes.get(path).lastChange = now;
|
|
||||||
}
|
|
||||||
const pw = writes.get(path);
|
|
||||||
const df = now - pw.lastChange;
|
|
||||||
if (df >= threshold) {
|
|
||||||
writes.delete(path);
|
|
||||||
awfEmit(undefined, curStat);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!writes.has(path)) {
|
|
||||||
writes.set(path, {
|
|
||||||
lastChange: now,
|
|
||||||
cancelWait: () => {
|
|
||||||
writes.delete(path);
|
|
||||||
clearTimeout(timeoutHandler);
|
|
||||||
return event;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Determines whether user has asked to ignore this path.
|
|
||||||
*/
|
|
||||||
_isIgnored(path, stats) {
|
|
||||||
if (this.options.atomic && DOT_RE.test(path))
|
|
||||||
return true;
|
|
||||||
if (!this._userIgnored) {
|
|
||||||
const { cwd } = this.options;
|
|
||||||
const ign = this.options.ignored;
|
|
||||||
const ignored = (ign || []).map(normalizeIgnored(cwd));
|
|
||||||
const ignoredPaths = [...this._ignoredPaths];
|
|
||||||
const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
|
|
||||||
this._userIgnored = anymatch(list, undefined);
|
|
||||||
}
|
|
||||||
return this._userIgnored(path, stats);
|
|
||||||
}
|
|
||||||
_isntIgnored(path, stat) {
|
|
||||||
return !this._isIgnored(path, stat);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Provides a set of common helpers and properties relating to symlink handling.
|
|
||||||
* @param path file or directory pattern being watched
|
|
||||||
*/
|
|
||||||
_getWatchHelpers(path) {
|
|
||||||
return new WatchHelper(path, this.options.followSymlinks, this);
|
|
||||||
}
|
|
||||||
// Directory helpers
|
|
||||||
// -----------------
|
|
||||||
/**
|
|
||||||
* Provides directory tracking objects
|
|
||||||
* @param directory path of the directory
|
|
||||||
*/
|
|
||||||
_getWatchedDir(directory) {
|
|
||||||
const dir = sysPath.resolve(directory);
|
|
||||||
if (!this._watched.has(dir))
|
|
||||||
this._watched.set(dir, new DirEntry(dir, this._boundRemove));
|
|
||||||
return this._watched.get(dir);
|
|
||||||
}
|
|
||||||
// File helpers
|
|
||||||
// ------------
|
|
||||||
/**
|
|
||||||
* Check for read permissions: https://stackoverflow.com/a/11781404/1358405
|
|
||||||
*/
|
|
||||||
_hasReadPermissions(stats) {
|
|
||||||
if (this.options.ignorePermissionErrors)
|
|
||||||
return true;
|
|
||||||
return Boolean(Number(stats.mode) & 0o400);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handles emitting unlink events for
|
|
||||||
* files and directories, and via recursion, for
|
|
||||||
* files and directories within directories that are unlinked
|
|
||||||
* @param directory within which the following item is located
|
|
||||||
* @param item base path of item/directory
|
|
||||||
*/
|
|
||||||
_remove(directory, item, isDirectory) {
|
|
||||||
// if what is being deleted is a directory, get that directory's paths
|
|
||||||
// for recursive deleting and cleaning of watched object
|
|
||||||
// if it is not a directory, nestedDirectoryChildren will be empty array
|
|
||||||
const path = sysPath.join(directory, item);
|
|
||||||
const fullPath = sysPath.resolve(path);
|
|
||||||
isDirectory =
|
|
||||||
isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath);
|
|
||||||
// prevent duplicate handling in case of arriving here nearly simultaneously
|
|
||||||
// via multiple paths (such as _handleFile and _handleDir)
|
|
||||||
if (!this._throttle('remove', path, 100))
|
|
||||||
return;
|
|
||||||
// if the only watched file is removed, watch for its return
|
|
||||||
if (!isDirectory && this._watched.size === 1) {
|
|
||||||
this.add(directory, item, true);
|
|
||||||
}
|
|
||||||
// This will create a new entry in the watched object in either case
|
|
||||||
// so we got to do the directory check beforehand
|
|
||||||
const wp = this._getWatchedDir(path);
|
|
||||||
const nestedDirectoryChildren = wp.getChildren();
|
|
||||||
// Recursively remove children directories / files.
|
|
||||||
nestedDirectoryChildren.forEach((nested) => this._remove(path, nested));
|
|
||||||
// Check if item was on the watched list and remove it
|
|
||||||
const parent = this._getWatchedDir(directory);
|
|
||||||
const wasTracked = parent.has(item);
|
|
||||||
parent.remove(item);
|
|
||||||
// Fixes issue #1042 -> Relative paths were detected and added as symlinks
|
|
||||||
// (https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L612),
|
|
||||||
// but never removed from the map in case the path was deleted.
|
|
||||||
// This leads to an incorrect state if the path was recreated:
|
|
||||||
// https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L553
|
|
||||||
if (this._symlinkPaths.has(fullPath)) {
|
|
||||||
this._symlinkPaths.delete(fullPath);
|
|
||||||
}
|
|
||||||
// If we wait for this file to be fully written, cancel the wait.
|
|
||||||
let relPath = path;
|
|
||||||
if (this.options.cwd)
|
|
||||||
relPath = sysPath.relative(this.options.cwd, path);
|
|
||||||
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
||||||
const event = this._pendingWrites.get(relPath).cancelWait();
|
|
||||||
if (event === EV.ADD)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The Entry will either be a directory that just got removed
|
|
||||||
// or a bogus entry to a file, in either case we have to remove it
|
|
||||||
this._watched.delete(path);
|
|
||||||
this._watched.delete(fullPath);
|
|
||||||
const eventName = isDirectory ? EV.UNLINK_DIR : EV.UNLINK;
|
|
||||||
if (wasTracked && !this._isIgnored(path))
|
|
||||||
this._emit(eventName, path);
|
|
||||||
// Avoid conflicts if we later create another file with the same name
|
|
||||||
this._closePath(path);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Closes all watchers for a path
|
|
||||||
*/
|
|
||||||
_closePath(path) {
|
|
||||||
this._closeFile(path);
|
|
||||||
const dir = sysPath.dirname(path);
|
|
||||||
this._getWatchedDir(dir).remove(sysPath.basename(path));
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Closes only file-specific watchers
|
|
||||||
*/
|
|
||||||
_closeFile(path) {
|
|
||||||
const closers = this._closers.get(path);
|
|
||||||
if (!closers)
|
|
||||||
return;
|
|
||||||
closers.forEach((closer) => closer());
|
|
||||||
this._closers.delete(path);
|
|
||||||
}
|
|
||||||
_addPathCloser(path, closer) {
|
|
||||||
if (!closer)
|
|
||||||
return;
|
|
||||||
let list = this._closers.get(path);
|
|
||||||
if (!list) {
|
|
||||||
list = [];
|
|
||||||
this._closers.set(path, list);
|
|
||||||
}
|
|
||||||
list.push(closer);
|
|
||||||
}
|
|
||||||
_readdirp(root, opts) {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
const options = { type: EV.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
|
|
||||||
let stream = readdirp(root, options);
|
|
||||||
this._streams.add(stream);
|
|
||||||
stream.once(STR_CLOSE, () => {
|
|
||||||
stream = undefined;
|
|
||||||
});
|
|
||||||
stream.once(STR_END, () => {
|
|
||||||
if (stream) {
|
|
||||||
this._streams.delete(stream);
|
|
||||||
stream = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Instantiates watcher with paths to be tracked.
|
|
||||||
* @param paths file / directory paths
|
|
||||||
* @param options opts, such as `atomic`, `awaitWriteFinish`, `ignored`, and others
|
|
||||||
* @returns an instance of FSWatcher for chaining.
|
|
||||||
* @example
|
|
||||||
* const watcher = watch('.').on('all', (event, path) => { console.log(event, path); });
|
|
||||||
* watch('.', { atomic: true, awaitWriteFinish: true, ignored: (f, stats) => stats?.isFile() && !f.endsWith('.js') })
|
|
||||||
*/
|
|
||||||
export function watch(paths, options = {}) {
|
|
||||||
const watcher = new FSWatcher(options);
|
|
||||||
watcher.add(paths);
|
|
||||||
return watcher;
|
|
||||||
}
|
|
||||||
export default { watch, FSWatcher };
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{ "type": "module", "sideEffects": false }
|
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
import type { WatchEventType, Stats, FSWatcher as NativeFsWatcher } from 'fs';
|
|
||||||
import type { FSWatcher, WatchHelper, Throttler } from './index.js';
|
|
||||||
import type { EntryInfo } from 'readdirp';
|
|
||||||
export type Path = string;
|
|
||||||
export declare const STR_DATA = "data";
|
|
||||||
export declare const STR_END = "end";
|
|
||||||
export declare const STR_CLOSE = "close";
|
|
||||||
export declare const EMPTY_FN: () => void;
|
|
||||||
export declare const IDENTITY_FN: (val: unknown) => unknown;
|
|
||||||
export declare const isWindows: boolean;
|
|
||||||
export declare const isMacos: boolean;
|
|
||||||
export declare const isLinux: boolean;
|
|
||||||
export declare const isFreeBSD: boolean;
|
|
||||||
export declare const isIBMi: boolean;
|
|
||||||
export declare const EVENTS: {
|
|
||||||
readonly ALL: "all";
|
|
||||||
readonly READY: "ready";
|
|
||||||
readonly ADD: "add";
|
|
||||||
readonly CHANGE: "change";
|
|
||||||
readonly ADD_DIR: "addDir";
|
|
||||||
readonly UNLINK: "unlink";
|
|
||||||
readonly UNLINK_DIR: "unlinkDir";
|
|
||||||
readonly RAW: "raw";
|
|
||||||
readonly ERROR: "error";
|
|
||||||
};
|
|
||||||
export type EventName = (typeof EVENTS)[keyof typeof EVENTS];
|
|
||||||
export type FsWatchContainer = {
|
|
||||||
listeners: (path: string) => void | Set<any>;
|
|
||||||
errHandlers: (err: unknown) => void | Set<any>;
|
|
||||||
rawEmitters: (ev: WatchEventType, path: string, opts: unknown) => void | Set<any>;
|
|
||||||
watcher: NativeFsWatcher;
|
|
||||||
watcherUnusable?: boolean;
|
|
||||||
};
|
|
||||||
export interface WatchHandlers {
|
|
||||||
listener: (path: string) => void;
|
|
||||||
errHandler: (err: unknown) => void;
|
|
||||||
rawEmitter: (ev: WatchEventType, path: string, opts: unknown) => void;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @mixin
|
|
||||||
*/
|
|
||||||
export declare class NodeFsHandler {
|
|
||||||
fsw: FSWatcher;
|
|
||||||
_boundHandleError: (error: unknown) => void;
|
|
||||||
constructor(fsW: FSWatcher);
|
|
||||||
/**
|
|
||||||
* Watch file for changes with fs_watchFile or fs_watch.
|
|
||||||
* @param path to file or dir
|
|
||||||
* @param listener on fs change
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_watchWithNodeFs(path: string, listener: (path: string, newStats?: any) => void | Promise<void>): (() => void) | undefined;
|
|
||||||
/**
|
|
||||||
* Watch a file and emit add event if warranted.
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_handleFile(file: Path, stats: Stats, initialAdd: boolean): (() => void) | undefined;
|
|
||||||
/**
|
|
||||||
* Handle symlinks encountered while reading a dir.
|
|
||||||
* @param entry returned by readdirp
|
|
||||||
* @param directory path of dir being read
|
|
||||||
* @param path of this item
|
|
||||||
* @param item basename of this item
|
|
||||||
* @returns true if no more processing is needed for this entry.
|
|
||||||
*/
|
|
||||||
_handleSymlink(entry: EntryInfo, directory: string, path: Path, item: string): Promise<boolean | undefined>;
|
|
||||||
_handleRead(directory: string, initialAdd: boolean, wh: WatchHelper, target: Path, dir: Path, depth: number, throttler: Throttler): Promise<unknown> | undefined;
|
|
||||||
/**
|
|
||||||
* Read directory to add / remove files from `@watched` list and re-read it on change.
|
|
||||||
* @param dir fs path
|
|
||||||
* @param stats
|
|
||||||
* @param initialAdd
|
|
||||||
* @param depth relative to user-supplied path
|
|
||||||
* @param target child path targeted for watch
|
|
||||||
* @param wh Common watch helpers for this path
|
|
||||||
* @param realpath
|
|
||||||
* @returns closer for the watcher instance.
|
|
||||||
*/
|
|
||||||
_handleDir(dir: string, stats: Stats, initialAdd: boolean, depth: number, target: string, wh: WatchHelper, realpath: string): Promise<(() => void) | undefined>;
|
|
||||||
/**
|
|
||||||
* Handle added file, directory, or glob pattern.
|
|
||||||
* Delegates call to _handleFile / _handleDir after checks.
|
|
||||||
* @param path to file or ir
|
|
||||||
* @param initialAdd was the file added at watch instantiation?
|
|
||||||
* @param priorWh depth relative to user-supplied path
|
|
||||||
* @param depth Child path actually targeted for watch
|
|
||||||
* @param target Child path actually targeted for watch
|
|
||||||
*/
|
|
||||||
_addToNodeFs(path: string, initialAdd: boolean, priorWh: WatchHelper | undefined, depth: number, target?: string): Promise<string | false | undefined>;
|
|
||||||
}
|
|
||||||
-635
@@ -1,635 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.NodeFsHandler = exports.EVENTS = exports.isIBMi = exports.isFreeBSD = exports.isLinux = exports.isMacos = exports.isWindows = exports.IDENTITY_FN = exports.EMPTY_FN = exports.STR_CLOSE = exports.STR_END = exports.STR_DATA = void 0;
|
|
||||||
const fs_1 = require("fs");
|
|
||||||
const promises_1 = require("fs/promises");
|
|
||||||
const sysPath = require("path");
|
|
||||||
const os_1 = require("os");
|
|
||||||
exports.STR_DATA = 'data';
|
|
||||||
exports.STR_END = 'end';
|
|
||||||
exports.STR_CLOSE = 'close';
|
|
||||||
const EMPTY_FN = () => { };
|
|
||||||
exports.EMPTY_FN = EMPTY_FN;
|
|
||||||
const IDENTITY_FN = (val) => val;
|
|
||||||
exports.IDENTITY_FN = IDENTITY_FN;
|
|
||||||
const pl = process.platform;
|
|
||||||
exports.isWindows = pl === 'win32';
|
|
||||||
exports.isMacos = pl === 'darwin';
|
|
||||||
exports.isLinux = pl === 'linux';
|
|
||||||
exports.isFreeBSD = pl === 'freebsd';
|
|
||||||
exports.isIBMi = (0, os_1.type)() === 'OS400';
|
|
||||||
exports.EVENTS = {
|
|
||||||
ALL: 'all',
|
|
||||||
READY: 'ready',
|
|
||||||
ADD: 'add',
|
|
||||||
CHANGE: 'change',
|
|
||||||
ADD_DIR: 'addDir',
|
|
||||||
UNLINK: 'unlink',
|
|
||||||
UNLINK_DIR: 'unlinkDir',
|
|
||||||
RAW: 'raw',
|
|
||||||
ERROR: 'error',
|
|
||||||
};
|
|
||||||
const EV = exports.EVENTS;
|
|
||||||
const THROTTLE_MODE_WATCH = 'watch';
|
|
||||||
const statMethods = { lstat: promises_1.lstat, stat: promises_1.stat };
|
|
||||||
const KEY_LISTENERS = 'listeners';
|
|
||||||
const KEY_ERR = 'errHandlers';
|
|
||||||
const KEY_RAW = 'rawEmitters';
|
|
||||||
const HANDLER_KEYS = [KEY_LISTENERS, KEY_ERR, KEY_RAW];
|
|
||||||
// prettier-ignore
|
|
||||||
const binaryExtensions = new Set([
|
|
||||||
'3dm', '3ds', '3g2', '3gp', '7z', 'a', 'aac', 'adp', 'afdesign', 'afphoto', 'afpub', 'ai',
|
|
||||||
'aif', 'aiff', 'alz', 'ape', 'apk', 'appimage', 'ar', 'arj', 'asf', 'au', 'avi',
|
|
||||||
'bak', 'baml', 'bh', 'bin', 'bk', 'bmp', 'btif', 'bz2', 'bzip2',
|
|
||||||
'cab', 'caf', 'cgm', 'class', 'cmx', 'cpio', 'cr2', 'cur', 'dat', 'dcm', 'deb', 'dex', 'djvu',
|
|
||||||
'dll', 'dmg', 'dng', 'doc', 'docm', 'docx', 'dot', 'dotm', 'dra', 'DS_Store', 'dsk', 'dts',
|
|
||||||
'dtshd', 'dvb', 'dwg', 'dxf',
|
|
||||||
'ecelp4800', 'ecelp7470', 'ecelp9600', 'egg', 'eol', 'eot', 'epub', 'exe',
|
|
||||||
'f4v', 'fbs', 'fh', 'fla', 'flac', 'flatpak', 'fli', 'flv', 'fpx', 'fst', 'fvt',
|
|
||||||
'g3', 'gh', 'gif', 'graffle', 'gz', 'gzip',
|
|
||||||
'h261', 'h263', 'h264', 'icns', 'ico', 'ief', 'img', 'ipa', 'iso',
|
|
||||||
'jar', 'jpeg', 'jpg', 'jpgv', 'jpm', 'jxr', 'key', 'ktx',
|
|
||||||
'lha', 'lib', 'lvp', 'lz', 'lzh', 'lzma', 'lzo',
|
|
||||||
'm3u', 'm4a', 'm4v', 'mar', 'mdi', 'mht', 'mid', 'midi', 'mj2', 'mka', 'mkv', 'mmr', 'mng',
|
|
||||||
'mobi', 'mov', 'movie', 'mp3',
|
|
||||||
'mp4', 'mp4a', 'mpeg', 'mpg', 'mpga', 'mxu',
|
|
||||||
'nef', 'npx', 'numbers', 'nupkg',
|
|
||||||
'o', 'odp', 'ods', 'odt', 'oga', 'ogg', 'ogv', 'otf', 'ott',
|
|
||||||
'pages', 'pbm', 'pcx', 'pdb', 'pdf', 'pea', 'pgm', 'pic', 'png', 'pnm', 'pot', 'potm',
|
|
||||||
'potx', 'ppa', 'ppam',
|
|
||||||
'ppm', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx', 'psd', 'pya', 'pyc', 'pyo', 'pyv',
|
|
||||||
'qt',
|
|
||||||
'rar', 'ras', 'raw', 'resources', 'rgb', 'rip', 'rlc', 'rmf', 'rmvb', 'rpm', 'rtf', 'rz',
|
|
||||||
's3m', 's7z', 'scpt', 'sgi', 'shar', 'snap', 'sil', 'sketch', 'slk', 'smv', 'snk', 'so',
|
|
||||||
'stl', 'suo', 'sub', 'swf',
|
|
||||||
'tar', 'tbz', 'tbz2', 'tga', 'tgz', 'thmx', 'tif', 'tiff', 'tlz', 'ttc', 'ttf', 'txz',
|
|
||||||
'udf', 'uvh', 'uvi', 'uvm', 'uvp', 'uvs', 'uvu',
|
|
||||||
'viv', 'vob',
|
|
||||||
'war', 'wav', 'wax', 'wbmp', 'wdp', 'weba', 'webm', 'webp', 'whl', 'wim', 'wm', 'wma',
|
|
||||||
'wmv', 'wmx', 'woff', 'woff2', 'wrm', 'wvx',
|
|
||||||
'xbm', 'xif', 'xla', 'xlam', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx', 'xm',
|
|
||||||
'xmind', 'xpi', 'xpm', 'xwd', 'xz',
|
|
||||||
'z', 'zip', 'zipx',
|
|
||||||
]);
|
|
||||||
const isBinaryPath = (filePath) => binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
|
|
||||||
// TODO: emit errors properly. Example: EMFILE on Macos.
|
|
||||||
const foreach = (val, fn) => {
|
|
||||||
if (val instanceof Set) {
|
|
||||||
val.forEach(fn);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fn(val);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const addAndConvert = (main, prop, item) => {
|
|
||||||
let container = main[prop];
|
|
||||||
if (!(container instanceof Set)) {
|
|
||||||
main[prop] = container = new Set([container]);
|
|
||||||
}
|
|
||||||
container.add(item);
|
|
||||||
};
|
|
||||||
const clearItem = (cont) => (key) => {
|
|
||||||
const set = cont[key];
|
|
||||||
if (set instanceof Set) {
|
|
||||||
set.clear();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete cont[key];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const delFromSet = (main, prop, item) => {
|
|
||||||
const container = main[prop];
|
|
||||||
if (container instanceof Set) {
|
|
||||||
container.delete(item);
|
|
||||||
}
|
|
||||||
else if (container === item) {
|
|
||||||
delete main[prop];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const isEmptySet = (val) => (val instanceof Set ? val.size === 0 : !val);
|
|
||||||
const FsWatchInstances = new Map();
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watch interface
|
|
||||||
* @param path to be watched
|
|
||||||
* @param options to be passed to fs_watch
|
|
||||||
* @param listener main event handler
|
|
||||||
* @param errHandler emits info about errors
|
|
||||||
* @param emitRaw emits raw event data
|
|
||||||
* @returns {NativeFsWatcher}
|
|
||||||
*/
|
|
||||||
function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
||||||
const handleEvent = (rawEvent, evPath) => {
|
|
||||||
listener(path);
|
|
||||||
emitRaw(rawEvent, evPath, { watchedPath: path });
|
|
||||||
// emit based on events occurring for files from a directory's watcher in
|
|
||||||
// case the file's watcher misses it (and rely on throttling to de-dupe)
|
|
||||||
if (evPath && path !== evPath) {
|
|
||||||
fsWatchBroadcast(sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
return (0, fs_1.watch)(path, {
|
|
||||||
persistent: options.persistent,
|
|
||||||
}, handleEvent);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
errHandler(error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Helper for passing fs_watch event data to a collection of listeners
|
|
||||||
* @param fullPath absolute path bound to fs_watch instance
|
|
||||||
*/
|
|
||||||
const fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
|
|
||||||
const cont = FsWatchInstances.get(fullPath);
|
|
||||||
if (!cont)
|
|
||||||
return;
|
|
||||||
foreach(cont[listenerType], (listener) => {
|
|
||||||
listener(val1, val2, val3);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watch interface or binds listeners
|
|
||||||
* to an existing one covering the same file system entry
|
|
||||||
* @param path
|
|
||||||
* @param fullPath absolute path
|
|
||||||
* @param options to be passed to fs_watch
|
|
||||||
* @param handlers container for event listener functions
|
|
||||||
*/
|
|
||||||
const setFsWatchListener = (path, fullPath, options, handlers) => {
|
|
||||||
const { listener, errHandler, rawEmitter } = handlers;
|
|
||||||
let cont = FsWatchInstances.get(fullPath);
|
|
||||||
let watcher;
|
|
||||||
if (!options.persistent) {
|
|
||||||
watcher = createFsWatchInstance(path, options, listener, errHandler, rawEmitter);
|
|
||||||
if (!watcher)
|
|
||||||
return;
|
|
||||||
return watcher.close.bind(watcher);
|
|
||||||
}
|
|
||||||
if (cont) {
|
|
||||||
addAndConvert(cont, KEY_LISTENERS, listener);
|
|
||||||
addAndConvert(cont, KEY_ERR, errHandler);
|
|
||||||
addAndConvert(cont, KEY_RAW, rawEmitter);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
watcher = createFsWatchInstance(path, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, // no need to use broadcast here
|
|
||||||
fsWatchBroadcast.bind(null, fullPath, KEY_RAW));
|
|
||||||
if (!watcher)
|
|
||||||
return;
|
|
||||||
watcher.on(EV.ERROR, async (error) => {
|
|
||||||
const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
|
|
||||||
if (cont)
|
|
||||||
cont.watcherUnusable = true; // documented since Node 10.4.1
|
|
||||||
// Workaround for https://github.com/joyent/node/issues/4337
|
|
||||||
if (exports.isWindows && error.code === 'EPERM') {
|
|
||||||
try {
|
|
||||||
const fd = await (0, promises_1.open)(path, 'r');
|
|
||||||
await fd.close();
|
|
||||||
broadcastErr(error);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
broadcastErr(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cont = {
|
|
||||||
listeners: listener,
|
|
||||||
errHandlers: errHandler,
|
|
||||||
rawEmitters: rawEmitter,
|
|
||||||
watcher,
|
|
||||||
};
|
|
||||||
FsWatchInstances.set(fullPath, cont);
|
|
||||||
}
|
|
||||||
// const index = cont.listeners.indexOf(listener);
|
|
||||||
// removes this instance's listeners and closes the underlying fs_watch
|
|
||||||
// instance if there are no more listeners left
|
|
||||||
return () => {
|
|
||||||
delFromSet(cont, KEY_LISTENERS, listener);
|
|
||||||
delFromSet(cont, KEY_ERR, errHandler);
|
|
||||||
delFromSet(cont, KEY_RAW, rawEmitter);
|
|
||||||
if (isEmptySet(cont.listeners)) {
|
|
||||||
// Check to protect against issue gh-730.
|
|
||||||
// if (cont.watcherUnusable) {
|
|
||||||
cont.watcher.close();
|
|
||||||
// }
|
|
||||||
FsWatchInstances.delete(fullPath);
|
|
||||||
HANDLER_KEYS.forEach(clearItem(cont));
|
|
||||||
// @ts-ignore
|
|
||||||
cont.watcher = undefined;
|
|
||||||
Object.freeze(cont);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// fs_watchFile helpers
|
|
||||||
// object to hold per-process fs_watchFile instances
|
|
||||||
// (may be shared across chokidar FSWatcher instances)
|
|
||||||
const FsWatchFileInstances = new Map();
|
|
||||||
/**
|
|
||||||
* Instantiates the fs_watchFile interface or binds listeners
|
|
||||||
* to an existing one covering the same file system entry
|
|
||||||
* @param path to be watched
|
|
||||||
* @param fullPath absolute path
|
|
||||||
* @param options options to be passed to fs_watchFile
|
|
||||||
* @param handlers container for event listener functions
|
|
||||||
* @returns closer
|
|
||||||
*/
|
|
||||||
const setFsWatchFileListener = (path, fullPath, options, handlers) => {
|
|
||||||
const { listener, rawEmitter } = handlers;
|
|
||||||
let cont = FsWatchFileInstances.get(fullPath);
|
|
||||||
// let listeners = new Set();
|
|
||||||
// let rawEmitters = new Set();
|
|
||||||
const copts = cont && cont.options;
|
|
||||||
if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) {
|
|
||||||
// "Upgrade" the watcher to persistence or a quicker interval.
|
|
||||||
// This creates some unlikely edge case issues if the user mixes
|
|
||||||
// settings in a very weird way, but solving for those cases
|
|
||||||
// doesn't seem worthwhile for the added complexity.
|
|
||||||
// listeners = cont.listeners;
|
|
||||||
// rawEmitters = cont.rawEmitters;
|
|
||||||
(0, fs_1.unwatchFile)(fullPath);
|
|
||||||
cont = undefined;
|
|
||||||
}
|
|
||||||
if (cont) {
|
|
||||||
addAndConvert(cont, KEY_LISTENERS, listener);
|
|
||||||
addAndConvert(cont, KEY_RAW, rawEmitter);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO
|
|
||||||
// listeners.add(listener);
|
|
||||||
// rawEmitters.add(rawEmitter);
|
|
||||||
cont = {
|
|
||||||
listeners: listener,
|
|
||||||
rawEmitters: rawEmitter,
|
|
||||||
options,
|
|
||||||
watcher: (0, fs_1.watchFile)(fullPath, options, (curr, prev) => {
|
|
||||||
foreach(cont.rawEmitters, (rawEmitter) => {
|
|
||||||
rawEmitter(EV.CHANGE, fullPath, { curr, prev });
|
|
||||||
});
|
|
||||||
const currmtime = curr.mtimeMs;
|
|
||||||
if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
|
|
||||||
foreach(cont.listeners, (listener) => listener(path, curr));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
FsWatchFileInstances.set(fullPath, cont);
|
|
||||||
}
|
|
||||||
// const index = cont.listeners.indexOf(listener);
|
|
||||||
// Removes this instance's listeners and closes the underlying fs_watchFile
|
|
||||||
// instance if there are no more listeners left.
|
|
||||||
return () => {
|
|
||||||
delFromSet(cont, KEY_LISTENERS, listener);
|
|
||||||
delFromSet(cont, KEY_RAW, rawEmitter);
|
|
||||||
if (isEmptySet(cont.listeners)) {
|
|
||||||
FsWatchFileInstances.delete(fullPath);
|
|
||||||
(0, fs_1.unwatchFile)(fullPath);
|
|
||||||
cont.options = cont.watcher = undefined;
|
|
||||||
Object.freeze(cont);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @mixin
|
|
||||||
*/
|
|
||||||
class NodeFsHandler {
|
|
||||||
constructor(fsW) {
|
|
||||||
this.fsw = fsW;
|
|
||||||
this._boundHandleError = (error) => fsW._handleError(error);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Watch file for changes with fs_watchFile or fs_watch.
|
|
||||||
* @param path to file or dir
|
|
||||||
* @param listener on fs change
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_watchWithNodeFs(path, listener) {
|
|
||||||
const opts = this.fsw.options;
|
|
||||||
const directory = sysPath.dirname(path);
|
|
||||||
const basename = sysPath.basename(path);
|
|
||||||
const parent = this.fsw._getWatchedDir(directory);
|
|
||||||
parent.add(basename);
|
|
||||||
const absolutePath = sysPath.resolve(path);
|
|
||||||
const options = {
|
|
||||||
persistent: opts.persistent,
|
|
||||||
};
|
|
||||||
if (!listener)
|
|
||||||
listener = exports.EMPTY_FN;
|
|
||||||
let closer;
|
|
||||||
if (opts.usePolling) {
|
|
||||||
const enableBin = opts.interval !== opts.binaryInterval;
|
|
||||||
options.interval = enableBin && isBinaryPath(basename) ? opts.binaryInterval : opts.interval;
|
|
||||||
closer = setFsWatchFileListener(path, absolutePath, options, {
|
|
||||||
listener,
|
|
||||||
rawEmitter: this.fsw._emitRaw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
closer = setFsWatchListener(path, absolutePath, options, {
|
|
||||||
listener,
|
|
||||||
errHandler: this._boundHandleError,
|
|
||||||
rawEmitter: this.fsw._emitRaw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Watch a file and emit add event if warranted.
|
|
||||||
* @returns closer for the watcher instance
|
|
||||||
*/
|
|
||||||
_handleFile(file, stats, initialAdd) {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirname = sysPath.dirname(file);
|
|
||||||
const basename = sysPath.basename(file);
|
|
||||||
const parent = this.fsw._getWatchedDir(dirname);
|
|
||||||
// stats is always present
|
|
||||||
let prevStats = stats;
|
|
||||||
// if the file is already being watched, do nothing
|
|
||||||
if (parent.has(basename))
|
|
||||||
return;
|
|
||||||
const listener = async (path, newStats) => {
|
|
||||||
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
|
|
||||||
return;
|
|
||||||
if (!newStats || newStats.mtimeMs === 0) {
|
|
||||||
try {
|
|
||||||
const newStats = await (0, promises_1.stat)(file);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// Check that change event was not fired because of changed only accessTime.
|
|
||||||
const at = newStats.atimeMs;
|
|
||||||
const mt = newStats.mtimeMs;
|
|
||||||
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
||||||
this.fsw._emit(EV.CHANGE, file, newStats);
|
|
||||||
}
|
|
||||||
if ((exports.isMacos || exports.isLinux || exports.isFreeBSD) && prevStats.ino !== newStats.ino) {
|
|
||||||
this.fsw._closeFile(path);
|
|
||||||
prevStats = newStats;
|
|
||||||
const closer = this._watchWithNodeFs(file, listener);
|
|
||||||
if (closer)
|
|
||||||
this.fsw._addPathCloser(path, closer);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
prevStats = newStats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// Fix issues where mtime is null but file is still present
|
|
||||||
this.fsw._remove(dirname, basename);
|
|
||||||
}
|
|
||||||
// add is about to be emitted if file not already tracked in parent
|
|
||||||
}
|
|
||||||
else if (parent.has(basename)) {
|
|
||||||
// Check that change event was not fired because of changed only accessTime.
|
|
||||||
const at = newStats.atimeMs;
|
|
||||||
const mt = newStats.mtimeMs;
|
|
||||||
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
||||||
this.fsw._emit(EV.CHANGE, file, newStats);
|
|
||||||
}
|
|
||||||
prevStats = newStats;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// kick off the watcher
|
|
||||||
const closer = this._watchWithNodeFs(file, listener);
|
|
||||||
// emit an add event if we're supposed to
|
|
||||||
if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
|
|
||||||
if (!this.fsw._throttle(EV.ADD, file, 0))
|
|
||||||
return;
|
|
||||||
this.fsw._emit(EV.ADD, file, stats);
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle symlinks encountered while reading a dir.
|
|
||||||
* @param entry returned by readdirp
|
|
||||||
* @param directory path of dir being read
|
|
||||||
* @param path of this item
|
|
||||||
* @param item basename of this item
|
|
||||||
* @returns true if no more processing is needed for this entry.
|
|
||||||
*/
|
|
||||||
async _handleSymlink(entry, directory, path, item) {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const full = entry.fullPath;
|
|
||||||
const dir = this.fsw._getWatchedDir(directory);
|
|
||||||
if (!this.fsw.options.followSymlinks) {
|
|
||||||
// watch symlink directly (don't follow) and detect changes
|
|
||||||
this.fsw._incrReadyCount();
|
|
||||||
let linkPath;
|
|
||||||
try {
|
|
||||||
linkPath = await (0, promises_1.realpath)(path);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.fsw._emitReady();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
if (dir.has(item)) {
|
|
||||||
if (this.fsw._symlinkPaths.get(full) !== linkPath) {
|
|
||||||
this.fsw._symlinkPaths.set(full, linkPath);
|
|
||||||
this.fsw._emit(EV.CHANGE, path, entry.stats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dir.add(item);
|
|
||||||
this.fsw._symlinkPaths.set(full, linkPath);
|
|
||||||
this.fsw._emit(EV.ADD, path, entry.stats);
|
|
||||||
}
|
|
||||||
this.fsw._emitReady();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// don't follow the same symlink more than once
|
|
||||||
if (this.fsw._symlinkPaths.has(full)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this.fsw._symlinkPaths.set(full, true);
|
|
||||||
}
|
|
||||||
_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
|
|
||||||
// Normalize the directory name on Windows
|
|
||||||
directory = sysPath.join(directory, '');
|
|
||||||
throttler = this.fsw._throttle('readdir', directory, 1000);
|
|
||||||
if (!throttler)
|
|
||||||
return;
|
|
||||||
const previous = this.fsw._getWatchedDir(wh.path);
|
|
||||||
const current = new Set();
|
|
||||||
let stream = this.fsw._readdirp(directory, {
|
|
||||||
fileFilter: (entry) => wh.filterPath(entry),
|
|
||||||
directoryFilter: (entry) => wh.filterDir(entry),
|
|
||||||
});
|
|
||||||
if (!stream)
|
|
||||||
return;
|
|
||||||
stream
|
|
||||||
.on(exports.STR_DATA, async (entry) => {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const item = entry.path;
|
|
||||||
let path = sysPath.join(directory, item);
|
|
||||||
current.add(item);
|
|
||||||
if (entry.stats.isSymbolicLink() &&
|
|
||||||
(await this._handleSymlink(entry, directory, path, item))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Files that present in current directory snapshot
|
|
||||||
// but absent in previous are added to watch list and
|
|
||||||
// emit `add` event.
|
|
||||||
if (item === target || (!target && !previous.has(item))) {
|
|
||||||
this.fsw._incrReadyCount();
|
|
||||||
// ensure relativeness of path is preserved in case of watcher reuse
|
|
||||||
path = sysPath.join(dir, sysPath.relative(dir, path));
|
|
||||||
this._addToNodeFs(path, initialAdd, wh, depth + 1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on(EV.ERROR, this._boundHandleError);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!stream)
|
|
||||||
return reject();
|
|
||||||
stream.once(exports.STR_END, () => {
|
|
||||||
if (this.fsw.closed) {
|
|
||||||
stream = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const wasThrottled = throttler ? throttler.clear() : false;
|
|
||||||
resolve(undefined);
|
|
||||||
// Files that absent in current directory snapshot
|
|
||||||
// but present in previous emit `remove` event
|
|
||||||
// and are removed from @watched[directory].
|
|
||||||
previous
|
|
||||||
.getChildren()
|
|
||||||
.filter((item) => {
|
|
||||||
return item !== directory && !current.has(item);
|
|
||||||
})
|
|
||||||
.forEach((item) => {
|
|
||||||
this.fsw._remove(directory, item);
|
|
||||||
});
|
|
||||||
stream = undefined;
|
|
||||||
// one more time for any missed in case changes came in extremely quickly
|
|
||||||
if (wasThrottled)
|
|
||||||
this._handleRead(directory, false, wh, target, dir, depth, throttler);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read directory to add / remove files from `@watched` list and re-read it on change.
|
|
||||||
* @param dir fs path
|
|
||||||
* @param stats
|
|
||||||
* @param initialAdd
|
|
||||||
* @param depth relative to user-supplied path
|
|
||||||
* @param target child path targeted for watch
|
|
||||||
* @param wh Common watch helpers for this path
|
|
||||||
* @param realpath
|
|
||||||
* @returns closer for the watcher instance.
|
|
||||||
*/
|
|
||||||
async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) {
|
|
||||||
const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
|
|
||||||
const tracked = parentDir.has(sysPath.basename(dir));
|
|
||||||
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
|
|
||||||
this.fsw._emit(EV.ADD_DIR, dir, stats);
|
|
||||||
}
|
|
||||||
// ensure dir is tracked (harmless if redundant)
|
|
||||||
parentDir.add(sysPath.basename(dir));
|
|
||||||
this.fsw._getWatchedDir(dir);
|
|
||||||
let throttler;
|
|
||||||
let closer;
|
|
||||||
const oDepth = this.fsw.options.depth;
|
|
||||||
if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) {
|
|
||||||
if (!target) {
|
|
||||||
await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
closer = this._watchWithNodeFs(dir, (dirPath, stats) => {
|
|
||||||
// if current directory is removed, do nothing
|
|
||||||
if (stats && stats.mtimeMs === 0)
|
|
||||||
return;
|
|
||||||
this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return closer;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle added file, directory, or glob pattern.
|
|
||||||
* Delegates call to _handleFile / _handleDir after checks.
|
|
||||||
* @param path to file or ir
|
|
||||||
* @param initialAdd was the file added at watch instantiation?
|
|
||||||
* @param priorWh depth relative to user-supplied path
|
|
||||||
* @param depth Child path actually targeted for watch
|
|
||||||
* @param target Child path actually targeted for watch
|
|
||||||
*/
|
|
||||||
async _addToNodeFs(path, initialAdd, priorWh, depth, target) {
|
|
||||||
const ready = this.fsw._emitReady;
|
|
||||||
if (this.fsw._isIgnored(path) || this.fsw.closed) {
|
|
||||||
ready();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const wh = this.fsw._getWatchHelpers(path);
|
|
||||||
if (priorWh) {
|
|
||||||
wh.filterPath = (entry) => priorWh.filterPath(entry);
|
|
||||||
wh.filterDir = (entry) => priorWh.filterDir(entry);
|
|
||||||
}
|
|
||||||
// evaluate what is at the path we're being asked to watch
|
|
||||||
try {
|
|
||||||
const stats = await statMethods[wh.statMethod](wh.watchPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
if (this.fsw._isIgnored(wh.watchPath, stats)) {
|
|
||||||
ready();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const follow = this.fsw.options.followSymlinks;
|
|
||||||
let closer;
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
const absPath = sysPath.resolve(path);
|
|
||||||
const targetPath = follow ? await (0, promises_1.realpath)(path) : path;
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// preserve this symlink's target path
|
|
||||||
if (absPath !== targetPath && targetPath !== undefined) {
|
|
||||||
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (stats.isSymbolicLink()) {
|
|
||||||
const targetPath = follow ? await (0, promises_1.realpath)(path) : path;
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
const parent = sysPath.dirname(wh.watchPath);
|
|
||||||
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
||||||
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
||||||
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
|
|
||||||
if (this.fsw.closed)
|
|
||||||
return;
|
|
||||||
// preserve this symlink's target path
|
|
||||||
if (targetPath !== undefined) {
|
|
||||||
this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
||||||
}
|
|
||||||
ready();
|
|
||||||
if (closer)
|
|
||||||
this.fsw._addPathCloser(path, closer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (this.fsw._handleError(error)) {
|
|
||||||
ready();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.NodeFsHandler = NodeFsHandler;
|
|
||||||
-215
@@ -1,215 +0,0 @@
|
|||||||
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
||||||
import { Stats } from 'fs';
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { ReaddirpStream, ReaddirpOptions, EntryInfo } from 'readdirp';
|
|
||||||
import { NodeFsHandler, EventName, Path, EVENTS as EV, WatchHandlers } from './handler.js';
|
|
||||||
type AWF = {
|
|
||||||
stabilityThreshold: number;
|
|
||||||
pollInterval: number;
|
|
||||||
};
|
|
||||||
type BasicOpts = {
|
|
||||||
persistent: boolean;
|
|
||||||
ignoreInitial: boolean;
|
|
||||||
followSymlinks: boolean;
|
|
||||||
cwd?: string;
|
|
||||||
usePolling: boolean;
|
|
||||||
interval: number;
|
|
||||||
binaryInterval: number;
|
|
||||||
alwaysStat?: boolean;
|
|
||||||
depth?: number;
|
|
||||||
ignorePermissionErrors: boolean;
|
|
||||||
atomic: boolean | number;
|
|
||||||
};
|
|
||||||
export type Throttler = {
|
|
||||||
timeoutObject: NodeJS.Timeout;
|
|
||||||
clear: () => void;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
export type ChokidarOptions = Partial<BasicOpts & {
|
|
||||||
ignored: Matcher | Matcher[];
|
|
||||||
awaitWriteFinish: boolean | Partial<AWF>;
|
|
||||||
}>;
|
|
||||||
export type FSWInstanceOptions = BasicOpts & {
|
|
||||||
ignored: Matcher[];
|
|
||||||
awaitWriteFinish: false | AWF;
|
|
||||||
};
|
|
||||||
export type ThrottleType = 'readdir' | 'watch' | 'add' | 'remove' | 'change';
|
|
||||||
export type EmitArgs = [path: Path, stats?: Stats];
|
|
||||||
export type EmitErrorArgs = [error: Error, stats?: Stats];
|
|
||||||
export type EmitArgsWithName = [event: EventName, ...EmitArgs];
|
|
||||||
export type MatchFunction = (val: string, stats?: Stats) => boolean;
|
|
||||||
export interface MatcherObject {
|
|
||||||
path: string;
|
|
||||||
recursive?: boolean;
|
|
||||||
}
|
|
||||||
export type Matcher = string | RegExp | MatchFunction | MatcherObject;
|
|
||||||
/**
|
|
||||||
* Directory entry.
|
|
||||||
*/
|
|
||||||
declare class DirEntry {
|
|
||||||
path: Path;
|
|
||||||
_removeWatcher: (dir: string, base: string) => void;
|
|
||||||
items: Set<Path>;
|
|
||||||
constructor(dir: Path, removeWatcher: (dir: string, base: string) => void);
|
|
||||||
add(item: string): void;
|
|
||||||
remove(item: string): Promise<void>;
|
|
||||||
has(item: string): boolean | undefined;
|
|
||||||
getChildren(): string[];
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
export declare class WatchHelper {
|
|
||||||
fsw: FSWatcher;
|
|
||||||
path: string;
|
|
||||||
watchPath: string;
|
|
||||||
fullWatchPath: string;
|
|
||||||
dirParts: string[][];
|
|
||||||
followSymlinks: boolean;
|
|
||||||
statMethod: 'stat' | 'lstat';
|
|
||||||
constructor(path: string, follow: boolean, fsw: FSWatcher);
|
|
||||||
entryPath(entry: EntryInfo): Path;
|
|
||||||
filterPath(entry: EntryInfo): boolean;
|
|
||||||
filterDir(entry: EntryInfo): boolean;
|
|
||||||
}
|
|
||||||
export interface FSWatcherKnownEventMap {
|
|
||||||
[EV.READY]: [];
|
|
||||||
[EV.RAW]: Parameters<WatchHandlers['rawEmitter']>;
|
|
||||||
[EV.ERROR]: Parameters<WatchHandlers['errHandler']>;
|
|
||||||
[EV.ALL]: [event: EventName, ...EmitArgs];
|
|
||||||
}
|
|
||||||
export type FSWatcherEventMap = FSWatcherKnownEventMap & {
|
|
||||||
[k in Exclude<EventName, keyof FSWatcherKnownEventMap>]: EmitArgs;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Watches files & directories for changes. Emitted events:
|
|
||||||
* `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
|
|
||||||
*
|
|
||||||
* new FSWatcher()
|
|
||||||
* .add(directories)
|
|
||||||
* .on('add', path => log('File', path, 'was added'))
|
|
||||||
*/
|
|
||||||
export declare class FSWatcher extends EventEmitter<FSWatcherEventMap> {
|
|
||||||
closed: boolean;
|
|
||||||
options: FSWInstanceOptions;
|
|
||||||
_closers: Map<string, Array<any>>;
|
|
||||||
_ignoredPaths: Set<Matcher>;
|
|
||||||
_throttled: Map<ThrottleType, Map<any, any>>;
|
|
||||||
_streams: Set<ReaddirpStream>;
|
|
||||||
_symlinkPaths: Map<Path, string | boolean>;
|
|
||||||
_watched: Map<string, DirEntry>;
|
|
||||||
_pendingWrites: Map<string, any>;
|
|
||||||
_pendingUnlinks: Map<string, EmitArgsWithName>;
|
|
||||||
_readyCount: number;
|
|
||||||
_emitReady: () => void;
|
|
||||||
_closePromise?: Promise<void>;
|
|
||||||
_userIgnored?: MatchFunction;
|
|
||||||
_readyEmitted: boolean;
|
|
||||||
_emitRaw: WatchHandlers['rawEmitter'];
|
|
||||||
_boundRemove: (dir: string, item: string) => void;
|
|
||||||
_nodeFsHandler: NodeFsHandler;
|
|
||||||
constructor(_opts?: ChokidarOptions);
|
|
||||||
_addIgnoredPath(matcher: Matcher): void;
|
|
||||||
_removeIgnoredPath(matcher: Matcher): void;
|
|
||||||
/**
|
|
||||||
* Adds paths to be watched on an existing FSWatcher instance.
|
|
||||||
* @param paths_ file or file list. Other arguments are unused
|
|
||||||
*/
|
|
||||||
add(paths_: Path | Path[], _origAdd?: string, _internal?: boolean): FSWatcher;
|
|
||||||
/**
|
|
||||||
* Close watchers or start ignoring events from specified paths.
|
|
||||||
*/
|
|
||||||
unwatch(paths_: Path | Path[]): FSWatcher;
|
|
||||||
/**
|
|
||||||
* Close watchers and remove all listeners from watched paths.
|
|
||||||
*/
|
|
||||||
close(): Promise<void>;
|
|
||||||
/**
|
|
||||||
* Expose list of watched paths
|
|
||||||
* @returns for chaining
|
|
||||||
*/
|
|
||||||
getWatched(): Record<string, string[]>;
|
|
||||||
emitWithAll(event: EventName, args: EmitArgs): void;
|
|
||||||
/**
|
|
||||||
* Normalize and emit events.
|
|
||||||
* Calling _emit DOES NOT MEAN emit() would be called!
|
|
||||||
* @param event Type of event
|
|
||||||
* @param path File or directory path
|
|
||||||
* @param stats arguments to be passed with event
|
|
||||||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_emit(event: EventName, path: Path, stats?: Stats): Promise<this | undefined>;
|
|
||||||
/**
|
|
||||||
* Common handler for errors
|
|
||||||
* @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_handleError(error: Error): Error | boolean;
|
|
||||||
/**
|
|
||||||
* Helper utility for throttling
|
|
||||||
* @param actionType type being throttled
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param timeout duration of time to suppress duplicate actions
|
|
||||||
* @returns tracking object or false if action should be suppressed
|
|
||||||
*/
|
|
||||||
_throttle(actionType: ThrottleType, path: Path, timeout: number): Throttler | false;
|
|
||||||
_incrReadyCount(): number;
|
|
||||||
/**
|
|
||||||
* Awaits write operation to finish.
|
|
||||||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
|
|
||||||
* @param event
|
|
||||||
* @param awfEmit Callback to be called when ready for event to be emitted.
|
|
||||||
*/
|
|
||||||
_awaitWriteFinish(path: Path, threshold: number, event: EventName, awfEmit: (err?: Error, stat?: Stats) => void): void;
|
|
||||||
/**
|
|
||||||
* Determines whether user has asked to ignore this path.
|
|
||||||
*/
|
|
||||||
_isIgnored(path: Path, stats?: Stats): boolean;
|
|
||||||
_isntIgnored(path: Path, stat?: Stats): boolean;
|
|
||||||
/**
|
|
||||||
* Provides a set of common helpers and properties relating to symlink handling.
|
|
||||||
* @param path file or directory pattern being watched
|
|
||||||
*/
|
|
||||||
_getWatchHelpers(path: Path): WatchHelper;
|
|
||||||
/**
|
|
||||||
* Provides directory tracking objects
|
|
||||||
* @param directory path of the directory
|
|
||||||
*/
|
|
||||||
_getWatchedDir(directory: string): DirEntry;
|
|
||||||
/**
|
|
||||||
* Check for read permissions: https://stackoverflow.com/a/11781404/1358405
|
|
||||||
*/
|
|
||||||
_hasReadPermissions(stats: Stats): boolean;
|
|
||||||
/**
|
|
||||||
* Handles emitting unlink events for
|
|
||||||
* files and directories, and via recursion, for
|
|
||||||
* files and directories within directories that are unlinked
|
|
||||||
* @param directory within which the following item is located
|
|
||||||
* @param item base path of item/directory
|
|
||||||
*/
|
|
||||||
_remove(directory: string, item: string, isDirectory?: boolean): void;
|
|
||||||
/**
|
|
||||||
* Closes all watchers for a path
|
|
||||||
*/
|
|
||||||
_closePath(path: Path): void;
|
|
||||||
/**
|
|
||||||
* Closes only file-specific watchers
|
|
||||||
*/
|
|
||||||
_closeFile(path: Path): void;
|
|
||||||
_addPathCloser(path: Path, closer: () => void): void;
|
|
||||||
_readdirp(root: Path, opts?: Partial<ReaddirpOptions>): ReaddirpStream | undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Instantiates watcher with paths to be tracked.
|
|
||||||
* @param paths file / directory paths
|
|
||||||
* @param options opts, such as `atomic`, `awaitWriteFinish`, `ignored`, and others
|
|
||||||
* @returns an instance of FSWatcher for chaining.
|
|
||||||
* @example
|
|
||||||
* const watcher = watch('.').on('all', (event, path) => { console.log(event, path); });
|
|
||||||
* watch('.', { atomic: true, awaitWriteFinish: true, ignored: (f, stats) => stats?.isFile() && !f.endsWith('.js') })
|
|
||||||
*/
|
|
||||||
export declare function watch(paths: string | string[], options?: ChokidarOptions): FSWatcher;
|
|
||||||
declare const _default: {
|
|
||||||
watch: typeof watch;
|
|
||||||
FSWatcher: typeof FSWatcher;
|
|
||||||
};
|
|
||||||
export default _default;
|
|
||||||
-804
@@ -1,804 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.FSWatcher = exports.WatchHelper = void 0;
|
|
||||||
exports.watch = watch;
|
|
||||||
/*! chokidar - MIT License (c) 2012 Paul Miller (paulmillr.com) */
|
|
||||||
const fs_1 = require("fs");
|
|
||||||
const promises_1 = require("fs/promises");
|
|
||||||
const events_1 = require("events");
|
|
||||||
const sysPath = require("path");
|
|
||||||
const readdirp_1 = require("readdirp");
|
|
||||||
const handler_js_1 = require("./handler.js");
|
|
||||||
const SLASH = '/';
|
|
||||||
const SLASH_SLASH = '//';
|
|
||||||
const ONE_DOT = '.';
|
|
||||||
const TWO_DOTS = '..';
|
|
||||||
const STRING_TYPE = 'string';
|
|
||||||
const BACK_SLASH_RE = /\\/g;
|
|
||||||
const DOUBLE_SLASH_RE = /\/\//;
|
|
||||||
const DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
|
|
||||||
const REPLACER_RE = /^\.[/\\]/;
|
|
||||||
function arrify(item) {
|
|
||||||
return Array.isArray(item) ? item : [item];
|
|
||||||
}
|
|
||||||
const isMatcherObject = (matcher) => typeof matcher === 'object' && matcher !== null && !(matcher instanceof RegExp);
|
|
||||||
function createPattern(matcher) {
|
|
||||||
if (typeof matcher === 'function')
|
|
||||||
return matcher;
|
|
||||||
if (typeof matcher === 'string')
|
|
||||||
return (string) => matcher === string;
|
|
||||||
if (matcher instanceof RegExp)
|
|
||||||
return (string) => matcher.test(string);
|
|
||||||
if (typeof matcher === 'object' && matcher !== null) {
|
|
||||||
return (string) => {
|
|
||||||
if (matcher.path === string)
|
|
||||||
return true;
|
|
||||||
if (matcher.recursive) {
|
|
||||||
const relative = sysPath.relative(matcher.path, string);
|
|
||||||
if (!relative) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !relative.startsWith('..') && !sysPath.isAbsolute(relative);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return () => false;
|
|
||||||
}
|
|
||||||
function normalizePath(path) {
|
|
||||||
if (typeof path !== 'string')
|
|
||||||
throw new Error('string expected');
|
|
||||||
path = sysPath.normalize(path);
|
|
||||||
path = path.replace(/\\/g, '/');
|
|
||||||
let prepend = false;
|
|
||||||
if (path.startsWith('//'))
|
|
||||||
prepend = true;
|
|
||||||
const DOUBLE_SLASH_RE = /\/\//;
|
|
||||||
while (path.match(DOUBLE_SLASH_RE))
|
|
||||||
path = path.replace(DOUBLE_SLASH_RE, '/');
|
|
||||||
if (prepend)
|
|
||||||
path = '/' + path;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
function matchPatterns(patterns, testString, stats) {
|
|
||||||
const path = normalizePath(testString);
|
|
||||||
for (let index = 0; index < patterns.length; index++) {
|
|
||||||
const pattern = patterns[index];
|
|
||||||
if (pattern(path, stats)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
function anymatch(matchers, testString) {
|
|
||||||
if (matchers == null) {
|
|
||||||
throw new TypeError('anymatch: specify first argument');
|
|
||||||
}
|
|
||||||
// Early cache for matchers.
|
|
||||||
const matchersArray = arrify(matchers);
|
|
||||||
const patterns = matchersArray.map((matcher) => createPattern(matcher));
|
|
||||||
if (testString == null) {
|
|
||||||
return (testString, stats) => {
|
|
||||||
return matchPatterns(patterns, testString, stats);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return matchPatterns(patterns, testString);
|
|
||||||
}
|
|
||||||
const unifyPaths = (paths_) => {
|
|
||||||
const paths = arrify(paths_).flat();
|
|
||||||
if (!paths.every((p) => typeof p === STRING_TYPE)) {
|
|
||||||
throw new TypeError(`Non-string provided as watch path: ${paths}`);
|
|
||||||
}
|
|
||||||
return paths.map(normalizePathToUnix);
|
|
||||||
};
|
|
||||||
// If SLASH_SLASH occurs at the beginning of path, it is not replaced
|
|
||||||
// because "//StoragePC/DrivePool/Movies" is a valid network path
|
|
||||||
const toUnix = (string) => {
|
|
||||||
let str = string.replace(BACK_SLASH_RE, SLASH);
|
|
||||||
let prepend = false;
|
|
||||||
if (str.startsWith(SLASH_SLASH)) {
|
|
||||||
prepend = true;
|
|
||||||
}
|
|
||||||
while (str.match(DOUBLE_SLASH_RE)) {
|
|
||||||
str = str.replace(DOUBLE_SLASH_RE, SLASH);
|
|
||||||
}
|
|
||||||
if (prepend) {
|
|
||||||
str = SLASH + str;
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
// Our version of upath.normalize
|
|
||||||
// TODO: this is not equal to path-normalize module - investigate why
|
|
||||||
const normalizePathToUnix = (path) => toUnix(sysPath.normalize(toUnix(path)));
|
|
||||||
// TODO: refactor
|
|
||||||
const normalizeIgnored = (cwd = '') => (path) => {
|
|
||||||
if (typeof path === 'string') {
|
|
||||||
return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getAbsolutePath = (path, cwd) => {
|
|
||||||
if (sysPath.isAbsolute(path)) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return sysPath.join(cwd, path);
|
|
||||||
};
|
|
||||||
const EMPTY_SET = Object.freeze(new Set());
|
|
||||||
/**
|
|
||||||
* Directory entry.
|
|
||||||
*/
|
|
||||||
class DirEntry {
|
|
||||||
constructor(dir, removeWatcher) {
|
|
||||||
this.path = dir;
|
|
||||||
this._removeWatcher = removeWatcher;
|
|
||||||
this.items = new Set();
|
|
||||||
}
|
|
||||||
add(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
if (item !== ONE_DOT && item !== TWO_DOTS)
|
|
||||||
items.add(item);
|
|
||||||
}
|
|
||||||
async remove(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
items.delete(item);
|
|
||||||
if (items.size > 0)
|
|
||||||
return;
|
|
||||||
const dir = this.path;
|
|
||||||
try {
|
|
||||||
await (0, promises_1.readdir)(dir);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (this._removeWatcher) {
|
|
||||||
this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has(item) {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return;
|
|
||||||
return items.has(item);
|
|
||||||
}
|
|
||||||
getChildren() {
|
|
||||||
const { items } = this;
|
|
||||||
if (!items)
|
|
||||||
return [];
|
|
||||||
return [...items.values()];
|
|
||||||
}
|
|
||||||
dispose() {
|
|
||||||
this.items.clear();
|
|
||||||
this.path = '';
|
|
||||||
this._removeWatcher = handler_js_1.EMPTY_FN;
|
|
||||||
this.items = EMPTY_SET;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const STAT_METHOD_F = 'stat';
|
|
||||||
const STAT_METHOD_L = 'lstat';
|
|
||||||
class WatchHelper {
|
|
||||||
constructor(path, follow, fsw) {
|
|
||||||
this.fsw = fsw;
|
|
||||||
const watchPath = path;
|
|
||||||
this.path = path = path.replace(REPLACER_RE, '');
|
|
||||||
this.watchPath = watchPath;
|
|
||||||
this.fullWatchPath = sysPath.resolve(watchPath);
|
|
||||||
this.dirParts = [];
|
|
||||||
this.dirParts.forEach((parts) => {
|
|
||||||
if (parts.length > 1)
|
|
||||||
parts.pop();
|
|
||||||
});
|
|
||||||
this.followSymlinks = follow;
|
|
||||||
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
|
|
||||||
}
|
|
||||||
entryPath(entry) {
|
|
||||||
return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, entry.fullPath));
|
|
||||||
}
|
|
||||||
filterPath(entry) {
|
|
||||||
const { stats } = entry;
|
|
||||||
if (stats && stats.isSymbolicLink())
|
|
||||||
return this.filterDir(entry);
|
|
||||||
const resolvedPath = this.entryPath(entry);
|
|
||||||
// TODO: what if stats is undefined? remove !
|
|
||||||
return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
|
|
||||||
}
|
|
||||||
filterDir(entry) {
|
|
||||||
return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.WatchHelper = WatchHelper;
|
|
||||||
/**
|
|
||||||
* Watches files & directories for changes. Emitted events:
|
|
||||||
* `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
|
|
||||||
*
|
|
||||||
* new FSWatcher()
|
|
||||||
* .add(directories)
|
|
||||||
* .on('add', path => log('File', path, 'was added'))
|
|
||||||
*/
|
|
||||||
class FSWatcher extends events_1.EventEmitter {
|
|
||||||
// Not indenting methods for history sake; for now.
|
|
||||||
constructor(_opts = {}) {
|
|
||||||
super();
|
|
||||||
this.closed = false;
|
|
||||||
this._closers = new Map();
|
|
||||||
this._ignoredPaths = new Set();
|
|
||||||
this._throttled = new Map();
|
|
||||||
this._streams = new Set();
|
|
||||||
this._symlinkPaths = new Map();
|
|
||||||
this._watched = new Map();
|
|
||||||
this._pendingWrites = new Map();
|
|
||||||
this._pendingUnlinks = new Map();
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyEmitted = false;
|
|
||||||
const awf = _opts.awaitWriteFinish;
|
|
||||||
const DEF_AWF = { stabilityThreshold: 2000, pollInterval: 100 };
|
|
||||||
const opts = {
|
|
||||||
// Defaults
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: false,
|
|
||||||
ignorePermissionErrors: false,
|
|
||||||
interval: 100,
|
|
||||||
binaryInterval: 300,
|
|
||||||
followSymlinks: true,
|
|
||||||
usePolling: false,
|
|
||||||
// useAsync: false,
|
|
||||||
atomic: true, // NOTE: overwritten later (depends on usePolling)
|
|
||||||
..._opts,
|
|
||||||
// Change format
|
|
||||||
ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
|
|
||||||
awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === 'object' ? { ...DEF_AWF, ...awf } : false,
|
|
||||||
};
|
|
||||||
// Always default to polling on IBM i because fs.watch() is not available on IBM i.
|
|
||||||
if (handler_js_1.isIBMi)
|
|
||||||
opts.usePolling = true;
|
|
||||||
// Editor atomic write normalization enabled by default with fs.watch
|
|
||||||
if (opts.atomic === undefined)
|
|
||||||
opts.atomic = !opts.usePolling;
|
|
||||||
// opts.atomic = typeof _opts.atomic === 'number' ? _opts.atomic : 100;
|
|
||||||
// Global override. Useful for developers, who need to force polling for all
|
|
||||||
// instances of chokidar, regardless of usage / dependency depth
|
|
||||||
const envPoll = process.env.CHOKIDAR_USEPOLLING;
|
|
||||||
if (envPoll !== undefined) {
|
|
||||||
const envLower = envPoll.toLowerCase();
|
|
||||||
if (envLower === 'false' || envLower === '0')
|
|
||||||
opts.usePolling = false;
|
|
||||||
else if (envLower === 'true' || envLower === '1')
|
|
||||||
opts.usePolling = true;
|
|
||||||
else
|
|
||||||
opts.usePolling = !!envLower;
|
|
||||||
}
|
|
||||||
const envInterval = process.env.CHOKIDAR_INTERVAL;
|
|
||||||
if (envInterval)
|
|
||||||
opts.interval = Number.parseInt(envInterval, 10);
|
|
||||||
// This is done to emit ready only once, but each 'add' will increase that?
|
|
||||||
let readyCalls = 0;
|
|
||||||
this._emitReady = () => {
|
|
||||||
readyCalls++;
|
|
||||||
if (readyCalls >= this._readyCount) {
|
|
||||||
this._emitReady = handler_js_1.EMPTY_FN;
|
|
||||||
this._readyEmitted = true;
|
|
||||||
// use process.nextTick to allow time for listener to be bound
|
|
||||||
process.nextTick(() => this.emit(handler_js_1.EVENTS.READY));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this._emitRaw = (...args) => this.emit(handler_js_1.EVENTS.RAW, ...args);
|
|
||||||
this._boundRemove = this._remove.bind(this);
|
|
||||||
this.options = opts;
|
|
||||||
this._nodeFsHandler = new handler_js_1.NodeFsHandler(this);
|
|
||||||
// You’re frozen when your heart’s not open.
|
|
||||||
Object.freeze(opts);
|
|
||||||
}
|
|
||||||
_addIgnoredPath(matcher) {
|
|
||||||
if (isMatcherObject(matcher)) {
|
|
||||||
// return early if we already have a deeply equal matcher object
|
|
||||||
for (const ignored of this._ignoredPaths) {
|
|
||||||
if (isMatcherObject(ignored) &&
|
|
||||||
ignored.path === matcher.path &&
|
|
||||||
ignored.recursive === matcher.recursive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._ignoredPaths.add(matcher);
|
|
||||||
}
|
|
||||||
_removeIgnoredPath(matcher) {
|
|
||||||
this._ignoredPaths.delete(matcher);
|
|
||||||
// now find any matcher objects with the matcher as path
|
|
||||||
if (typeof matcher === 'string') {
|
|
||||||
for (const ignored of this._ignoredPaths) {
|
|
||||||
// TODO (43081j): make this more efficient.
|
|
||||||
// probably just make a `this._ignoredDirectories` or some
|
|
||||||
// such thing.
|
|
||||||
if (isMatcherObject(ignored) && ignored.path === matcher) {
|
|
||||||
this._ignoredPaths.delete(ignored);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Public methods
|
|
||||||
/**
|
|
||||||
* Adds paths to be watched on an existing FSWatcher instance.
|
|
||||||
* @param paths_ file or file list. Other arguments are unused
|
|
||||||
*/
|
|
||||||
add(paths_, _origAdd, _internal) {
|
|
||||||
const { cwd } = this.options;
|
|
||||||
this.closed = false;
|
|
||||||
this._closePromise = undefined;
|
|
||||||
let paths = unifyPaths(paths_);
|
|
||||||
if (cwd) {
|
|
||||||
paths = paths.map((path) => {
|
|
||||||
const absPath = getAbsolutePath(path, cwd);
|
|
||||||
// Check `path` instead of `absPath` because the cwd portion can't be a glob
|
|
||||||
return absPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
paths.forEach((path) => {
|
|
||||||
this._removeIgnoredPath(path);
|
|
||||||
});
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
if (!this._readyCount)
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyCount += paths.length;
|
|
||||||
Promise.all(paths.map(async (path) => {
|
|
||||||
const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, undefined, 0, _origAdd);
|
|
||||||
if (res)
|
|
||||||
this._emitReady();
|
|
||||||
return res;
|
|
||||||
})).then((results) => {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
results.forEach((item) => {
|
|
||||||
if (item)
|
|
||||||
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Close watchers or start ignoring events from specified paths.
|
|
||||||
*/
|
|
||||||
unwatch(paths_) {
|
|
||||||
if (this.closed)
|
|
||||||
return this;
|
|
||||||
const paths = unifyPaths(paths_);
|
|
||||||
const { cwd } = this.options;
|
|
||||||
paths.forEach((path) => {
|
|
||||||
// convert to absolute path unless relative path already matches
|
|
||||||
if (!sysPath.isAbsolute(path) && !this._closers.has(path)) {
|
|
||||||
if (cwd)
|
|
||||||
path = sysPath.join(cwd, path);
|
|
||||||
path = sysPath.resolve(path);
|
|
||||||
}
|
|
||||||
this._closePath(path);
|
|
||||||
this._addIgnoredPath(path);
|
|
||||||
if (this._watched.has(path)) {
|
|
||||||
this._addIgnoredPath({
|
|
||||||
path,
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// reset the cached userIgnored anymatch fn
|
|
||||||
// to make ignoredPaths changes effective
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Close watchers and remove all listeners from watched paths.
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
if (this._closePromise) {
|
|
||||||
return this._closePromise;
|
|
||||||
}
|
|
||||||
this.closed = true;
|
|
||||||
// Memory management.
|
|
||||||
this.removeAllListeners();
|
|
||||||
const closers = [];
|
|
||||||
this._closers.forEach((closerList) => closerList.forEach((closer) => {
|
|
||||||
const promise = closer();
|
|
||||||
if (promise instanceof Promise)
|
|
||||||
closers.push(promise);
|
|
||||||
}));
|
|
||||||
this._streams.forEach((stream) => stream.destroy());
|
|
||||||
this._userIgnored = undefined;
|
|
||||||
this._readyCount = 0;
|
|
||||||
this._readyEmitted = false;
|
|
||||||
this._watched.forEach((dirent) => dirent.dispose());
|
|
||||||
this._closers.clear();
|
|
||||||
this._watched.clear();
|
|
||||||
this._streams.clear();
|
|
||||||
this._symlinkPaths.clear();
|
|
||||||
this._throttled.clear();
|
|
||||||
this._closePromise = closers.length
|
|
||||||
? Promise.all(closers).then(() => undefined)
|
|
||||||
: Promise.resolve();
|
|
||||||
return this._closePromise;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Expose list of watched paths
|
|
||||||
* @returns for chaining
|
|
||||||
*/
|
|
||||||
getWatched() {
|
|
||||||
const watchList = {};
|
|
||||||
this._watched.forEach((entry, dir) => {
|
|
||||||
const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir;
|
|
||||||
const index = key || ONE_DOT;
|
|
||||||
watchList[index] = entry.getChildren().sort();
|
|
||||||
});
|
|
||||||
return watchList;
|
|
||||||
}
|
|
||||||
emitWithAll(event, args) {
|
|
||||||
this.emit(event, ...args);
|
|
||||||
if (event !== handler_js_1.EVENTS.ERROR)
|
|
||||||
this.emit(handler_js_1.EVENTS.ALL, event, ...args);
|
|
||||||
}
|
|
||||||
// Common helpers
|
|
||||||
// --------------
|
|
||||||
/**
|
|
||||||
* Normalize and emit events.
|
|
||||||
* Calling _emit DOES NOT MEAN emit() would be called!
|
|
||||||
* @param event Type of event
|
|
||||||
* @param path File or directory path
|
|
||||||
* @param stats arguments to be passed with event
|
|
||||||
* @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
async _emit(event, path, stats) {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
const opts = this.options;
|
|
||||||
if (handler_js_1.isWindows)
|
|
||||||
path = sysPath.normalize(path);
|
|
||||||
if (opts.cwd)
|
|
||||||
path = sysPath.relative(opts.cwd, path);
|
|
||||||
const args = [path];
|
|
||||||
if (stats != null)
|
|
||||||
args.push(stats);
|
|
||||||
const awf = opts.awaitWriteFinish;
|
|
||||||
let pw;
|
|
||||||
if (awf && (pw = this._pendingWrites.get(path))) {
|
|
||||||
pw.lastChange = new Date();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (opts.atomic) {
|
|
||||||
if (event === handler_js_1.EVENTS.UNLINK) {
|
|
||||||
this._pendingUnlinks.set(path, [event, ...args]);
|
|
||||||
setTimeout(() => {
|
|
||||||
this._pendingUnlinks.forEach((entry, path) => {
|
|
||||||
this.emit(...entry);
|
|
||||||
this.emit(handler_js_1.EVENTS.ALL, ...entry);
|
|
||||||
this._pendingUnlinks.delete(path);
|
|
||||||
});
|
|
||||||
}, typeof opts.atomic === 'number' ? opts.atomic : 100);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (event === handler_js_1.EVENTS.ADD && this._pendingUnlinks.has(path)) {
|
|
||||||
event = handler_js_1.EVENTS.CHANGE;
|
|
||||||
this._pendingUnlinks.delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (awf && (event === handler_js_1.EVENTS.ADD || event === handler_js_1.EVENTS.CHANGE) && this._readyEmitted) {
|
|
||||||
const awfEmit = (err, stats) => {
|
|
||||||
if (err) {
|
|
||||||
event = handler_js_1.EVENTS.ERROR;
|
|
||||||
args[0] = err;
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
}
|
|
||||||
else if (stats) {
|
|
||||||
// if stats doesn't exist the file must have been deleted
|
|
||||||
if (args.length > 1) {
|
|
||||||
args[1] = stats;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
args.push(stats);
|
|
||||||
}
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (event === handler_js_1.EVENTS.CHANGE) {
|
|
||||||
const isThrottled = !this._throttle(handler_js_1.EVENTS.CHANGE, path, 50);
|
|
||||||
if (isThrottled)
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (opts.alwaysStat &&
|
|
||||||
stats === undefined &&
|
|
||||||
(event === handler_js_1.EVENTS.ADD || event === handler_js_1.EVENTS.ADD_DIR || event === handler_js_1.EVENTS.CHANGE)) {
|
|
||||||
const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path;
|
|
||||||
let stats;
|
|
||||||
try {
|
|
||||||
stats = await (0, promises_1.stat)(fullPath);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
// Suppress event when fs_stat fails, to avoid sending undefined 'stat'
|
|
||||||
if (!stats || this.closed)
|
|
||||||
return;
|
|
||||||
args.push(stats);
|
|
||||||
}
|
|
||||||
this.emitWithAll(event, args);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Common handler for errors
|
|
||||||
* @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
|
|
||||||
*/
|
|
||||||
_handleError(error) {
|
|
||||||
const code = error && error.code;
|
|
||||||
if (error &&
|
|
||||||
code !== 'ENOENT' &&
|
|
||||||
code !== 'ENOTDIR' &&
|
|
||||||
(!this.options.ignorePermissionErrors || (code !== 'EPERM' && code !== 'EACCES'))) {
|
|
||||||
this.emit(handler_js_1.EVENTS.ERROR, error);
|
|
||||||
}
|
|
||||||
return error || this.closed;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Helper utility for throttling
|
|
||||||
* @param actionType type being throttled
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param timeout duration of time to suppress duplicate actions
|
|
||||||
* @returns tracking object or false if action should be suppressed
|
|
||||||
*/
|
|
||||||
_throttle(actionType, path, timeout) {
|
|
||||||
if (!this._throttled.has(actionType)) {
|
|
||||||
this._throttled.set(actionType, new Map());
|
|
||||||
}
|
|
||||||
const action = this._throttled.get(actionType);
|
|
||||||
if (!action)
|
|
||||||
throw new Error('invalid throttle');
|
|
||||||
const actionPath = action.get(path);
|
|
||||||
if (actionPath) {
|
|
||||||
actionPath.count++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
let timeoutObject;
|
|
||||||
const clear = () => {
|
|
||||||
const item = action.get(path);
|
|
||||||
const count = item ? item.count : 0;
|
|
||||||
action.delete(path);
|
|
||||||
clearTimeout(timeoutObject);
|
|
||||||
if (item)
|
|
||||||
clearTimeout(item.timeoutObject);
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
timeoutObject = setTimeout(clear, timeout);
|
|
||||||
const thr = { timeoutObject, clear, count: 0 };
|
|
||||||
action.set(path, thr);
|
|
||||||
return thr;
|
|
||||||
}
|
|
||||||
_incrReadyCount() {
|
|
||||||
return this._readyCount++;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Awaits write operation to finish.
|
|
||||||
* Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
|
|
||||||
* @param path being acted upon
|
|
||||||
* @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
|
|
||||||
* @param event
|
|
||||||
* @param awfEmit Callback to be called when ready for event to be emitted.
|
|
||||||
*/
|
|
||||||
_awaitWriteFinish(path, threshold, event, awfEmit) {
|
|
||||||
const awf = this.options.awaitWriteFinish;
|
|
||||||
if (typeof awf !== 'object')
|
|
||||||
return;
|
|
||||||
const pollInterval = awf.pollInterval;
|
|
||||||
let timeoutHandler;
|
|
||||||
let fullPath = path;
|
|
||||||
if (this.options.cwd && !sysPath.isAbsolute(path)) {
|
|
||||||
fullPath = sysPath.join(this.options.cwd, path);
|
|
||||||
}
|
|
||||||
const now = new Date();
|
|
||||||
const writes = this._pendingWrites;
|
|
||||||
function awaitWriteFinishFn(prevStat) {
|
|
||||||
(0, fs_1.stat)(fullPath, (err, curStat) => {
|
|
||||||
if (err || !writes.has(path)) {
|
|
||||||
if (err && err.code !== 'ENOENT')
|
|
||||||
awfEmit(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = Number(new Date());
|
|
||||||
if (prevStat && curStat.size !== prevStat.size) {
|
|
||||||
writes.get(path).lastChange = now;
|
|
||||||
}
|
|
||||||
const pw = writes.get(path);
|
|
||||||
const df = now - pw.lastChange;
|
|
||||||
if (df >= threshold) {
|
|
||||||
writes.delete(path);
|
|
||||||
awfEmit(undefined, curStat);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!writes.has(path)) {
|
|
||||||
writes.set(path, {
|
|
||||||
lastChange: now,
|
|
||||||
cancelWait: () => {
|
|
||||||
writes.delete(path);
|
|
||||||
clearTimeout(timeoutHandler);
|
|
||||||
return event;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Determines whether user has asked to ignore this path.
|
|
||||||
*/
|
|
||||||
_isIgnored(path, stats) {
|
|
||||||
if (this.options.atomic && DOT_RE.test(path))
|
|
||||||
return true;
|
|
||||||
if (!this._userIgnored) {
|
|
||||||
const { cwd } = this.options;
|
|
||||||
const ign = this.options.ignored;
|
|
||||||
const ignored = (ign || []).map(normalizeIgnored(cwd));
|
|
||||||
const ignoredPaths = [...this._ignoredPaths];
|
|
||||||
const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
|
|
||||||
this._userIgnored = anymatch(list, undefined);
|
|
||||||
}
|
|
||||||
return this._userIgnored(path, stats);
|
|
||||||
}
|
|
||||||
_isntIgnored(path, stat) {
|
|
||||||
return !this._isIgnored(path, stat);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Provides a set of common helpers and properties relating to symlink handling.
|
|
||||||
* @param path file or directory pattern being watched
|
|
||||||
*/
|
|
||||||
_getWatchHelpers(path) {
|
|
||||||
return new WatchHelper(path, this.options.followSymlinks, this);
|
|
||||||
}
|
|
||||||
// Directory helpers
|
|
||||||
// -----------------
|
|
||||||
/**
|
|
||||||
* Provides directory tracking objects
|
|
||||||
* @param directory path of the directory
|
|
||||||
*/
|
|
||||||
_getWatchedDir(directory) {
|
|
||||||
const dir = sysPath.resolve(directory);
|
|
||||||
if (!this._watched.has(dir))
|
|
||||||
this._watched.set(dir, new DirEntry(dir, this._boundRemove));
|
|
||||||
return this._watched.get(dir);
|
|
||||||
}
|
|
||||||
// File helpers
|
|
||||||
// ------------
|
|
||||||
/**
|
|
||||||
* Check for read permissions: https://stackoverflow.com/a/11781404/1358405
|
|
||||||
*/
|
|
||||||
_hasReadPermissions(stats) {
|
|
||||||
if (this.options.ignorePermissionErrors)
|
|
||||||
return true;
|
|
||||||
return Boolean(Number(stats.mode) & 0o400);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handles emitting unlink events for
|
|
||||||
* files and directories, and via recursion, for
|
|
||||||
* files and directories within directories that are unlinked
|
|
||||||
* @param directory within which the following item is located
|
|
||||||
* @param item base path of item/directory
|
|
||||||
*/
|
|
||||||
_remove(directory, item, isDirectory) {
|
|
||||||
// if what is being deleted is a directory, get that directory's paths
|
|
||||||
// for recursive deleting and cleaning of watched object
|
|
||||||
// if it is not a directory, nestedDirectoryChildren will be empty array
|
|
||||||
const path = sysPath.join(directory, item);
|
|
||||||
const fullPath = sysPath.resolve(path);
|
|
||||||
isDirectory =
|
|
||||||
isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath);
|
|
||||||
// prevent duplicate handling in case of arriving here nearly simultaneously
|
|
||||||
// via multiple paths (such as _handleFile and _handleDir)
|
|
||||||
if (!this._throttle('remove', path, 100))
|
|
||||||
return;
|
|
||||||
// if the only watched file is removed, watch for its return
|
|
||||||
if (!isDirectory && this._watched.size === 1) {
|
|
||||||
this.add(directory, item, true);
|
|
||||||
}
|
|
||||||
// This will create a new entry in the watched object in either case
|
|
||||||
// so we got to do the directory check beforehand
|
|
||||||
const wp = this._getWatchedDir(path);
|
|
||||||
const nestedDirectoryChildren = wp.getChildren();
|
|
||||||
// Recursively remove children directories / files.
|
|
||||||
nestedDirectoryChildren.forEach((nested) => this._remove(path, nested));
|
|
||||||
// Check if item was on the watched list and remove it
|
|
||||||
const parent = this._getWatchedDir(directory);
|
|
||||||
const wasTracked = parent.has(item);
|
|
||||||
parent.remove(item);
|
|
||||||
// Fixes issue #1042 -> Relative paths were detected and added as symlinks
|
|
||||||
// (https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L612),
|
|
||||||
// but never removed from the map in case the path was deleted.
|
|
||||||
// This leads to an incorrect state if the path was recreated:
|
|
||||||
// https://github.com/paulmillr/chokidar/blob/e1753ddbc9571bdc33b4a4af172d52cb6e611c10/lib/nodefs-handler.js#L553
|
|
||||||
if (this._symlinkPaths.has(fullPath)) {
|
|
||||||
this._symlinkPaths.delete(fullPath);
|
|
||||||
}
|
|
||||||
// If we wait for this file to be fully written, cancel the wait.
|
|
||||||
let relPath = path;
|
|
||||||
if (this.options.cwd)
|
|
||||||
relPath = sysPath.relative(this.options.cwd, path);
|
|
||||||
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
||||||
const event = this._pendingWrites.get(relPath).cancelWait();
|
|
||||||
if (event === handler_js_1.EVENTS.ADD)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The Entry will either be a directory that just got removed
|
|
||||||
// or a bogus entry to a file, in either case we have to remove it
|
|
||||||
this._watched.delete(path);
|
|
||||||
this._watched.delete(fullPath);
|
|
||||||
const eventName = isDirectory ? handler_js_1.EVENTS.UNLINK_DIR : handler_js_1.EVENTS.UNLINK;
|
|
||||||
if (wasTracked && !this._isIgnored(path))
|
|
||||||
this._emit(eventName, path);
|
|
||||||
// Avoid conflicts if we later create another file with the same name
|
|
||||||
this._closePath(path);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Closes all watchers for a path
|
|
||||||
*/
|
|
||||||
_closePath(path) {
|
|
||||||
this._closeFile(path);
|
|
||||||
const dir = sysPath.dirname(path);
|
|
||||||
this._getWatchedDir(dir).remove(sysPath.basename(path));
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Closes only file-specific watchers
|
|
||||||
*/
|
|
||||||
_closeFile(path) {
|
|
||||||
const closers = this._closers.get(path);
|
|
||||||
if (!closers)
|
|
||||||
return;
|
|
||||||
closers.forEach((closer) => closer());
|
|
||||||
this._closers.delete(path);
|
|
||||||
}
|
|
||||||
_addPathCloser(path, closer) {
|
|
||||||
if (!closer)
|
|
||||||
return;
|
|
||||||
let list = this._closers.get(path);
|
|
||||||
if (!list) {
|
|
||||||
list = [];
|
|
||||||
this._closers.set(path, list);
|
|
||||||
}
|
|
||||||
list.push(closer);
|
|
||||||
}
|
|
||||||
_readdirp(root, opts) {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
const options = { type: handler_js_1.EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
|
|
||||||
let stream = (0, readdirp_1.readdirp)(root, options);
|
|
||||||
this._streams.add(stream);
|
|
||||||
stream.once(handler_js_1.STR_CLOSE, () => {
|
|
||||||
stream = undefined;
|
|
||||||
});
|
|
||||||
stream.once(handler_js_1.STR_END, () => {
|
|
||||||
if (stream) {
|
|
||||||
this._streams.delete(stream);
|
|
||||||
stream = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.FSWatcher = FSWatcher;
|
|
||||||
/**
|
|
||||||
* Instantiates watcher with paths to be tracked.
|
|
||||||
* @param paths file / directory paths
|
|
||||||
* @param options opts, such as `atomic`, `awaitWriteFinish`, `ignored`, and others
|
|
||||||
* @returns an instance of FSWatcher for chaining.
|
|
||||||
* @example
|
|
||||||
* const watcher = watch('.').on('all', (event, path) => { console.log(event, path); });
|
|
||||||
* watch('.', { atomic: true, awaitWriteFinish: true, ignored: (f, stats) => stats?.isFile() && !f.endsWith('.js') })
|
|
||||||
*/
|
|
||||||
function watch(paths, options = {}) {
|
|
||||||
const watcher = new FSWatcher(options);
|
|
||||||
watcher.add(paths);
|
|
||||||
return watcher;
|
|
||||||
}
|
|
||||||
exports.default = { watch, FSWatcher };
|
|
||||||
-69
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "chokidar",
|
|
||||||
"description": "Minimal and efficient cross-platform file watching library",
|
|
||||||
"version": "4.0.3",
|
|
||||||
"homepage": "https://github.com/paulmillr/chokidar",
|
|
||||||
"author": "Paul Miller (https://paulmillr.com)",
|
|
||||||
"files": [
|
|
||||||
"index.js",
|
|
||||||
"index.d.ts",
|
|
||||||
"handler.js",
|
|
||||||
"handler.d.ts",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"main": "./index.js",
|
|
||||||
"module": "./esm/index.js",
|
|
||||||
"types": "./index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./esm/index.js",
|
|
||||||
"require": "./index.js"
|
|
||||||
},
|
|
||||||
"./handler.js": {
|
|
||||||
"import": "./esm/handler.js",
|
|
||||||
"require": "./handler.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^4.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@paulmillr/jsbt": "0.2.1",
|
|
||||||
"@types/node": "20.14.8",
|
|
||||||
"chai": "4.3.4",
|
|
||||||
"prettier": "3.1.1",
|
|
||||||
"rimraf": "5.0.5",
|
|
||||||
"sinon": "12.0.1",
|
|
||||||
"sinon-chai": "3.7.0",
|
|
||||||
"typescript": "5.5.2",
|
|
||||||
"upath": "2.0.1"
|
|
||||||
},
|
|
||||||
"sideEffects": false,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.16.0"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/paulmillr/chokidar.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/paulmillr/chokidar/issues"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc && tsc -p tsconfig.esm.json",
|
|
||||||
"lint": "prettier --check src",
|
|
||||||
"format": "prettier --write src",
|
|
||||||
"test": "node --test"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"fs",
|
|
||||||
"watch",
|
|
||||||
"watchFile",
|
|
||||||
"watcher",
|
|
||||||
"watching",
|
|
||||||
"file",
|
|
||||||
"fsevents"
|
|
||||||
],
|
|
||||||
"funding": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
0.5.4 / 2021-12-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* deps: safe-buffer@5.2.1
|
|
||||||
|
|
||||||
0.5.3 / 2018-12-17
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Use `safe-buffer` for improved Buffer API
|
|
||||||
|
|
||||||
0.5.2 / 2016-12-08
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix `parse` to accept any linear whitespace character
|
|
||||||
|
|
||||||
0.5.1 / 2016-01-17
|
|
||||||
==================
|
|
||||||
|
|
||||||
* perf: enable strict mode
|
|
||||||
|
|
||||||
0.5.0 / 2014-10-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Add `parse` function
|
|
||||||
|
|
||||||
0.4.0 / 2014-09-21
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Expand non-Unicode `filename` to the full ISO-8859-1 charset
|
|
||||||
|
|
||||||
0.3.0 / 2014-09-20
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Add `fallback` option
|
|
||||||
* Add `type` option
|
|
||||||
|
|
||||||
0.2.0 / 2014-09-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Reduce ambiguity of file names with hex escape in buggy browsers
|
|
||||||
|
|
||||||
0.1.2 / 2014-09-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix periodic invalid Unicode filename header
|
|
||||||
|
|
||||||
0.1.1 / 2014-09-19
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Fix invalid characters appearing in `filename*` parameter
|
|
||||||
|
|
||||||
0.1.0 / 2014-09-18
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Make the `filename` argument optional
|
|
||||||
|
|
||||||
0.0.0 / 2014-09-18
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Initial release
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2014-2017 Douglas Christopher Wilson
|
|
||||||
|
|
||||||
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.
|
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
# content-disposition
|
|
||||||
|
|
||||||
[![NPM Version][npm-image]][npm-url]
|
|
||||||
[![NPM Downloads][downloads-image]][downloads-url]
|
|
||||||
[![Node.js Version][node-version-image]][node-version-url]
|
|
||||||
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
|
||||||
[![Test Coverage][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
Create and parse HTTP `Content-Disposition` header
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm install content-disposition
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```js
|
|
||||||
var contentDisposition = require('content-disposition')
|
|
||||||
```
|
|
||||||
|
|
||||||
### contentDisposition(filename, options)
|
|
||||||
|
|
||||||
Create an attachment `Content-Disposition` header value using the given file name,
|
|
||||||
if supplied. The `filename` is optional and if no file name is desired, but you
|
|
||||||
want to specify `options`, set `filename` to `undefined`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf'))
|
|
||||||
```
|
|
||||||
|
|
||||||
**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this
|
|
||||||
header through a means different from `setHeader` in Node.js, you'll want to specify
|
|
||||||
the `'binary'` encoding in Node.js.
|
|
||||||
|
|
||||||
#### Options
|
|
||||||
|
|
||||||
`contentDisposition` accepts these properties in the options object.
|
|
||||||
|
|
||||||
##### fallback
|
|
||||||
|
|
||||||
If the `filename` option is outside ISO-8859-1, then the file name is actually
|
|
||||||
stored in a supplemental field for clients that support Unicode file names and
|
|
||||||
a ISO-8859-1 version of the file name is automatically generated.
|
|
||||||
|
|
||||||
This specifies the ISO-8859-1 file name to override the automatic generation or
|
|
||||||
disables the generation all together, defaults to `true`.
|
|
||||||
|
|
||||||
- A string will specify the ISO-8859-1 file name to use in place of automatic
|
|
||||||
generation.
|
|
||||||
- `false` will disable including a ISO-8859-1 file name and only include the
|
|
||||||
Unicode version (unless the file name is already ISO-8859-1).
|
|
||||||
- `true` will enable automatic generation if the file name is outside ISO-8859-1.
|
|
||||||
|
|
||||||
If the `filename` option is ISO-8859-1 and this option is specified and has a
|
|
||||||
different value, then the `filename` option is encoded in the extended field
|
|
||||||
and this set as the fallback field, even though they are both ISO-8859-1.
|
|
||||||
|
|
||||||
##### type
|
|
||||||
|
|
||||||
Specifies the disposition type, defaults to `"attachment"`. This can also be
|
|
||||||
`"inline"`, or any other value (all values except inline are treated like
|
|
||||||
`attachment`, but can convey additional information if both parties agree to
|
|
||||||
it). The type is normalized to lower-case.
|
|
||||||
|
|
||||||
### contentDisposition.parse(string)
|
|
||||||
|
|
||||||
```js
|
|
||||||
var disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt')
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse a `Content-Disposition` header string. This automatically handles extended
|
|
||||||
("Unicode") parameters by decoding them and providing them under the standard
|
|
||||||
parameter name. This will return an object with the following properties (examples
|
|
||||||
are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`):
|
|
||||||
|
|
||||||
- `type`: The disposition type (always lower case). Example: `'attachment'`
|
|
||||||
|
|
||||||
- `parameters`: An object of the parameters in the disposition (name of parameter
|
|
||||||
always lower case and extended versions replace non-extended versions). Example:
|
|
||||||
`{filename: "€ rates.txt"}`
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Send a file for download
|
|
||||||
|
|
||||||
```js
|
|
||||||
var contentDisposition = require('content-disposition')
|
|
||||||
var destroy = require('destroy')
|
|
||||||
var fs = require('fs')
|
|
||||||
var http = require('http')
|
|
||||||
var onFinished = require('on-finished')
|
|
||||||
|
|
||||||
var filePath = '/path/to/public/plans.pdf'
|
|
||||||
|
|
||||||
http.createServer(function onRequest (req, res) {
|
|
||||||
// set headers
|
|
||||||
res.setHeader('Content-Type', 'application/pdf')
|
|
||||||
res.setHeader('Content-Disposition', contentDisposition(filePath))
|
|
||||||
|
|
||||||
// send file
|
|
||||||
var stream = fs.createReadStream(filePath)
|
|
||||||
stream.pipe(res)
|
|
||||||
onFinished(res, function () {
|
|
||||||
destroy(stream)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616]
|
|
||||||
- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987]
|
|
||||||
- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266]
|
|
||||||
- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231]
|
|
||||||
|
|
||||||
[rfc-2616]: https://tools.ietf.org/html/rfc2616
|
|
||||||
[rfc-5987]: https://tools.ietf.org/html/rfc5987
|
|
||||||
[rfc-6266]: https://tools.ietf.org/html/rfc6266
|
|
||||||
[tc-2231]: http://greenbytes.de/tech/tc2231/
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[npm-image]: https://img.shields.io/npm/v/content-disposition.svg
|
|
||||||
[npm-url]: https://npmjs.org/package/content-disposition
|
|
||||||
[node-version-image]: https://img.shields.io/node/v/content-disposition.svg
|
|
||||||
[node-version-url]: https://nodejs.org/en/download
|
|
||||||
[coveralls-image]: https://img.shields.io/coveralls/jshttp/content-disposition.svg
|
|
||||||
[coveralls-url]: https://coveralls.io/r/jshttp/content-disposition?branch=master
|
|
||||||
[downloads-image]: https://img.shields.io/npm/dm/content-disposition.svg
|
|
||||||
[downloads-url]: https://npmjs.org/package/content-disposition
|
|
||||||
[github-actions-ci-image]: https://img.shields.io/github/workflow/status/jshttp/content-disposition/ci/master?label=ci
|
|
||||||
[github-actions-ci-url]: https://github.com/jshttp/content-disposition?query=workflow%3Aci
|
|
||||||
-458
@@ -1,458 +0,0 @@
|
|||||||
/*!
|
|
||||||
* content-disposition
|
|
||||||
* Copyright(c) 2014-2017 Douglas Christopher Wilson
|
|
||||||
* MIT Licensed
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module exports.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = contentDisposition
|
|
||||||
module.exports.parse = parse
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var basename = require('path').basename
|
|
||||||
var Buffer = require('safe-buffer').Buffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match percent encoding escape.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/
|
|
||||||
var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match non-latin1 characters.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match quoted-pair in RFC 2616
|
|
||||||
*
|
|
||||||
* quoted-pair = "\" CHAR
|
|
||||||
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp to match chars that must be quoted-pair in RFC 2616
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var QUOTE_REGEXP = /([\\"])/g
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp for various RFC 2616 grammar
|
|
||||||
*
|
|
||||||
* parameter = token "=" ( token | quoted-string )
|
|
||||||
* token = 1*<any CHAR except CTLs or separators>
|
|
||||||
* separators = "(" | ")" | "<" | ">" | "@"
|
|
||||||
* | "," | ";" | ":" | "\" | <">
|
|
||||||
* | "/" | "[" | "]" | "?" | "="
|
|
||||||
* | "{" | "}" | SP | HT
|
|
||||||
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
|
||||||
* qdtext = <any TEXT except <">>
|
|
||||||
* quoted-pair = "\" CHAR
|
|
||||||
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
||||||
* TEXT = <any OCTET except CTLs, but including LWS>
|
|
||||||
* LWS = [CRLF] 1*( SP | HT )
|
|
||||||
* CRLF = CR LF
|
|
||||||
* CR = <US-ASCII CR, carriage return (13)>
|
|
||||||
* LF = <US-ASCII LF, linefeed (10)>
|
|
||||||
* SP = <US-ASCII SP, space (32)>
|
|
||||||
* HT = <US-ASCII HT, horizontal-tab (9)>
|
|
||||||
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
||||||
* OCTET = <any 8-bit sequence of data>
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex
|
|
||||||
var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/
|
|
||||||
var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp for various RFC 5987 grammar
|
|
||||||
*
|
|
||||||
* ext-value = charset "'" [ language ] "'" value-chars
|
|
||||||
* charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
|
||||||
* mime-charset = 1*mime-charsetc
|
|
||||||
* mime-charsetc = ALPHA / DIGIT
|
|
||||||
* / "!" / "#" / "$" / "%" / "&"
|
|
||||||
* / "+" / "-" / "^" / "_" / "`"
|
|
||||||
* / "{" / "}" / "~"
|
|
||||||
* language = ( 2*3ALPHA [ extlang ] )
|
|
||||||
* / 4ALPHA
|
|
||||||
* / 5*8ALPHA
|
|
||||||
* extlang = *3( "-" 3ALPHA )
|
|
||||||
* value-chars = *( pct-encoded / attr-char )
|
|
||||||
* pct-encoded = "%" HEXDIG HEXDIG
|
|
||||||
* attr-char = ALPHA / DIGIT
|
|
||||||
* / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
|
||||||
* / "^" / "_" / "`" / "|" / "~"
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegExp for various RFC 6266 grammar
|
|
||||||
*
|
|
||||||
* disposition-type = "inline" | "attachment" | disp-ext-type
|
|
||||||
* disp-ext-type = token
|
|
||||||
* disposition-parm = filename-parm | disp-ext-parm
|
|
||||||
* filename-parm = "filename" "=" value
|
|
||||||
* | "filename*" "=" ext-value
|
|
||||||
* disp-ext-parm = token "=" value
|
|
||||||
* | ext-token "=" ext-value
|
|
||||||
* ext-token = <the characters in token, followed by "*">
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an attachment Content-Disposition header.
|
|
||||||
*
|
|
||||||
* @param {string} [filename]
|
|
||||||
* @param {object} [options]
|
|
||||||
* @param {string} [options.type=attachment]
|
|
||||||
* @param {string|boolean} [options.fallback=true]
|
|
||||||
* @return {string}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function contentDisposition (filename, options) {
|
|
||||||
var opts = options || {}
|
|
||||||
|
|
||||||
// get type
|
|
||||||
var type = opts.type || 'attachment'
|
|
||||||
|
|
||||||
// get parameters
|
|
||||||
var params = createparams(filename, opts.fallback)
|
|
||||||
|
|
||||||
// format into string
|
|
||||||
return format(new ContentDisposition(type, params))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create parameters object from filename and fallback.
|
|
||||||
*
|
|
||||||
* @param {string} [filename]
|
|
||||||
* @param {string|boolean} [fallback=true]
|
|
||||||
* @return {object}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function createparams (filename, fallback) {
|
|
||||||
if (filename === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var params = {}
|
|
||||||
|
|
||||||
if (typeof filename !== 'string') {
|
|
||||||
throw new TypeError('filename must be a string')
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback defaults to true
|
|
||||||
if (fallback === undefined) {
|
|
||||||
fallback = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {
|
|
||||||
throw new TypeError('fallback must be a string or boolean')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {
|
|
||||||
throw new TypeError('fallback must be ISO-8859-1 string')
|
|
||||||
}
|
|
||||||
|
|
||||||
// restrict to file base name
|
|
||||||
var name = basename(filename)
|
|
||||||
|
|
||||||
// determine if name is suitable for quoted string
|
|
||||||
var isQuotedString = TEXT_REGEXP.test(name)
|
|
||||||
|
|
||||||
// generate fallback name
|
|
||||||
var fallbackName = typeof fallback !== 'string'
|
|
||||||
? fallback && getlatin1(name)
|
|
||||||
: basename(fallback)
|
|
||||||
var hasFallback = typeof fallbackName === 'string' && fallbackName !== name
|
|
||||||
|
|
||||||
// set extended filename parameter
|
|
||||||
if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) {
|
|
||||||
params['filename*'] = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// set filename parameter
|
|
||||||
if (isQuotedString || hasFallback) {
|
|
||||||
params.filename = hasFallback
|
|
||||||
? fallbackName
|
|
||||||
: name
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format object to Content-Disposition header.
|
|
||||||
*
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {string} obj.type
|
|
||||||
* @param {object} [obj.parameters]
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function format (obj) {
|
|
||||||
var parameters = obj.parameters
|
|
||||||
var type = obj.type
|
|
||||||
|
|
||||||
if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {
|
|
||||||
throw new TypeError('invalid type')
|
|
||||||
}
|
|
||||||
|
|
||||||
// start with normalized type
|
|
||||||
var string = String(type).toLowerCase()
|
|
||||||
|
|
||||||
// append parameters
|
|
||||||
if (parameters && typeof parameters === 'object') {
|
|
||||||
var param
|
|
||||||
var params = Object.keys(parameters).sort()
|
|
||||||
|
|
||||||
for (var i = 0; i < params.length; i++) {
|
|
||||||
param = params[i]
|
|
||||||
|
|
||||||
var val = param.substr(-1) === '*'
|
|
||||||
? ustring(parameters[param])
|
|
||||||
: qstring(parameters[param])
|
|
||||||
|
|
||||||
string += '; ' + param + '=' + val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a RFC 5987 field value (gracefully).
|
|
||||||
*
|
|
||||||
* @param {string} str
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function decodefield (str) {
|
|
||||||
var match = EXT_VALUE_REGEXP.exec(str)
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new TypeError('invalid extended field value')
|
|
||||||
}
|
|
||||||
|
|
||||||
var charset = match[1].toLowerCase()
|
|
||||||
var encoded = match[2]
|
|
||||||
var value
|
|
||||||
|
|
||||||
// to binary string
|
|
||||||
var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode)
|
|
||||||
|
|
||||||
switch (charset) {
|
|
||||||
case 'iso-8859-1':
|
|
||||||
value = getlatin1(binary)
|
|
||||||
break
|
|
||||||
case 'utf-8':
|
|
||||||
value = Buffer.from(binary, 'binary').toString('utf8')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new TypeError('unsupported charset in extended field')
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get ISO-8859-1 version of string.
|
|
||||||
*
|
|
||||||
* @param {string} val
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getlatin1 (val) {
|
|
||||||
// simple Unicode -> ISO-8859-1 transformation
|
|
||||||
return String(val).replace(NON_LATIN1_REGEXP, '?')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse Content-Disposition header string.
|
|
||||||
*
|
|
||||||
* @param {string} string
|
|
||||||
* @return {object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parse (string) {
|
|
||||||
if (!string || typeof string !== 'string') {
|
|
||||||
throw new TypeError('argument string is required')
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = DISPOSITION_TYPE_REGEXP.exec(string)
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new TypeError('invalid type format')
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalize type
|
|
||||||
var index = match[0].length
|
|
||||||
var type = match[1].toLowerCase()
|
|
||||||
|
|
||||||
var key
|
|
||||||
var names = []
|
|
||||||
var params = {}
|
|
||||||
var value
|
|
||||||
|
|
||||||
// calculate index to start at
|
|
||||||
index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';'
|
|
||||||
? index - 1
|
|
||||||
: index
|
|
||||||
|
|
||||||
// match parameters
|
|
||||||
while ((match = PARAM_REGEXP.exec(string))) {
|
|
||||||
if (match.index !== index) {
|
|
||||||
throw new TypeError('invalid parameter format')
|
|
||||||
}
|
|
||||||
|
|
||||||
index += match[0].length
|
|
||||||
key = match[1].toLowerCase()
|
|
||||||
value = match[2]
|
|
||||||
|
|
||||||
if (names.indexOf(key) !== -1) {
|
|
||||||
throw new TypeError('invalid duplicate parameter')
|
|
||||||
}
|
|
||||||
|
|
||||||
names.push(key)
|
|
||||||
|
|
||||||
if (key.indexOf('*') + 1 === key.length) {
|
|
||||||
// decode extended value
|
|
||||||
key = key.slice(0, -1)
|
|
||||||
value = decodefield(value)
|
|
||||||
|
|
||||||
// overwrite existing value
|
|
||||||
params[key] = value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof params[key] === 'string') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value[0] === '"') {
|
|
||||||
// remove quotes and escapes
|
|
||||||
value = value
|
|
||||||
.substr(1, value.length - 2)
|
|
||||||
.replace(QESC_REGEXP, '$1')
|
|
||||||
}
|
|
||||||
|
|
||||||
params[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index !== -1 && index !== string.length) {
|
|
||||||
throw new TypeError('invalid parameter format')
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ContentDisposition(type, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percent decode a single character.
|
|
||||||
*
|
|
||||||
* @param {string} str
|
|
||||||
* @param {string} hex
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function pdecode (str, hex) {
|
|
||||||
return String.fromCharCode(parseInt(hex, 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Percent encode a single character.
|
|
||||||
*
|
|
||||||
* @param {string} char
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function pencode (char) {
|
|
||||||
return '%' + String(char)
|
|
||||||
.charCodeAt(0)
|
|
||||||
.toString(16)
|
|
||||||
.toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quote a string for HTTP.
|
|
||||||
*
|
|
||||||
* @param {string} val
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function qstring (val) {
|
|
||||||
var str = String(val)
|
|
||||||
|
|
||||||
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode a Unicode string for HTTP (RFC 5987).
|
|
||||||
*
|
|
||||||
* @param {string} val
|
|
||||||
* @return {string}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function ustring (val) {
|
|
||||||
var str = String(val)
|
|
||||||
|
|
||||||
// percent encode as UTF-8
|
|
||||||
var encoded = encodeURIComponent(str)
|
|
||||||
.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)
|
|
||||||
|
|
||||||
return 'UTF-8\'\'' + encoded
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for parsed Content-Disposition header for v8 optimization
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {string} type
|
|
||||||
* @param {object} parameters
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
|
|
||||||
function ContentDisposition (type, parameters) {
|
|
||||||
this.type = type
|
|
||||||
this.parameters = parameters
|
|
||||||
}
|
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "content-disposition",
|
|
||||||
"description": "Create and parse Content-Disposition header",
|
|
||||||
"version": "0.5.4",
|
|
||||||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"content-disposition",
|
|
||||||
"http",
|
|
||||||
"rfc6266",
|
|
||||||
"res"
|
|
||||||
],
|
|
||||||
"repository": "jshttp/content-disposition",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.2.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"deep-equal": "1.0.1",
|
|
||||||
"eslint": "7.32.0",
|
|
||||||
"eslint-config-standard": "13.0.1",
|
|
||||||
"eslint-plugin-import": "2.25.3",
|
|
||||||
"eslint-plugin-markdown": "2.2.1",
|
|
||||||
"eslint-plugin-node": "11.1.0",
|
|
||||||
"eslint-plugin-promise": "5.2.0",
|
|
||||||
"eslint-plugin-standard": "4.1.0",
|
|
||||||
"istanbul": "0.4.5",
|
|
||||||
"mocha": "9.1.3"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"LICENSE",
|
|
||||||
"HISTORY.md",
|
|
||||||
"README.md",
|
|
||||||
"index.js"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "mocha --reporter spec --bail --check-leaks test/",
|
|
||||||
"test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/",
|
|
||||||
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
1.0.5 / 2023-01-29
|
|
||||||
==================
|
|
||||||
|
|
||||||
* perf: skip value escaping when unnecessary
|
|
||||||
|
|
||||||
1.0.4 / 2017-09-11
|
|
||||||
==================
|
|
||||||
|
|
||||||
* perf: skip parameter parsing when no parameters
|
|
||||||
|
|
||||||
1.0.3 / 2017-09-10
|
|
||||||
==================
|
|
||||||
|
|
||||||
* perf: remove argument reassignment
|
|
||||||
|
|
||||||
1.0.2 / 2016-05-09
|
|
||||||
==================
|
|
||||||
|
|
||||||
* perf: enable strict mode
|
|
||||||
|
|
||||||
1.0.1 / 2015-02-13
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Improve missing `Content-Type` header error message
|
|
||||||
|
|
||||||
1.0.0 / 2015-02-01
|
|
||||||
==================
|
|
||||||
|
|
||||||
* Initial implementation, derived from `media-typer@0.3.0`
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2015 Douglas Christopher Wilson
|
|
||||||
|
|
||||||
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.
|
|
||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
# content-type
|
|
||||||
|
|
||||||
[![NPM Version][npm-version-image]][npm-url]
|
|
||||||
[![NPM Downloads][npm-downloads-image]][npm-url]
|
|
||||||
[![Node.js Version][node-image]][node-url]
|
|
||||||
[![Build Status][ci-image]][ci-url]
|
|
||||||
[![Coverage Status][coveralls-image]][coveralls-url]
|
|
||||||
|
|
||||||
Create and parse HTTP Content-Type header according to RFC 7231
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ npm install content-type
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```js
|
|
||||||
var contentType = require('content-type')
|
|
||||||
```
|
|
||||||
|
|
||||||
### contentType.parse(string)
|
|
||||||
|
|
||||||
```js
|
|
||||||
var obj = contentType.parse('image/svg+xml; charset=utf-8')
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse a `Content-Type` header. This will return an object with the following
|
|
||||||
properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`):
|
|
||||||
|
|
||||||
- `type`: The media type (the type and subtype, always lower case).
|
|
||||||
Example: `'image/svg+xml'`
|
|
||||||
|
|
||||||
- `parameters`: An object of the parameters in the media type (name of parameter
|
|
||||||
always lower case). Example: `{charset: 'utf-8'}`
|
|
||||||
|
|
||||||
Throws a `TypeError` if the string is missing or invalid.
|
|
||||||
|
|
||||||
### contentType.parse(req)
|
|
||||||
|
|
||||||
```js
|
|
||||||
var obj = contentType.parse(req)
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse the `Content-Type` header from the given `req`. Short-cut for
|
|
||||||
`contentType.parse(req.headers['content-type'])`.
|
|
||||||
|
|
||||||
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
|
||||||
|
|
||||||
### contentType.parse(res)
|
|
||||||
|
|
||||||
```js
|
|
||||||
var obj = contentType.parse(res)
|
|
||||||
```
|
|
||||||
|
|
||||||
Parse the `Content-Type` header set on the given `res`. Short-cut for
|
|
||||||
`contentType.parse(res.getHeader('content-type'))`.
|
|
||||||
|
|
||||||
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
|
||||||
|
|
||||||
### contentType.format(obj)
|
|
||||||
|
|
||||||
```js
|
|
||||||
var str = contentType.format({
|
|
||||||
type: 'image/svg+xml',
|
|
||||||
parameters: { charset: 'utf-8' }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Format an object into a `Content-Type` header. This will return a string of the
|
|
||||||
content type for the given object with the following properties (examples are
|
|
||||||
shown that produce the string `'image/svg+xml; charset=utf-8'`):
|
|
||||||
|
|
||||||
- `type`: The media type (will be lower-cased). Example: `'image/svg+xml'`
|
|
||||||
|
|
||||||
- `parameters`: An object of the parameters in the media type (name of the
|
|
||||||
parameter will be lower-cased). Example: `{charset: 'utf-8'}`
|
|
||||||
|
|
||||||
Throws a `TypeError` if the object contains an invalid type or parameter names.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE)
|
|
||||||
|
|
||||||
[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci
|
|
||||||
[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml
|
|
||||||
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master
|
|
||||||
[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master
|
|
||||||
[node-image]: https://badgen.net/npm/node/content-type
|
|
||||||
[node-url]: https://nodejs.org/en/download
|
|
||||||
[npm-downloads-image]: https://badgen.net/npm/dm/content-type
|
|
||||||
[npm-url]: https://npmjs.org/package/content-type
|
|
||||||
[npm-version-image]: https://badgen.net/npm/v/content-type
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user