primate 0.15.7 → 0.16.0
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/README.md +2 -3
- package/package.json +8 -13
- package/src/Logger.js +5 -5
- package/src/{env.js → app.js} +65 -28
- package/src/commands/create.js +8 -8
- package/src/commands/dev.js +1 -1
- package/src/commands/help.js +2 -2
- package/src/commands/serve.js +1 -1
- package/src/{primate.config.js → defaults/primate.config.js} +6 -2
- package/src/exports.js +4 -0
- package/src/fromNull.js +1 -0
- package/src/handlers/html.js +12 -8
- package/src/handlers/view.js +8 -9
- package/src/hooks/bundle.js +29 -0
- package/src/hooks/compile.js +7 -0
- package/src/hooks/exports.js +6 -0
- package/src/hooks/handle/exports.js +4 -0
- package/src/{mimes.js → hooks/handle/mime.js} +7 -1
- package/src/hooks/handle/mime.spec.js +13 -0
- package/src/{respond.js → hooks/handle/respond.js} +6 -4
- package/src/hooks/handle/respond.spec.js +12 -0
- package/src/{serve.js → hooks/handle.js} +59 -46
- package/src/hooks/publish.js +36 -0
- package/src/hooks/register.js +7 -0
- package/src/hooks/route.js +69 -0
- package/src/run.js +2 -2
- package/src/start.js +13 -23
- package/LICENSE +0 -19
- package/src/bundle.js +0 -23
- package/src/cache.js +0 -17
- package/src/compile.js +0 -5
- package/src/config.js +0 -5
- package/src/errors/Info.js +0 -1
- package/src/errors/InternalServer.js +0 -1
- package/src/errors/Predicate.js +0 -1
- package/src/errors/Route.js +0 -1
- package/src/errors.js +0 -2
- package/src/publish.js +0 -5
- package/src/register.js +0 -5
- package/src/route.js +0 -63
- /package/src/{index.html → defaults/index.html} +0 -0
- /package/src/{duck.js → hooks/handle/duck.js} +0 -0
- /package/src/{http-statuses.js → hooks/handle/http-statuses.js} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Primate
|
|
2
2
|
|
|
3
|
-
Expressive, minimal and extensible framework
|
|
3
|
+
Expressive, minimal and extensible web framework
|
|
4
4
|
|
|
5
5
|
## Getting started
|
|
6
6
|
|
|
@@ -14,10 +14,9 @@ export default {
|
|
|
14
14
|
return "Hello, world!";
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
|
-
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
Run `npm i && npm start` and visit
|
|
19
|
+
Run `npm i && npm start` and visit http://localhost:6161 in your browser.
|
|
21
20
|
|
|
22
21
|
## Table of Contents
|
|
23
22
|
|
package/package.json
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Expressive, minimal and extensible framework
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "Expressive, minimal and extensible web framework",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
6
|
"bugs": "https://github.com/primatejs/primate/issues",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"files": [
|
|
9
9
|
"src/**",
|
|
10
|
-
"!src/**/*.spec.js"
|
|
11
|
-
"!readme/**"
|
|
10
|
+
"!src/**/*.spec.js"
|
|
12
11
|
],
|
|
13
12
|
"bin": "src/bin.js",
|
|
14
|
-
"repository":
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"lint": "npx eslint ."
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/primatejs/primate",
|
|
16
|
+
"directory": "packages/primate"
|
|
19
17
|
},
|
|
20
18
|
"dependencies": {
|
|
21
|
-
"runtime-compat": "^0.
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"maximin": "^0.1.2"
|
|
19
|
+
"runtime-compat": "^0.16.3"
|
|
25
20
|
},
|
|
26
21
|
"type": "module",
|
|
27
22
|
"exports": "./src/exports.js"
|
package/src/Logger.js
CHANGED
|
@@ -27,13 +27,13 @@ const levels = new Map([
|
|
|
27
27
|
const print = (...messages) => process.stdout.write(messages.join(" "));
|
|
28
28
|
|
|
29
29
|
const Logger = class Logger {
|
|
30
|
-
#level; #
|
|
30
|
+
#level; #trace;
|
|
31
31
|
|
|
32
|
-
constructor({level = Error,
|
|
32
|
+
constructor({level = Error, trace = false} = {}) {
|
|
33
33
|
assert(level !== undefined && levels.get(level) <= info);
|
|
34
|
-
is(
|
|
34
|
+
is(trace).boolean();
|
|
35
35
|
this.#level = level;
|
|
36
|
-
this.#
|
|
36
|
+
this.#trace = trace;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
static get Error() {
|
|
@@ -51,7 +51,7 @@ const Logger = class Logger {
|
|
|
51
51
|
#print(pre, error) {
|
|
52
52
|
if (error instanceof Error) {
|
|
53
53
|
print(colors.bold(pre), error.message, "\n");
|
|
54
|
-
if (this.#
|
|
54
|
+
if (this.#trace) {
|
|
55
55
|
console.log(error);
|
|
56
56
|
}
|
|
57
57
|
} else {
|
package/src/{env.js → app.js}
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import crypto from "runtime-compat/crypto";
|
|
2
2
|
import {is} from "runtime-compat/dyndef";
|
|
3
3
|
import {File, Path} from "runtime-compat/fs";
|
|
4
|
-
import cache from "./cache.js";
|
|
5
4
|
import extend from "./extend.js";
|
|
6
|
-
import defaults from "./primate.config.js";
|
|
5
|
+
import defaults from "./defaults/primate.config.js";
|
|
7
6
|
import {colors, print, default as Logger} from "./Logger.js";
|
|
8
7
|
import * as handlers from "./handlers/exports.js";
|
|
9
8
|
|
|
@@ -16,10 +15,22 @@ const qualify = (root, paths) =>
|
|
|
16
15
|
return sofar;
|
|
17
16
|
}, {});
|
|
18
17
|
|
|
18
|
+
const configName = "primate.config.js";
|
|
19
|
+
|
|
19
20
|
const getConfig = async (root, filename) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const config = root.join(filename);
|
|
22
|
+
if (await config.exists) {
|
|
23
|
+
try {
|
|
24
|
+
const imported = await import(config);
|
|
25
|
+
if (imported.default === undefined) {
|
|
26
|
+
print(`${colors.yellow("??")} ${configName} has no default export\n`);
|
|
27
|
+
}
|
|
28
|
+
return extend(defaults, imported.default);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
print(`${colors.red("!!")} couldn't load config file\n`);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
23
34
|
return defaults;
|
|
24
35
|
}
|
|
25
36
|
};
|
|
@@ -34,14 +45,16 @@ const getRoot = async () => {
|
|
|
34
45
|
}
|
|
35
46
|
};
|
|
36
47
|
|
|
37
|
-
const
|
|
48
|
+
const src = new Path(import.meta.url).up(1);
|
|
49
|
+
|
|
50
|
+
const index = async app => {
|
|
38
51
|
const name = "index.html";
|
|
39
52
|
try {
|
|
40
53
|
// user-provided file
|
|
41
|
-
return await File.read(`${
|
|
54
|
+
return await File.read(`${app.paths.static.join(name)}`);
|
|
42
55
|
} catch (error) {
|
|
43
56
|
// fallback
|
|
44
|
-
return
|
|
57
|
+
return src.join("defaults", name).text();
|
|
45
58
|
}
|
|
46
59
|
};
|
|
47
60
|
|
|
@@ -52,39 +65,47 @@ const hash = async (string, algorithm = "sha-384") => {
|
|
|
52
65
|
return `${algo}-${btoa(String.fromCharCode(...new Uint8Array(bytes)))}`;
|
|
53
66
|
};
|
|
54
67
|
|
|
55
|
-
export default async (filename =
|
|
68
|
+
export default async (filename = configName) => {
|
|
56
69
|
is(filename).string();
|
|
57
70
|
const root = await getRoot();
|
|
58
71
|
const config = await getConfig(root, filename);
|
|
59
72
|
|
|
60
|
-
const {name, version} =
|
|
61
|
-
.directory.directory.join("package.json").file.read());
|
|
73
|
+
const {name, version} = await src.up(1).join("package.json").json();
|
|
62
74
|
|
|
63
75
|
// if ssl activated, resolve key and cert early
|
|
64
76
|
if (config.http.ssl) {
|
|
65
77
|
config.http.ssl.key = root.join(config.http.ssl.key);
|
|
66
78
|
config.http.ssl.cert = root.join(config.http.ssl.cert);
|
|
67
|
-
config.secure = true;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
const
|
|
71
|
-
|
|
81
|
+
const app = {
|
|
82
|
+
config,
|
|
83
|
+
secure: config.http?.ssl !== undefined,
|
|
72
84
|
name, version,
|
|
85
|
+
library: {},
|
|
86
|
+
identifiers: {},
|
|
87
|
+
replace(code) {
|
|
88
|
+
const joined = Object.keys(app.identifiers).join("|");
|
|
89
|
+
const re = `(?<=import (?:.*) from ['|"])(${joined})(?=['|"])`;
|
|
90
|
+
return code.replaceAll(new RegExp(re, "gus"), (_, p1) => {
|
|
91
|
+
if (app.library[p1] === undefined) {
|
|
92
|
+
app.library[p1] = app.identifiers[p1];
|
|
93
|
+
}
|
|
94
|
+
return app.identifiers[p1];
|
|
95
|
+
});
|
|
96
|
+
},
|
|
73
97
|
resources: [],
|
|
74
98
|
entrypoints: [],
|
|
75
99
|
paths: qualify(root, config.paths),
|
|
76
100
|
root,
|
|
77
101
|
log: new Logger(config.logger),
|
|
78
|
-
register: (name, handler) => {
|
|
79
|
-
env.handlers[name] = handler;
|
|
80
|
-
},
|
|
81
102
|
handlers: {...handlers},
|
|
82
103
|
render: async ({body = "", head = ""} = {}) => {
|
|
83
|
-
const html = await index(
|
|
84
|
-
const heads =
|
|
104
|
+
const html = await index(app);
|
|
105
|
+
const heads = app.resources.map(({src, code, type, inline, integrity}) => {
|
|
85
106
|
const tag = type === "style" ? "link" : "script";
|
|
86
107
|
const pre = type === "style"
|
|
87
|
-
? `<${tag} rel="stylesheet"
|
|
108
|
+
? `<${tag} rel="stylesheet"`
|
|
88
109
|
: `<${tag} type="${type}" integrity="${integrity}"`;
|
|
89
110
|
const middle = type === "style"
|
|
90
111
|
? ` href="${src}">`
|
|
@@ -97,24 +118,40 @@ export default async (filename = "primate.config.js") => {
|
|
|
97
118
|
.replace("%head%", () => `${head}${heads}`);
|
|
98
119
|
},
|
|
99
120
|
publish: async ({src, code, type = "", inline = false}) => {
|
|
121
|
+
if (type === "module") {
|
|
122
|
+
code = app.replace(code);
|
|
123
|
+
}
|
|
124
|
+
// while integrity is only really needed for scripts, it is also later
|
|
125
|
+
// used for the etag header
|
|
100
126
|
const integrity = await hash(code);
|
|
101
|
-
|
|
127
|
+
const _src = new Path(config.http.static.root).join(src ?? "");
|
|
128
|
+
app.resources.push({src: `${_src}`, code, type, inline, integrity});
|
|
102
129
|
return integrity;
|
|
103
130
|
},
|
|
104
131
|
bootstrap: ({type, code}) => {
|
|
105
|
-
|
|
132
|
+
app.entrypoints.push({type, code});
|
|
133
|
+
},
|
|
134
|
+
resolve: (pkg, name) => {
|
|
135
|
+
const exports = Object.fromEntries(Object.entries(pkg.exports)
|
|
136
|
+
.filter(([, _export]) => _export.import !== undefined)
|
|
137
|
+
.map(([key, value]) => [
|
|
138
|
+
key.replace(".", name),
|
|
139
|
+
value.import.replace(".", `./${name}`),
|
|
140
|
+
]));
|
|
141
|
+
app.identifiers = {...exports, ...app.identifiers};
|
|
106
142
|
},
|
|
143
|
+
modules: [...config.modules],
|
|
107
144
|
};
|
|
108
145
|
print(colors.blue(colors.bold(name)), colors.blue(version), "");
|
|
109
|
-
const type =
|
|
146
|
+
const type = app.secure ? "https" : "http";
|
|
110
147
|
const address = `${type}://${config.http.host}:${config.http.port}`;
|
|
111
148
|
print(colors.gray(`at ${address}`), "\n");
|
|
112
|
-
const {modules} = config;
|
|
113
149
|
// modules may load other modules
|
|
114
|
-
|
|
150
|
+
await Promise.all(app.modules
|
|
115
151
|
.filter(module => module.load !== undefined)
|
|
116
|
-
.map(module => module.load()
|
|
152
|
+
.map(module => module.load({...app, load(dependent) {
|
|
153
|
+
app.modules.push(dependent);
|
|
154
|
+
}})));
|
|
117
155
|
|
|
118
|
-
return
|
|
119
|
-
modules: modules.concat(loads.flat())}));
|
|
156
|
+
return app;
|
|
120
157
|
};
|
package/src/commands/create.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Path} from "runtime-compat/fs";
|
|
2
2
|
|
|
3
|
-
const createModule = async
|
|
3
|
+
const createModule = async app => {
|
|
4
4
|
const space = 2;
|
|
5
5
|
try {
|
|
6
6
|
// will throw if cannot find a package.json up the filesystem hierarchy
|
|
@@ -10,7 +10,7 @@ const createModule = async env => {
|
|
|
10
10
|
name: "primate-app",
|
|
11
11
|
private: true,
|
|
12
12
|
dependencies: {
|
|
13
|
-
primate: `^${
|
|
13
|
+
primate: `^${app.version}`,
|
|
14
14
|
},
|
|
15
15
|
scripts: {
|
|
16
16
|
start: "npx primate",
|
|
@@ -23,20 +23,20 @@ const createModule = async env => {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const createConfig = async
|
|
26
|
+
const createConfig = async app => {
|
|
27
27
|
const name = "primate.config.js";
|
|
28
28
|
const template = "export default {};";
|
|
29
29
|
const root = (await Path.root()).join(name);
|
|
30
30
|
if (await root.exists) {
|
|
31
|
-
|
|
31
|
+
app.log.warn(`${root} already exists`);
|
|
32
32
|
} else {
|
|
33
33
|
await root.file.write(template);
|
|
34
|
-
|
|
34
|
+
app.log.info(`created config at ${root}`);
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export default async
|
|
39
|
-
await createModule(
|
|
40
|
-
await createConfig(
|
|
38
|
+
export default async app => {
|
|
39
|
+
await createModule(app);
|
|
40
|
+
await createConfig(app);
|
|
41
41
|
};
|
|
42
42
|
|
package/src/commands/dev.js
CHANGED
package/src/commands/help.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export default
|
|
2
|
-
|
|
1
|
+
export default app => {
|
|
2
|
+
app.log.info("available commands: create dev serve");
|
|
3
3
|
};
|
package/src/commands/serve.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Logger from "../Logger.js";
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
base: "/",
|
|
@@ -15,10 +15,14 @@ export default {
|
|
|
15
15
|
"form-action": "'self'",
|
|
16
16
|
"base-uri": "'self'",
|
|
17
17
|
},
|
|
18
|
+
static: {
|
|
19
|
+
root: "/",
|
|
20
|
+
pure: false,
|
|
21
|
+
},
|
|
18
22
|
},
|
|
19
23
|
paths: {
|
|
20
|
-
public: "public",
|
|
21
24
|
static: "static",
|
|
25
|
+
public: "public",
|
|
22
26
|
routes: "routes",
|
|
23
27
|
components: "components",
|
|
24
28
|
},
|
package/src/exports.js
CHANGED
package/src/fromNull.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default object => Object.assign(Object.create(null), object);
|
package/src/handlers/html.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
export default (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
export default (component, flags = {}) => {
|
|
2
|
+
const {status = 200, partial = false, load = false} = flags;
|
|
3
|
+
|
|
4
|
+
return async (app, headers) => {
|
|
5
|
+
const body = load ?
|
|
6
|
+
await app.paths.components.join(component).text() : component;
|
|
7
|
+
|
|
8
|
+
return [partial ? body : await app.render({body}), {
|
|
9
|
+
status,
|
|
10
|
+
headers: {...headers, "Content-Type": "text/html"},
|
|
11
|
+
}];
|
|
9
12
|
};
|
|
13
|
+
};
|
package/src/handlers/view.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export default (name, props, options) =>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
1
|
+
export default (name, props, options) => async (app, headers) => {
|
|
2
|
+
const ending = name.slice(name.lastIndexOf(".") + 1);
|
|
3
|
+
const handler = app.handlers[ending];
|
|
4
|
+
if (handler === undefined) {
|
|
5
|
+
return app.log.error(new Error(`no handler for ${ending} components`));
|
|
6
|
+
}
|
|
7
|
+
return handler(name, {load: true, ...props}, options)(app, headers);
|
|
8
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {File} from "runtime-compat/fs";
|
|
2
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
3
|
+
|
|
4
|
+
const pre = async app => {
|
|
5
|
+
const {paths} = app;
|
|
6
|
+
|
|
7
|
+
// remove public directory in case exists
|
|
8
|
+
if (await paths.public.exists) {
|
|
9
|
+
await paths.public.file.remove();
|
|
10
|
+
}
|
|
11
|
+
await paths.public.file.create();
|
|
12
|
+
|
|
13
|
+
if (await paths.static.exists) {
|
|
14
|
+
// copy static files to public
|
|
15
|
+
const filter = file => app.config.http.static.pure
|
|
16
|
+
? true
|
|
17
|
+
: !file.endsWith(".js") && !file.endsWith(".css");
|
|
18
|
+
await File.copy(paths.static, paths.public, filter);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default async (app, bundle) => {
|
|
23
|
+
await pre(app);
|
|
24
|
+
if (bundle) {
|
|
25
|
+
app.log.info("running bundle hooks");
|
|
26
|
+
await [...filter("bundle", app.modules), _ => _]
|
|
27
|
+
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
|
+
|
|
3
|
+
export default async app => {
|
|
4
|
+
app.log.info("running compile hooks");
|
|
5
|
+
await [...filter("compile", app.modules), _ => _]
|
|
6
|
+
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export {default as register} from "./register.js";
|
|
2
|
+
export {default as compile} from "./compile.js";
|
|
3
|
+
export {default as publish} from "./publish.js";
|
|
4
|
+
export {default as bundle} from "./bundle.js";
|
|
5
|
+
export {default as route} from "./route.js";
|
|
6
|
+
export {default as handle} from "./handle.js";
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
const mimes = {
|
|
2
2
|
binary: "application/octet-stream",
|
|
3
3
|
css: "text/css",
|
|
4
4
|
html: "text/html",
|
|
5
5
|
jpg: "image/jpeg",
|
|
6
6
|
js: "text/javascript",
|
|
7
|
+
mjs: "text/javascript",
|
|
7
8
|
json: "application/json",
|
|
8
9
|
png: "image/png",
|
|
9
10
|
svg: "image/svg+xml",
|
|
10
11
|
woff2: "font/woff2",
|
|
11
12
|
webp: "image/webp",
|
|
12
13
|
};
|
|
14
|
+
|
|
15
|
+
const regex = /\.(?<extension>[a-z1-9]*)$/u;
|
|
16
|
+
const match = filename => filename.match(regex)?.groups.extension;
|
|
17
|
+
|
|
18
|
+
export default filename => mimes[match(filename)] ?? mimes.binary;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import mime from "./mime.js";
|
|
2
|
+
|
|
3
|
+
export default test => {
|
|
4
|
+
test.case("match", assert => {
|
|
5
|
+
assert(mime("/app.js")).equals("text/javascript");
|
|
6
|
+
});
|
|
7
|
+
test.case("no extension", assert => {
|
|
8
|
+
assert(mime("/app")).equals("application/octet-stream");
|
|
9
|
+
});
|
|
10
|
+
test.case("unknown extension", assert => {
|
|
11
|
+
assert(mime("/app.unknown")).equals("application/octet-stream");
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {Blob} from "runtime-compat/fs";
|
|
2
|
-
import {
|
|
2
|
+
import {URL} from "runtime-compat/http";
|
|
3
|
+
import {text, json, stream, redirect} from "primate";
|
|
3
4
|
import {isResponse as isResponseDuck} from "./duck.js";
|
|
4
|
-
import RouteError from "./errors/Route.js";
|
|
5
5
|
|
|
6
6
|
const isText = value => {
|
|
7
7
|
if (typeof value === "string") {
|
|
8
8
|
return text(value);
|
|
9
9
|
}
|
|
10
|
-
throw new
|
|
10
|
+
throw new Error(`no handler found for ${value}`);
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const isNonNullObject = value => typeof value === "object" && value !== null;
|
|
@@ -19,6 +19,8 @@ const isStream = value => value instanceof ReadableStream
|
|
|
19
19
|
? stream(value) : isResponse(value);
|
|
20
20
|
const isBlob = value => value instanceof Blob
|
|
21
21
|
? stream(value) : isStream(value);
|
|
22
|
-
const
|
|
22
|
+
const isURL = value => value instanceof URL
|
|
23
|
+
? redirect(value.href) : isBlob(value);
|
|
24
|
+
const guess = value => isURL(value);
|
|
23
25
|
|
|
24
26
|
export default result => typeof result === "function" ? result : guess(result);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import respond from "./respond.js";
|
|
2
|
+
|
|
3
|
+
export default test => {
|
|
4
|
+
test.case("guess URL", assert => {
|
|
5
|
+
const url = "https://primatejs.com/";
|
|
6
|
+
const status = 302;
|
|
7
|
+
const [body, options] = respond(new URL(url))();
|
|
8
|
+
assert(body).null();
|
|
9
|
+
assert(options.status).equals(status);
|
|
10
|
+
assert(options.headers.Location).equals(url);
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
import {Path} from "runtime-compat/fs";
|
|
2
|
-
import {serve, Response} from "runtime-compat/http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {isResponse} from "./duck.js";
|
|
7
|
-
import respond from "./respond.js";
|
|
8
|
-
|
|
9
|
-
const regex = /\.([a-z1-9]*)$/u;
|
|
10
|
-
const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
|
|
2
|
+
import {serve, Response, URL} from "runtime-compat/http";
|
|
3
|
+
import {http404} from "../handlers/http.js";
|
|
4
|
+
import {statuses, mime, isResponse, respond} from "./handle/exports.js";
|
|
5
|
+
import fromNull from "../fromNull.js";
|
|
11
6
|
|
|
12
7
|
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
13
8
|
|
|
14
9
|
const contents = {
|
|
15
10
|
"application/x-www-form-urlencoded": body =>
|
|
16
|
-
Object.fromEntries(body.split("&").map(part => part.split("=")
|
|
17
|
-
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " ")))),
|
|
11
|
+
fromNull(Object.fromEntries(body.split("&").map(part => part.split("=")
|
|
12
|
+
.map(subpart => decodeURIComponent(subpart).replaceAll("+", " "))))),
|
|
18
13
|
"application/json": body => JSON.parse(body),
|
|
19
14
|
};
|
|
20
15
|
|
|
21
|
-
export default
|
|
16
|
+
export default async app => {
|
|
17
|
+
const {config} = app;
|
|
18
|
+
const {http} = config;
|
|
19
|
+
|
|
22
20
|
const _respond = async request => {
|
|
23
|
-
const csp = Object.keys(
|
|
24
|
-
`${policy_string}${key} ${
|
|
25
|
-
const scripts =
|
|
21
|
+
const csp = Object.keys(config.http.csp).reduce((policy_string, key) =>
|
|
22
|
+
`${policy_string}${key} ${config.http.csp[key]};`, "");
|
|
23
|
+
const scripts = app.resources
|
|
26
24
|
.map(resource => `'${resource.integrity}'`).join(" ");
|
|
27
25
|
const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
|
|
28
26
|
// remove inline resources
|
|
29
|
-
for (let i =
|
|
30
|
-
const resource =
|
|
27
|
+
for (let i = app.resources.length - 1; i >= 0; i--) {
|
|
28
|
+
const resource = app.resources[i];
|
|
31
29
|
if (resource.inline) {
|
|
32
|
-
|
|
30
|
+
app.resources.splice(i, 1);
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -39,15 +37,15 @@ export default env => {
|
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
try {
|
|
42
|
-
const {router} =
|
|
43
|
-
const modules = filter("route",
|
|
40
|
+
const {router} = app;
|
|
41
|
+
const modules = filter("route", app.modules);
|
|
44
42
|
// handle is the last module to be executed
|
|
45
43
|
const handlers = [...modules, router.route].reduceRight((acc, handler) =>
|
|
46
44
|
input => handler(input, acc));
|
|
47
|
-
return await respond(await handlers(
|
|
45
|
+
return await respond(await handlers(request))(app, headers);
|
|
48
46
|
} catch (error) {
|
|
49
|
-
|
|
50
|
-
return http404()(
|
|
47
|
+
app.log.auto(error);
|
|
48
|
+
return http404()(app, headers);
|
|
51
49
|
}
|
|
52
50
|
};
|
|
53
51
|
|
|
@@ -56,7 +54,7 @@ export default env => {
|
|
|
56
54
|
return isResponse(response) ? response : new Response(...response);
|
|
57
55
|
};
|
|
58
56
|
|
|
59
|
-
const
|
|
57
|
+
const staticResource = async file => new Response(file.readable, {
|
|
60
58
|
status: statuses.OK,
|
|
61
59
|
headers: {
|
|
62
60
|
"Content-Type": mime(file.name),
|
|
@@ -65,8 +63,8 @@ export default env => {
|
|
|
65
63
|
});
|
|
66
64
|
|
|
67
65
|
const publishedResource = request => {
|
|
68
|
-
const published =
|
|
69
|
-
|
|
66
|
+
const published = app.resources.find(({src}) =>
|
|
67
|
+
src === request.url.pathname);
|
|
70
68
|
if (published !== undefined) {
|
|
71
69
|
return new Response(published.code, {
|
|
72
70
|
status: statuses.OK,
|
|
@@ -80,16 +78,23 @@ export default env => {
|
|
|
80
78
|
return route(request);
|
|
81
79
|
};
|
|
82
80
|
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
81
|
+
const resource = async request => {
|
|
82
|
+
const {pathname} = request.url;
|
|
83
|
+
const {root} = http.static;
|
|
84
|
+
if (pathname.startsWith(root)) {
|
|
85
|
+
const path = app.paths.public.join(pathname.replace(root, ""));
|
|
86
|
+
return await path.isFile
|
|
87
|
+
? staticResource(path.file)
|
|
88
|
+
: publishedResource(request);
|
|
89
|
+
}
|
|
90
|
+
return route(request);
|
|
86
91
|
};
|
|
87
92
|
|
|
88
93
|
const handle = async request => {
|
|
89
94
|
try {
|
|
90
|
-
return await
|
|
95
|
+
return await resource(request);
|
|
91
96
|
} catch (error) {
|
|
92
|
-
|
|
97
|
+
app.log.auto(error);
|
|
93
98
|
return new Response(null, {status: statuses.InternalServerError});
|
|
94
99
|
}
|
|
95
100
|
};
|
|
@@ -103,21 +108,14 @@ export default env => {
|
|
|
103
108
|
try {
|
|
104
109
|
return parseContentType(request.headers.get("content-type"), body);
|
|
105
110
|
} catch (error) {
|
|
106
|
-
|
|
111
|
+
app.log.warn(error);
|
|
107
112
|
return body;
|
|
108
113
|
}
|
|
109
114
|
};
|
|
110
115
|
|
|
111
|
-
const {http} = env;
|
|
112
|
-
const modules = filter("serve", env.modules);
|
|
113
|
-
|
|
114
|
-
// handle is the last module to be executed
|
|
115
|
-
const handlers = [...modules, handle].reduceRight((acc, handler) =>
|
|
116
|
-
input => handler(input, acc));
|
|
117
|
-
|
|
118
116
|
const decoder = new TextDecoder();
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
|
|
118
|
+
const parseBody = async request => {
|
|
121
119
|
const reader = request.body.getReader();
|
|
122
120
|
const chunks = [];
|
|
123
121
|
let result;
|
|
@@ -128,11 +126,26 @@ export default env => {
|
|
|
128
126
|
}
|
|
129
127
|
} while (!result.done);
|
|
130
128
|
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
return chunks.length === 0 ? null : parseContent(request, chunks.join());
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const parseRequest = async request => {
|
|
133
|
+
const cookies = request.headers.get("cookie");
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
request,
|
|
137
|
+
url: new URL(request.url),
|
|
138
|
+
body: await parseBody(request),
|
|
139
|
+
cookies: fromNull(cookies === null
|
|
140
|
+
? {}
|
|
141
|
+
: Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
|
|
142
|
+
headers: fromNull(Object.fromEntries(request.headers)),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
133
145
|
|
|
134
|
-
|
|
146
|
+
// handle is the last module to be executed
|
|
147
|
+
const handlers = [...filter("handle", app.modules), handle]
|
|
148
|
+
.reduceRight((acc, handler) => input => handler(input, acc));
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
}, http);
|
|
150
|
+
serve(async request => handlers(await parseRequest(request)), config.http);
|
|
138
151
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {Path} from "runtime-compat/fs";
|
|
2
|
+
|
|
3
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
4
|
+
|
|
5
|
+
const post = async app => {
|
|
6
|
+
// after hook, publish a zero assumptions app.js (no css imports)
|
|
7
|
+
const code = app.entrypoints.filter(({type}) => type === "script")
|
|
8
|
+
.map(entrypoint => entrypoint.code).join("");
|
|
9
|
+
await app.publish({src: `${app.config.dist}.js`, code, type: "module"});
|
|
10
|
+
|
|
11
|
+
if (!app.config.http.static.pure) {
|
|
12
|
+
const memoryFiles = await Path.collect(app.paths.static, /\.(?:js|css)$/u,
|
|
13
|
+
{recursive: false});
|
|
14
|
+
await Promise.all(memoryFiles.map(async file => {
|
|
15
|
+
const code = await file.text();
|
|
16
|
+
const src = file.name;
|
|
17
|
+
await app.publish({src, code, type: file.extension === ".js" ?
|
|
18
|
+
"module" : "style"});
|
|
19
|
+
if (file.extension === ".css") {
|
|
20
|
+
app.bootstrap({type: "style", code: `import "./${file.name}";`});
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
await Promise.all(Object.entries(app.library).map(async libfile => {
|
|
25
|
+
const [, src] = libfile;
|
|
26
|
+
const code = await Path.resolve().join("node_modules", src).text();
|
|
27
|
+
await app.publish({src, code, type: "module"});
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default async app => {
|
|
32
|
+
app.log.info("running publish hooks");
|
|
33
|
+
await [...filter("publish", app.modules), _ => _]
|
|
34
|
+
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
35
|
+
await post(app);
|
|
36
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
2
|
+
|
|
3
|
+
export default async app => {
|
|
4
|
+
app.log.info("running register hooks");
|
|
5
|
+
await [...filter("register", app.modules), _ => _]
|
|
6
|
+
.reduceRight((acc, handler) => input => handler(input, acc))(app);
|
|
7
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {Path} from "runtime-compat/fs";
|
|
2
|
+
import {Logger} from "primate";
|
|
3
|
+
import fromNull from "../fromNull.js";
|
|
4
|
+
|
|
5
|
+
// insensitive-case equal
|
|
6
|
+
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
7
|
+
// HTTP verbs
|
|
8
|
+
const verbs = [
|
|
9
|
+
// CRUD
|
|
10
|
+
"post", "get", "put", "delete",
|
|
11
|
+
// extended
|
|
12
|
+
"delete", "connect", "options", "trace", "patch",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const toRoute = file => {
|
|
16
|
+
const ending = -3;
|
|
17
|
+
const route = file
|
|
18
|
+
// remove ending
|
|
19
|
+
.slice(0, ending)
|
|
20
|
+
// transform /index -> ""
|
|
21
|
+
.replace("/index", "")
|
|
22
|
+
// transform index -> ""
|
|
23
|
+
.replace("index", "")
|
|
24
|
+
// prepare for regex
|
|
25
|
+
.replaceAll(/\{(?<named>.*)\}/gu, (_, name) => `(?<${name}>.*?)`)
|
|
26
|
+
;
|
|
27
|
+
return new RegExp(`^/${route}$`, "u");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default async app => {
|
|
31
|
+
const routes = (await Promise.all(
|
|
32
|
+
(await Path.collect(app.paths.routes, /^.*.js$/u))
|
|
33
|
+
.map(async route => {
|
|
34
|
+
const imported = (await import(route)).default;
|
|
35
|
+
const file = `${route}`.replace(app.paths.routes, "").slice(1);
|
|
36
|
+
if (imported === undefined) {
|
|
37
|
+
app.log.warn(`empty route file at ${file}`);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const path = toRoute(file);
|
|
42
|
+
return Object.entries(imported)
|
|
43
|
+
.filter(([verb]) => verbs.includes(verb))
|
|
44
|
+
.map(([method, handler]) => ({method, handler, path}));
|
|
45
|
+
}))).flat();
|
|
46
|
+
const find = (method, path) => routes.find(route =>
|
|
47
|
+
ieq(route.method, method) && route.path.test(path));
|
|
48
|
+
|
|
49
|
+
const router = {
|
|
50
|
+
async route({request, url, ...rest}) {
|
|
51
|
+
const {method} = request;
|
|
52
|
+
const {pathname, searchParams} = url;
|
|
53
|
+
const verb = find(method, pathname) ?? (() => {
|
|
54
|
+
throw new Logger.Warn(`no ${method} route to ${pathname}`);
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
const data = {
|
|
58
|
+
request,
|
|
59
|
+
url,
|
|
60
|
+
path: verb.path?.exec(pathname)?.groups ?? Object.create(null),
|
|
61
|
+
query: fromNull(Object.fromEntries(searchParams)),
|
|
62
|
+
...rest,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return verb.handler(data);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return router;
|
|
69
|
+
};
|
package/src/run.js
CHANGED
package/src/start.js
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import register
|
|
2
|
-
|
|
3
|
-
import publish from "./publish.js";
|
|
4
|
-
import bundle from "./bundle.js";
|
|
5
|
-
import route from "./route.js";
|
|
6
|
-
import serve from "./serve.js";
|
|
7
|
-
import config from "./config.js";
|
|
1
|
+
import {register, compile, publish, bundle, route, handle}
|
|
2
|
+
from "./hooks/exports.js";
|
|
8
3
|
|
|
9
|
-
export default async (
|
|
10
|
-
// read/write configuration
|
|
11
|
-
await config(env);
|
|
4
|
+
export default async (app, operations = {}) => {
|
|
12
5
|
// register handlers
|
|
13
|
-
await register(
|
|
6
|
+
await register({...app, register(name, handler) {
|
|
7
|
+
app.handlers[name] = handler;
|
|
8
|
+
}});
|
|
9
|
+
|
|
14
10
|
// compile server-side code
|
|
15
|
-
await compile(
|
|
11
|
+
await compile(app);
|
|
16
12
|
// publish client-side code
|
|
17
|
-
await publish(
|
|
13
|
+
await publish(app);
|
|
18
14
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
.map(({code}) => code).join("");
|
|
22
|
-
await env.publish({src: `${env.dist}.js`, code, type: "module"});
|
|
15
|
+
// bundle client-side code
|
|
16
|
+
await bundle(app, operations?.bundle);
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await bundle(env);
|
|
27
|
-
}
|
|
28
|
-
// serve
|
|
29
|
-
serve({router: await route(env), ...env});
|
|
18
|
+
// handle
|
|
19
|
+
await handle({router: await route(app), ...app});
|
|
30
20
|
};
|
package/LICENSE
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Copyright (c) Terrablue <terrablue@proton.me> and contributors.
|
|
2
|
-
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
-
in the Software without restriction, including without limitation the rights
|
|
6
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
-
furnished to do so, subject to the following conditions:
|
|
9
|
-
|
|
10
|
-
The above copyright notice and this permission notice shall be included in
|
|
11
|
-
all copies or substantial portions of the Software.
|
|
12
|
-
|
|
13
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
-
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
-
THE SOFTWARE.
|
package/src/bundle.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import {File} from "runtime-compat/fs";
|
|
2
|
-
const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
|
|
3
|
-
|
|
4
|
-
const makePublic = async env => {
|
|
5
|
-
const {paths} = env;
|
|
6
|
-
|
|
7
|
-
// remove public directory in case exists
|
|
8
|
-
if (await paths.public.exists) {
|
|
9
|
-
await paths.public.file.remove();
|
|
10
|
-
}
|
|
11
|
-
await paths.public.file.create();
|
|
12
|
-
|
|
13
|
-
if (await paths.static.exists) {
|
|
14
|
-
// copy static files to public
|
|
15
|
-
await File.copy(paths.static, paths.public);
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export default async env => {
|
|
20
|
-
await makePublic(env);
|
|
21
|
-
[...filter("bundle", env.modules), _ => _].reduceRight((acc, handler) =>
|
|
22
|
-
input => handler(input, acc))(env);
|
|
23
|
-
};
|
package/src/cache.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const Cache = class Cache {
|
|
2
|
-
static #caches = [];
|
|
3
|
-
|
|
4
|
-
static get(object, property) {
|
|
5
|
-
return Cache.#caches.find(entry =>
|
|
6
|
-
entry.object === object && entry.property === property)?.value;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
static put(object, property, cacher) {
|
|
10
|
-
const value = cacher();
|
|
11
|
-
Cache.#caches.push({object, property, value});
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export default (object, property, cacher) =>
|
|
17
|
-
Cache.get(object, property) ?? Cache.put(object, property, cacher);
|
package/src/compile.js
DELETED
package/src/config.js
DELETED
package/src/errors/Info.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default class InfoError extends Error {}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default class InternalServerError extends Error {}
|
package/src/errors/Predicate.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default class PredicateError extends Error {}
|
package/src/errors/Route.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default class RouteError extends Error {}
|
package/src/errors.js
DELETED
package/src/publish.js
DELETED
package/src/register.js
DELETED
package/src/route.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {Path} from "runtime-compat/fs";
|
|
2
|
-
import Logger from "./Logger.js";
|
|
3
|
-
|
|
4
|
-
// insensitive-case equal
|
|
5
|
-
const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
6
|
-
// HTTP verbs
|
|
7
|
-
const verbs = [
|
|
8
|
-
// CRUD
|
|
9
|
-
"post", "get", "put", "delete",
|
|
10
|
-
// extended
|
|
11
|
-
"delete", "connect", "options", "trace", "patch",
|
|
12
|
-
];
|
|
13
|
-
export default async env => {
|
|
14
|
-
const routes = [];
|
|
15
|
-
const find = (method, path, fallback = {handler: r => r}) =>
|
|
16
|
-
routes.find(route =>
|
|
17
|
-
ieq(route.method, method) && route.path.test(path)) ?? fallback;
|
|
18
|
-
|
|
19
|
-
const router = {
|
|
20
|
-
route: async ({request}) => {
|
|
21
|
-
const {method} = request.original;
|
|
22
|
-
const url = new URL(`https://primatejs.com${request.pathname}`);
|
|
23
|
-
const {pathname, searchParams} = url;
|
|
24
|
-
const params = Object.fromEntries(searchParams);
|
|
25
|
-
const verb = find(method, pathname, {handler: () => {
|
|
26
|
-
throw new Logger.Info(`no ${method.toUpperCase()} route to ${pathname}`);
|
|
27
|
-
}});
|
|
28
|
-
const path = pathname.split("/").filter(part => part !== "");
|
|
29
|
-
const named = verb.path?.exec(pathname)?.groups ?? {};
|
|
30
|
-
|
|
31
|
-
return verb.handler(await find("map", pathname)
|
|
32
|
-
.handler({...request, pathname, params, path, named}));
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const toRoute = file => {
|
|
36
|
-
const ending = -3;
|
|
37
|
-
const route = file
|
|
38
|
-
// remove ending
|
|
39
|
-
.slice(0, ending)
|
|
40
|
-
// transform /index -> ""
|
|
41
|
-
.replace("/index", "")
|
|
42
|
-
// transform index -> ""
|
|
43
|
-
.replace("index", "")
|
|
44
|
-
// prepare for regex
|
|
45
|
-
.replaceAll(/\{(?<named>.*)\}/gu, (_, name) => `(?<${name}>.*?)`)
|
|
46
|
-
;
|
|
47
|
-
return new RegExp(`^/${route}$`, "u");
|
|
48
|
-
};
|
|
49
|
-
for (const route of await Path.collect(env.paths.routes, /^.*.js$/u)) {
|
|
50
|
-
const imported = (await import(route)).default;
|
|
51
|
-
const file = `${route}`.replace(env.paths.routes, "").slice(1);
|
|
52
|
-
if (imported === undefined) {
|
|
53
|
-
env.log.warn(`empty route file at ${file}`);
|
|
54
|
-
} else {
|
|
55
|
-
const valids = Object.entries(imported)
|
|
56
|
-
.filter(([verb]) => verbs.includes(verb));
|
|
57
|
-
for (const [method, handler] of valids) {
|
|
58
|
-
routes.push({method, path: toRoute(file), handler});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return router;
|
|
63
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|