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 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(acceptedLanguages) {
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 = getLanguageList(req.acceptsLanguages());
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 name = (0, node_path.basename)(filePath);
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((0, node_path.extname)(name).slice(1)).send(content);
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$1: Middleware<LangParams | void>;
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, 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 };
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(acceptedLanguages) {
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 = getLanguageList(req.acceptsLanguages());
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 name = basename(filePath);
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(extname(name).slice(1)).send(content);
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, dir, emitLog, files, getEffectiveLocale, getLocales, getStatusMessage, init, injectNonce, lang, log, renderStatus, requestEvents, resolveFilePath, serializeState, servePipeableStream, toLanguage, unhandledError, unhandledRoute };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstage",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -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(acceptedLanguages: string[]) {
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 = getLanguageList(req.acceptsLanguages());
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 name = basename(filePath);
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(extname(name).slice(1)).send(content);
157
+ res.type(ext.slice(1)).send(content);
150
158
  };
151
159
  };
@@ -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
- }