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 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
@@ -1,4 +1,5 @@
1
1
  export * from "./src/controllers/dir.ts";
2
+ export * from "./src/controllers/files.ts";
2
3
  export * from "./src/controllers/unhandledError.ts";
3
4
  export * from "./src/controllers/unhandledRoute.ts";
4
5
  export * from "./src/lib/lang/getEffectiveLocale.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstage",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -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
+ };