appstage 0.2.1
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/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/bin/build.js +532 -0
- package/dist/bin/startDev.js +545 -0
- package/dist/bin/startProd.js +545 -0
- package/dist/index.cjs +671 -0
- package/dist/index.d.ts +1162 -0
- package/dist/index.mjs +622 -0
- package/index.ts +32 -0
- package/package.json +39 -0
- package/src/controllers/dir.ts +119 -0
- package/src/controllers/unhandledError.ts +15 -0
- package/src/controllers/unhandledRoute.ts +14 -0
- package/src/lib/lang/getEffectiveLocale.ts +52 -0
- package/src/lib/lang/getLocales.ts +10 -0
- package/src/lib/lang/toLanguage.ts +3 -0
- package/src/lib/logger/LogOptions.ts +8 -0
- package/src/lib/logger/ansiEscapeCodes.ts +6 -0
- package/src/lib/logger/levelColors.ts +4 -0
- package/src/lib/logger/log.ts +82 -0
- package/src/middleware/init.ts +22 -0
- package/src/middleware/lang.ts +83 -0
- package/src/middleware/requestEvents.ts +29 -0
- package/src/scripts/bin/build.ts +5 -0
- package/src/scripts/bin/startDev.ts +5 -0
- package/src/scripts/bin/startProd.ts +5 -0
- package/src/scripts/build.ts +45 -0
- package/src/scripts/cli.ts +46 -0
- package/src/scripts/const/commonBuildOptions.ts +13 -0
- package/src/scripts/const/entryExtensions.ts +1 -0
- package/src/scripts/start.ts +18 -0
- package/src/scripts/types/BuildParams.ts +9 -0
- package/src/scripts/utils/buildClient.ts +41 -0
- package/src/scripts/utils/buildServer.ts +35 -0
- package/src/scripts/utils/buildServerCSS.ts +38 -0
- package/src/scripts/utils/createPostbuildPlugins.ts +66 -0
- package/src/scripts/utils/getEntries.ts +22 -0
- package/src/scripts/utils/getEntryPoints.ts +25 -0
- package/src/scripts/utils/getFirstAvailable.ts +22 -0
- package/src/scripts/utils/populateEntries.ts +28 -0
- package/src/scripts/utils/toImportPath.ts +12 -0
- package/src/types/Controller.ts +4 -0
- package/src/types/ErrorController.ts +3 -0
- package/src/types/LogEventPayload.ts +12 -0
- package/src/types/LogLevel.ts +1 -0
- package/src/types/Middleware.ts +7 -0
- package/src/types/MiddlewareSet.ts +3 -0
- package/src/types/RenderStatus.ts +9 -0
- package/src/types/ReqCtx.ts +11 -0
- package/src/types/TransformContent.ts +11 -0
- package/src/types/express.d.ts +15 -0
- package/src/types/global.d.ts +17 -0
- package/src/utils/createApp.ts +44 -0
- package/src/utils/cspNonce.ts +6 -0
- package/src/utils/emitLog.ts +18 -0
- package/src/utils/getEntries.ts +22 -0
- package/src/utils/getStatusMessage.ts +5 -0
- package/src/utils/injectNonce.ts +7 -0
- package/src/utils/renderStatus.ts +20 -0
- package/src/utils/resolveFilePath.ts +78 -0
- package/src/utils/serializeState.ts +3 -0
- package/src/utils/servePipeableStream.ts +32 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import esbuild, { type BuildOptions, type Plugin } from "esbuild";
|
|
2
|
+
import { commonBuildOptions } from "../const/commonBuildOptions.ts";
|
|
3
|
+
import type { BuildParams } from "../types/BuildParams.ts";
|
|
4
|
+
import { populateEntries } from "./populateEntries.ts";
|
|
5
|
+
|
|
6
|
+
export async function buildServer(
|
|
7
|
+
{ targetDir, watch, watchServer }: BuildParams,
|
|
8
|
+
plugins?: Plugin[],
|
|
9
|
+
) {
|
|
10
|
+
await populateEntries();
|
|
11
|
+
|
|
12
|
+
let buildOptions: BuildOptions = {
|
|
13
|
+
...commonBuildOptions,
|
|
14
|
+
entryPoints: ["src/server/index.ts"],
|
|
15
|
+
bundle: true,
|
|
16
|
+
splitting: true,
|
|
17
|
+
outdir: `${targetDir}/server`,
|
|
18
|
+
platform: "node",
|
|
19
|
+
format: "esm",
|
|
20
|
+
packages: "external",
|
|
21
|
+
plugins,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (watch || watchServer) {
|
|
25
|
+
let ctx = await esbuild.context(buildOptions);
|
|
26
|
+
|
|
27
|
+
await ctx.watch();
|
|
28
|
+
|
|
29
|
+
return async () => {
|
|
30
|
+
await ctx.dispose();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await esbuild.build(buildOptions);
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import esbuild, { type BuildOptions, type Plugin } from "esbuild";
|
|
2
|
+
import { commonBuildOptions } from "../const/commonBuildOptions.ts";
|
|
3
|
+
import type { BuildParams } from "../types/BuildParams.ts";
|
|
4
|
+
import { getEntryPoints } from "./getEntryPoints.ts";
|
|
5
|
+
|
|
6
|
+
export async function buildServerCSS(
|
|
7
|
+
{ targetDir, watch, watchServer }: BuildParams,
|
|
8
|
+
plugins?: Plugin[],
|
|
9
|
+
) {
|
|
10
|
+
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
11
|
+
|
|
12
|
+
let buildOptions: BuildOptions = {
|
|
13
|
+
...commonBuildOptions,
|
|
14
|
+
entryPoints: serverEntries.map(({ name, path }) => ({
|
|
15
|
+
in: path,
|
|
16
|
+
out: name,
|
|
17
|
+
})),
|
|
18
|
+
bundle: true,
|
|
19
|
+
splitting: false,
|
|
20
|
+
outdir: `${targetDir}/server-css`,
|
|
21
|
+
platform: "node",
|
|
22
|
+
format: "esm",
|
|
23
|
+
packages: "external",
|
|
24
|
+
plugins,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (watch || watchServer) {
|
|
28
|
+
let ctx = await esbuild.context(buildOptions);
|
|
29
|
+
|
|
30
|
+
await ctx.watch();
|
|
31
|
+
|
|
32
|
+
return async () => {
|
|
33
|
+
await ctx.dispose();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await esbuild.build(buildOptions);
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir, readdir, rename, rm } from "node:fs/promises";
|
|
2
|
+
import type { Plugin } from "esbuild";
|
|
3
|
+
import type { BuildParams } from "../types/BuildParams.ts";
|
|
4
|
+
|
|
5
|
+
export function createPostbuildPlugins(
|
|
6
|
+
{ targetDir, publicAssetsDir }: BuildParams,
|
|
7
|
+
onServerRebuild: () => void,
|
|
8
|
+
) {
|
|
9
|
+
let serverPlugins: Plugin[] = [
|
|
10
|
+
{
|
|
11
|
+
name: "skip-css",
|
|
12
|
+
setup(build) {
|
|
13
|
+
/** @see https://github.com/evanw/esbuild/issues/599#issuecomment-745118158 */
|
|
14
|
+
build.onLoad({ filter: /\.css$/ }, () => ({ contents: "" }));
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "postbuild-server",
|
|
19
|
+
setup(build) {
|
|
20
|
+
build.onEnd(() => {
|
|
21
|
+
onServerRebuild();
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
let serverCSSPlugins: Plugin[] = [
|
|
28
|
+
{
|
|
29
|
+
name: "postbuild-server-css",
|
|
30
|
+
setup(build) {
|
|
31
|
+
build.onEnd(async () => {
|
|
32
|
+
let dir = `${targetDir}/server-css`;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let files = (await readdir(dir)).filter((name) =>
|
|
36
|
+
name.endsWith(".css"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (files.length === 0) return;
|
|
40
|
+
|
|
41
|
+
await mkdir(`${publicAssetsDir}/-`, { recursive: true });
|
|
42
|
+
|
|
43
|
+
await Promise.all(
|
|
44
|
+
files.map(async (name) => {
|
|
45
|
+
let dir = `${publicAssetsDir}/-/${name.slice(0, -4)}`;
|
|
46
|
+
|
|
47
|
+
await mkdir(dir, { recursive: true });
|
|
48
|
+
await rename(
|
|
49
|
+
`${targetDir}/server-css/${name}`,
|
|
50
|
+
`${dir}/index.css`,
|
|
51
|
+
);
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await rm(dir, { recursive: true, force: true });
|
|
56
|
+
} catch {}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
serverPlugins,
|
|
64
|
+
serverCSSPlugins,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function getEntries() {
|
|
5
|
+
let cwd = process.cwd();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
let list = await readdir(join(cwd, "src/entries"));
|
|
9
|
+
|
|
10
|
+
let dirs = await Promise.all(
|
|
11
|
+
list.map(async (name) => {
|
|
12
|
+
let itemStat = await lstat(join(cwd, "src/entries", name));
|
|
13
|
+
|
|
14
|
+
return itemStat.isDirectory() ? name : undefined;
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return dirs.filter((dir) => dir !== undefined);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getEntries } from "./getEntries.ts";
|
|
2
|
+
import { getFirstAvailable } from "./getFirstAvailable.ts";
|
|
3
|
+
|
|
4
|
+
type EntryPoint = {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function getEntryPoints(
|
|
10
|
+
path: string | string[],
|
|
11
|
+
): Promise<EntryPoint[]> {
|
|
12
|
+
let entries = await getEntries();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
await Promise.all(
|
|
16
|
+
entries.map(async (name) => {
|
|
17
|
+
let resolvedPath = await getFirstAvailable(`src/entries/${name}`, path);
|
|
18
|
+
|
|
19
|
+
return resolvedPath === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: { name, path: resolvedPath };
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
).filter((item) => item !== undefined);
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { entryExtensions } from "../const/entryExtensions.ts";
|
|
4
|
+
|
|
5
|
+
export async function getFirstAvailable(
|
|
6
|
+
dirPath: string,
|
|
7
|
+
path: string | string[],
|
|
8
|
+
) {
|
|
9
|
+
let paths = Array.isArray(path) ? path : [path];
|
|
10
|
+
|
|
11
|
+
for (let filePath of paths) {
|
|
12
|
+
for (let ext of entryExtensions) {
|
|
13
|
+
let path = join(process.cwd(), dirPath, `${filePath}.${ext}`);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await access(path);
|
|
17
|
+
|
|
18
|
+
return path;
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { getEntryPoints } from "./getEntryPoints.ts";
|
|
3
|
+
import { toImportPath } from "./toImportPath.ts";
|
|
4
|
+
|
|
5
|
+
export async function populateEntries() {
|
|
6
|
+
let serverEntries = await getEntryPoints(["server", "server/index"]);
|
|
7
|
+
let content = "";
|
|
8
|
+
|
|
9
|
+
if (serverEntries.length === 0) content = "export const entries = [];";
|
|
10
|
+
else {
|
|
11
|
+
content = "export const entries = (\n await Promise.all([";
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < serverEntries.length; i++) {
|
|
14
|
+
content += `\n // ${serverEntries[i].name}
|
|
15
|
+
import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
content += "\n ])\n).map(({ server }) => server);";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await writeFile(
|
|
22
|
+
"src/server/entries.ts",
|
|
23
|
+
`// Populated automatically during the build phase by picking
|
|
24
|
+
// all server exports from 'src/entries/<entry_name>/server(/index)?.(js|ts)'
|
|
25
|
+
${content}
|
|
26
|
+
`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { join, posix, relative, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function toImportPath(relativePath: string, referencePath = ".") {
|
|
4
|
+
let cwd = process.cwd();
|
|
5
|
+
let importPath = posix.join(
|
|
6
|
+
...relative(join(cwd, referencePath), relativePath).split(sep),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
if (importPath && !/^\.+\//.test(importPath)) importPath = `./${importPath}`;
|
|
10
|
+
|
|
11
|
+
return importPath;
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import type { LogLevel } from "./LogLevel.ts";
|
|
3
|
+
|
|
4
|
+
export type LogEventPayload = {
|
|
5
|
+
timestamp?: number;
|
|
6
|
+
level?: LogLevel;
|
|
7
|
+
message?: string | Error;
|
|
8
|
+
status?: number;
|
|
9
|
+
req?: Request;
|
|
10
|
+
res?: Response;
|
|
11
|
+
data?: unknown;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type LogLevel = "error" | "debug" | "info" | "warn";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Session request data collected on the server via `req.ctx` */
|
|
2
|
+
export type ReqCtx = {
|
|
3
|
+
/** Request ID */
|
|
4
|
+
id?: string;
|
|
5
|
+
/** CSP nonce */
|
|
6
|
+
nonce?: string;
|
|
7
|
+
/** When the request started to be handled */
|
|
8
|
+
startTime?: number;
|
|
9
|
+
/** User locale */
|
|
10
|
+
lang?: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import type { RenderStatus } from "../types/RenderStatus.ts";
|
|
3
|
+
import type { ReqCtx } from "./ReqCtx.ts";
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Express {
|
|
7
|
+
interface Request {
|
|
8
|
+
ctx: ReqCtx;
|
|
9
|
+
}
|
|
10
|
+
interface Application {
|
|
11
|
+
events?: EventEmitter;
|
|
12
|
+
renderStatus?: RenderStatus;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare module "*.svg" {
|
|
2
|
+
let content: string;
|
|
3
|
+
|
|
4
|
+
export default content;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module "*.txt" {
|
|
8
|
+
let content: string;
|
|
9
|
+
|
|
10
|
+
export default content;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module "*.html" {
|
|
14
|
+
let content: string;
|
|
15
|
+
|
|
16
|
+
export default content;
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { log } from "../lib/logger/log.ts";
|
|
4
|
+
import { init } from "../middleware/init.ts";
|
|
5
|
+
import { requestEvents } from "../middleware/requestEvents.ts";
|
|
6
|
+
import type { LogEventPayload } from "../types/LogEventPayload.ts";
|
|
7
|
+
import { emitLog } from "./emitLog.ts";
|
|
8
|
+
import { renderStatus } from "./renderStatus.ts";
|
|
9
|
+
|
|
10
|
+
export function createApp(callback?: () => void | Promise<void>) {
|
|
11
|
+
let app = express();
|
|
12
|
+
|
|
13
|
+
if (!app.events) app.events = new EventEmitter();
|
|
14
|
+
|
|
15
|
+
let host = process.env.APP_HOST || "localhost";
|
|
16
|
+
let port = Number(process.env.APP_PORT) || 80;
|
|
17
|
+
|
|
18
|
+
let listen = () => {
|
|
19
|
+
app.listen(port, host, () => {
|
|
20
|
+
let location = `http://${host}:${port}/`;
|
|
21
|
+
let env = `NODE_ENV=${process.env.NODE_ENV}`;
|
|
22
|
+
|
|
23
|
+
emitLog(app, `Server running at ${location} (${env})`);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (process.env.NODE_ENV === "development")
|
|
28
|
+
app.events?.on("log", ({ message, ...payload }: LogEventPayload) => {
|
|
29
|
+
log(message, payload);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!app.renderStatus) app.renderStatus = renderStatus;
|
|
33
|
+
|
|
34
|
+
app.disable("x-powered-by");
|
|
35
|
+
app.use(init());
|
|
36
|
+
app.use(requestEvents());
|
|
37
|
+
|
|
38
|
+
let callbackResult = typeof callback === "function" ? callback() : null;
|
|
39
|
+
|
|
40
|
+
if (callbackResult instanceof Promise) callbackResult.then(listen);
|
|
41
|
+
else listen();
|
|
42
|
+
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Application } from "express";
|
|
2
|
+
import type { LogEventPayload } from "../types/LogEventPayload.ts";
|
|
3
|
+
|
|
4
|
+
export function emitLog(
|
|
5
|
+
app: Application,
|
|
6
|
+
message?: LogEventPayload["message"] | LogEventPayload,
|
|
7
|
+
payload?: LogEventPayload,
|
|
8
|
+
) {
|
|
9
|
+
let normalizedPayload: LogEventPayload = {
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
...payload,
|
|
12
|
+
...(typeof message === "string" || message instanceof Error
|
|
13
|
+
? { message }
|
|
14
|
+
: message),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return app.events?.emit("log", normalizedPayload);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function getEntries() {
|
|
5
|
+
let cwd = process.cwd();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
let list = await readdir(join(cwd, "src/entries"));
|
|
9
|
+
|
|
10
|
+
let dirs = await Promise.all(
|
|
11
|
+
list.map(async (name) => {
|
|
12
|
+
let itemStat = await lstat(join(cwd, "src/entries", name));
|
|
13
|
+
|
|
14
|
+
return itemStat.isDirectory() ? name : undefined;
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return dirs.filter((dir) => dir !== undefined);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
2
|
+
import type { RenderStatus } from "../types/RenderStatus.ts";
|
|
3
|
+
|
|
4
|
+
export const renderStatus: RenderStatus = async (req, res) => {
|
|
5
|
+
let { id, nonce } = req.ctx;
|
|
6
|
+
let statusText = `${res.statusCode} ${STATUS_CODES[res.statusCode]}`;
|
|
7
|
+
let date = `${new Date().toISOString().replace(/T/, " ").replace(/Z$/, "")} UTC`;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
"<!DOCTYPE html>" +
|
|
11
|
+
'<html><head><meta charset="utf-8"/>' +
|
|
12
|
+
'<meta name="viewport" content="width=device-width"/>' +
|
|
13
|
+
`<title>${statusText}</title>` +
|
|
14
|
+
`<style${nonce ? ` nonce="${nonce}"` : ""}>` +
|
|
15
|
+
"body{text-align:center;}</style></head>" +
|
|
16
|
+
`<body><h1>${statusText}</h1><hr/><p>` +
|
|
17
|
+
(id ? `<code>ID: ${id}</code><br/>` : "") +
|
|
18
|
+
`<code>${date}</code></p></body></html>`
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { emitLog } from "./emitLog.ts";
|
|
3
|
+
import { getStatusMessage } from "./getStatusMessage.ts";
|
|
4
|
+
|
|
5
|
+
type PipeableStream = {
|
|
6
|
+
pipe: <Writable extends NodeJS.WritableStream>(
|
|
7
|
+
destination: Writable,
|
|
8
|
+
) => Writable;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function servePipeableStream(req: Request, res: Response) {
|
|
12
|
+
return async ({ pipe }: PipeableStream, error?: unknown) => {
|
|
13
|
+
let statusCode = error ? 500 : 200;
|
|
14
|
+
|
|
15
|
+
emitLog(req.app, getStatusMessage("Stream", statusCode), {
|
|
16
|
+
level: error ? "error" : undefined,
|
|
17
|
+
req,
|
|
18
|
+
res,
|
|
19
|
+
data: error,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
res.status(statusCode);
|
|
23
|
+
|
|
24
|
+
if (statusCode >= 400) {
|
|
25
|
+
res.send(await req.app.renderStatus?.(req, res));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.set("Content-Type", "text/html");
|
|
30
|
+
pipe(res);
|
|
31
|
+
};
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["./index.ts", "./src"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"emitDeclarationOnly": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"lib": ["ESNext"],
|
|
8
|
+
"target": "ESNext",
|
|
9
|
+
"module": "nodenext",
|
|
10
|
+
"moduleResolution": "nodenext",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true
|
|
17
|
+
}
|
|
18
|
+
}
|