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 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
@@ -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.9",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -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
+ };