@e22m4u/js-http-static-router 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.mocharc.json +2 -1
  2. package/README.md +265 -17
  3. package/build-cjs.js +1 -1
  4. package/dist/cjs/index.cjs +111 -73
  5. package/example/server.js +23 -13
  6. package/mocha.setup.js +4 -0
  7. package/package.json +5 -3
  8. package/src/http-static-router.d.ts +7 -2
  9. package/src/http-static-router.js +70 -58
  10. package/src/http-static-router.spec.js +899 -0
  11. package/src/static-route.js +18 -8
  12. package/src/types.d.ts +7 -0
  13. package/src/utils/create-cookie-string.d.ts +6 -0
  14. package/src/utils/create-cookie-string.js +28 -0
  15. package/src/utils/create-cookie-string.spec.js +32 -0
  16. package/src/utils/create-error.d.ts +14 -0
  17. package/src/utils/create-error.js +29 -0
  18. package/src/utils/create-error.spec.js +42 -0
  19. package/src/utils/create-request-mock.d.ts +35 -0
  20. package/src/utils/create-request-mock.js +483 -0
  21. package/src/utils/create-request-mock.spec.js +646 -0
  22. package/src/utils/create-response-mock.d.ts +18 -0
  23. package/src/utils/create-response-mock.js +131 -0
  24. package/src/utils/create-response-mock.spec.js +150 -0
  25. package/src/utils/fetch-request-body.d.ts +26 -0
  26. package/src/utils/fetch-request-body.js +148 -0
  27. package/src/utils/fetch-request-body.spec.js +209 -0
  28. package/src/utils/get-pathname-from-url.d.ts +1 -1
  29. package/src/utils/{get-request-pathname.spec.js → get-pathname-from-url.spec.js} +1 -1
  30. package/src/utils/index.d.ts +8 -0
  31. package/src/utils/index.js +8 -0
  32. package/src/utils/is-readable-stream.d.ts +9 -0
  33. package/src/utils/is-readable-stream.js +12 -0
  34. package/src/utils/is-readable-stream.spec.js +23 -0
  35. package/src/utils/parse-content-type.d.ts +15 -0
  36. package/src/utils/parse-content-type.js +34 -0
  37. package/src/utils/parse-content-type.spec.js +79 -0
  38. package/src/utils/parse-cookie-string.d.ts +19 -0
  39. package/src/utils/parse-cookie-string.js +36 -0
  40. package/src/utils/parse-cookie-string.spec.js +45 -0
  41. package/{example/static → static}/index.html +2 -2
  42. /package/{example/static/nested/file.txt → static/assets/nested/heart.txt} +0 -0
  43. /package/{example/static/file.txt → static/assets/rabbit.txt} +0 -0
package/.mocharc.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "extension": ["js"],
3
- "spec": "src/**/*.spec.js"
3
+ "spec": "src/**/*.spec.js",
4
+ "require": "./mocha.setup.js"
4
5
  }
package/README.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  HTTP-маршрутизатор статичных ресурсов для Node.js.
4
4
 
5
+ Модуль удобен для встраивания документации или административных панелей
6
+ непосредственно в серверное приложение, позволяя избежать развертывания
7
+ дополнительной инфраструктуры.
8
+
9
+ - Интеграция в существующий *http*-сервер.
10
+ - Управление доступом к файловой системе через маршруты.
11
+ - Использование потоков для экономии оперативной памяти.
12
+
13
+ ## Содержание
14
+
15
+ - [Установка](#установка)
16
+ - [Базовый пример](#базовый-пример)
17
+ - [Маршрутизатор](#маршрутизатор)
18
+ - [Создание экземпляра](#создание-экземпляра)
19
+ - [Регистрация маршрута](#регистрация-маршрута)
20
+ - [Обработка запросов](#обработка-запросов)
21
+ - [Тесты](#тесты)
22
+ - [Лицензия](#лицензия)
23
+
5
24
  ## Установка
6
25
 
7
26
  ```bash
@@ -22,9 +41,24 @@ import {HttpStaticRouter} from '@e22m4u/js-http-static-router';
22
41
  const {HttpStaticRouter} = require('@e22m4u/js-http-static-router');
23
42
  ```
24
43
 
25
- ## Использование
44
+ ## Базовый пример
45
+
46
+ Пример предполагает следующую структуру проекта.
47
+
48
+ ```txt
49
+ /static
50
+ ├── index.html
51
+ └── /assets
52
+ └── rabbit.txt
53
+ /src
54
+ └── server.js
55
+ ```
56
+
57
+ Создание маршрутизатора, определение маршрутов и запуск сервера.
26
58
 
27
59
  ```js
60
+ // src/server.js
61
+ import path from 'path';
28
62
  import http from 'http';
29
63
  import {HttpStaticRouter} from '@e22m4u/js-http-static-router';
30
64
 
@@ -33,32 +67,33 @@ const staticRouter = new HttpStaticRouter({
33
67
  // при использовании опции "baseDir", относительные пути
34
68
  // в регистрируемых маршрутах будут разрешаться относительно
35
69
  // указанного адреса файловой системы
36
- baseDir: import.meta.dirname,
37
- // в данном случае "baseDir" указывает
38
- // на путь к директории текущего модуля
70
+ baseDir: path.join(import.meta.dirname, '../static'),
39
71
  });
40
- // доступ к import.meta.dirname возможен
41
- // только для ESM начиная с Node.js 20.11.0
72
+ // доступ к import.meta.dirname (директория текущего модуля)
73
+ // возможен только для ESM начиная с Node.js 20.11.0
42
74
 
43
- // экспозиция содержимого директории "/static"
44
- // для доступа по адресу "/assets/{file_name}"
75
+ // объявление файла "index.html"
76
+ // в качестве индексной страницы
45
77
  staticRouter.defineRoute({
46
- remotePath: '/assets', // путь маршрута
47
- resourcePath: '../static', // файловый путь
78
+ remotePath: '/',
79
+ resourcePath: './index.html',
48
80
  });
49
81
 
50
- // объявление файла "./index.html"
51
- // для доступа по адресу "/home"
82
+ // экспозиция содержимого директории "assets"
83
+ // для доступа относительно корня
84
+ // пример: http://localhost:3000/rabbit.txt
52
85
  staticRouter.defineRoute({
53
- remotePath: '/home',
54
- resourcePath: './static/index.html',
86
+ remotePath: '/',
87
+ resourcePath: './assets',
55
88
  });
56
89
 
57
90
  // создание HTTP сервера и определение
58
- // функции для обработки запросов
91
+ // слушателя для обработки запросов
59
92
  const server = new http.Server();
60
93
  server.on('request', async (req, res) => {
61
94
  const fileSent = await staticRouter.handleRequest(req, res);
95
+ // если файл не был отправлен,
96
+ // то возвращается 404 Not Found
62
97
  if (!fileSent) {
63
98
  res.writeHead(404, {'Content-Type': 'text/plain'});
64
99
  res.write('404 Not Found');
@@ -66,14 +101,227 @@ server.on('request', async (req, res) => {
66
101
  }
67
102
  });
68
103
 
104
+ // запуск сервера
69
105
  server.listen(3000, () => {
70
106
  console.log('Server is running on http://localhost:3000');
71
107
  console.log('Try to open:');
72
- console.log('http://localhost:3000/home');
73
- console.log('http://localhost:3000/assets/file.txt');
108
+ console.log('http://localhost:3000');
109
+ console.log('http://localhost:3000/rabbit.txt');
74
110
  });
75
111
  ```
76
112
 
113
+ Запуск Node.js процесса.
114
+
115
+ ```bash
116
+ node ./src/server.js
117
+ ```
118
+
119
+ ## Маршрутизатор
120
+
121
+ Класс `HttpStaticRouter` является основным компонентом модуля. Он отвечает
122
+ за хранение определений маршрутов, сопоставление входящих HTTP-запросов
123
+ с ресурсами файловой системы и потоковую передачу данных клиенту.
124
+
125
+ ### Создание экземпляра
126
+
127
+ Конструктор маршрутизатора принимает объект настроек, который позволяет
128
+ задать базовые параметры работы.
129
+
130
+ Сигнатура:
131
+
132
+ ```ts
133
+ type HttpStaticRouterOptions = {
134
+ baseDir?: string;
135
+ };
136
+
137
+ constructor(options?: HttpStaticRouterOptions);
138
+ ```
139
+
140
+ **Параметры**
141
+
142
+ - `baseDir: string` - абсолютный путь к базовой директории.
143
+ Если параметр указан, все относительные пути при регистрации маршрутов
144
+ будут разрешаться относительно этой директории.
145
+
146
+ **Пример**
147
+
148
+ Создание экземпляра с указанием базовой директории.
149
+
150
+ ```js
151
+ import path from 'path';
152
+ import {HttpStaticRouter} from '@e22m4u/js-http-static-router';
153
+
154
+ // создание маршрутизатора с указанием абсолютного пути
155
+ // к директории, в которой хранятся статические файлы
156
+ const staticRouter = new HttpStaticRouter({
157
+ baseDir: path.join(import.meta.dirname, '../public'),
158
+ });
159
+ ```
160
+
161
+ *i. Доступ к переменной `import.meta.dirname` (директория текущего модуля)
162
+ возможен только при использовании *ESM* стандарта, начиная с версии
163
+ Node.js 20.11.0. Для более ранних версий или *CommonJS*
164
+ используется `__dirname`.*
165
+
166
+ ### Регистрация маршрута
167
+
168
+ Метод `defineRoute` добавляет новое правило маршрутизации, связывая виртуальный
169
+ путь (*URL*) с реальным файлом или директорией на сервере. В момент вызова
170
+ данного метода маршрутизатор проверяет физическое существование указанного
171
+ ресурса в файловой системе.
172
+
173
+ Сигнатура:
174
+
175
+ ```ts
176
+ type StaticRouteDefinition = {
177
+ remotePath: string;
178
+ resourcePath: string;
179
+ };
180
+
181
+ defineRoute(routeDef: StaticRouteDefinition): StaticRoute;
182
+ ```
183
+
184
+ **Параметры**
185
+
186
+ - `remotePath: string`
187
+ Префикс URL-адреса, с которого должен начинаться входящий запрос
188
+ (обязательно должен начинаться со слеша `/`).
189
+
190
+ - `resourcePath: string`
191
+ Путь к существующему файлу или директории в файловой системе.
192
+
193
+ *i. Если при [создании экземпляра](#создание-экземпляра) маршрутизатора не была
194
+ указана опция `baseDir`, значение параметра `resourcePath` обязано быть
195
+ абсолютным путем.*
196
+
197
+ **Примеры**
198
+
199
+ Регистрация конкретного файла. Запрос по указанному пути в параметре
200
+ `remotePath` вернет содержимое связанного файла. Маршрутизатор учитывает
201
+ наличие/отсутствие завершающего слеша в конце пути.
202
+
203
+ ```js
204
+ // предполагается, что маршрутизатор был создан
205
+ // с указанием базовой директории (опция "baseDir")
206
+ staticRouter.defineRoute({
207
+ remotePath: '/about',
208
+ resourcePath: './pages/about.html',
209
+ });
210
+
211
+ // GET /about
212
+ // -> вернет содержимое ./pages/about.html
213
+ ```
214
+
215
+ Экспозиция содержимого директории. Если маршрут указывает на директорию,
216
+ дополнительная часть URL-адреса будет автоматически добавлена к пути
217
+ файловой системы.
218
+
219
+ ```js
220
+ // открытие доступа ко всем файлам
221
+ // внутри директории "assets"
222
+ staticRouter.defineRoute({
223
+ remotePath: '/public',
224
+ resourcePath: './assets',
225
+ });
226
+
227
+ // GET /public/images/logo.png
228
+ // -> вернет содержимое ./assets/images/logo.png
229
+ ```
230
+
231
+ Регистрация маршрута с использованием абсолютного пути.
232
+
233
+ ```js
234
+ import path from 'path';
235
+ import {HttpStaticRouter} from '@e22m4u/js-http-static-router';
236
+
237
+ // создание экземпляра без параметров
238
+ const router = new HttpStaticRouter();
239
+
240
+ // так как опция "baseDir" не задана, маршрутизатор требует
241
+ // передачи абсолютного пути для свойства "resourcePath",
242
+ // иначе будет выброшена ошибка InvalidArgumentError
243
+ router.defineRoute({
244
+ remotePath: '/robots.txt',
245
+ resourcePath: path.join(import.meta.dirname, '../public/robots.txt'),
246
+ });
247
+ ```
248
+
249
+ *i. Маршрутизатор выполняет проверку безопасности. Если при запросе клиент
250
+ попытается выйти за пределы каталога с помощью относительных переходов
251
+ (например, `GET /public/../../etc/passwd`), обработчик прервет поиск
252
+ и файл не будет отправлен.*
253
+
254
+ ### Обработка запросов
255
+
256
+ Метод `handleRequest` выполняет сопоставление входящего HTTP-запроса
257
+ с зарегистрированными маршрутами и выполняет отправку найденного файла
258
+ клиенту. При чтении файла маршрутизатор использует потоки, что позволяет
259
+ безопасно отдавать файлы большого размера без переполнения оперативной
260
+ памяти сервера.
261
+
262
+ Маршрутизатор автоматически определяет *MIME-тип* ресурса на основе его
263
+ расширения, устанавливает необходимые заголовки, а также корректно обрабатывает
264
+ обрыв соединения со стороны клиента, своевременно закрывая файловый поток
265
+ для предотвращения утечек памяти.
266
+
267
+ Сигнатура:
268
+
269
+ ```ts
270
+ handleRequest(
271
+ request: IncomingMessage,
272
+ response: ServerResponse,
273
+ ): Promise<boolean>;
274
+ ```
275
+
276
+ **Параметры**
277
+
278
+ - `request: IncomingMessage` - нативный поток входящего запроса Node.js;
279
+ - `response: ServerResponse` - нативный поток исходящего ответа Node.js;
280
+
281
+ **Возвращаемое значение**
282
+
283
+ Метод возвращает `Promise`, который разрешается логическим значением `false`,
284
+ если маршрут не совпал, целевой файл физически отсутствует или метод запроса
285
+ не поддерживается. Во всех остальных случаях значением будет `true`, что
286
+ позволяет определить, взял ли на себя ответственность за обработку запроса
287
+ маршрутизатор.
288
+
289
+ **Пример**
290
+
291
+ Интеграция метода в обработчик событий нативного HTTP-сервера.
292
+ Если `handleRequest` возвращает `false`, сервер берет на себя
293
+ ответственность за отправку ответа с ошибкой клиенту.
294
+
295
+ ```js
296
+ import http from 'http';
297
+ import {HttpStaticRouter} from '@e22m4u/js-http-static-router';
298
+
299
+ // создание экземпляра маршрутизатора
300
+ const staticRouter = new HttpStaticRouter();
301
+ // staticRouter.defineRoute(...)
302
+
303
+ const server = new http.Server();
304
+
305
+ server.on('request', async (req, res) => {
306
+ // передача объектов запроса и ответа маршрутизатору
307
+ const fileSent = await staticRouter.handleRequest(req, res);
308
+
309
+ // если маршрутизатор вернул false, файл не был отправлен
310
+ if (!fileSent) {
311
+ // ручная отправка статуса 404 Not Found
312
+ res.writeHead(404, {'Content-Type': 'text/plain; charset=utf-8'});
313
+ res.write('404 Not Found');
314
+ res.end();
315
+ }
316
+ });
317
+ ```
318
+
319
+ Маршрутизатор обрабатывает исключительно запросы с методами `GET` и `HEAD`.
320
+ При получении запроса с любым другим методом, обработка прерывается и метод
321
+ возвращает `false`. В случае `HEAD` запроса маршрутизатор корректно вычисляет
322
+ размер файла и отправляет соответствующие заголовки, пропуская отправку
323
+ тела ответа.
324
+
77
325
  ## Тесты
78
326
 
79
327
  ```bash
package/build-cjs.js CHANGED
@@ -6,7 +6,7 @@ await esbuild.build({
6
6
  outfile: 'dist/cjs/index.cjs',
7
7
  format: 'cjs',
8
8
  platform: 'node',
9
- target: ['node12'],
9
+ target: ['node18'],
10
10
  bundle: true,
11
11
  keepNames: true,
12
12
  external: [