@tymber/common 0.0.1-alpha.0 → 0.0.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 +22 -0
- package/dist/App.d.ts +14 -0
- package/dist/App.js +227 -0
- package/dist/Component.d.ts +16 -0
- package/dist/Component.js +116 -0
- package/dist/ConfigService.d.ts +31 -0
- package/dist/ConfigService.js +75 -0
- package/dist/Context.d.ts +32 -0
- package/dist/Context.js +10 -0
- package/dist/DB.d.ts +15 -0
- package/dist/DB.js +7 -0
- package/dist/Endpoint.d.ts +21 -0
- package/dist/Endpoint.js +87 -0
- package/dist/EventEmitter.d.ts +9 -0
- package/dist/EventEmitter.js +21 -0
- package/dist/Handler.d.ts +8 -0
- package/dist/Handler.js +8 -0
- package/dist/HttpContext.d.ts +24 -0
- package/dist/HttpContext.js +10 -0
- package/dist/I18nService.d.ts +18 -0
- package/dist/I18nService.js +72 -0
- package/dist/Middleware.d.ts +6 -0
- package/dist/Middleware.js +4 -0
- package/dist/Module.d.ts +47 -0
- package/dist/Module.js +12 -0
- package/dist/PubSubService.d.ts +20 -0
- package/dist/PubSubService.js +60 -0
- package/dist/Repository.d.ts +48 -0
- package/dist/Repository.js +133 -0
- package/dist/Router.d.ts +10 -0
- package/dist/Router.js +53 -0
- package/dist/TemplateService.d.ts +22 -0
- package/dist/TemplateService.js +66 -0
- package/dist/View.d.ts +17 -0
- package/dist/View.js +48 -0
- package/dist/ViewRenderer.d.ts +15 -0
- package/dist/ViewRenderer.js +58 -0
- package/dist/contrib/accept-language-parser.d.ts +9 -0
- package/dist/contrib/accept-language-parser.js +73 -0
- package/dist/contrib/cookie.d.ts +33 -0
- package/dist/contrib/cookie.js +207 -0
- package/dist/contrib/template.d.ts +1 -0
- package/dist/contrib/template.js +107 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +31 -0
- package/dist/utils/ajv.d.ts +2 -0
- package/dist/utils/ajv.js +10 -0
- package/dist/utils/camelToSnakeCase.d.ts +1 -0
- package/dist/utils/camelToSnakeCase.js +3 -0
- package/dist/utils/computeBaseUrl.d.ts +1 -0
- package/dist/utils/computeBaseUrl.js +37 -0
- package/dist/utils/computeContentType.d.ts +1 -0
- package/dist/utils/computeContentType.js +17 -0
- package/dist/utils/createDebug.d.ts +1 -0
- package/dist/utils/createDebug.js +4 -0
- package/dist/utils/createTestApp.d.ts +8 -0
- package/dist/utils/createTestApp.js +54 -0
- package/dist/utils/escapeValue.d.ts +1 -0
- package/dist/utils/escapeValue.js +3 -0
- package/dist/utils/fs.d.ts +6 -0
- package/dist/utils/fs.js +13 -0
- package/dist/utils/isAdmin.d.ts +2 -0
- package/dist/utils/isAdmin.js +4 -0
- package/dist/utils/isProduction.d.ts +1 -0
- package/dist/utils/isProduction.js +1 -0
- package/dist/utils/loadModules.d.ts +3 -0
- package/dist/utils/loadModules.js +83 -0
- package/dist/utils/randomId.d.ts +1 -0
- package/dist/utils/randomId.js +4 -0
- package/dist/utils/randomUUID.d.ts +1 -0
- package/dist/utils/randomUUID.js +4 -0
- package/dist/utils/runMigrations.d.ts +3 -0
- package/dist/utils/runMigrations.js +85 -0
- package/dist/utils/snakeToCamelCase.d.ts +1 -0
- package/dist/utils/snakeToCamelCase.js +3 -0
- package/dist/utils/sortBy.d.ts +1 -0
- package/dist/utils/sortBy.js +8 -0
- package/dist/utils/sql.d.ts +120 -0
- package/dist/utils/sql.js +433 -0
- package/dist/utils/toNodeHandler.d.ts +2 -0
- package/dist/utils/toNodeHandler.js +91 -0
- package/dist/utils/types.d.ts +3 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/waitFor.d.ts +1 -0
- package/dist/utils/waitFor.js +5 -0
- package/package.json +28 -2
- package/src/App.ts +302 -0
- package/src/Component.ts +166 -0
- package/src/ConfigService.ts +121 -0
- package/src/Context.ts +49 -0
- package/src/DB.ts +28 -0
- package/src/Endpoint.ts +118 -0
- package/src/EventEmitter.ts +32 -0
- package/src/Handler.ts +14 -0
- package/src/HttpContext.ts +33 -0
- package/src/I18nService.ts +96 -0
- package/src/Middleware.ts +10 -0
- package/src/Module.ts +60 -0
- package/src/PubSubService.ts +77 -0
- package/src/Repository.ts +204 -0
- package/src/Router.ts +77 -0
- package/src/TemplateService.ts +97 -0
- package/src/View.ts +60 -0
- package/src/ViewRenderer.ts +71 -0
- package/src/contrib/accept-language-parser.ts +94 -0
- package/src/contrib/cookie.ts +256 -0
- package/src/contrib/template.ts +134 -0
- package/src/index.ts +57 -0
- package/src/utils/ajv.ts +13 -0
- package/src/utils/camelToSnakeCase.ts +3 -0
- package/src/utils/computeBaseUrl.ts +46 -0
- package/src/utils/computeContentType.ts +17 -0
- package/src/utils/createDebug.ts +5 -0
- package/src/utils/createTestApp.ts +81 -0
- package/src/utils/escapeValue.ts +3 -0
- package/src/utils/fs.ts +15 -0
- package/src/utils/isAdmin.ts +5 -0
- package/src/utils/isProduction.ts +2 -0
- package/src/utils/loadModules.ts +105 -0
- package/src/utils/randomId.ts +5 -0
- package/src/utils/randomUUID.ts +5 -0
- package/src/utils/runMigrations.ts +122 -0
- package/src/utils/snakeToCamelCase.ts +3 -0
- package/src/utils/sortBy.ts +8 -0
- package/src/utils/sql.ts +553 -0
- package/src/utils/toNodeHandler.ts +121 -0
- package/src/utils/types.ts +1 -0
- package/src/utils/waitFor.ts +5 -0
package/src/Router.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Handler } from "./Handler.js";
|
|
2
|
+
|
|
3
|
+
export type HttpMethod =
|
|
4
|
+
| "GET"
|
|
5
|
+
| "POST"
|
|
6
|
+
| "PUT"
|
|
7
|
+
| "DELETE"
|
|
8
|
+
| "PATCH"
|
|
9
|
+
| "OPTIONS"
|
|
10
|
+
| "HEAD";
|
|
11
|
+
|
|
12
|
+
type RouteMatcher = (path: string) => Record<string, string> | undefined;
|
|
13
|
+
|
|
14
|
+
type Route = {
|
|
15
|
+
path: string;
|
|
16
|
+
matcher: RouteMatcher;
|
|
17
|
+
handlers: Map<HttpMethod, Handler>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function createMatcher(path: string): RouteMatcher {
|
|
21
|
+
if (!path.includes(":")) {
|
|
22
|
+
return (p: string) => {
|
|
23
|
+
return p === path ? {} : undefined;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const pathParams: string[] = [];
|
|
27
|
+
const regex = new RegExp(
|
|
28
|
+
"^" +
|
|
29
|
+
path.replace(/:(\w+)/g, (pathParam) => {
|
|
30
|
+
pathParams.push(pathParam.substring(1));
|
|
31
|
+
return "([\\w-_]+)";
|
|
32
|
+
}) +
|
|
33
|
+
"$",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return (path: string) => {
|
|
37
|
+
const match = regex.exec(path);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const params: Record<string, string> = {};
|
|
42
|
+
for (let i = 0; i < pathParams.length; i++) {
|
|
43
|
+
params[pathParams[i]] = match[i + 1];
|
|
44
|
+
}
|
|
45
|
+
return params;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Router {
|
|
50
|
+
private routes: Route[] = [];
|
|
51
|
+
|
|
52
|
+
public registerRoute(method: HttpMethod, path: string, handler: Handler) {
|
|
53
|
+
for (const route of this.routes) {
|
|
54
|
+
if (route.path === path) {
|
|
55
|
+
route.handlers.set(method, handler);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.routes.push({
|
|
60
|
+
path,
|
|
61
|
+
matcher: createMatcher(path),
|
|
62
|
+
handlers: new Map([[method, handler]]),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public findRoute(method: HttpMethod, path: string) {
|
|
67
|
+
for (const route of this.routes) {
|
|
68
|
+
const pathParams = route.matcher(path);
|
|
69
|
+
if (pathParams !== undefined && route.handlers.has(method)) {
|
|
70
|
+
return {
|
|
71
|
+
handler: route.handlers.get(method)!,
|
|
72
|
+
pathParams,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { compileTemplate } from "./contrib/template.js";
|
|
2
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
3
|
+
import { createDebug } from "./utils/createDebug.js";
|
|
4
|
+
import { Component, INJECT } from "./Component.js";
|
|
5
|
+
import { FS } from "./utils/fs.js";
|
|
6
|
+
import { isProduction } from "./utils/isProduction.js";
|
|
7
|
+
|
|
8
|
+
const debug = createDebug("TemplateService");
|
|
9
|
+
|
|
10
|
+
export abstract class TemplateService extends Component {
|
|
11
|
+
public abstract canRender(templateName: string): boolean;
|
|
12
|
+
|
|
13
|
+
public abstract render(
|
|
14
|
+
templateName: string,
|
|
15
|
+
data: Record<string, any>,
|
|
16
|
+
): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export abstract class FileBasedTemplateService extends TemplateService {
|
|
20
|
+
static [INJECT] = [ModuleDefinitions];
|
|
21
|
+
|
|
22
|
+
abstract readonly fileExtension: string;
|
|
23
|
+
protected templates = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
constructor(private readonly modules: ModuleDefinitions) {
|
|
26
|
+
super();
|
|
27
|
+
this.modules = modules;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override init() {
|
|
31
|
+
return this.loadTemplates();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async loadTemplates() {
|
|
35
|
+
for (const module of this.modules.modules) {
|
|
36
|
+
if (!module.assetsDir) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
for (const filename of await FS.readDirRecursively(
|
|
41
|
+
FS.join(module.assetsDir, "templates"),
|
|
42
|
+
)) {
|
|
43
|
+
const fileExtension = filename.slice(filename.lastIndexOf("."));
|
|
44
|
+
|
|
45
|
+
if (fileExtension === this.fileExtension) {
|
|
46
|
+
const viewName = filename.slice(0, filename.lastIndexOf("."));
|
|
47
|
+
|
|
48
|
+
debug("adding template %s", viewName);
|
|
49
|
+
|
|
50
|
+
if (this.templates.has(viewName)) {
|
|
51
|
+
debug("overriding existing template %s", viewName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.templates.set(
|
|
55
|
+
viewName,
|
|
56
|
+
FS.join(module.assetsDir, "templates", filename),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override canRender(templateName: string) {
|
|
65
|
+
return this.templates.has(templateName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class BaseTemplateService extends FileBasedTemplateService {
|
|
70
|
+
override fileExtension = ".html";
|
|
71
|
+
|
|
72
|
+
// TODO use a LRU cache
|
|
73
|
+
private compiledTemplates = new Map<
|
|
74
|
+
string,
|
|
75
|
+
(data: Record<string, any>) => string
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
override render(templateName: string, data: Record<string, any>) {
|
|
79
|
+
return this.getTemplate(templateName).then((template) => template(data));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async getTemplate(templateName: string) {
|
|
83
|
+
if (isProduction && this.compiledTemplates.has(templateName)) {
|
|
84
|
+
return this.compiledTemplates.get(templateName)!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const absolutePath = this.templates.get(templateName)!;
|
|
88
|
+
const content = await FS.readFile(absolutePath);
|
|
89
|
+
const compiledTemplate = compileTemplate(content);
|
|
90
|
+
|
|
91
|
+
if (isProduction) {
|
|
92
|
+
this.compiledTemplates.set(templateName, compiledTemplate);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return compiledTemplate;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/View.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ValidateFunction } from "ajv";
|
|
2
|
+
import { type HttpContext } from "./HttpContext.js";
|
|
3
|
+
import { Handler } from "./Handler.js";
|
|
4
|
+
import { AJV_INSTANCE } from "./utils/ajv.js";
|
|
5
|
+
|
|
6
|
+
export abstract class BaseView extends Handler {
|
|
7
|
+
protected pathParamsSchema: unknown; // JSONSchemaType<Params>;
|
|
8
|
+
protected querySchema: unknown; // JSONSchemaType<Query>;
|
|
9
|
+
|
|
10
|
+
private validatePathParams?: ValidateFunction;
|
|
11
|
+
private validateQuery?: ValidateFunction;
|
|
12
|
+
|
|
13
|
+
override doHandle(ctx: HttpContext) {
|
|
14
|
+
const { pathParams, query } = ctx;
|
|
15
|
+
|
|
16
|
+
if (this.pathParamsSchema && !this.validatePathParams) {
|
|
17
|
+
this.validatePathParams = AJV_INSTANCE.compile(this.pathParamsSchema);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.validatePathParams) {
|
|
21
|
+
this.validatePathParams(pathParams);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (this.querySchema && !this.validateQuery) {
|
|
25
|
+
this.validateQuery = AJV_INSTANCE.compile(this.querySchema);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.validateQuery) {
|
|
29
|
+
this.validateQuery(query);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.handle(ctx);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export abstract class View extends BaseView {
|
|
37
|
+
private _viewBrand!: void; // nominal typing
|
|
38
|
+
|
|
39
|
+
override doHandle(ctx: HttpContext) {
|
|
40
|
+
if (!this.allowAnonymous && !ctx.user) {
|
|
41
|
+
return ctx.redirect("/login");
|
|
42
|
+
}
|
|
43
|
+
if (!this.hasPermission(ctx)) {
|
|
44
|
+
return ctx.redirect("/forbidden");
|
|
45
|
+
}
|
|
46
|
+
return super.doHandle(ctx);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export abstract class AdminView extends BaseView {
|
|
51
|
+
private _adminViewBrand!: void; // nominal typing
|
|
52
|
+
|
|
53
|
+
override doHandle(ctx: HttpContext) {
|
|
54
|
+
if (!this.allowAnonymous && !ctx.admin) {
|
|
55
|
+
return ctx.redirect("/admin/login");
|
|
56
|
+
}
|
|
57
|
+
// note: no hasPermission() check for admins
|
|
58
|
+
return super.doHandle(ctx);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Component, INJECT } from "./Component.js";
|
|
2
|
+
import { BaseTemplateService, TemplateService } from "./TemplateService.js";
|
|
3
|
+
import { I18nService } from "./I18nService.js";
|
|
4
|
+
import { pick } from "./contrib/accept-language-parser.js";
|
|
5
|
+
import { ModuleDefinitions } from "./Module.js";
|
|
6
|
+
import type { HttpContext } from "./HttpContext.js";
|
|
7
|
+
import { isProduction } from "./utils/isProduction.js";
|
|
8
|
+
|
|
9
|
+
export class ViewRenderer extends Component {
|
|
10
|
+
static [INJECT] = [
|
|
11
|
+
I18nService,
|
|
12
|
+
BaseTemplateService,
|
|
13
|
+
TemplateService,
|
|
14
|
+
ModuleDefinitions,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly i18nService: I18nService,
|
|
19
|
+
private readonly templateService: BaseTemplateService,
|
|
20
|
+
private readonly customTemplateService: TemplateService,
|
|
21
|
+
private readonly modules: ModuleDefinitions,
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async render(
|
|
27
|
+
ctx: HttpContext,
|
|
28
|
+
view: string | string[],
|
|
29
|
+
data: Record<string, any> = {},
|
|
30
|
+
) {
|
|
31
|
+
const templates = Array.isArray(view) ? view.reverse() : [view];
|
|
32
|
+
|
|
33
|
+
data.TITLE = "Tymber";
|
|
34
|
+
data.CTX = ctx;
|
|
35
|
+
data.CTX.app = data.CTX.app || {};
|
|
36
|
+
data.CTX.app.isProduction = isProduction;
|
|
37
|
+
data.CTX.app.modules = this.modules.modules;
|
|
38
|
+
|
|
39
|
+
const locale = pick(
|
|
40
|
+
this.i18nService.availableLocales(),
|
|
41
|
+
ctx.headers.get("accept-language") || "",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
data.CTX.locale = locale;
|
|
45
|
+
|
|
46
|
+
data.$t = (key: string, ...args: any[]) => {
|
|
47
|
+
return this.i18nService.translate(ctx, locale, key, ...args);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const template of templates) {
|
|
51
|
+
data.VIEW = await this.renderTemplate(template, data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Response(data.VIEW, {
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "text/html",
|
|
57
|
+
"cache-control": "no-cache",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private renderTemplate(templateName: string, data: Record<string, any>) {
|
|
63
|
+
if (this.customTemplateService.canRender(templateName)) {
|
|
64
|
+
return this.customTemplateService.render(templateName, data);
|
|
65
|
+
} else if (this.templateService.canRender(templateName)) {
|
|
66
|
+
return this.templateService.render(templateName, data);
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`template ${templateName} not found`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// from https://github.com/opentable/accept-language-parser
|
|
2
|
+
import { type Brand } from "../utils/types.js";
|
|
3
|
+
|
|
4
|
+
var regex = /((([a-zA-Z]+(-[a-zA-Z0-9]+){0,2})|\*)(;q=[0-1](\.[0-9]+)?)?)*/g;
|
|
5
|
+
|
|
6
|
+
var isString = function (s: any) {
|
|
7
|
+
return typeof s === "string";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Locale = Brand<string, "Locale">;
|
|
11
|
+
|
|
12
|
+
export function parse(al: string) {
|
|
13
|
+
var strings = (al || "").match(regex);
|
|
14
|
+
return strings!
|
|
15
|
+
.map(function (m) {
|
|
16
|
+
if (!m) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var bits = m.split(";");
|
|
21
|
+
var ietf = bits[0].split("-");
|
|
22
|
+
var hasScript = ietf.length === 3;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
code: ietf[0],
|
|
26
|
+
script: hasScript ? ietf[1] : null,
|
|
27
|
+
region: hasScript ? ietf[2] : ietf[1],
|
|
28
|
+
quality: bits[1] ? parseFloat(bits[1].split("=")[1]) : 1.0,
|
|
29
|
+
};
|
|
30
|
+
})
|
|
31
|
+
.filter(function (r) {
|
|
32
|
+
return r;
|
|
33
|
+
})
|
|
34
|
+
.sort(function (a: any, b: any) {
|
|
35
|
+
return b.quality - a.quality;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pick(
|
|
40
|
+
supportedLanguages: string[],
|
|
41
|
+
acceptLanguage: any,
|
|
42
|
+
options?: any,
|
|
43
|
+
) {
|
|
44
|
+
options = options || {};
|
|
45
|
+
|
|
46
|
+
// if (!supportedLanguages || !supportedLanguages.length || !acceptLanguage) {
|
|
47
|
+
// return null;
|
|
48
|
+
// }
|
|
49
|
+
|
|
50
|
+
if (!acceptLanguage) {
|
|
51
|
+
return supportedLanguages[0] as Locale; // pick first as default
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isString(acceptLanguage)) {
|
|
55
|
+
acceptLanguage = parse(acceptLanguage);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var supported = supportedLanguages.map(function (support) {
|
|
59
|
+
var bits = support.split("-");
|
|
60
|
+
var hasScript = bits.length === 3;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
code: bits[0],
|
|
64
|
+
script: hasScript ? bits[1] : null,
|
|
65
|
+
region: hasScript ? bits[2] : bits[1],
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
for (var i = 0; i < acceptLanguage.length; i++) {
|
|
70
|
+
var lang = acceptLanguage[i];
|
|
71
|
+
var langCode = lang.code.toLowerCase();
|
|
72
|
+
var langRegion = lang.region ? lang.region.toLowerCase() : lang.region;
|
|
73
|
+
var langScript = lang.script ? lang.script.toLowerCase() : lang.script;
|
|
74
|
+
for (var j = 0; j < supported.length; j++) {
|
|
75
|
+
var supportedCode = supported[j].code.toLowerCase();
|
|
76
|
+
var supportedScript = supported[j].script
|
|
77
|
+
? supported[j].script!.toLowerCase()
|
|
78
|
+
: supported[j].script;
|
|
79
|
+
var supportedRegion = supported[j].region
|
|
80
|
+
? supported[j].region.toLowerCase()
|
|
81
|
+
: supported[j].region;
|
|
82
|
+
if (
|
|
83
|
+
langCode === supportedCode &&
|
|
84
|
+
(options.loose || !langScript || langScript === supportedScript) &&
|
|
85
|
+
(options.loose || !langRegion || langRegion === supportedRegion)
|
|
86
|
+
) {
|
|
87
|
+
return supportedLanguages[j] as Locale;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// return null;
|
|
93
|
+
return supportedLanguages[0] as Locale; // pick first as default
|
|
94
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// from https://www.npmjs.com/package/cookie
|
|
2
|
+
/**
|
|
3
|
+
* Module variables.
|
|
4
|
+
* @private
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var __toString = Object.prototype.toString;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* RegExp to match field-content in RFC 7230 sec 3.2
|
|
11
|
+
*
|
|
12
|
+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
|
13
|
+
* field-vchar = VCHAR / obs-text
|
|
14
|
+
* obs-text = %x80-FF
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a cookie header.
|
|
21
|
+
*
|
|
22
|
+
* Parse the given cookie header string into an object
|
|
23
|
+
* The object has the various cookies as keys(names) => values
|
|
24
|
+
*
|
|
25
|
+
* @param {string} str
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @return {object}
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function parse(str: string, options?: any) {
|
|
32
|
+
if (typeof str !== "string") {
|
|
33
|
+
throw new TypeError("argument str must be a string");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var obj = {} as Record<string, string>;
|
|
37
|
+
var opt = options || {};
|
|
38
|
+
var dec = opt.decode || decode;
|
|
39
|
+
|
|
40
|
+
var index = 0;
|
|
41
|
+
while (index < str.length) {
|
|
42
|
+
var eqIdx = str.indexOf("=", index);
|
|
43
|
+
|
|
44
|
+
// no more cookie pairs
|
|
45
|
+
if (eqIdx === -1) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var endIdx = str.indexOf(";", index);
|
|
50
|
+
|
|
51
|
+
if (endIdx === -1) {
|
|
52
|
+
endIdx = str.length;
|
|
53
|
+
} else if (endIdx < eqIdx) {
|
|
54
|
+
// backtrack on prior semicolon
|
|
55
|
+
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var key = str.slice(index, eqIdx).trim();
|
|
60
|
+
|
|
61
|
+
// only assign once
|
|
62
|
+
if (undefined === obj[key]) {
|
|
63
|
+
var val = str.slice(eqIdx + 1, endIdx).trim();
|
|
64
|
+
|
|
65
|
+
// quoted values
|
|
66
|
+
if (val.charCodeAt(0) === 0x22) {
|
|
67
|
+
val = val.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
obj[key] = tryDecode(val, dec);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
index = endIdx + 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Serialize data into a cookie header.
|
|
81
|
+
*
|
|
82
|
+
* Serialize the a name value pair into a cookie string suitable for
|
|
83
|
+
* http headers. An optional options object specified cookie parameters.
|
|
84
|
+
*
|
|
85
|
+
* serialize('foo', 'bar', { httpOnly: true })
|
|
86
|
+
* => "foo=bar; httpOnly"
|
|
87
|
+
*
|
|
88
|
+
* @param {string} name
|
|
89
|
+
* @param {string} val
|
|
90
|
+
* @param {object} [options]
|
|
91
|
+
* @return {string}
|
|
92
|
+
* @public
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
function serialize(name: string, val: string, options: any) {
|
|
96
|
+
var opt = options || {};
|
|
97
|
+
var enc = opt.encode || encode;
|
|
98
|
+
|
|
99
|
+
if (typeof enc !== "function") {
|
|
100
|
+
throw new TypeError("option encode is invalid");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!fieldContentRegExp.test(name)) {
|
|
104
|
+
throw new TypeError("argument name is invalid");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var value = enc(val);
|
|
108
|
+
|
|
109
|
+
if (value && !fieldContentRegExp.test(value)) {
|
|
110
|
+
throw new TypeError("argument val is invalid");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var str = name + "=" + value;
|
|
114
|
+
|
|
115
|
+
if (null != opt.maxAge) {
|
|
116
|
+
var maxAge = opt.maxAge - 0;
|
|
117
|
+
|
|
118
|
+
if (isNaN(maxAge) || !isFinite(maxAge)) {
|
|
119
|
+
throw new TypeError("option maxAge is invalid");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
str += "; Max-Age=" + Math.floor(maxAge);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (opt.domain) {
|
|
126
|
+
if (!fieldContentRegExp.test(opt.domain)) {
|
|
127
|
+
throw new TypeError("option domain is invalid");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
str += "; Domain=" + opt.domain;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opt.path) {
|
|
134
|
+
if (!fieldContentRegExp.test(opt.path)) {
|
|
135
|
+
throw new TypeError("option path is invalid");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
str += "; Path=" + opt.path;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (opt.expires) {
|
|
142
|
+
var expires = opt.expires;
|
|
143
|
+
|
|
144
|
+
if (!isDate(expires) || isNaN(expires.valueOf())) {
|
|
145
|
+
throw new TypeError("option expires is invalid");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
str += "; Expires=" + expires.toUTCString();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (opt.httpOnly) {
|
|
152
|
+
str += "; HttpOnly";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (opt.secure) {
|
|
156
|
+
str += "; Secure";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (opt.priority) {
|
|
160
|
+
var priority =
|
|
161
|
+
typeof opt.priority === "string"
|
|
162
|
+
? opt.priority.toLowerCase()
|
|
163
|
+
: opt.priority;
|
|
164
|
+
|
|
165
|
+
switch (priority) {
|
|
166
|
+
case "low":
|
|
167
|
+
str += "; Priority=Low";
|
|
168
|
+
break;
|
|
169
|
+
case "medium":
|
|
170
|
+
str += "; Priority=Medium";
|
|
171
|
+
break;
|
|
172
|
+
case "high":
|
|
173
|
+
str += "; Priority=High";
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
throw new TypeError("option priority is invalid");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (opt.sameSite) {
|
|
181
|
+
var sameSite =
|
|
182
|
+
typeof opt.sameSite === "string"
|
|
183
|
+
? opt.sameSite.toLowerCase()
|
|
184
|
+
: opt.sameSite;
|
|
185
|
+
|
|
186
|
+
switch (sameSite) {
|
|
187
|
+
case true:
|
|
188
|
+
str += "; SameSite=Strict";
|
|
189
|
+
break;
|
|
190
|
+
case "lax":
|
|
191
|
+
str += "; SameSite=Lax";
|
|
192
|
+
break;
|
|
193
|
+
case "strict":
|
|
194
|
+
str += "; SameSite=Strict";
|
|
195
|
+
break;
|
|
196
|
+
case "none":
|
|
197
|
+
str += "; SameSite=None";
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
throw new TypeError("option sameSite is invalid");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return str;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* URL-decode string value. Optimized to skip native call when no %.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} str
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
function decode(str: string) {
|
|
215
|
+
return str.indexOf("%") !== -1 ? decodeURIComponent(str) : str;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* URL-encode value.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} str
|
|
222
|
+
* @returns {string}
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
function encode(val: string) {
|
|
226
|
+
return encodeURIComponent(val);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Determine if value is a Date.
|
|
231
|
+
*
|
|
232
|
+
* @param {*} val
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
function isDate(val: any) {
|
|
237
|
+
return __toString.call(val) === "[object Date]" || val instanceof Date;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Try decoding a string using a decoding function.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} str
|
|
244
|
+
* @param {function} decode
|
|
245
|
+
* @private
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
function tryDecode(str: string, decode: any) {
|
|
249
|
+
try {
|
|
250
|
+
return decode(str);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
return str;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export { parse as parseCookieHeader, serialize as createCookie };
|