appstage 0.2.8 → 0.2.10
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 +84 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.mjs +84 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/controllers/files.ts +158 -0
package/dist/index.cjs
CHANGED
|
@@ -124,6 +124,89 @@ const dir = ({ path, name = defaultName, ext = defaultExt, transform, supportedL
|
|
|
124
124
|
};
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
const maxLanguages = 3;
|
|
128
|
+
async function resolve(...parts) {
|
|
129
|
+
let fullPath = (0, node_path.join)(...parts);
|
|
130
|
+
try {
|
|
131
|
+
if ((await (0, node_fs_promises.lstat)(fullPath)).isFile()) return fullPath;
|
|
132
|
+
} catch {}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function getLanguageList(req) {
|
|
136
|
+
let langParam = req.query.lang;
|
|
137
|
+
if (langParam) return [String(langParam)];
|
|
138
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
139
|
+
let langs = /* @__PURE__ */ new Set();
|
|
140
|
+
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
141
|
+
let s = acceptedLanguages[i];
|
|
142
|
+
let [lang] = s.split(/[-_]/);
|
|
143
|
+
if (s === lang) langs.add(s);
|
|
144
|
+
else {
|
|
145
|
+
langs.add(s);
|
|
146
|
+
langs.add(lang);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Array.from(langs);
|
|
150
|
+
}
|
|
151
|
+
const defaultExtensions = ["html", "htm"];
|
|
152
|
+
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
153
|
+
const defaultLanguages = getLanguageList;
|
|
154
|
+
/**
|
|
155
|
+
* Serves files from the specified directory path in a locale-aware
|
|
156
|
+
* fashion after applying optional transforms.
|
|
157
|
+
*/
|
|
158
|
+
const files = (params) => {
|
|
159
|
+
let p = typeof params === "string" ? { base: params } : params;
|
|
160
|
+
let bases = Array.isArray(p.base) ? p.base : [p.base];
|
|
161
|
+
let exts = p.extensions ?? defaultExtensions;
|
|
162
|
+
return async (req, res) => {
|
|
163
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
164
|
+
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
165
|
+
if (path.includes("../")) {
|
|
166
|
+
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
167
|
+
res.status(400).send(await req.app.renderStatus?.(req, res, {
|
|
168
|
+
code: "invalid_path",
|
|
169
|
+
path
|
|
170
|
+
}));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let filePath = null;
|
|
174
|
+
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
175
|
+
let base = bases[k];
|
|
176
|
+
for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
177
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
178
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
|
|
179
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
180
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
|
|
181
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
|
|
182
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
|
|
183
|
+
}
|
|
184
|
+
if (filePath === null) {
|
|
185
|
+
emitLog(req.app, "Unknown path", { data: { path } });
|
|
186
|
+
res.status(404).send(await req.app.renderStatus?.(req, res, {
|
|
187
|
+
code: "unknown_path",
|
|
188
|
+
path
|
|
189
|
+
}));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!p.transform?.length) {
|
|
193
|
+
res.sendFile(filePath);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let content = (await (0, node_fs_promises.readFile)(filePath)).toString();
|
|
197
|
+
let name = (0, node_path.basename)(filePath);
|
|
198
|
+
for (let transform of p.transform) {
|
|
199
|
+
let result = transform(req, res, {
|
|
200
|
+
content,
|
|
201
|
+
path,
|
|
202
|
+
name
|
|
203
|
+
});
|
|
204
|
+
content = result instanceof Promise ? await result : result;
|
|
205
|
+
}
|
|
206
|
+
res.type((0, node_path.extname)(name).slice(1)).send(content);
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
127
210
|
const unhandledError = () => async (err, req, res) => {
|
|
128
211
|
emitLog(req.app, "Unhandled error", {
|
|
129
212
|
level: "error",
|
|
@@ -639,6 +722,7 @@ exports.createApp = createApp;
|
|
|
639
722
|
exports.cspNonce = cspNonce;
|
|
640
723
|
exports.dir = dir;
|
|
641
724
|
exports.emitLog = emitLog;
|
|
725
|
+
exports.files = files;
|
|
642
726
|
exports.getEffectiveLocale = getEffectiveLocale;
|
|
643
727
|
exports.getLocales = getLocales;
|
|
644
728
|
exports.getStatusMessage = getStatusMessage;
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,19 @@ type DirParams = Partial<Pick<ResolveFilePathParams, "supportedLocales" | "index
|
|
|
75
75
|
*/
|
|
76
76
|
declare const dir$1: Controller<DirParams>;
|
|
77
77
|
|
|
78
|
+
type FilesParams = {
|
|
79
|
+
base: string | string[];
|
|
80
|
+
path?: string | ((req: Request) => string);
|
|
81
|
+
extensions?: string[];
|
|
82
|
+
languages?: (req: Request) => string[];
|
|
83
|
+
transform?: TransformContent[];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Serves files from the specified directory path in a locale-aware
|
|
87
|
+
* fashion after applying optional transforms.
|
|
88
|
+
*/
|
|
89
|
+
declare const files: Controller<string | FilesParams>;
|
|
90
|
+
|
|
78
91
|
type ErrorController<T = void> = (params: T) => ErrorRequestHandler;
|
|
79
92
|
|
|
80
93
|
declare const unhandledError: ErrorController;
|
|
@@ -1166,4 +1179,4 @@ declare function servePipeableStream(req: Request, res: Response): ({
|
|
|
1166
1179
|
pipe
|
|
1167
1180
|
}: PipeableStream, error?: unknown) => Promise<void>;
|
|
1168
1181
|
|
|
1169
|
-
export { Controller, DirParams, ErrorController, LangParams, LogEventPayload, LogLevel, LogOptions, Middleware, MiddlewareSet, RenderStatus, ReqCtx, ResolveFilePathParams, TransformContent, build, cli, createApp, cspNonce, dir$1 as dir, emitLog, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang$1 as lang, log, renderStatus, requestEvents, resolveFilePath, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
|
1182
|
+
export { Controller, DirParams, ErrorController, FilesParams, LangParams, LogEventPayload, LogLevel, LogOptions, Middleware, MiddlewareSet, RenderStatus, ReqCtx, ResolveFilePathParams, TransformContent, build, cli, createApp, cspNonce, dir$1 as dir, emitLog, files, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang$1 as lang, log, renderStatus, requestEvents, resolveFilePath, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
package/dist/index.mjs
CHANGED
|
@@ -98,6 +98,89 @@ const dir = ({ path, name = defaultName, ext = defaultExt, transform, supportedL
|
|
|
98
98
|
};
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
const maxLanguages = 3;
|
|
102
|
+
async function resolve(...parts) {
|
|
103
|
+
let fullPath = join(...parts);
|
|
104
|
+
try {
|
|
105
|
+
if ((await lstat(fullPath)).isFile()) return fullPath;
|
|
106
|
+
} catch {}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
function getLanguageList(req) {
|
|
110
|
+
let langParam = req.query.lang;
|
|
111
|
+
if (langParam) return [String(langParam)];
|
|
112
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
113
|
+
let langs = /* @__PURE__ */ new Set();
|
|
114
|
+
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
115
|
+
let s = acceptedLanguages[i];
|
|
116
|
+
let [lang] = s.split(/[-_]/);
|
|
117
|
+
if (s === lang) langs.add(s);
|
|
118
|
+
else {
|
|
119
|
+
langs.add(s);
|
|
120
|
+
langs.add(lang);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Array.from(langs);
|
|
124
|
+
}
|
|
125
|
+
const defaultExtensions = ["html", "htm"];
|
|
126
|
+
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
127
|
+
const defaultLanguages = getLanguageList;
|
|
128
|
+
/**
|
|
129
|
+
* Serves files from the specified directory path in a locale-aware
|
|
130
|
+
* fashion after applying optional transforms.
|
|
131
|
+
*/
|
|
132
|
+
const files = (params) => {
|
|
133
|
+
let p = typeof params === "string" ? { base: params } : params;
|
|
134
|
+
let bases = Array.isArray(p.base) ? p.base : [p.base];
|
|
135
|
+
let exts = p.extensions ?? defaultExtensions;
|
|
136
|
+
return async (req, res) => {
|
|
137
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
138
|
+
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
139
|
+
if (path.includes("../")) {
|
|
140
|
+
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
141
|
+
res.status(400).send(await req.app.renderStatus?.(req, res, {
|
|
142
|
+
code: "invalid_path",
|
|
143
|
+
path
|
|
144
|
+
}));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
let filePath = null;
|
|
148
|
+
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
149
|
+
let base = bases[k];
|
|
150
|
+
for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
151
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
152
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
|
|
153
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
154
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
|
|
155
|
+
for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
|
|
156
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
|
|
157
|
+
}
|
|
158
|
+
if (filePath === null) {
|
|
159
|
+
emitLog(req.app, "Unknown path", { data: { path } });
|
|
160
|
+
res.status(404).send(await req.app.renderStatus?.(req, res, {
|
|
161
|
+
code: "unknown_path",
|
|
162
|
+
path
|
|
163
|
+
}));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!p.transform?.length) {
|
|
167
|
+
res.sendFile(filePath);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
let content = (await readFile(filePath)).toString();
|
|
171
|
+
let name = basename(filePath);
|
|
172
|
+
for (let transform of p.transform) {
|
|
173
|
+
let result = transform(req, res, {
|
|
174
|
+
content,
|
|
175
|
+
path,
|
|
176
|
+
name
|
|
177
|
+
});
|
|
178
|
+
content = result instanceof Promise ? await result : result;
|
|
179
|
+
}
|
|
180
|
+
res.type(extname(name).slice(1)).send(content);
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
101
184
|
const unhandledError = () => async (err, req, res) => {
|
|
102
185
|
emitLog(req.app, "Unhandled error", {
|
|
103
186
|
level: "error",
|
|
@@ -607,4 +690,4 @@ function servePipeableStream(req, res) {
|
|
|
607
690
|
};
|
|
608
691
|
}
|
|
609
692
|
|
|
610
|
-
export { build, cli, createApp, cspNonce, dir, emitLog, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang, log, renderStatus, requestEvents, resolveFilePath, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
|
693
|
+
export { build, cli, createApp, cspNonce, dir, emitLog, files, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang, log, renderStatus, requestEvents, resolveFilePath, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, join } from "node:path";
|
|
3
|
+
import type { Request } 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
|
+
|
|
8
|
+
const maxLanguages = 3;
|
|
9
|
+
|
|
10
|
+
async function resolve(...parts: string[]) {
|
|
11
|
+
let fullPath = join(...parts);
|
|
12
|
+
try {
|
|
13
|
+
if ((await lstat(fullPath)).isFile()) return fullPath;
|
|
14
|
+
} catch {}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ["en-US", "ru"] > ["en-US", "en", "ru"]
|
|
19
|
+
function getLanguageList(req: Request) {
|
|
20
|
+
let langParam = req.query.lang;
|
|
21
|
+
|
|
22
|
+
if (langParam) return [String(langParam)];
|
|
23
|
+
|
|
24
|
+
let acceptedLanguages = req.acceptsLanguages();
|
|
25
|
+
let langs = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < acceptedLanguages.length && i < maxLanguages; i++) {
|
|
28
|
+
let s = acceptedLanguages[i];
|
|
29
|
+
let [lang] = s.split(/[-_]/);
|
|
30
|
+
|
|
31
|
+
if (s === lang) langs.add(s);
|
|
32
|
+
else {
|
|
33
|
+
langs.add(s);
|
|
34
|
+
langs.add(lang);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return Array.from(langs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type FilesParams = {
|
|
42
|
+
base: string | string[];
|
|
43
|
+
path?: string | ((req: Request) => string);
|
|
44
|
+
extensions?: string[];
|
|
45
|
+
languages?: (req: Request) => string[];
|
|
46
|
+
transform?: TransformContent[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultExtensions = ["html", "htm"];
|
|
50
|
+
const defaultPath = (req: Request) => req.originalUrl.split("?")[0];
|
|
51
|
+
const defaultLanguages = getLanguageList;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Serves files from the specified directory path in a locale-aware
|
|
55
|
+
* fashion after applying optional transforms.
|
|
56
|
+
*/
|
|
57
|
+
export const files: Controller<string | FilesParams> = (params) => {
|
|
58
|
+
let p: FilesParams = typeof params === "string" ? { base: params } : params;
|
|
59
|
+
|
|
60
|
+
let bases = Array.isArray(p.base) ? p.base : [p.base];
|
|
61
|
+
let exts = p.extensions ?? defaultExtensions;
|
|
62
|
+
|
|
63
|
+
return async (req, res) => {
|
|
64
|
+
let langs = (p.languages ?? defaultLanguages)(req);
|
|
65
|
+
|
|
66
|
+
let path =
|
|
67
|
+
typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
68
|
+
|
|
69
|
+
if (path.includes("../")) {
|
|
70
|
+
emitLog(req.app, "Invalid path (potential traversal attempt)", {
|
|
71
|
+
data: { path },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
res.status(400).send(
|
|
75
|
+
await req.app.renderStatus?.(req, res, {
|
|
76
|
+
code: "invalid_path",
|
|
77
|
+
path,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// path: /x
|
|
85
|
+
// langs: en, ru
|
|
86
|
+
let filePath: string | null = null;
|
|
87
|
+
|
|
88
|
+
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
89
|
+
let base = bases[k];
|
|
90
|
+
|
|
91
|
+
// /x.en /x.ru
|
|
92
|
+
for (let i = 0; i < langs.length && filePath === null; i++)
|
|
93
|
+
filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
94
|
+
|
|
95
|
+
// /x
|
|
96
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
97
|
+
|
|
98
|
+
// /x.en.html /x.en.htm /x.ru.html /x.ru.htm
|
|
99
|
+
for (let i = 0; i < langs.length && filePath === null; i++) {
|
|
100
|
+
for (let j = 0; j < exts.length && filePath === null; j++)
|
|
101
|
+
filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// /x.html /x.htm
|
|
105
|
+
for (let i = 0; i < exts.length && filePath === null; i++)
|
|
106
|
+
filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
107
|
+
|
|
108
|
+
// /x.en/index.html /x.en/index.htm /x.ru/index.html /x.ru/index.htm
|
|
109
|
+
for (let i = 0; i < langs.length && filePath === null; i++) {
|
|
110
|
+
for (let j = 0; j < exts.length && filePath === null; j++)
|
|
111
|
+
filePath = await resolve(
|
|
112
|
+
base,
|
|
113
|
+
`${path}.${langs[i]}`,
|
|
114
|
+
`index.${exts[j]}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// /x/index.en.html /x/index.en.htm /x/index.ru.html /x/index.ru.htm
|
|
119
|
+
for (let i = 0; i < langs.length && filePath === null; i++) {
|
|
120
|
+
for (let j = 0; j < exts.length && filePath === null; j++)
|
|
121
|
+
filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// /x/index.html /x/index.htm
|
|
125
|
+
for (let i = 0; i < exts.length && filePath === null; i++)
|
|
126
|
+
filePath = await resolve(base, path, `index.${exts[i]}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (filePath === null) {
|
|
130
|
+
emitLog(req.app, "Unknown path", { data: { path } });
|
|
131
|
+
|
|
132
|
+
res.status(404).send(
|
|
133
|
+
await req.app.renderStatus?.(req, res, {
|
|
134
|
+
code: "unknown_path",
|
|
135
|
+
path,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!p.transform?.length) {
|
|
143
|
+
res.sendFile(filePath);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let content = (await readFile(filePath)).toString();
|
|
148
|
+
let name = basename(filePath);
|
|
149
|
+
|
|
150
|
+
for (let transform of p.transform) {
|
|
151
|
+
let result = transform(req, res, { content, path, name });
|
|
152
|
+
|
|
153
|
+
content = result instanceof Promise ? await result : result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
res.type(extname(name).slice(1)).send(content);
|
|
157
|
+
};
|
|
158
|
+
};
|