appstage 0.2.9 → 0.2.11
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/dist/index.cjs +14 -87
- package/dist/index.d.ts +3 -61
- package/dist/index.mjs +15 -86
- package/index.ts +0 -2
- package/package.json +1 -1
- package/src/controllers/files.ts +15 -7
- package/src/controllers/dir.ts +0 -119
- package/src/utils/resolveFilePath.ts +0 -78
package/dist/index.cjs
CHANGED
|
@@ -44,86 +44,6 @@ function emitLog(app, message, payload) {
|
|
|
44
44
|
return app.events?.emit("log", normalizedPayload);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function toLanguage(locale) {
|
|
48
|
-
return locale.split(/[-_]/)[0];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function resolveFilePath({ name, dir = ".", lang, supportedLocales = [], ext, index }) {
|
|
52
|
-
let cwd = process.cwd();
|
|
53
|
-
let localeSet = new Set(supportedLocales);
|
|
54
|
-
let langSet = new Set(supportedLocales.map(toLanguage));
|
|
55
|
-
let availableNames = [name, ...[...localeSet, ...langSet].map((item) => `${name}.${item}`)];
|
|
56
|
-
let preferredLangNames;
|
|
57
|
-
if (lang && (!supportedLocales.length || localeSet.has(lang) || langSet.has(lang))) preferredLangNames = [`${name}.${lang}`, `${name}.${toLanguage(lang)}`];
|
|
58
|
-
let names = new Set(preferredLangNames ? [...preferredLangNames, ...availableNames] : availableNames);
|
|
59
|
-
let exts = Array.isArray(ext) ? ext : [ext];
|
|
60
|
-
for (let item of names) for (let itemExt of exts) {
|
|
61
|
-
let path = (0, node_path.join)(cwd, dir, `${item}${itemExt ? `.${itemExt}` : ""}`);
|
|
62
|
-
try {
|
|
63
|
-
await (0, node_fs_promises.access)(path);
|
|
64
|
-
return path;
|
|
65
|
-
} catch {}
|
|
66
|
-
}
|
|
67
|
-
if (index) for (let item of names) for (let itemExt of exts) {
|
|
68
|
-
let path = (0, node_path.join)(cwd, dir, item, `index${itemExt ? `.${itemExt}` : ""}`);
|
|
69
|
-
try {
|
|
70
|
-
await (0, node_fs_promises.access)(path);
|
|
71
|
-
return path;
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const defaultExt = ["html", "htm"];
|
|
77
|
-
const defaultName = (req) => req.path.split("/").at(-1);
|
|
78
|
-
/**
|
|
79
|
-
* Serves files from the specified directory path in a locale-aware
|
|
80
|
-
* fashion after applying optional transforms.
|
|
81
|
-
*
|
|
82
|
-
* A file ending with `.<lang>.<ext>` is picked first if the `<lang>`
|
|
83
|
-
* part matches `req.ctx.lang`. If the `supportedLocales` array is
|
|
84
|
-
* provided, the `*.<lang>.<ext>` file is picked only if the given
|
|
85
|
-
* array contains `req.ctx.lang`. Otherwise, a file without the locale
|
|
86
|
-
* in its path (`*.<ext>`) is picked.
|
|
87
|
-
*/
|
|
88
|
-
const dir = ({ path, name = defaultName, ext = defaultExt, transform, supportedLocales, index = true }) => {
|
|
89
|
-
if (typeof path !== "string") throw new Error(`'path' is not a string`);
|
|
90
|
-
let transformSet = (Array.isArray(transform) ? transform : [transform]).filter((item) => typeof item === "function");
|
|
91
|
-
return async (req, res) => {
|
|
92
|
-
let fileName = typeof name === "function" ? name(req, res) : name;
|
|
93
|
-
emitLog(req.app, `Name: ${JSON.stringify(fileName)}`, {
|
|
94
|
-
req,
|
|
95
|
-
res
|
|
96
|
-
});
|
|
97
|
-
if (fileName === void 0) {
|
|
98
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
let filePath = await resolveFilePath({
|
|
102
|
-
name: fileName,
|
|
103
|
-
dir: path,
|
|
104
|
-
ext,
|
|
105
|
-
supportedLocales,
|
|
106
|
-
lang: req.ctx?.lang,
|
|
107
|
-
index
|
|
108
|
-
});
|
|
109
|
-
emitLog(req.app, `Path: ${JSON.stringify(filePath)}`, {
|
|
110
|
-
req,
|
|
111
|
-
res
|
|
112
|
-
});
|
|
113
|
-
if (!filePath) {
|
|
114
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
let content = (await (0, node_fs_promises.readFile)(filePath)).toString();
|
|
118
|
-
for (let transformItem of transformSet) content = await transformItem(req, res, {
|
|
119
|
-
content,
|
|
120
|
-
path: filePath,
|
|
121
|
-
name: (0, node_path.basename)(filePath, (0, node_path.extname)(filePath))
|
|
122
|
-
});
|
|
123
|
-
res.send(content);
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
|
|
127
47
|
const maxLanguages = 3;
|
|
128
48
|
async function resolve(...parts) {
|
|
129
49
|
let fullPath = (0, node_path.join)(...parts);
|
|
@@ -132,7 +52,10 @@ async function resolve(...parts) {
|
|
|
132
52
|
} catch {}
|
|
133
53
|
return null;
|
|
134
54
|
}
|
|
135
|
-
function getLanguageList(
|
|
55
|
+
function getLanguageList(req) {
|
|
56
|
+
let langParam = req.query.lang;
|
|
57
|
+
if (langParam) return [String(langParam)];
|
|
58
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
136
59
|
let langs = /* @__PURE__ */ new Set();
|
|
137
60
|
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
138
61
|
let s = acceptedLanguages[i];
|
|
@@ -147,6 +70,7 @@ function getLanguageList(acceptedLanguages) {
|
|
|
147
70
|
}
|
|
148
71
|
const defaultExtensions = ["html", "htm"];
|
|
149
72
|
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
73
|
+
const defaultLanguages = getLanguageList;
|
|
150
74
|
/**
|
|
151
75
|
* Serves files from the specified directory path in a locale-aware
|
|
152
76
|
* fashion after applying optional transforms.
|
|
@@ -156,7 +80,7 @@ const files = (params) => {
|
|
|
156
80
|
let bases = Array.isArray(p.base) ? p.base : [p.base];
|
|
157
81
|
let exts = p.extensions ?? defaultExtensions;
|
|
158
82
|
return async (req, res) => {
|
|
159
|
-
let langs =
|
|
83
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
160
84
|
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
161
85
|
if (path.includes("../")) {
|
|
162
86
|
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
@@ -190,16 +114,17 @@ const files = (params) => {
|
|
|
190
114
|
return;
|
|
191
115
|
}
|
|
192
116
|
let content = (await (0, node_fs_promises.readFile)(filePath)).toString();
|
|
193
|
-
let
|
|
117
|
+
let ext = (0, node_path.extname)(filePath);
|
|
118
|
+
let name = (0, node_path.basename)(filePath, ext);
|
|
194
119
|
for (let transform of p.transform) {
|
|
195
120
|
let result = transform(req, res, {
|
|
196
121
|
content,
|
|
197
|
-
path,
|
|
122
|
+
path: filePath,
|
|
198
123
|
name
|
|
199
124
|
});
|
|
200
125
|
content = result instanceof Promise ? await result : result;
|
|
201
126
|
}
|
|
202
|
-
res.type(
|
|
127
|
+
res.type(ext.slice(1)).send(content);
|
|
203
128
|
};
|
|
204
129
|
};
|
|
205
130
|
|
|
@@ -222,6 +147,10 @@ const unhandledRoute = () => async (req, res) => {
|
|
|
222
147
|
res.status(404).send(await req.app.renderStatus?.(req, res, "unhandled_route"));
|
|
223
148
|
};
|
|
224
149
|
|
|
150
|
+
function toLanguage(locale) {
|
|
151
|
+
return locale.split(/[-_]/)[0];
|
|
152
|
+
}
|
|
153
|
+
|
|
225
154
|
function getEffectiveLocale(preferredLocales, supportedLocales) {
|
|
226
155
|
if (!supportedLocales || supportedLocales.length === 0) return void 0;
|
|
227
156
|
if (!preferredLocales || preferredLocales.length === 0) return supportedLocales[0];
|
|
@@ -716,7 +645,6 @@ exports.build = build;
|
|
|
716
645
|
exports.cli = cli;
|
|
717
646
|
exports.createApp = createApp;
|
|
718
647
|
exports.cspNonce = cspNonce;
|
|
719
|
-
exports.dir = dir;
|
|
720
648
|
exports.emitLog = emitLog;
|
|
721
649
|
exports.files = files;
|
|
722
650
|
exports.getEffectiveLocale = getEffectiveLocale;
|
|
@@ -728,7 +656,6 @@ exports.lang = lang;
|
|
|
728
656
|
exports.log = log;
|
|
729
657
|
exports.renderStatus = renderStatus;
|
|
730
658
|
exports.requestEvents = requestEvents;
|
|
731
|
-
exports.resolveFilePath = resolveFilePath;
|
|
732
659
|
exports.serializeState = serializeState;
|
|
733
660
|
exports.servePipeableStream = servePipeableStream;
|
|
734
661
|
exports.toLanguage = toLanguage;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,69 +16,11 @@ type TransformContent = (req: Request, res: Response, params: {
|
|
|
16
16
|
name?: string;
|
|
17
17
|
}) => string | Promise<string>;
|
|
18
18
|
|
|
19
|
-
type ResolveFilePathParams = {
|
|
20
|
-
name: string;
|
|
21
|
-
dir?: string;
|
|
22
|
-
lang?: string;
|
|
23
|
-
supportedLocales?: string[]; /** Allowed file extensions. */
|
|
24
|
-
ext?: string | string[];
|
|
25
|
-
/**
|
|
26
|
-
* Whether an index file should be checked if the resolved file name
|
|
27
|
-
* doesn't correspond to an existing file.
|
|
28
|
-
*
|
|
29
|
-
* @defaultValue `true`
|
|
30
|
-
*/
|
|
31
|
-
index?: boolean;
|
|
32
|
-
};
|
|
33
|
-
declare function resolveFilePath({
|
|
34
|
-
name,
|
|
35
|
-
dir,
|
|
36
|
-
lang,
|
|
37
|
-
supportedLocales,
|
|
38
|
-
ext,
|
|
39
|
-
index
|
|
40
|
-
}: ResolveFilePathParams): Promise<string | undefined>;
|
|
41
|
-
|
|
42
|
-
type ZeroTransform = false | null | undefined;
|
|
43
|
-
type DirParams = Partial<Pick<ResolveFilePathParams, "supportedLocales" | "index">> & {
|
|
44
|
-
/** Directory path to serve files from. */path: string;
|
|
45
|
-
/**
|
|
46
|
-
* File name.
|
|
47
|
-
* By default, the portion of `req.path` after the last slash.
|
|
48
|
-
*/
|
|
49
|
-
name?: string | undefined | ((req: Request, res: Response) => string | undefined);
|
|
50
|
-
/**
|
|
51
|
-
* Allowed file extensions.
|
|
52
|
-
*
|
|
53
|
-
* @defaultValue `['html', 'htm']`
|
|
54
|
-
*/
|
|
55
|
-
ext?: ResolveFilePathParams["ext"];
|
|
56
|
-
/**
|
|
57
|
-
* Custom transforms applied to the file content.
|
|
58
|
-
*
|
|
59
|
-
* Example: Use `injectNonce` from this package to inject the `nonce`
|
|
60
|
-
* value generated for the current request into the `{{nonce}}`
|
|
61
|
-
* placeholders in an HTML file.
|
|
62
|
-
*/
|
|
63
|
-
transform?: TransformContent | ZeroTransform | (TransformContent | ZeroTransform)[];
|
|
64
|
-
supportedLocales?: string[];
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Serves files from the specified directory path in a locale-aware
|
|
68
|
-
* fashion after applying optional transforms.
|
|
69
|
-
*
|
|
70
|
-
* A file ending with `.<lang>.<ext>` is picked first if the `<lang>`
|
|
71
|
-
* part matches `req.ctx.lang`. If the `supportedLocales` array is
|
|
72
|
-
* provided, the `*.<lang>.<ext>` file is picked only if the given
|
|
73
|
-
* array contains `req.ctx.lang`. Otherwise, a file without the locale
|
|
74
|
-
* in its path (`*.<ext>`) is picked.
|
|
75
|
-
*/
|
|
76
|
-
declare const dir$1: Controller<DirParams>;
|
|
77
|
-
|
|
78
19
|
type FilesParams = {
|
|
79
20
|
base: string | string[];
|
|
80
21
|
path?: string | ((req: Request) => string);
|
|
81
22
|
extensions?: string[];
|
|
23
|
+
languages?: (req: Request) => string[];
|
|
82
24
|
transform?: TransformContent[];
|
|
83
25
|
};
|
|
84
26
|
/**
|
|
@@ -135,7 +77,7 @@ type LangParams = {
|
|
|
135
77
|
shouldRedirect?: boolean;
|
|
136
78
|
langCookieOptions?: CookieOptions;
|
|
137
79
|
};
|
|
138
|
-
declare const lang
|
|
80
|
+
declare const lang: Middleware<LangParams | void>;
|
|
139
81
|
|
|
140
82
|
/**
|
|
141
83
|
* Adds event handlers, like logging, to essential request phases.
|
|
@@ -1178,4 +1120,4 @@ declare function servePipeableStream(req: Request, res: Response): ({
|
|
|
1178
1120
|
pipe
|
|
1179
1121
|
}: PipeableStream, error?: unknown) => Promise<void>;
|
|
1180
1122
|
|
|
1181
|
-
export { Controller,
|
|
1123
|
+
export { Controller, ErrorController, FilesParams, LangParams, LogEventPayload, LogLevel, LogOptions, Middleware, MiddlewareSet, RenderStatus, ReqCtx, TransformContent, build, cli, createApp, cspNonce, emitLog, files, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang, log, renderStatus, requestEvents, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
package/dist/index.mjs
CHANGED
|
@@ -18,86 +18,6 @@ function emitLog(app, message, payload) {
|
|
|
18
18
|
return app.events?.emit("log", normalizedPayload);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function toLanguage(locale) {
|
|
22
|
-
return locale.split(/[-_]/)[0];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function resolveFilePath({ name, dir = ".", lang, supportedLocales = [], ext, index }) {
|
|
26
|
-
let cwd = process.cwd();
|
|
27
|
-
let localeSet = new Set(supportedLocales);
|
|
28
|
-
let langSet = new Set(supportedLocales.map(toLanguage));
|
|
29
|
-
let availableNames = [name, ...[...localeSet, ...langSet].map((item) => `${name}.${item}`)];
|
|
30
|
-
let preferredLangNames;
|
|
31
|
-
if (lang && (!supportedLocales.length || localeSet.has(lang) || langSet.has(lang))) preferredLangNames = [`${name}.${lang}`, `${name}.${toLanguage(lang)}`];
|
|
32
|
-
let names = new Set(preferredLangNames ? [...preferredLangNames, ...availableNames] : availableNames);
|
|
33
|
-
let exts = Array.isArray(ext) ? ext : [ext];
|
|
34
|
-
for (let item of names) for (let itemExt of exts) {
|
|
35
|
-
let path = join(cwd, dir, `${item}${itemExt ? `.${itemExt}` : ""}`);
|
|
36
|
-
try {
|
|
37
|
-
await access(path);
|
|
38
|
-
return path;
|
|
39
|
-
} catch {}
|
|
40
|
-
}
|
|
41
|
-
if (index) for (let item of names) for (let itemExt of exts) {
|
|
42
|
-
let path = join(cwd, dir, item, `index${itemExt ? `.${itemExt}` : ""}`);
|
|
43
|
-
try {
|
|
44
|
-
await access(path);
|
|
45
|
-
return path;
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const defaultExt = ["html", "htm"];
|
|
51
|
-
const defaultName = (req) => req.path.split("/").at(-1);
|
|
52
|
-
/**
|
|
53
|
-
* Serves files from the specified directory path in a locale-aware
|
|
54
|
-
* fashion after applying optional transforms.
|
|
55
|
-
*
|
|
56
|
-
* A file ending with `.<lang>.<ext>` is picked first if the `<lang>`
|
|
57
|
-
* part matches `req.ctx.lang`. If the `supportedLocales` array is
|
|
58
|
-
* provided, the `*.<lang>.<ext>` file is picked only if the given
|
|
59
|
-
* array contains `req.ctx.lang`. Otherwise, a file without the locale
|
|
60
|
-
* in its path (`*.<ext>`) is picked.
|
|
61
|
-
*/
|
|
62
|
-
const dir = ({ path, name = defaultName, ext = defaultExt, transform, supportedLocales, index = true }) => {
|
|
63
|
-
if (typeof path !== "string") throw new Error(`'path' is not a string`);
|
|
64
|
-
let transformSet = (Array.isArray(transform) ? transform : [transform]).filter((item) => typeof item === "function");
|
|
65
|
-
return async (req, res) => {
|
|
66
|
-
let fileName = typeof name === "function" ? name(req, res) : name;
|
|
67
|
-
emitLog(req.app, `Name: ${JSON.stringify(fileName)}`, {
|
|
68
|
-
req,
|
|
69
|
-
res
|
|
70
|
-
});
|
|
71
|
-
if (fileName === void 0) {
|
|
72
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
let filePath = await resolveFilePath({
|
|
76
|
-
name: fileName,
|
|
77
|
-
dir: path,
|
|
78
|
-
ext,
|
|
79
|
-
supportedLocales,
|
|
80
|
-
lang: req.ctx?.lang,
|
|
81
|
-
index
|
|
82
|
-
});
|
|
83
|
-
emitLog(req.app, `Path: ${JSON.stringify(filePath)}`, {
|
|
84
|
-
req,
|
|
85
|
-
res
|
|
86
|
-
});
|
|
87
|
-
if (!filePath) {
|
|
88
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
let content = (await readFile(filePath)).toString();
|
|
92
|
-
for (let transformItem of transformSet) content = await transformItem(req, res, {
|
|
93
|
-
content,
|
|
94
|
-
path: filePath,
|
|
95
|
-
name: basename(filePath, extname(filePath))
|
|
96
|
-
});
|
|
97
|
-
res.send(content);
|
|
98
|
-
};
|
|
99
|
-
};
|
|
100
|
-
|
|
101
21
|
const maxLanguages = 3;
|
|
102
22
|
async function resolve(...parts) {
|
|
103
23
|
let fullPath = join(...parts);
|
|
@@ -106,7 +26,10 @@ async function resolve(...parts) {
|
|
|
106
26
|
} catch {}
|
|
107
27
|
return null;
|
|
108
28
|
}
|
|
109
|
-
function getLanguageList(
|
|
29
|
+
function getLanguageList(req) {
|
|
30
|
+
let langParam = req.query.lang;
|
|
31
|
+
if (langParam) return [String(langParam)];
|
|
32
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
110
33
|
let langs = /* @__PURE__ */ new Set();
|
|
111
34
|
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
112
35
|
let s = acceptedLanguages[i];
|
|
@@ -121,6 +44,7 @@ function getLanguageList(acceptedLanguages) {
|
|
|
121
44
|
}
|
|
122
45
|
const defaultExtensions = ["html", "htm"];
|
|
123
46
|
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
47
|
+
const defaultLanguages = getLanguageList;
|
|
124
48
|
/**
|
|
125
49
|
* Serves files from the specified directory path in a locale-aware
|
|
126
50
|
* fashion after applying optional transforms.
|
|
@@ -130,7 +54,7 @@ const files = (params) => {
|
|
|
130
54
|
let bases = Array.isArray(p.base) ? p.base : [p.base];
|
|
131
55
|
let exts = p.extensions ?? defaultExtensions;
|
|
132
56
|
return async (req, res) => {
|
|
133
|
-
let langs =
|
|
57
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
134
58
|
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
135
59
|
if (path.includes("../")) {
|
|
136
60
|
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
@@ -164,16 +88,17 @@ const files = (params) => {
|
|
|
164
88
|
return;
|
|
165
89
|
}
|
|
166
90
|
let content = (await readFile(filePath)).toString();
|
|
167
|
-
let
|
|
91
|
+
let ext = extname(filePath);
|
|
92
|
+
let name = basename(filePath, ext);
|
|
168
93
|
for (let transform of p.transform) {
|
|
169
94
|
let result = transform(req, res, {
|
|
170
95
|
content,
|
|
171
|
-
path,
|
|
96
|
+
path: filePath,
|
|
172
97
|
name
|
|
173
98
|
});
|
|
174
99
|
content = result instanceof Promise ? await result : result;
|
|
175
100
|
}
|
|
176
|
-
res.type(
|
|
101
|
+
res.type(ext.slice(1)).send(content);
|
|
177
102
|
};
|
|
178
103
|
};
|
|
179
104
|
|
|
@@ -196,6 +121,10 @@ const unhandledRoute = () => async (req, res) => {
|
|
|
196
121
|
res.status(404).send(await req.app.renderStatus?.(req, res, "unhandled_route"));
|
|
197
122
|
};
|
|
198
123
|
|
|
124
|
+
function toLanguage(locale) {
|
|
125
|
+
return locale.split(/[-_]/)[0];
|
|
126
|
+
}
|
|
127
|
+
|
|
199
128
|
function getEffectiveLocale(preferredLocales, supportedLocales) {
|
|
200
129
|
if (!supportedLocales || supportedLocales.length === 0) return void 0;
|
|
201
130
|
if (!preferredLocales || preferredLocales.length === 0) return supportedLocales[0];
|
|
@@ -686,4 +615,4 @@ function servePipeableStream(req, res) {
|
|
|
686
615
|
};
|
|
687
616
|
}
|
|
688
617
|
|
|
689
|
-
export { build, cli, createApp, cspNonce,
|
|
618
|
+
export { build, cli, createApp, cspNonce, emitLog, files, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang, log, renderStatus, requestEvents, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
package/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export * from "./src/controllers/dir.ts";
|
|
2
1
|
export * from "./src/controllers/files.ts";
|
|
3
2
|
export * from "./src/controllers/unhandledError.ts";
|
|
4
3
|
export * from "./src/controllers/unhandledRoute.ts";
|
|
@@ -27,6 +26,5 @@ export * from "./src/utils/emitLog.ts";
|
|
|
27
26
|
export * from "./src/utils/getStatusMessage.ts";
|
|
28
27
|
export * from "./src/utils/injectNonce.ts";
|
|
29
28
|
export * from "./src/utils/renderStatus.ts";
|
|
30
|
-
export * from "./src/utils/resolveFilePath.ts";
|
|
31
29
|
export * from "./src/utils/serializeState.ts";
|
|
32
30
|
export * from "./src/utils/servePipeableStream.ts";
|
package/package.json
CHANGED
package/src/controllers/files.ts
CHANGED
|
@@ -16,7 +16,12 @@ async function resolve(...parts: string[]) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// ["en-US", "ru"] > ["en-US", "en", "ru"]
|
|
19
|
-
function getLanguageList(
|
|
19
|
+
function getLanguageList(req: Request) {
|
|
20
|
+
let langParam = req.query.lang;
|
|
21
|
+
|
|
22
|
+
if (langParam) return [String(langParam)];
|
|
23
|
+
|
|
24
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
20
25
|
let langs = new Set<string>();
|
|
21
26
|
|
|
22
27
|
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
@@ -37,11 +42,13 @@ export type FilesParams = {
|
|
|
37
42
|
base: string | string[];
|
|
38
43
|
path?: string | ((req: Request) => string);
|
|
39
44
|
extensions?: string[];
|
|
45
|
+
languages?: (req: Request) => string[];
|
|
40
46
|
transform?: TransformContent[];
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
const defaultExtensions = ["html", "htm"];
|
|
44
50
|
const defaultPath = (req: Request) => req.originalUrl.split("?")[0];
|
|
51
|
+
const defaultLanguages = getLanguageList;
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
54
|
* Serves files from the specified directory path in a locale-aware
|
|
@@ -54,7 +61,7 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
54
61
|
let exts = p.extensions ?? defaultExtensions;
|
|
55
62
|
|
|
56
63
|
return async (req, res) => {
|
|
57
|
-
let langs =
|
|
64
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
58
65
|
|
|
59
66
|
let path =
|
|
60
67
|
typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
@@ -74,10 +81,10 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
// path: /x
|
|
78
|
-
// langs: en, ru
|
|
79
84
|
let filePath: string | null = null;
|
|
80
85
|
|
|
86
|
+
// path: /x
|
|
87
|
+
// langs: en, ru
|
|
81
88
|
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
82
89
|
let base = bases[k];
|
|
83
90
|
|
|
@@ -138,14 +145,15 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
let content = (await readFile(filePath)).toString();
|
|
141
|
-
let
|
|
148
|
+
let ext = extname(filePath);
|
|
149
|
+
let name = basename(filePath, ext);
|
|
142
150
|
|
|
143
151
|
for (let transform of p.transform) {
|
|
144
|
-
let result = transform(req, res, { content, path, name });
|
|
152
|
+
let result = transform(req, res, { content, path: filePath, name });
|
|
145
153
|
|
|
146
154
|
content = result instanceof Promise ? await result : result;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
res.type(
|
|
157
|
+
res.type(ext.slice(1)).send(content);
|
|
150
158
|
};
|
|
151
159
|
};
|
package/src/controllers/dir.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { basename, extname } from "node:path";
|
|
3
|
-
import type { Request, Response } from "express";
|
|
4
|
-
import type { Controller } from "../types/Controller.ts";
|
|
5
|
-
import type { TransformContent } from "../types/TransformContent.ts";
|
|
6
|
-
import { emitLog } from "../utils/emitLog.ts";
|
|
7
|
-
import {
|
|
8
|
-
type ResolveFilePathParams,
|
|
9
|
-
resolveFilePath,
|
|
10
|
-
} from "../utils/resolveFilePath.ts";
|
|
11
|
-
|
|
12
|
-
const defaultExt = ["html", "htm"];
|
|
13
|
-
const defaultName = (req: Request) => req.path.split("/").at(-1);
|
|
14
|
-
|
|
15
|
-
type ZeroTransform = false | null | undefined;
|
|
16
|
-
|
|
17
|
-
export type DirParams = Partial<
|
|
18
|
-
Pick<ResolveFilePathParams, "supportedLocales" | "index">
|
|
19
|
-
> & {
|
|
20
|
-
/** Directory path to serve files from. */
|
|
21
|
-
path: string;
|
|
22
|
-
/**
|
|
23
|
-
* File name.
|
|
24
|
-
* By default, the portion of `req.path` after the last slash.
|
|
25
|
-
*/
|
|
26
|
-
name?:
|
|
27
|
-
| string
|
|
28
|
-
| undefined
|
|
29
|
-
| ((req: Request, res: Response) => string | undefined);
|
|
30
|
-
/**
|
|
31
|
-
* Allowed file extensions.
|
|
32
|
-
*
|
|
33
|
-
* @defaultValue `['html', 'htm']`
|
|
34
|
-
*/
|
|
35
|
-
ext?: ResolveFilePathParams["ext"];
|
|
36
|
-
/**
|
|
37
|
-
* Custom transforms applied to the file content.
|
|
38
|
-
*
|
|
39
|
-
* Example: Use `injectNonce` from this package to inject the `nonce`
|
|
40
|
-
* value generated for the current request into the `{{nonce}}`
|
|
41
|
-
* placeholders in an HTML file.
|
|
42
|
-
*/
|
|
43
|
-
transform?:
|
|
44
|
-
| TransformContent
|
|
45
|
-
| ZeroTransform
|
|
46
|
-
| (TransformContent | ZeroTransform)[];
|
|
47
|
-
supportedLocales?: string[];
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Serves files from the specified directory path in a locale-aware
|
|
52
|
-
* fashion after applying optional transforms.
|
|
53
|
-
*
|
|
54
|
-
* A file ending with `.<lang>.<ext>` is picked first if the `<lang>`
|
|
55
|
-
* part matches `req.ctx.lang`. If the `supportedLocales` array is
|
|
56
|
-
* provided, the `*.<lang>.<ext>` file is picked only if the given
|
|
57
|
-
* array contains `req.ctx.lang`. Otherwise, a file without the locale
|
|
58
|
-
* in its path (`*.<ext>`) is picked.
|
|
59
|
-
*/
|
|
60
|
-
export const dir: Controller<DirParams> = ({
|
|
61
|
-
path,
|
|
62
|
-
name = defaultName,
|
|
63
|
-
ext = defaultExt,
|
|
64
|
-
transform,
|
|
65
|
-
supportedLocales,
|
|
66
|
-
index = true,
|
|
67
|
-
}) => {
|
|
68
|
-
if (typeof path !== "string") throw new Error(`'path' is not a string`);
|
|
69
|
-
|
|
70
|
-
let transformSet = (
|
|
71
|
-
Array.isArray(transform) ? transform : [transform]
|
|
72
|
-
).filter((item) => typeof item === "function");
|
|
73
|
-
|
|
74
|
-
return async (req, res) => {
|
|
75
|
-
let fileName = typeof name === "function" ? name(req, res) : name;
|
|
76
|
-
|
|
77
|
-
emitLog(req.app, `Name: ${JSON.stringify(fileName)}`, {
|
|
78
|
-
req,
|
|
79
|
-
res,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (fileName === undefined) {
|
|
83
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let filePath = await resolveFilePath({
|
|
89
|
-
name: fileName,
|
|
90
|
-
dir: path,
|
|
91
|
-
ext,
|
|
92
|
-
supportedLocales,
|
|
93
|
-
lang: req.ctx?.lang,
|
|
94
|
-
index,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
emitLog(req.app, `Path: ${JSON.stringify(filePath)}`, {
|
|
98
|
-
req,
|
|
99
|
-
res,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (!filePath) {
|
|
103
|
-
res.status(404).send(await req.app.renderStatus?.(req, res));
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let content = (await readFile(filePath)).toString();
|
|
109
|
-
|
|
110
|
-
for (let transformItem of transformSet)
|
|
111
|
-
content = await transformItem(req, res, {
|
|
112
|
-
content,
|
|
113
|
-
path: filePath,
|
|
114
|
-
name: basename(filePath, extname(filePath)),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
res.send(content);
|
|
118
|
-
};
|
|
119
|
-
};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { toLanguage } from "../lib/lang/toLanguage.ts";
|
|
4
|
-
|
|
5
|
-
export type ResolveFilePathParams = {
|
|
6
|
-
name: string;
|
|
7
|
-
dir?: string;
|
|
8
|
-
lang?: string;
|
|
9
|
-
supportedLocales?: string[];
|
|
10
|
-
/** Allowed file extensions. */
|
|
11
|
-
ext?: string | string[];
|
|
12
|
-
/**
|
|
13
|
-
* Whether an index file should be checked if the resolved file name
|
|
14
|
-
* doesn't correspond to an existing file.
|
|
15
|
-
*
|
|
16
|
-
* @defaultValue `true`
|
|
17
|
-
*/
|
|
18
|
-
index?: boolean;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export async function resolveFilePath({
|
|
22
|
-
name,
|
|
23
|
-
dir = ".",
|
|
24
|
-
lang,
|
|
25
|
-
supportedLocales = [],
|
|
26
|
-
ext,
|
|
27
|
-
index,
|
|
28
|
-
}: ResolveFilePathParams) {
|
|
29
|
-
let cwd = process.cwd();
|
|
30
|
-
|
|
31
|
-
let localeSet = new Set(supportedLocales);
|
|
32
|
-
let langSet = new Set(supportedLocales.map(toLanguage));
|
|
33
|
-
|
|
34
|
-
let availableNames = [
|
|
35
|
-
name,
|
|
36
|
-
...[...localeSet, ...langSet].map((item) => `${name}.${item}`),
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
let preferredLangNames: string[] | undefined;
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
lang &&
|
|
43
|
-
(!supportedLocales.length || localeSet.has(lang) || langSet.has(lang))
|
|
44
|
-
)
|
|
45
|
-
preferredLangNames = [`${name}.${lang}`, `${name}.${toLanguage(lang)}`];
|
|
46
|
-
|
|
47
|
-
let names = new Set(
|
|
48
|
-
preferredLangNames
|
|
49
|
-
? [...preferredLangNames, ...availableNames]
|
|
50
|
-
: availableNames,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
let exts = Array.isArray(ext) ? ext : [ext];
|
|
54
|
-
|
|
55
|
-
for (let item of names) {
|
|
56
|
-
for (let itemExt of exts) {
|
|
57
|
-
let path = join(cwd, dir, `${item}${itemExt ? `.${itemExt}` : ""}`);
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
await access(path);
|
|
61
|
-
return path;
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (index) {
|
|
67
|
-
for (let item of names) {
|
|
68
|
-
for (let itemExt of exts) {
|
|
69
|
-
let path = join(cwd, dir, item, `index${itemExt ? `.${itemExt}` : ""}`);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await access(path);
|
|
73
|
-
return path;
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|