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