koa-files 2.3.7 → 3.0.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.
- package/README.md +86 -33
- package/cjs/Service.cjs +280 -0
- package/cjs/index.cjs +20 -14
- package/cjs/utils/fs.cjs +29 -0
- package/cjs/utils/hash.cjs +46 -0
- package/cjs/utils/http.cjs +227 -0
- package/cjs/utils/path.cjs +46 -0
- package/esm/Service.js +271 -0
- package/esm/index.js +14 -14
- package/esm/utils/fs.js +27 -0
- package/esm/utils/hash.js +44 -0
- package/esm/utils/http.js +216 -0
- package/esm/utils/path.js +42 -0
- package/package.json +5 -5
- package/types/Service.d.ts +81 -0
- package/types/index.d.ts +5 -5
- package/types/utils/fs.d.ts +25 -0
- package/types/utils/hash.d.ts +9 -0
- package/types/utils/http.d.ts +39 -0
- package/types/utils/path.d.ts +22 -0
- package/cjs/Send.cjs +0 -376
- package/cjs/utils.cjs +0 -153
- package/esm/Send.js +0 -367
- package/esm/utils.js +0 -139
- package/types/Send.d.ts +0 -86
- package/types/utils.d.ts +0 -66
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @package koa-files
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @version 3.0.0
|
|
5
|
+
* @author nuintun <nuintun@qq.com>
|
|
6
|
+
* @description A static files serving middleware for koa.
|
|
7
|
+
* @see https://github.com/nuintun/koa-files#readme
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { generate } from './hash.js';
|
|
11
|
+
import parseRange from 'range-parser';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @module http
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @function isETag
|
|
18
|
+
* @description Check if etag is valid.
|
|
19
|
+
* @param value The value to check.
|
|
20
|
+
*/
|
|
21
|
+
function isETag(value) {
|
|
22
|
+
return /^(?:W\/)?"[\s\S]+"$/.test(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @function parseTokens
|
|
26
|
+
* @description Parse HTTP tokens.
|
|
27
|
+
* @param value The tokens value string.
|
|
28
|
+
*/
|
|
29
|
+
function parseTokens(value) {
|
|
30
|
+
let end = 0;
|
|
31
|
+
let start = 0;
|
|
32
|
+
let tokens = [];
|
|
33
|
+
// Gather tokens.
|
|
34
|
+
for (let i = 0, length = value.length; i < length; i++) {
|
|
35
|
+
switch (value.charCodeAt(i)) {
|
|
36
|
+
case 0x20:
|
|
37
|
+
// ' '.
|
|
38
|
+
if (start === end) {
|
|
39
|
+
start = end = i + 1;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case 0x2c:
|
|
43
|
+
// ','.
|
|
44
|
+
tokens.push(value.substring(start, end));
|
|
45
|
+
start = end = i + 1;
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
end = i + 1;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Final token.
|
|
53
|
+
tokens.push(value.substring(start, end));
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @function isRangeFresh
|
|
58
|
+
* @description Check if request range fresh.
|
|
59
|
+
* @param context Koa context.
|
|
60
|
+
*/
|
|
61
|
+
function isRangeFresh(context) {
|
|
62
|
+
const { request, response } = context;
|
|
63
|
+
const ifRange = request.get('If-Range');
|
|
64
|
+
// No If-Range.
|
|
65
|
+
if (!ifRange) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// If-Range as etag.
|
|
69
|
+
if (isETag(ifRange)) {
|
|
70
|
+
const etag = response.get('ETag');
|
|
71
|
+
return !!(etag && isETagFresh(ifRange, etag));
|
|
72
|
+
}
|
|
73
|
+
// If-Range as modified date.
|
|
74
|
+
const lastModified = response.get('Last-Modified');
|
|
75
|
+
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* @function isETagFresh
|
|
79
|
+
* @description Check if etag is fresh.
|
|
80
|
+
* @param match The match value.
|
|
81
|
+
* @param etag The etag value.
|
|
82
|
+
*/
|
|
83
|
+
function isETagFresh(match, etag) {
|
|
84
|
+
return parseTokens(match).some(match => {
|
|
85
|
+
return match === etag || match === 'W/' + etag || 'W/' + match === etag;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @function decodeURI
|
|
90
|
+
* @description Decode URI component.
|
|
91
|
+
* @param URI The URI to decode.
|
|
92
|
+
*/
|
|
93
|
+
function decodeURI(URI) {
|
|
94
|
+
try {
|
|
95
|
+
return decodeURIComponent(URI);
|
|
96
|
+
} catch (_a) {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* @function isConditionalGET
|
|
102
|
+
* @description Check if request is conditional GET.
|
|
103
|
+
* @param context Koa context.
|
|
104
|
+
*/
|
|
105
|
+
function isConditionalGET(context) {
|
|
106
|
+
const { request } = context;
|
|
107
|
+
return !!(
|
|
108
|
+
request.get('If-Match') ||
|
|
109
|
+
request.get('If-None-Match') ||
|
|
110
|
+
request.get('If-Modified-Since') ||
|
|
111
|
+
request.get('If-Unmodified-Since')
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @function isPreconditionFailure
|
|
116
|
+
* @description Check if request precondition failure.
|
|
117
|
+
* @param context Koa context.
|
|
118
|
+
*/
|
|
119
|
+
function isPreconditionFailure({ request, response }) {
|
|
120
|
+
// If-Match.
|
|
121
|
+
const match = request.get('If-Match');
|
|
122
|
+
// Check if request match.
|
|
123
|
+
if (match) {
|
|
124
|
+
// Etag.
|
|
125
|
+
const etag = response.get('ETag');
|
|
126
|
+
return !etag || match === '*' || !isETagFresh(match, etag);
|
|
127
|
+
}
|
|
128
|
+
// If-Unmodified-Since.
|
|
129
|
+
const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
|
|
130
|
+
// Check if request unmodified.
|
|
131
|
+
if (!Number.isNaN(unmodifiedSince)) {
|
|
132
|
+
// Last-Modified.
|
|
133
|
+
const lastModified = Date.parse(response.get('Last-Modified'));
|
|
134
|
+
return Number.isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
135
|
+
}
|
|
136
|
+
// Check precondition passed.
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @function parseRanges
|
|
141
|
+
* @description Parse ranges.
|
|
142
|
+
* @param context Koa context.
|
|
143
|
+
* @param stats File stats.
|
|
144
|
+
*/
|
|
145
|
+
function parseRanges(context, stats) {
|
|
146
|
+
const { size } = stats;
|
|
147
|
+
// Range support.
|
|
148
|
+
if (/^bytes$/i.test(context.response.get('Accept-Ranges'))) {
|
|
149
|
+
const range = context.request.get('Range');
|
|
150
|
+
// Range fresh.
|
|
151
|
+
if (range && isRangeFresh(context)) {
|
|
152
|
+
// Parse range -1 -2 or [].
|
|
153
|
+
const parsed = parseRange(size, range, { combine: true });
|
|
154
|
+
// -1 signals an unsatisfiable range.
|
|
155
|
+
// -2 signals a malformed header string.
|
|
156
|
+
if (parsed === -1 || parsed === -2) {
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
// Ranges ok, support multiple ranges.
|
|
160
|
+
if (parsed.type === 'bytes') {
|
|
161
|
+
// Set 206 status.
|
|
162
|
+
context.status = 206;
|
|
163
|
+
const { length } = parsed;
|
|
164
|
+
// Multiple ranges.
|
|
165
|
+
if (length > 1) {
|
|
166
|
+
// Content-Length.
|
|
167
|
+
let contentLength = 0;
|
|
168
|
+
// Ranges.
|
|
169
|
+
const ranges = [];
|
|
170
|
+
// Range boundary.
|
|
171
|
+
const boundary = `<${generate()}>`;
|
|
172
|
+
// Range suffix.
|
|
173
|
+
const suffix = `\r\n--${boundary}--\r\n`;
|
|
174
|
+
// Multipart Content-Type.
|
|
175
|
+
const contentType = `Content-Type: ${context.type}`;
|
|
176
|
+
// Override Content-Type.
|
|
177
|
+
context.type = `multipart/byteranges; boundary=${boundary}`;
|
|
178
|
+
// Map ranges.
|
|
179
|
+
for (let index = 0; index < length; index++) {
|
|
180
|
+
const { start, end } = parsed[index];
|
|
181
|
+
// The first prefix boundary no \r\n.
|
|
182
|
+
const head = index > 0 ? '\r\n' : '';
|
|
183
|
+
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
184
|
+
const prefix = `${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
|
|
185
|
+
// Compute Content-Length
|
|
186
|
+
contentLength += end - start + 1 + Buffer.byteLength(prefix);
|
|
187
|
+
// Cache range.
|
|
188
|
+
ranges.push({ start, end, prefix });
|
|
189
|
+
}
|
|
190
|
+
// The last add suffix boundary.
|
|
191
|
+
ranges[length - 1].suffix = suffix;
|
|
192
|
+
// Compute Content-Length.
|
|
193
|
+
contentLength += Buffer.byteLength(suffix);
|
|
194
|
+
// Set Content-Length.
|
|
195
|
+
context.length = contentLength;
|
|
196
|
+
// Return ranges.
|
|
197
|
+
return ranges;
|
|
198
|
+
} else {
|
|
199
|
+
const [{ start, end }] = parsed;
|
|
200
|
+
// Set Content-Length.
|
|
201
|
+
context.length = end - start + 1;
|
|
202
|
+
// Set Content-Range.
|
|
203
|
+
context.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
204
|
+
// Return ranges.
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Set Content-Length.
|
|
211
|
+
context.length = size;
|
|
212
|
+
// Return ranges.
|
|
213
|
+
return [{ start: 0, end: Math.max(size - 1) }];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { decodeURI, isConditionalGET, isPreconditionFailure, parseRanges };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @package koa-files
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @version 3.0.0
|
|
5
|
+
* @author nuintun <nuintun@qq.com>
|
|
6
|
+
* @description A static files serving middleware for koa.
|
|
7
|
+
* @see https://github.com/nuintun/koa-files#readme
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { relative, isAbsolute } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @module path
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @function unixify
|
|
17
|
+
* @description Convert path to unix style.
|
|
18
|
+
* @param path The path to convert.
|
|
19
|
+
*/
|
|
20
|
+
function unixify(path) {
|
|
21
|
+
return path.replace(/\\/g, '/');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @function hasTrailingSlash
|
|
25
|
+
* @description Check if path has trailing slash.
|
|
26
|
+
* @param path The path to check.
|
|
27
|
+
*/
|
|
28
|
+
function hasTrailingSlash(path) {
|
|
29
|
+
return /\/$/.test(path);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @function isOutRoot
|
|
33
|
+
* @description Check if path is out of root.
|
|
34
|
+
* @param path The path to check.
|
|
35
|
+
* @param root The root path.
|
|
36
|
+
*/
|
|
37
|
+
function isOutRoot(path, root) {
|
|
38
|
+
path = relative(root, path);
|
|
39
|
+
return /\.\.(?:[\\/]|$)/.test(path) || isAbsolute(path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { hasTrailingSlash, isOutRoot, unixify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-files",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A static files serving middleware for koa.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "cjs/index.cjs",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"test": "node tests",
|
|
46
46
|
"lint": "tsc --noEmit",
|
|
47
47
|
"clean": "node tools/clean.js",
|
|
48
|
-
"format": "prettier --write .",
|
|
49
48
|
"prepublishOnly": "npm run build",
|
|
50
49
|
"build:cjs": "rollup -c tools/rollup.cjs.js",
|
|
51
50
|
"build:esm": "rollup -c tools/rollup.esm.js",
|
|
51
|
+
"format": "prettier --write . --ignore-path .prettierignore",
|
|
52
52
|
"build": "npm run clean && npm run build:main && npm run format",
|
|
53
53
|
"build:main": "npm run build:cjs && npm run build:esm && npm run build:types",
|
|
54
54
|
"build:types": "tsc --declaration --emitDeclarationOnly --declarationDir types"
|
|
@@ -65,12 +65,12 @@
|
|
|
65
65
|
"@types/etag": "^1.8.3",
|
|
66
66
|
"@types/koa": "^2.15.0",
|
|
67
67
|
"@types/range-parser": "^1.2.7",
|
|
68
|
-
"koa": "^2.15.
|
|
68
|
+
"koa": "^2.15.2",
|
|
69
69
|
"magic-string": "^0.30.8",
|
|
70
70
|
"prettier": "^3.2.5",
|
|
71
71
|
"rimraf": "^5.0.5",
|
|
72
|
-
"rollup": "^4.
|
|
73
|
-
"typescript": "^5.
|
|
72
|
+
"rollup": "^4.13.0",
|
|
73
|
+
"typescript": "^5.4.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"koa": "^2.0.0"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Service
|
|
3
|
+
*/
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
import { Stats } from 'fs';
|
|
6
|
+
import { Context } from 'koa';
|
|
7
|
+
import { FileSystem } from './utils/fs';
|
|
8
|
+
interface IgnoreFunction {
|
|
9
|
+
(path: string): boolean;
|
|
10
|
+
}
|
|
11
|
+
interface Headers {
|
|
12
|
+
[key: string]: string | string[];
|
|
13
|
+
}
|
|
14
|
+
interface HeaderFunction {
|
|
15
|
+
(path: string, stats: Stats): Headers | void;
|
|
16
|
+
}
|
|
17
|
+
export interface Options {
|
|
18
|
+
fs: FileSystem;
|
|
19
|
+
etag?: boolean;
|
|
20
|
+
acceptRanges?: boolean;
|
|
21
|
+
lastModified?: boolean;
|
|
22
|
+
ignore?: IgnoreFunction;
|
|
23
|
+
headers?: Headers | HeaderFunction;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @class Service
|
|
27
|
+
*/
|
|
28
|
+
export default class Service {
|
|
29
|
+
private root;
|
|
30
|
+
private options;
|
|
31
|
+
/**
|
|
32
|
+
* @constructor
|
|
33
|
+
* @description Create file service.
|
|
34
|
+
* @param root File service root.
|
|
35
|
+
* @param options File service options.
|
|
36
|
+
*/
|
|
37
|
+
constructor(root: string, options: Options);
|
|
38
|
+
/**
|
|
39
|
+
* @private
|
|
40
|
+
* @method isIgnore
|
|
41
|
+
* @description Check if path is ignore.
|
|
42
|
+
* @param path File path.
|
|
43
|
+
*/
|
|
44
|
+
private isIgnore;
|
|
45
|
+
/**
|
|
46
|
+
* @private
|
|
47
|
+
* @method setupHeaders
|
|
48
|
+
* @description Setup headers
|
|
49
|
+
* @param context Koa context
|
|
50
|
+
* @param path File path
|
|
51
|
+
* @param stats File stats
|
|
52
|
+
*/
|
|
53
|
+
private setupHeaders;
|
|
54
|
+
/**
|
|
55
|
+
* @private
|
|
56
|
+
* @method write
|
|
57
|
+
* @description Write file to stream.
|
|
58
|
+
* @param stream Destination stream.
|
|
59
|
+
* @param path The file path to read.
|
|
60
|
+
* @param range The range to read.
|
|
61
|
+
* @param end Is destory destination stream after read complete.
|
|
62
|
+
*/
|
|
63
|
+
private write;
|
|
64
|
+
/**
|
|
65
|
+
* @private
|
|
66
|
+
* @method send
|
|
67
|
+
* @description Send file.
|
|
68
|
+
* @param context Koa context.
|
|
69
|
+
* @param path File path.
|
|
70
|
+
* @param ranges Read ranges.
|
|
71
|
+
*/
|
|
72
|
+
private send;
|
|
73
|
+
/**
|
|
74
|
+
* @public
|
|
75
|
+
* @method response
|
|
76
|
+
* @description Response to koa context.
|
|
77
|
+
* @param context Koa context.
|
|
78
|
+
*/
|
|
79
|
+
response(context: Context): Promise<boolean>;
|
|
80
|
+
}
|
|
81
|
+
export {};
|
package/types/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module index
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author nuintun
|
|
5
3
|
*/
|
|
6
4
|
import { Middleware } from 'koa';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { FileSystem } from './utils/fs';
|
|
6
|
+
import { Options as ServiceOptions } from './Service';
|
|
7
|
+
export interface Options extends Omit<ServiceOptions, 'fs'> {
|
|
8
|
+
fs?: FileSystem;
|
|
9
|
+
defer?: boolean;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* @function server
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module fs
|
|
3
|
+
*/
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
/// <reference types="node" />
|
|
6
|
+
import { ReadStream, Stats } from 'fs';
|
|
7
|
+
type PathLike = string | Buffer | URL;
|
|
8
|
+
/**
|
|
9
|
+
* @function stat
|
|
10
|
+
* @description Get file stats.
|
|
11
|
+
* @param fs The file system to used.
|
|
12
|
+
* @param path The file path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stat(fs: FileSystem, path: string): Promise<Stats | null>;
|
|
15
|
+
export interface FileSystem {
|
|
16
|
+
stat(path: PathLike, callback: (error: Error | null, stats: Stats) => void): void;
|
|
17
|
+
createReadStream(
|
|
18
|
+
path: PathLike,
|
|
19
|
+
options?: {
|
|
20
|
+
start?: number;
|
|
21
|
+
end?: number;
|
|
22
|
+
}
|
|
23
|
+
): ReadStream;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module http
|
|
3
|
+
*/
|
|
4
|
+
/// <reference types="node" />
|
|
5
|
+
import { Stats } from 'fs';
|
|
6
|
+
import { Context } from 'koa';
|
|
7
|
+
export interface Range {
|
|
8
|
+
start: number;
|
|
9
|
+
end?: number;
|
|
10
|
+
prefix?: string;
|
|
11
|
+
suffix?: string;
|
|
12
|
+
}
|
|
13
|
+
type Ranges = Range[] | -1 | -2;
|
|
14
|
+
/**
|
|
15
|
+
* @function decodeURI
|
|
16
|
+
* @description Decode URI component.
|
|
17
|
+
* @param URI The URI to decode.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decodeURI(URI: string): string | -1;
|
|
20
|
+
/**
|
|
21
|
+
* @function isConditionalGET
|
|
22
|
+
* @description Check if request is conditional GET.
|
|
23
|
+
* @param context Koa context.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isConditionalGET(context: Context): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* @function isPreconditionFailure
|
|
28
|
+
* @description Check if request precondition failure.
|
|
29
|
+
* @param context Koa context.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isPreconditionFailure({ request, response }: Context): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* @function parseRanges
|
|
34
|
+
* @description Parse ranges.
|
|
35
|
+
* @param context Koa context.
|
|
36
|
+
* @param stats File stats.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseRanges(context: Context, stats: Stats): Ranges;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module path
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @function unixify
|
|
6
|
+
* @description Convert path to unix style.
|
|
7
|
+
* @param path The path to convert.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unixify(path: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* @function hasTrailingSlash
|
|
12
|
+
* @description Check if path has trailing slash.
|
|
13
|
+
* @param path The path to check.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hasTrailingSlash(path: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* @function isOutRoot
|
|
18
|
+
* @description Check if path is out of root.
|
|
19
|
+
* @param path The path to check.
|
|
20
|
+
* @param root The root path.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isOutRoot(path: string, root: string): boolean;
|