appstage 0.2.10 → 0.2.12
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/bin.js +7 -8
- package/dist/index.cjs +36 -94
- package/dist/index.d.ts +5 -62
- package/dist/index.mjs +37 -93
- package/index.ts +0 -2
- package/package.json +1 -1
- package/src/controllers/files.ts +59 -18
- package/src/scripts/utils/buildServer.ts +2 -2
- package/src/scripts/utils/{populateEntries.ts → setEntriesExport.ts} +5 -6
- package/src/controllers/dir.ts +0 -119
- package/src/utils/resolveFilePath.ts +0 -78
package/dist/bin.js
CHANGED
|
@@ -460,7 +460,7 @@ async function buildClient({ clientDir, watch, watchClient }, plugins) {
|
|
|
460
460
|
// src/scripts/utils/buildServer.ts
|
|
461
461
|
import esbuild2 from "esbuild";
|
|
462
462
|
|
|
463
|
-
// src/scripts/utils/
|
|
463
|
+
// src/scripts/utils/setEntriesExport.ts
|
|
464
464
|
import { writeFile } from "node:fs/promises";
|
|
465
465
|
|
|
466
466
|
// src/scripts/utils/toImportPath.ts
|
|
@@ -474,25 +474,24 @@ function toImportPath(relativePath, referencePath = ".") {
|
|
|
474
474
|
return importPath;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
// src/scripts/utils/
|
|
478
|
-
async function
|
|
477
|
+
// src/scripts/utils/setEntriesExport.ts
|
|
478
|
+
async function setEntriesExport({ entriesPath }) {
|
|
479
479
|
if (entriesPath === null) return;
|
|
480
480
|
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
481
481
|
let content = "";
|
|
482
482
|
if (serverEntries.length === 0) content = "export const entries = [];";
|
|
483
483
|
else {
|
|
484
484
|
content = "export const entries = (\n await Promise.all([";
|
|
485
|
-
for (let i = 0; i < serverEntries.length; i++)
|
|
485
|
+
for (let i = 0; i < serverEntries.length; i++)
|
|
486
486
|
content += `
|
|
487
|
-
// ${serverEntries[i].name}
|
|
488
487
|
import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
489
|
-
}
|
|
490
488
|
content += "\n ])\n).map(({ server }) => server);";
|
|
491
489
|
}
|
|
492
490
|
await writeFile(
|
|
493
491
|
entriesPath ?? "src/server/entries.ts",
|
|
494
492
|
`// Populated automatically during the build phase by picking
|
|
495
|
-
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
|
|
493
|
+
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
|
|
494
|
+
// Ignore this file if a custom set of entry exports is required.
|
|
496
495
|
${content}
|
|
497
496
|
`
|
|
498
497
|
);
|
|
@@ -502,7 +501,7 @@ ${content}
|
|
|
502
501
|
var appServerEntryPoints = ["src/server/index.ts"];
|
|
503
502
|
async function buildServer(params, plugins) {
|
|
504
503
|
let { serverDir, watch, watchServer } = params;
|
|
505
|
-
await
|
|
504
|
+
await setEntriesExport(params);
|
|
506
505
|
let buildOptions = {
|
|
507
506
|
...commonBuildOptions,
|
|
508
507
|
entryPoints: appServerEntryPoints,
|
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);
|
|
@@ -148,6 +68,15 @@ function getLanguageList(req) {
|
|
|
148
68
|
}
|
|
149
69
|
return Array.from(langs);
|
|
150
70
|
}
|
|
71
|
+
function matches(x, matcher) {
|
|
72
|
+
if (matcher === null || matcher === void 0) return true;
|
|
73
|
+
if (typeof matcher === "function") return matcher(x);
|
|
74
|
+
let patterns = Array.isArray(matcher) ? matcher : [matcher];
|
|
75
|
+
for (let pattern of patterns) if (pattern instanceof RegExp) {
|
|
76
|
+
if (pattern.test(x)) return true;
|
|
77
|
+
} else if (pattern === x) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
151
80
|
const defaultExtensions = ["html", "htm"];
|
|
152
81
|
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
153
82
|
const defaultLanguages = getLanguageList;
|
|
@@ -162,6 +91,14 @@ const files = (params) => {
|
|
|
162
91
|
return async (req, res) => {
|
|
163
92
|
let langs = (p.languages ?? defaultLanguages)(req);
|
|
164
93
|
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
94
|
+
if (!matches(path, p.matches)) {
|
|
95
|
+
emitLog(req.app, "Unmatched path", { data: { path } });
|
|
96
|
+
res.status(404).send(await req.app.renderStatus?.(req, res, {
|
|
97
|
+
code: "unmatched_path",
|
|
98
|
+
path
|
|
99
|
+
}));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
165
102
|
if (path.includes("../")) {
|
|
166
103
|
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
167
104
|
res.status(400).send(await req.app.renderStatus?.(req, res, {
|
|
@@ -173,10 +110,12 @@ const files = (params) => {
|
|
|
173
110
|
let filePath = null;
|
|
174
111
|
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
175
112
|
let base = bases[k];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
113
|
+
if (!path.endsWith("/")) {
|
|
114
|
+
for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
115
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
116
|
+
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]}`);
|
|
117
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
118
|
+
}
|
|
180
119
|
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
120
|
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
121
|
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
|
|
@@ -194,16 +133,17 @@ const files = (params) => {
|
|
|
194
133
|
return;
|
|
195
134
|
}
|
|
196
135
|
let content = (await (0, node_fs_promises.readFile)(filePath)).toString();
|
|
197
|
-
let
|
|
136
|
+
let ext = (0, node_path.extname)(filePath);
|
|
137
|
+
let name = (0, node_path.basename)(filePath, ext);
|
|
198
138
|
for (let transform of p.transform) {
|
|
199
139
|
let result = transform(req, res, {
|
|
200
140
|
content,
|
|
201
|
-
path,
|
|
141
|
+
path: filePath,
|
|
202
142
|
name
|
|
203
143
|
});
|
|
204
144
|
content = result instanceof Promise ? await result : result;
|
|
205
145
|
}
|
|
206
|
-
res.type(
|
|
146
|
+
res.type(ext.slice(1)).send(content);
|
|
207
147
|
};
|
|
208
148
|
};
|
|
209
149
|
|
|
@@ -226,6 +166,10 @@ const unhandledRoute = () => async (req, res) => {
|
|
|
226
166
|
res.status(404).send(await req.app.renderStatus?.(req, res, "unhandled_route"));
|
|
227
167
|
};
|
|
228
168
|
|
|
169
|
+
function toLanguage(locale) {
|
|
170
|
+
return locale.split(/[-_]/)[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
229
173
|
function getEffectiveLocale(preferredLocales, supportedLocales) {
|
|
230
174
|
if (!supportedLocales || supportedLocales.length === 0) return void 0;
|
|
231
175
|
if (!preferredLocales || preferredLocales.length === 0) return supportedLocales[0];
|
|
@@ -485,19 +429,19 @@ function toImportPath(relativePath, referencePath = ".") {
|
|
|
485
429
|
return importPath;
|
|
486
430
|
}
|
|
487
431
|
|
|
488
|
-
async function
|
|
432
|
+
async function setEntriesExport({ entriesPath }) {
|
|
489
433
|
if (entriesPath === null) return;
|
|
490
434
|
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
491
435
|
let content = "";
|
|
492
436
|
if (serverEntries.length === 0) content = "export const entries = [];";
|
|
493
437
|
else {
|
|
494
438
|
content = "export const entries = (\n await Promise.all([";
|
|
495
|
-
for (let i = 0; i < serverEntries.length; i++) content += `\n
|
|
496
|
-
import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
439
|
+
for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
497
440
|
content += "\n ])\n).map(({ server }) => server);";
|
|
498
441
|
}
|
|
499
442
|
await (0, node_fs_promises.writeFile)(entriesPath ?? "src/server/entries.ts", `// Populated automatically during the build phase by picking
|
|
500
|
-
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
|
|
443
|
+
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
|
|
444
|
+
// Ignore this file if a custom set of entry exports is required.
|
|
501
445
|
${content}
|
|
502
446
|
`);
|
|
503
447
|
}
|
|
@@ -505,7 +449,7 @@ ${content}
|
|
|
505
449
|
const appServerEntryPoints = ["src/server/index.ts"];
|
|
506
450
|
async function buildServer(params, plugins) {
|
|
507
451
|
let { serverDir, watch, watchServer } = params;
|
|
508
|
-
await
|
|
452
|
+
await setEntriesExport(params);
|
|
509
453
|
let buildOptions = {
|
|
510
454
|
...commonBuildOptions,
|
|
511
455
|
entryPoints: appServerEntryPoints,
|
|
@@ -720,7 +664,6 @@ exports.build = build;
|
|
|
720
664
|
exports.cli = cli;
|
|
721
665
|
exports.createApp = createApp;
|
|
722
666
|
exports.cspNonce = cspNonce;
|
|
723
|
-
exports.dir = dir;
|
|
724
667
|
exports.emitLog = emitLog;
|
|
725
668
|
exports.files = files;
|
|
726
669
|
exports.getEffectiveLocale = getEffectiveLocale;
|
|
@@ -732,7 +675,6 @@ exports.lang = lang;
|
|
|
732
675
|
exports.log = log;
|
|
733
676
|
exports.renderStatus = renderStatus;
|
|
734
677
|
exports.requestEvents = requestEvents;
|
|
735
|
-
exports.resolveFilePath = resolveFilePath;
|
|
736
678
|
exports.serializeState = serializeState;
|
|
737
679
|
exports.servePipeableStream = servePipeableStream;
|
|
738
680
|
exports.toLanguage = toLanguage;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,68 +16,11 @@ type TransformContent = (req: Request, res: Response, params: {
|
|
|
16
16
|
name?: string;
|
|
17
17
|
}) => string | Promise<string>;
|
|
18
18
|
|
|
19
|
-
type
|
|
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
|
-
|
|
19
|
+
type StringMatcher = string | RegExp | (string | RegExp)[] | ((x: string) => boolean) | null;
|
|
78
20
|
type FilesParams = {
|
|
79
21
|
base: string | string[];
|
|
80
|
-
path?: string | ((req: Request) => string);
|
|
22
|
+
path?: string | ((req: Request) => string); /** Specifies which paths should be accepted. */
|
|
23
|
+
matches?: StringMatcher;
|
|
81
24
|
extensions?: string[];
|
|
82
25
|
languages?: (req: Request) => string[];
|
|
83
26
|
transform?: TransformContent[];
|
|
@@ -136,7 +79,7 @@ type LangParams = {
|
|
|
136
79
|
shouldRedirect?: boolean;
|
|
137
80
|
langCookieOptions?: CookieOptions;
|
|
138
81
|
};
|
|
139
|
-
declare const lang
|
|
82
|
+
declare const lang: Middleware<LangParams | void>;
|
|
140
83
|
|
|
141
84
|
/**
|
|
142
85
|
* Adds event handlers, like logging, to essential request phases.
|
|
@@ -1179,4 +1122,4 @@ declare function servePipeableStream(req: Request, res: Response): ({
|
|
|
1179
1122
|
pipe
|
|
1180
1123
|
}: PipeableStream, error?: unknown) => Promise<void>;
|
|
1181
1124
|
|
|
1182
|
-
export { Controller,
|
|
1125
|
+
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);
|
|
@@ -122,6 +42,15 @@ function getLanguageList(req) {
|
|
|
122
42
|
}
|
|
123
43
|
return Array.from(langs);
|
|
124
44
|
}
|
|
45
|
+
function matches(x, matcher) {
|
|
46
|
+
if (matcher === null || matcher === void 0) return true;
|
|
47
|
+
if (typeof matcher === "function") return matcher(x);
|
|
48
|
+
let patterns = Array.isArray(matcher) ? matcher : [matcher];
|
|
49
|
+
for (let pattern of patterns) if (pattern instanceof RegExp) {
|
|
50
|
+
if (pattern.test(x)) return true;
|
|
51
|
+
} else if (pattern === x) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
125
54
|
const defaultExtensions = ["html", "htm"];
|
|
126
55
|
const defaultPath = (req) => req.originalUrl.split("?")[0];
|
|
127
56
|
const defaultLanguages = getLanguageList;
|
|
@@ -136,6 +65,14 @@ const files = (params) => {
|
|
|
136
65
|
return async (req, res) => {
|
|
137
66
|
let langs = (p.languages ?? defaultLanguages)(req);
|
|
138
67
|
let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
68
|
+
if (!matches(path, p.matches)) {
|
|
69
|
+
emitLog(req.app, "Unmatched path", { data: { path } });
|
|
70
|
+
res.status(404).send(await req.app.renderStatus?.(req, res, {
|
|
71
|
+
code: "unmatched_path",
|
|
72
|
+
path
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
139
76
|
if (path.includes("../")) {
|
|
140
77
|
emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
|
|
141
78
|
res.status(400).send(await req.app.renderStatus?.(req, res, {
|
|
@@ -147,10 +84,12 @@ const files = (params) => {
|
|
|
147
84
|
let filePath = null;
|
|
148
85
|
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
149
86
|
let base = bases[k];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
87
|
+
if (!path.endsWith("/")) {
|
|
88
|
+
for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
89
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
90
|
+
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]}`);
|
|
91
|
+
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
92
|
+
}
|
|
154
93
|
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
94
|
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
95
|
for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
|
|
@@ -168,16 +107,17 @@ const files = (params) => {
|
|
|
168
107
|
return;
|
|
169
108
|
}
|
|
170
109
|
let content = (await readFile(filePath)).toString();
|
|
171
|
-
let
|
|
110
|
+
let ext = extname(filePath);
|
|
111
|
+
let name = basename(filePath, ext);
|
|
172
112
|
for (let transform of p.transform) {
|
|
173
113
|
let result = transform(req, res, {
|
|
174
114
|
content,
|
|
175
|
-
path,
|
|
115
|
+
path: filePath,
|
|
176
116
|
name
|
|
177
117
|
});
|
|
178
118
|
content = result instanceof Promise ? await result : result;
|
|
179
119
|
}
|
|
180
|
-
res.type(
|
|
120
|
+
res.type(ext.slice(1)).send(content);
|
|
181
121
|
};
|
|
182
122
|
};
|
|
183
123
|
|
|
@@ -200,6 +140,10 @@ const unhandledRoute = () => async (req, res) => {
|
|
|
200
140
|
res.status(404).send(await req.app.renderStatus?.(req, res, "unhandled_route"));
|
|
201
141
|
};
|
|
202
142
|
|
|
143
|
+
function toLanguage(locale) {
|
|
144
|
+
return locale.split(/[-_]/)[0];
|
|
145
|
+
}
|
|
146
|
+
|
|
203
147
|
function getEffectiveLocale(preferredLocales, supportedLocales) {
|
|
204
148
|
if (!supportedLocales || supportedLocales.length === 0) return void 0;
|
|
205
149
|
if (!preferredLocales || preferredLocales.length === 0) return supportedLocales[0];
|
|
@@ -459,19 +403,19 @@ function toImportPath(relativePath, referencePath = ".") {
|
|
|
459
403
|
return importPath;
|
|
460
404
|
}
|
|
461
405
|
|
|
462
|
-
async function
|
|
406
|
+
async function setEntriesExport({ entriesPath }) {
|
|
463
407
|
if (entriesPath === null) return;
|
|
464
408
|
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
465
409
|
let content = "";
|
|
466
410
|
if (serverEntries.length === 0) content = "export const entries = [];";
|
|
467
411
|
else {
|
|
468
412
|
content = "export const entries = (\n await Promise.all([";
|
|
469
|
-
for (let i = 0; i < serverEntries.length; i++) content += `\n
|
|
470
|
-
import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
413
|
+
for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
471
414
|
content += "\n ])\n).map(({ server }) => server);";
|
|
472
415
|
}
|
|
473
416
|
await writeFile(entriesPath ?? "src/server/entries.ts", `// Populated automatically during the build phase by picking
|
|
474
|
-
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
|
|
417
|
+
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
|
|
418
|
+
// Ignore this file if a custom set of entry exports is required.
|
|
475
419
|
${content}
|
|
476
420
|
`);
|
|
477
421
|
}
|
|
@@ -479,7 +423,7 @@ ${content}
|
|
|
479
423
|
const appServerEntryPoints = ["src/server/index.ts"];
|
|
480
424
|
async function buildServer(params, plugins) {
|
|
481
425
|
let { serverDir, watch, watchServer } = params;
|
|
482
|
-
await
|
|
426
|
+
await setEntriesExport(params);
|
|
483
427
|
let buildOptions = {
|
|
484
428
|
...commonBuildOptions,
|
|
485
429
|
entryPoints: appServerEntryPoints,
|
|
@@ -690,4 +634,4 @@ function servePipeableStream(req, res) {
|
|
|
690
634
|
};
|
|
691
635
|
}
|
|
692
636
|
|
|
693
|
-
export { build, cli, createApp, cspNonce,
|
|
637
|
+
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
package/src/controllers/files.ts
CHANGED
|
@@ -5,6 +5,13 @@ import type { Controller } from "../types/Controller.ts";
|
|
|
5
5
|
import type { TransformContent } from "../types/TransformContent.ts";
|
|
6
6
|
import { emitLog } from "../utils/emitLog.ts";
|
|
7
7
|
|
|
8
|
+
type StringMatcher =
|
|
9
|
+
| string
|
|
10
|
+
| RegExp
|
|
11
|
+
| (string | RegExp)[]
|
|
12
|
+
| ((x: string) => boolean)
|
|
13
|
+
| null;
|
|
14
|
+
|
|
8
15
|
const maxLanguages = 3;
|
|
9
16
|
|
|
10
17
|
async function resolve(...parts: string[]) {
|
|
@@ -38,9 +45,27 @@ function getLanguageList(req: Request) {
|
|
|
38
45
|
return Array.from(langs);
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function matches(x: string, matcher: StringMatcher | undefined) {
|
|
49
|
+
if (matcher === null || matcher === undefined) return true;
|
|
50
|
+
|
|
51
|
+
if (typeof matcher === "function") return matcher(x);
|
|
52
|
+
|
|
53
|
+
let patterns = Array.isArray(matcher) ? matcher : [matcher];
|
|
54
|
+
|
|
55
|
+
for (let pattern of patterns) {
|
|
56
|
+
if (pattern instanceof RegExp) {
|
|
57
|
+
if (pattern.test(x)) return true;
|
|
58
|
+
} else if (pattern === x) return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
export type FilesParams = {
|
|
42
65
|
base: string | string[];
|
|
43
66
|
path?: string | ((req: Request) => string);
|
|
67
|
+
/** Specifies which paths should be accepted. */
|
|
68
|
+
matches?: StringMatcher;
|
|
44
69
|
extensions?: string[];
|
|
45
70
|
languages?: (req: Request) => string[];
|
|
46
71
|
transform?: TransformContent[];
|
|
@@ -66,6 +91,19 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
66
91
|
let path =
|
|
67
92
|
typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
|
|
68
93
|
|
|
94
|
+
if (!matches(path, p.matches)) {
|
|
95
|
+
emitLog(req.app, "Unmatched path", { data: { path } });
|
|
96
|
+
|
|
97
|
+
res.status(404).send(
|
|
98
|
+
await req.app.renderStatus?.(req, res, {
|
|
99
|
+
code: "unmatched_path",
|
|
100
|
+
path,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
if (path.includes("../")) {
|
|
70
108
|
emitLog(req.app, "Invalid path (potential traversal attempt)", {
|
|
71
109
|
data: { path },
|
|
@@ -81,29 +119,31 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
81
119
|
return;
|
|
82
120
|
}
|
|
83
121
|
|
|
84
|
-
// path: /x
|
|
85
|
-
// langs: en, ru
|
|
86
122
|
let filePath: string | null = null;
|
|
87
123
|
|
|
124
|
+
// path: /x
|
|
125
|
+
// langs: en, ru
|
|
88
126
|
for (let k = 0; k < bases.length && filePath === null; k++) {
|
|
89
127
|
let base = bases[k];
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
if (!path.endsWith("/")) {
|
|
130
|
+
// /x.en /x.ru
|
|
131
|
+
for (let i = 0; i < langs.length && filePath === null; i++)
|
|
132
|
+
filePath = await resolve(base, `${path}.${langs[i]}`);
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
134
|
+
// /x
|
|
135
|
+
if (filePath === null) filePath = await resolve(base, path);
|
|
97
136
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
// /x.en.html /x.en.htm /x.ru.html /x.ru.htm
|
|
138
|
+
for (let i = 0; i < langs.length && filePath === null; i++) {
|
|
139
|
+
for (let j = 0; j < exts.length && filePath === null; j++)
|
|
140
|
+
filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
|
|
141
|
+
}
|
|
103
142
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
143
|
+
// /x.html /x.htm
|
|
144
|
+
for (let i = 0; i < exts.length && filePath === null; i++)
|
|
145
|
+
filePath = await resolve(base, `${path}.${exts[i]}`);
|
|
146
|
+
}
|
|
107
147
|
|
|
108
148
|
// /x.en/index.html /x.en/index.htm /x.ru/index.html /x.ru/index.htm
|
|
109
149
|
for (let i = 0; i < langs.length && filePath === null; i++) {
|
|
@@ -145,14 +185,15 @@ export const files: Controller<string | FilesParams> = (params) => {
|
|
|
145
185
|
}
|
|
146
186
|
|
|
147
187
|
let content = (await readFile(filePath)).toString();
|
|
148
|
-
let
|
|
188
|
+
let ext = extname(filePath);
|
|
189
|
+
let name = basename(filePath, ext);
|
|
149
190
|
|
|
150
191
|
for (let transform of p.transform) {
|
|
151
|
-
let result = transform(req, res, { content, path, name });
|
|
192
|
+
let result = transform(req, res, { content, path: filePath, name });
|
|
152
193
|
|
|
153
194
|
content = result instanceof Promise ? await result : result;
|
|
154
195
|
}
|
|
155
196
|
|
|
156
|
-
res.type(
|
|
197
|
+
res.type(ext.slice(1)).send(content);
|
|
157
198
|
};
|
|
158
199
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import esbuild, { type BuildOptions, type Plugin } from "esbuild";
|
|
2
2
|
import { commonBuildOptions } from "../const/commonBuildOptions.ts";
|
|
3
3
|
import type { BuildParams } from "../types/BuildParams.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { setEntriesExport } from "./setEntriesExport.ts";
|
|
5
5
|
|
|
6
6
|
const appServerEntryPoints = ["src/server/index.ts"];
|
|
7
7
|
|
|
8
8
|
export async function buildServer(params: BuildParams, plugins?: Plugin[]) {
|
|
9
9
|
let { serverDir, watch, watchServer } = params;
|
|
10
10
|
|
|
11
|
-
await
|
|
11
|
+
await setEntriesExport(params);
|
|
12
12
|
|
|
13
13
|
let buildOptions: BuildOptions = {
|
|
14
14
|
...commonBuildOptions,
|
|
@@ -3,7 +3,7 @@ import type { BuildParams } from "../types/BuildParams.ts";
|
|
|
3
3
|
import { getEntryPoints } from "./getEntryPoints.ts";
|
|
4
4
|
import { toImportPath } from "./toImportPath.ts";
|
|
5
5
|
|
|
6
|
-
export async function
|
|
6
|
+
export async function setEntriesExport({ entriesPath }: BuildParams) {
|
|
7
7
|
if (entriesPath === null) return;
|
|
8
8
|
|
|
9
9
|
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
@@ -13,10 +13,8 @@ export async function populateEntries({ entriesPath }: BuildParams) {
|
|
|
13
13
|
else {
|
|
14
14
|
content = "export const entries = (\n await Promise.all([";
|
|
15
15
|
|
|
16
|
-
for (let i = 0; i < serverEntries.length; i++)
|
|
17
|
-
content += `\n
|
|
18
|
-
import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
19
|
-
}
|
|
16
|
+
for (let i = 0; i < serverEntries.length; i++)
|
|
17
|
+
content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
20
18
|
|
|
21
19
|
content += "\n ])\n).map(({ server }) => server);";
|
|
22
20
|
}
|
|
@@ -24,7 +22,8 @@ export async function populateEntries({ entriesPath }: BuildParams) {
|
|
|
24
22
|
await writeFile(
|
|
25
23
|
entriesPath ?? "src/server/entries.ts",
|
|
26
24
|
`// Populated automatically during the build phase by picking
|
|
27
|
-
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
|
|
25
|
+
// all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
|
|
26
|
+
// Ignore this file if a custom set of entry exports is required.
|
|
28
27
|
${content}
|
|
29
28
|
`,
|
|
30
29
|
);
|
package/src/controllers/dir.ts
DELETED
|
@@ -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
|
-
}
|