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,119 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ErrorController } from "../types/ErrorController.ts";
|
|
2
|
+
import { emitLog } from "../utils/emitLog.ts";
|
|
3
|
+
|
|
4
|
+
export const unhandledError: ErrorController = () => async (err, req, res) => {
|
|
5
|
+
emitLog(req.app, "Unhandled error", {
|
|
6
|
+
level: "error",
|
|
7
|
+
data: err,
|
|
8
|
+
req,
|
|
9
|
+
res,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
res
|
|
13
|
+
.status(500)
|
|
14
|
+
.send(await req.app.renderStatus?.(req, res, "unhandled_error"));
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Controller } from "../types/Controller.ts";
|
|
2
|
+
import { emitLog } from "../utils/emitLog.ts";
|
|
3
|
+
|
|
4
|
+
export const unhandledRoute: Controller = () => async (req, res) => {
|
|
5
|
+
emitLog(req.app, "Unhandled route", {
|
|
6
|
+
level: "debug",
|
|
7
|
+
req,
|
|
8
|
+
res,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
res
|
|
12
|
+
.status(404)
|
|
13
|
+
.send(await req.app.renderStatus?.(req, res, "unhandled_route"));
|
|
14
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { toLanguage } from "./toLanguage.ts";
|
|
2
|
+
|
|
3
|
+
type Match = {
|
|
4
|
+
index?: number;
|
|
5
|
+
locale?: string | undefined;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function getEffectiveLocale(
|
|
9
|
+
preferredLocales: string[] | undefined,
|
|
10
|
+
supportedLocales: string[] | undefined,
|
|
11
|
+
): string | undefined {
|
|
12
|
+
if (!supportedLocales || supportedLocales.length === 0) return undefined;
|
|
13
|
+
|
|
14
|
+
if (!preferredLocales || preferredLocales.length === 0)
|
|
15
|
+
return supportedLocales[0];
|
|
16
|
+
|
|
17
|
+
let exactMatch: Match = {};
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < preferredLocales.length && !exactMatch.locale; i++) {
|
|
20
|
+
let k = supportedLocales.indexOf(preferredLocales[i]);
|
|
21
|
+
|
|
22
|
+
if (k !== -1) {
|
|
23
|
+
exactMatch.index = i;
|
|
24
|
+
exactMatch.locale = supportedLocales[k];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let languageMatch: Match = {};
|
|
29
|
+
|
|
30
|
+
let supportedLanguages = supportedLocales.map(toLanguage);
|
|
31
|
+
let preferredLanguages = preferredLocales.map(toLanguage);
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < preferredLanguages.length && !languageMatch.locale; i++) {
|
|
34
|
+
let k = supportedLanguages.indexOf(preferredLanguages[i]);
|
|
35
|
+
|
|
36
|
+
if (k !== -1) {
|
|
37
|
+
languageMatch.index = i;
|
|
38
|
+
languageMatch.locale = supportedLocales[k];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// if both an exact match and a language match are found, prefer
|
|
43
|
+
// the one with an index closer to the beginning of the preferred
|
|
44
|
+
// locale list
|
|
45
|
+
if (
|
|
46
|
+
exactMatch.locale &&
|
|
47
|
+
(!languageMatch.locale || exactMatch.index! < languageMatch.index!)
|
|
48
|
+
)
|
|
49
|
+
return exactMatch.locale;
|
|
50
|
+
|
|
51
|
+
return languageMatch.locale ?? supportedLocales[0];
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a language range string (typically a value of the 'Accept-Language'
|
|
3
|
+
* HTTP request header) and returns a corresponding array of locales
|
|
4
|
+
* @example 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
|
|
5
|
+
*/
|
|
6
|
+
export function getLocales(languageRange: string | undefined): string[] {
|
|
7
|
+
return (languageRange ?? "")
|
|
8
|
+
.split(/[,;]\s*/)
|
|
9
|
+
.filter((s) => !s.startsWith("q=") && s !== "*");
|
|
10
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { formatDate, formatDuration } from "dateshape";
|
|
2
|
+
import { ansiEscapeCodes } from "./ansiEscapeCodes.ts";
|
|
3
|
+
import type { LogOptions } from "./LogOptions.ts";
|
|
4
|
+
import { levelColors } from "./levelColors.ts";
|
|
5
|
+
|
|
6
|
+
function isEmpty(x: unknown) {
|
|
7
|
+
if (x === null || x === undefined || x === "") return true;
|
|
8
|
+
if (Array.isArray(x) && x.length === 0) return true;
|
|
9
|
+
if (typeof x === "object" && Object.keys(x).length === 0) return true;
|
|
10
|
+
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function log(
|
|
15
|
+
message: string | Error | undefined = "",
|
|
16
|
+
{ timestamp, level, data, req }: LogOptions = {},
|
|
17
|
+
): void {
|
|
18
|
+
let currentTime = timestamp ?? Date.now();
|
|
19
|
+
let error: Error | null = null;
|
|
20
|
+
|
|
21
|
+
if (message instanceof Error) {
|
|
22
|
+
error = message;
|
|
23
|
+
message = error.message;
|
|
24
|
+
|
|
25
|
+
data = {
|
|
26
|
+
error,
|
|
27
|
+
data,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (!level) level = "error";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data instanceof Error) {
|
|
34
|
+
error = data;
|
|
35
|
+
|
|
36
|
+
if (data.message)
|
|
37
|
+
message = `${message ? `${message} - ` : ""}${data.message}`;
|
|
38
|
+
|
|
39
|
+
if (!level) level = "error";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!level) level = "info";
|
|
43
|
+
|
|
44
|
+
let levelCode = ansiEscapeCodes[levelColors[level]] ?? "";
|
|
45
|
+
let requestTarget = req ? `${req.method} ${req.originalUrl}` : "";
|
|
46
|
+
|
|
47
|
+
let { startTime, id: sessionId } = req?.ctx ?? {};
|
|
48
|
+
|
|
49
|
+
console[level]();
|
|
50
|
+
|
|
51
|
+
console[level](
|
|
52
|
+
levelCode +
|
|
53
|
+
ansiEscapeCodes.dim +
|
|
54
|
+
formatDate(currentTime, "{isoDate} {isoTimeMs} {tz}") +
|
|
55
|
+
(sessionId ? ` <${sessionId}>` : "") +
|
|
56
|
+
(startTime === undefined
|
|
57
|
+
? ""
|
|
58
|
+
: ` +${formatDuration(currentTime - startTime)}`) +
|
|
59
|
+
ansiEscapeCodes.reset,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console[level](
|
|
63
|
+
levelCode +
|
|
64
|
+
requestTarget +
|
|
65
|
+
(requestTarget && message && !message.startsWith("\n") ? " - " : "") +
|
|
66
|
+
message +
|
|
67
|
+
ansiEscapeCodes.reset,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!isEmpty(data))
|
|
71
|
+
console[level](
|
|
72
|
+
levelCode +
|
|
73
|
+
ansiEscapeCodes.dim +
|
|
74
|
+
(typeof data === "string" ? data : JSON.stringify(data, null, 2)) +
|
|
75
|
+
ansiEscapeCodes.reset,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (error?.stack)
|
|
79
|
+
console[level](
|
|
80
|
+
levelCode + ansiEscapeCodes.dim + error.stack + ansiEscapeCodes.reset,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { Middleware } from "../types/Middleware.ts";
|
|
3
|
+
import { emitLog } from "../utils/emitLog.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initializes the request context on `req.ctx`.
|
|
7
|
+
*/
|
|
8
|
+
export const init: Middleware = () => (req, res, next) => {
|
|
9
|
+
req.ctx = {
|
|
10
|
+
...req.ctx,
|
|
11
|
+
id: randomBytes(16).toString("hex"),
|
|
12
|
+
nonce: randomBytes(8).toString("hex"),
|
|
13
|
+
startTime: Date.now(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
emitLog(req.app, "Inited", {
|
|
17
|
+
req,
|
|
18
|
+
res,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
next();
|
|
22
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CookieOptions } from "express";
|
|
2
|
+
import { getEffectiveLocale } from "../lib/lang/getEffectiveLocale.ts";
|
|
3
|
+
import { getLocales } from "../lib/lang/getLocales.ts";
|
|
4
|
+
import { toLanguage } from "../lib/lang/toLanguage.ts";
|
|
5
|
+
import type { Middleware } from "../types/Middleware.ts";
|
|
6
|
+
import { emitLog } from "../utils/emitLog.ts";
|
|
7
|
+
|
|
8
|
+
const defaultLangCookieOptions: CookieOptions = {
|
|
9
|
+
maxAge: 90 * 86_400_000,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LangParams = {
|
|
13
|
+
supportedLocales?: string[];
|
|
14
|
+
shouldSetCookie?: boolean;
|
|
15
|
+
shouldRedirect?: boolean;
|
|
16
|
+
langCookieOptions?: CookieOptions;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const lang: Middleware<LangParams | void> = ({
|
|
20
|
+
supportedLocales = [],
|
|
21
|
+
shouldSetCookie = true,
|
|
22
|
+
shouldRedirect = true,
|
|
23
|
+
langCookieOptions = defaultLangCookieOptions,
|
|
24
|
+
} = {}) => {
|
|
25
|
+
let langSet = new Set(supportedLocales.map(toLanguage));
|
|
26
|
+
let localeSet = new Set(supportedLocales);
|
|
27
|
+
|
|
28
|
+
return (req, res, next) => {
|
|
29
|
+
let langParam = req.query.lang as string[] | string | undefined;
|
|
30
|
+
let lang = (Array.isArray(langParam) ? langParam[0] : langParam) ?? "";
|
|
31
|
+
|
|
32
|
+
if (localeSet.has(lang) || langSet.has(lang)) {
|
|
33
|
+
if (shouldSetCookie) {
|
|
34
|
+
emitLog(req.app, `Set lang cookie: ${JSON.stringify(lang)}`, {
|
|
35
|
+
req,
|
|
36
|
+
res,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
res.cookie("lang", lang, langCookieOptions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (shouldRedirect) {
|
|
43
|
+
let { originalUrl } = req;
|
|
44
|
+
let nextUrl = originalUrl.replace(/[?&]lang=[^&]+/g, "");
|
|
45
|
+
|
|
46
|
+
if (nextUrl !== originalUrl) {
|
|
47
|
+
emitLog(req.app, "Strip lang param and redirect", {
|
|
48
|
+
req,
|
|
49
|
+
res,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
res.redirect(nextUrl);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let langCookie = shouldSetCookie
|
|
59
|
+
? (req.cookies.lang as string | undefined)
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
let userAgentLocales = getLocales(req.get("accept-language"));
|
|
63
|
+
let preferredLocales = langCookie
|
|
64
|
+
? [langCookie, ...userAgentLocales]
|
|
65
|
+
: userAgentLocales;
|
|
66
|
+
|
|
67
|
+
let effectiveLang = getEffectiveLocale(preferredLocales, supportedLocales);
|
|
68
|
+
|
|
69
|
+
if (req.ctx && effectiveLang) req.ctx.lang = effectiveLang;
|
|
70
|
+
|
|
71
|
+
emitLog(req.app, `Detected lang: ${JSON.stringify(effectiveLang)}`, {
|
|
72
|
+
req,
|
|
73
|
+
res,
|
|
74
|
+
data: {
|
|
75
|
+
userAgentLocales,
|
|
76
|
+
langCookie,
|
|
77
|
+
lang: effectiveLang,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
next();
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Middleware } from "../types/Middleware.ts";
|
|
2
|
+
import { emitLog } from "../utils/emitLog.ts";
|
|
3
|
+
import { getStatusMessage } from "../utils/getStatusMessage.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds event handlers, like logging, to essential request phases.
|
|
7
|
+
*/
|
|
8
|
+
export const requestEvents: Middleware = () => (req, res, next) => {
|
|
9
|
+
let finished = false;
|
|
10
|
+
|
|
11
|
+
res.on("finish", () => {
|
|
12
|
+
finished = true;
|
|
13
|
+
|
|
14
|
+
emitLog(req.app, getStatusMessage("Finished", res.statusCode), {
|
|
15
|
+
req,
|
|
16
|
+
res,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
res.on("close", () => {
|
|
21
|
+
if (!finished)
|
|
22
|
+
emitLog(req.app, getStatusMessage("Closed", res.statusCode), {
|
|
23
|
+
req,
|
|
24
|
+
res,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
next();
|
|
29
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { formatDuration } from "dateshape";
|
|
3
|
+
import type { BuildParams } from "./types/BuildParams.ts";
|
|
4
|
+
import { buildClient } from "./utils/buildClient.ts";
|
|
5
|
+
import { buildServer } from "./utils/buildServer.ts";
|
|
6
|
+
import { buildServerCSS } from "./utils/buildServerCSS.ts";
|
|
7
|
+
import { createPostbuildPlugins } from "./utils/createPostbuildPlugins.ts";
|
|
8
|
+
|
|
9
|
+
export async function build(params: BuildParams) {
|
|
10
|
+
let startTime = Date.now();
|
|
11
|
+
let log = params.silent ? () => {} : console.log;
|
|
12
|
+
|
|
13
|
+
log("Build started");
|
|
14
|
+
|
|
15
|
+
let serverProcess: ChildProcess | null = null;
|
|
16
|
+
let inited = false;
|
|
17
|
+
|
|
18
|
+
function handleServerRebuild() {
|
|
19
|
+
if (serverProcess) {
|
|
20
|
+
serverProcess.kill();
|
|
21
|
+
serverProcess = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!inited) {
|
|
25
|
+
log(`Build completed +${formatDuration(Date.now() - startTime)}`);
|
|
26
|
+
inited = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (params.start)
|
|
30
|
+
serverProcess = spawn("node", [`${params.targetDir}/server/index.js`], {
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let { serverPlugins, serverCSSPlugins } = createPostbuildPlugins(
|
|
36
|
+
params,
|
|
37
|
+
handleServerRebuild,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await Promise.all([
|
|
41
|
+
buildServer(params, serverPlugins),
|
|
42
|
+
buildServerCSS(params, serverCSSPlugins),
|
|
43
|
+
buildClient(params),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { build } from "./build.ts";
|
|
3
|
+
import type { BuildParams } from "./types/BuildParams.ts";
|
|
4
|
+
|
|
5
|
+
const defaultTargetDir = "dist";
|
|
6
|
+
|
|
7
|
+
async function clean({ targetDir, publicAssetsDir }: BuildParams) {
|
|
8
|
+
let dirs = [
|
|
9
|
+
`${targetDir}/server`,
|
|
10
|
+
`${targetDir}/server-css`,
|
|
11
|
+
`${publicAssetsDir}/-`,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
return Promise.all(
|
|
15
|
+
dirs.map((dir) => rm(dir, { recursive: true, force: true })),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function cli(args: string[] = []) {
|
|
20
|
+
let publicAssetsDir = args[0];
|
|
21
|
+
let targetDir = args[1];
|
|
22
|
+
|
|
23
|
+
if (!publicAssetsDir || publicAssetsDir.startsWith("--"))
|
|
24
|
+
throw new Error("Public assets directory is undefined");
|
|
25
|
+
|
|
26
|
+
if (!targetDir || targetDir.startsWith("--")) targetDir = defaultTargetDir;
|
|
27
|
+
|
|
28
|
+
let params: BuildParams = {
|
|
29
|
+
targetDir,
|
|
30
|
+
publicAssetsDir,
|
|
31
|
+
silent: args.includes("--silent"),
|
|
32
|
+
watch: args.includes("--watch"),
|
|
33
|
+
watchServer: args.includes("--watch-server"),
|
|
34
|
+
watchClient: args.includes("--watch-client"),
|
|
35
|
+
start: args.includes("--start"),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (args.includes("--clean-only")) {
|
|
39
|
+
await clean(params);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (args.includes("--clean")) await clean(params);
|
|
44
|
+
|
|
45
|
+
await build(params);
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BuildOptions } from "esbuild";
|
|
2
|
+
|
|
3
|
+
export const commonBuildOptions: Partial<BuildOptions> = {
|
|
4
|
+
format: "cjs",
|
|
5
|
+
jsx: "automatic",
|
|
6
|
+
jsxDev: process.env.NODE_ENV === "development",
|
|
7
|
+
loader: {
|
|
8
|
+
".png": "dataurl",
|
|
9
|
+
".svg": "dataurl",
|
|
10
|
+
".html": "text",
|
|
11
|
+
".txt": "text",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const entryExtensions = ["js", "jsx", "ts", "tsx"];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cli } from "./cli.ts";
|
|
2
|
+
|
|
3
|
+
export async function start(nodeEnv = "development", host?: string) {
|
|
4
|
+
if (nodeEnv) process.env.NODE_ENV = nodeEnv;
|
|
5
|
+
|
|
6
|
+
if (host) {
|
|
7
|
+
let [hostname, port] = host.split(":");
|
|
8
|
+
|
|
9
|
+
if (hostname) process.env.APP_HOST = hostname;
|
|
10
|
+
if (port) process.env.APP_PORT = port;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
await cli(
|
|
14
|
+
nodeEnv === "development"
|
|
15
|
+
? ["src/public", "--clean", "--start", "--watch"]
|
|
16
|
+
: ["src/public", "--clean", "--start", "--silent"],
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Builds the client-side code from the 'src/entries/<entry_name>/ui'
|
|
8
|
+
* directories. The directories should preferrably be called 'ui' rather
|
|
9
|
+
* than client since their contents can also be used with the server-side
|
|
10
|
+
* rendering.
|
|
11
|
+
*/
|
|
12
|
+
export async function buildClient(
|
|
13
|
+
{ publicAssetsDir, watch, watchClient }: BuildParams,
|
|
14
|
+
plugins?: Plugin[],
|
|
15
|
+
) {
|
|
16
|
+
let clientEntries = await getEntryPoints(["ui/index"]);
|
|
17
|
+
|
|
18
|
+
let buildOptions: BuildOptions = {
|
|
19
|
+
...commonBuildOptions,
|
|
20
|
+
entryPoints: clientEntries.map(({ path }) => path),
|
|
21
|
+
bundle: true,
|
|
22
|
+
splitting: true,
|
|
23
|
+
format: "esm",
|
|
24
|
+
outdir: `${publicAssetsDir}/-`,
|
|
25
|
+
outbase: "src/entries",
|
|
26
|
+
minify: process.env.NODE_ENV !== "development",
|
|
27
|
+
plugins,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (watch || watchClient) {
|
|
31
|
+
let ctx = await esbuild.context(buildOptions);
|
|
32
|
+
|
|
33
|
+
await ctx.watch();
|
|
34
|
+
|
|
35
|
+
return async () => {
|
|
36
|
+
await ctx.dispose();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await esbuild.build(buildOptions);
|
|
41
|
+
}
|