@remix-run/static-middleware 0.3.0 → 0.4.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.
@@ -2,7 +2,7 @@ import * as path from 'node:path';
2
2
  import * as fsp from 'node:fs/promises';
3
3
  import { html } from '@remix-run/html-template';
4
4
  import { createHtmlResponse } from '@remix-run/response/html';
5
- import { lookup } from 'mrmime';
5
+ import { detectMimeType } from '@remix-run/mime';
6
6
  export async function generateDirectoryListing(dirPath, pathname) {
7
7
  let entries = [];
8
8
  try {
@@ -19,7 +19,7 @@ export async function generateDirectoryListing(dirPath, pathname) {
19
19
  try {
20
20
  let stats = await fsp.stat(fullPath);
21
21
  size = stats.size;
22
- let mimeType = lookup(dirent.name);
22
+ let mimeType = detectMimeType(dirent.name);
23
23
  type = mimeType || 'application/octet-stream';
24
24
  }
25
25
  catch {
@@ -1,51 +1,74 @@
1
1
  import type { Middleware } from '@remix-run/fetch-router';
2
2
  import { type FileResponseOptions } from '@remix-run/response/file';
3
- export interface StaticFilesOptions extends FileResponseOptions {
3
+ /**
4
+ * Function that determines if HTTP Range requests should be supported for a given file.
5
+ *
6
+ * @param file - The File object being served
7
+ * @returns true if range requests should be supported
8
+ */
9
+ export type AcceptRangesFunction = (file: File) => boolean;
10
+ /**
11
+ * Options for the `staticFiles` middleware.
12
+ */
13
+ export interface StaticFilesOptions extends Omit<FileResponseOptions, 'acceptRanges'> {
4
14
  /**
5
15
  * Filter function to determine which files should be served.
6
16
  *
7
17
  * @param path The relative path being requested
8
- * @returns Whether to serve the file
18
+ * @return Whether to serve the file
9
19
  */
10
20
  filter?: (path: string) => boolean;
21
+ /**
22
+ * Whether to support HTTP Range requests for partial content.
23
+ *
24
+ * Can be a boolean or a function that receives the file.
25
+ * When enabled, includes Accept-Ranges header and handles Range requests
26
+ * with 206 Partial Content responses.
27
+ *
28
+ * Defaults to enabling ranges only for non-compressible MIME types,
29
+ * as defined by `isCompressibleMimeType()` from `@remix-run/mime`.
30
+ *
31
+ * Note: Range requests and compression are mutually exclusive. When
32
+ * `Accept-Ranges: bytes` is present in the response headers, the compression
33
+ * middleware will not compress the response. This is why the default behavior
34
+ * enables ranges only for non-compressible types.
35
+ *
36
+ * @example
37
+ * // Force range request support for all files
38
+ * acceptRanges: true
39
+ *
40
+ * @example
41
+ * // Enable ranges for videos only
42
+ * acceptRanges: (file) => file.type.startsWith('video/')
43
+ */
44
+ acceptRanges?: boolean | AcceptRangesFunction;
11
45
  /**
12
46
  * Files to try and serve as the index file when the request path targets a directory.
13
47
  *
14
- * - `true` (default): Use default index files `['index.html', 'index.htm']`
48
+ * - `true`: Use default index files `['index.html', 'index.htm']`
15
49
  * - `false`: Disable index file serving
16
50
  * - `string[]`: Custom list of index files to try in order
51
+ *
52
+ * @default true
17
53
  */
18
54
  index?: boolean | string[];
19
55
  /**
20
56
  * Whether to return an HTML page listing the files in a directory when the request path
21
57
  * targets a directory. If both this and `index` are set, `index` takes precedence.
58
+ *
59
+ * @default false
22
60
  */
23
61
  listFiles?: boolean;
24
62
  }
25
63
  /**
26
64
  * Creates a middleware that serves static files from the filesystem.
27
65
  *
28
- * Uses the URL pathname to resolve files, removing the leading slash to make it
29
- * a relative path. The middleware always falls through to the handler if the file
30
- * is not found or an error occurs.
66
+ * Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
67
+ * The middleware always falls through to the handler if the file is not found or an error occurs.
31
68
  *
32
69
  * @param root The root directory to serve files from (absolute or relative to cwd)
33
- * @param options (optional) configuration for file responses
34
- *
35
- * @example
36
- * let router = createRouter({
37
- * middleware: [staticFiles('./public')],
38
- * })
39
- *
40
- * @example
41
- * // With cache control
42
- * let router = createRouter({
43
- * middleware: [
44
- * staticFiles('./public', {
45
- * cacheControl: 'public, max-age=3600',
46
- * }),
47
- * ],
48
- * })
70
+ * @param options Configuration for file responses
71
+ * @return The static files middleware
49
72
  */
50
73
  export declare function staticFiles(root: string, options?: StaticFilesOptions): Middleware;
51
74
  //# sourceMappingURL=static.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAInG,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAClC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAiEtF"}
1
+ {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAInG;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC;IACnF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAElC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAA;IAE7C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA8EtF"}
@@ -6,32 +6,17 @@ import { generateDirectoryListing } from "./directory-listing.js";
6
6
  /**
7
7
  * Creates a middleware that serves static files from the filesystem.
8
8
  *
9
- * Uses the URL pathname to resolve files, removing the leading slash to make it
10
- * a relative path. The middleware always falls through to the handler if the file
11
- * is not found or an error occurs.
9
+ * Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
10
+ * The middleware always falls through to the handler if the file is not found or an error occurs.
12
11
  *
13
12
  * @param root The root directory to serve files from (absolute or relative to cwd)
14
- * @param options (optional) configuration for file responses
15
- *
16
- * @example
17
- * let router = createRouter({
18
- * middleware: [staticFiles('./public')],
19
- * })
20
- *
21
- * @example
22
- * // With cache control
23
- * let router = createRouter({
24
- * middleware: [
25
- * staticFiles('./public', {
26
- * cacheControl: 'public, max-age=3600',
27
- * }),
28
- * ],
29
- * })
13
+ * @param options Configuration for file responses
14
+ * @return The static files middleware
30
15
  */
31
16
  export function staticFiles(root, options = {}) {
32
17
  // Ensure root is an absolute path
33
18
  root = path.resolve(root);
34
- let { filter, index: indexOption, listFiles, ...fileOptions } = options;
19
+ let { acceptRanges, filter, index: indexOption, listFiles, ...fileOptions } = options;
35
20
  // Normalize index option
36
21
  let index;
37
22
  if (indexOption === false) {
@@ -85,7 +70,17 @@ export function staticFiles(root, options = {}) {
85
70
  if (filePath) {
86
71
  let fileName = path.relative(root, filePath);
87
72
  let file = openFile(filePath, { name: fileName });
88
- return sendFile(file, context.request, fileOptions);
73
+ let finalFileOptions = { ...fileOptions };
74
+ // If acceptRanges is a function, evaluate it with the file
75
+ // Otherwise, pass it directly to sendFile
76
+ if (typeof acceptRanges === 'function') {
77
+ finalFileOptions.acceptRanges = acceptRanges(file);
78
+ }
79
+ else if (acceptRanges !== undefined) {
80
+ finalFileOptions.acceptRanges = acceptRanges;
81
+ }
82
+ return sendFile(file, context.request, finalFileOptions);
89
83
  }
84
+ return next();
90
85
  };
91
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/static-middleware",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Middleware for serving static files from the filesystem",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -29,13 +29,16 @@
29
29
  "@types/node": "^24.6.0",
30
30
  "typescript": "^5.9.3",
31
31
  "@remix-run/fetch-router": "0.12.0",
32
- "@remix-run/method-override-middleware": "0.1.0",
33
- "@remix-run/form-data-middleware": "0.1.0"
32
+ "@remix-run/form-data-middleware": "0.1.0",
33
+ "@remix-run/method-override-middleware": "0.1.1",
34
+ "@remix-run/response": "^0.2.0",
35
+ "@remix-run/mime": "^0.1.0"
34
36
  },
35
37
  "peerDependencies": {
36
38
  "@remix-run/fetch-router": "^0.12.0",
37
- "@remix-run/fs": "^0.1.0",
38
- "@remix-run/response": "^0.1.0",
39
+ "@remix-run/fs": "^0.2.0",
40
+ "@remix-run/mime": "^0.1.0",
41
+ "@remix-run/response": "^0.2.0",
39
42
  "@remix-run/html-template": "^0.3.0"
40
43
  },
41
44
  "keywords": [
@@ -45,9 +48,6 @@
45
48
  "static-files",
46
49
  "file-server"
47
50
  ],
48
- "dependencies": {
49
- "mrmime": "^2.0.0"
50
- },
51
51
  "scripts": {
52
52
  "build": "tsc -p tsconfig.build.json",
53
53
  "clean": "git clean -fdX",
@@ -2,7 +2,7 @@ import * as path from 'node:path'
2
2
  import * as fsp from 'node:fs/promises'
3
3
  import { html } from '@remix-run/html-template'
4
4
  import { createHtmlResponse } from '@remix-run/response/html'
5
- import { lookup } from 'mrmime'
5
+ import { detectMimeType } from '@remix-run/mime'
6
6
 
7
7
  interface DirectoryEntry {
8
8
  name: string
@@ -32,7 +32,7 @@ export async function generateDirectoryListing(
32
32
  try {
33
33
  let stats = await fsp.stat(fullPath)
34
34
  size = stats.size
35
- let mimeType = lookup(dirent.name)
35
+ let mimeType = detectMimeType(dirent.name)
36
36
  type = mimeType || 'application/octet-stream'
37
37
  } catch {
38
38
  // Unable to stat file, use defaults
package/src/lib/static.ts CHANGED
@@ -6,25 +6,66 @@ import { createFileResponse as sendFile, type FileResponseOptions } from '@remix
6
6
 
7
7
  import { generateDirectoryListing } from './directory-listing.ts'
8
8
 
9
- export interface StaticFilesOptions extends FileResponseOptions {
9
+ /**
10
+ * Function that determines if HTTP Range requests should be supported for a given file.
11
+ *
12
+ * @param file - The File object being served
13
+ * @returns true if range requests should be supported
14
+ */
15
+ export type AcceptRangesFunction = (file: File) => boolean
16
+
17
+ /**
18
+ * Options for the `staticFiles` middleware.
19
+ */
20
+ export interface StaticFilesOptions extends Omit<FileResponseOptions, 'acceptRanges'> {
10
21
  /**
11
22
  * Filter function to determine which files should be served.
12
23
  *
13
24
  * @param path The relative path being requested
14
- * @returns Whether to serve the file
25
+ * @return Whether to serve the file
15
26
  */
16
27
  filter?: (path: string) => boolean
28
+
29
+ /**
30
+ * Whether to support HTTP Range requests for partial content.
31
+ *
32
+ * Can be a boolean or a function that receives the file.
33
+ * When enabled, includes Accept-Ranges header and handles Range requests
34
+ * with 206 Partial Content responses.
35
+ *
36
+ * Defaults to enabling ranges only for non-compressible MIME types,
37
+ * as defined by `isCompressibleMimeType()` from `@remix-run/mime`.
38
+ *
39
+ * Note: Range requests and compression are mutually exclusive. When
40
+ * `Accept-Ranges: bytes` is present in the response headers, the compression
41
+ * middleware will not compress the response. This is why the default behavior
42
+ * enables ranges only for non-compressible types.
43
+ *
44
+ * @example
45
+ * // Force range request support for all files
46
+ * acceptRanges: true
47
+ *
48
+ * @example
49
+ * // Enable ranges for videos only
50
+ * acceptRanges: (file) => file.type.startsWith('video/')
51
+ */
52
+ acceptRanges?: boolean | AcceptRangesFunction
53
+
17
54
  /**
18
55
  * Files to try and serve as the index file when the request path targets a directory.
19
56
  *
20
- * - `true` (default): Use default index files `['index.html', 'index.htm']`
57
+ * - `true`: Use default index files `['index.html', 'index.htm']`
21
58
  * - `false`: Disable index file serving
22
59
  * - `string[]`: Custom list of index files to try in order
60
+ *
61
+ * @default true
23
62
  */
24
63
  index?: boolean | string[]
25
64
  /**
26
65
  * Whether to return an HTML page listing the files in a directory when the request path
27
66
  * targets a directory. If both this and `index` are set, `index` takes precedence.
67
+ *
68
+ * @default false
28
69
  */
29
70
  listFiles?: boolean
30
71
  }
@@ -32,33 +73,18 @@ export interface StaticFilesOptions extends FileResponseOptions {
32
73
  /**
33
74
  * Creates a middleware that serves static files from the filesystem.
34
75
  *
35
- * Uses the URL pathname to resolve files, removing the leading slash to make it
36
- * a relative path. The middleware always falls through to the handler if the file
37
- * is not found or an error occurs.
76
+ * Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
77
+ * The middleware always falls through to the handler if the file is not found or an error occurs.
38
78
  *
39
79
  * @param root The root directory to serve files from (absolute or relative to cwd)
40
- * @param options (optional) configuration for file responses
41
- *
42
- * @example
43
- * let router = createRouter({
44
- * middleware: [staticFiles('./public')],
45
- * })
46
- *
47
- * @example
48
- * // With cache control
49
- * let router = createRouter({
50
- * middleware: [
51
- * staticFiles('./public', {
52
- * cacheControl: 'public, max-age=3600',
53
- * }),
54
- * ],
55
- * })
80
+ * @param options Configuration for file responses
81
+ * @return The static files middleware
56
82
  */
57
83
  export function staticFiles(root: string, options: StaticFilesOptions = {}): Middleware {
58
84
  // Ensure root is an absolute path
59
85
  root = path.resolve(root)
60
86
 
61
- let { filter, index: indexOption, listFiles, ...fileOptions } = options
87
+ let { acceptRanges, filter, index: indexOption, listFiles, ...fileOptions } = options
62
88
 
63
89
  // Normalize index option
64
90
  let index: string[]
@@ -116,7 +142,20 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
116
142
  if (filePath) {
117
143
  let fileName = path.relative(root, filePath)
118
144
  let file = openFile(filePath, { name: fileName })
119
- return sendFile(file, context.request, fileOptions)
145
+
146
+ let finalFileOptions: FileResponseOptions = { ...fileOptions }
147
+
148
+ // If acceptRanges is a function, evaluate it with the file
149
+ // Otherwise, pass it directly to sendFile
150
+ if (typeof acceptRanges === 'function') {
151
+ finalFileOptions.acceptRanges = acceptRanges(file)
152
+ } else if (acceptRanges !== undefined) {
153
+ finalFileOptions.acceptRanges = acceptRanges
154
+ }
155
+
156
+ return sendFile(file, context.request, finalFileOptions)
120
157
  }
158
+
159
+ return next()
121
160
  }
122
161
  }