@primate/core 0.1.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.
Files changed (70) hide show
  1. package/LICENSE +19 -0
  2. package/package.json +53 -0
  3. package/src/build/app.js +105 -0
  4. package/src/build/defaults/app.html +9 -0
  5. package/src/build/defaults/error.html +14 -0
  6. package/src/build/hook/build.js +179 -0
  7. package/src/build/hook/copy_includes.js +20 -0
  8. package/src/build/hook/exports.js +2 -0
  9. package/src/build/hook/init.js +3 -0
  10. package/src/build/hook/router.js +32 -0
  11. package/src/build/index.js +41 -0
  12. package/src/build/module_loader.js +30 -0
  13. package/src/build/targets/exports.js +1 -0
  14. package/src/build/targets/web.js +81 -0
  15. package/src/dispatch/index.js +1 -0
  16. package/src/log/index.js +1 -0
  17. package/src/private/bye.js +5 -0
  18. package/src/private/config-filename.js +1 -0
  19. package/src/private/config.js +48 -0
  20. package/src/private/depend.js +20 -0
  21. package/src/private/dispatch.js +18 -0
  22. package/src/private/error/bad-body.js +6 -0
  23. package/src/private/error/bad-default-export.js +6 -0
  24. package/src/private/error/bad-path.js +6 -0
  25. package/src/private/error/bad-type-export.js +6 -0
  26. package/src/private/error/bad-type-name.js +6 -0
  27. package/src/private/error/double-extension.js +6 -0
  28. package/src/private/error/double-module.js +6 -0
  29. package/src/private/error/double-path-parameter.js +6 -0
  30. package/src/private/error/double-route.js +6 -0
  31. package/src/private/error/empty-config-file.js +6 -0
  32. package/src/private/error/empty-directory.js +6 -0
  33. package/src/private/error/empty-path-parameter.js +6 -0
  34. package/src/private/error/empty-route-file.js +6 -0
  35. package/src/private/error/error-in-config-file.js +6 -0
  36. package/src/private/error/mismatched-body.js +6 -0
  37. package/src/private/error/mismatched-path.js +6 -0
  38. package/src/private/error/mismatched-type.js +6 -0
  39. package/src/private/error/module-no-name.js +6 -0
  40. package/src/private/error/modules-array.js +6 -0
  41. package/src/private/error/no-handler.js +6 -0
  42. package/src/private/error/no-route-to-path.js +6 -0
  43. package/src/private/error/optional-route.js +6 -0
  44. package/src/private/error/reserved-type-name.js +6 -0
  45. package/src/private/error/rest-route.js +6 -0
  46. package/src/private/error.js +15 -0
  47. package/src/private/log.js +69 -0
  48. package/src/private/loglevel.js +9 -0
  49. package/src/private/mark.js +5 -0
  50. package/src/private/validate.js +11 -0
  51. package/src/serve/app.js +191 -0
  52. package/src/serve/handler/error.js +10 -0
  53. package/src/serve/handler/json.js +10 -0
  54. package/src/serve/handler/redirect.js +11 -0
  55. package/src/serve/handler/shared/base.js +4 -0
  56. package/src/serve/handler/sse.js +24 -0
  57. package/src/serve/handler/stream.js +4 -0
  58. package/src/serve/handler/text.js +10 -0
  59. package/src/serve/handler/view.js +15 -0
  60. package/src/serve/handler/ws.js +2 -0
  61. package/src/serve/hook/exports.js +5 -0
  62. package/src/serve/hook/handle.js +120 -0
  63. package/src/serve/hook/init.js +3 -0
  64. package/src/serve/hook/parse.js +17 -0
  65. package/src/serve/hook/respond.js +28 -0
  66. package/src/serve/hook/route.js +50 -0
  67. package/src/serve/hook/serve.js +53 -0
  68. package/src/serve/index.js +9 -0
  69. package/src/serve/module_loader.js +16 -0
  70. package/src/serve/to_sorted.js +1 -0
package/LICENSE ADDED
@@ -0,0 +1,19 @@
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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@primate/core",
3
+ "version": "0.1.0",
4
+ "description": "Primate core",
5
+ "homepage": "https://primatejs.com",
6
+ "bugs": "https://github.com/primatejs/primate/issues",
7
+ "license": "MIT",
8
+ "files": [
9
+ "src/**/*.js",
10
+ "!src/**/*.spec.js",
11
+ "src/build/defaults/*.html"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/primatejs/primate",
16
+ "directory": "packages/core"
17
+ },
18
+ "dependencies": {
19
+ "@rcompat/array": "^0.3.0",
20
+ "@rcompat/async": "^0.3.0",
21
+ "@rcompat/build": "^0.4.0",
22
+ "@rcompat/cli": "^0.5.1",
23
+ "@rcompat/crypto": "^0.5.0",
24
+ "@rcompat/fs": "^0.4.0",
25
+ "@rcompat/function": "^0.4.0",
26
+ "@rcompat/http": "^0.5.1",
27
+ "@rcompat/invariant": "^0.5.0",
28
+ "@rcompat/object": "^0.5.0",
29
+ "@rcompat/package": "^0.7.0",
30
+ "@rcompat/platform": "^0.3.0",
31
+ "@rcompat/stdio": "^0.4.0",
32
+ "@rcompat/sync": "^0.3.0"
33
+ },
34
+ "type": "module",
35
+ "imports": {
36
+ "#*": {
37
+ "@primate/lt": "./src/private/*.js",
38
+ "default": "./src/private/*.js"
39
+ },
40
+ "#error/*": {
41
+ "@primate/lt": "./src/private/error/*.js",
42
+ "default": "./src/private/error/*.js"
43
+ }
44
+ },
45
+ "exports": {
46
+ "./serve": "./src/serve/index.js",
47
+ "./build": "./src/build/index.js",
48
+ "./error": "./src/error/index.js",
49
+ "./log": "./src/log/index.js",
50
+ "./dispatch": "./src/dispatch/index.js",
51
+ "./handler/*": "./src/serve/handler/*.js"
52
+ }
53
+ }
@@ -0,0 +1,105 @@
1
+ import join from "@rcompat/fs/join";
2
+ import get from "@rcompat/object/get";
3
+ import valmap from "@rcompat/object/valmap";
4
+ import module_loader from "./module_loader.js";
5
+ import { web } from "./targets/exports.js";
6
+
7
+ export default async (root, config) => {
8
+ const path = valmap(config.location, value => root.join(value));
9
+ const error = await path.routes.join("+error.js");
10
+
11
+ return {
12
+ postbuild: [],
13
+ bindings: {},
14
+ roots: [],
15
+ targets: { web: { target: web } },
16
+ importmaps: {},
17
+ assets: [],
18
+ path,
19
+ root,
20
+ // pseudostatic thus arrowbound
21
+ get: (config_key, fallback) => get(config, config_key) ?? fallback,
22
+ set: (key, value) => {
23
+ config[key] = value;
24
+ },
25
+ error: {
26
+ default: await error.exists() ? await error.import("default") : undefined,
27
+ },
28
+ extensions: {},
29
+ modules: await module_loader(root, config.modules ?? []),
30
+ fonts: [],
31
+ // copy files to build folder, potentially transforming them
32
+ async stage(source, directory, filter) {
33
+ const { define = {} } = this.get("build", {});
34
+
35
+ if (!await source.exists()) {
36
+ return;
37
+ }
38
+
39
+ const target_base = this.runpath(directory);
40
+ const defines = Object.entries(define);
41
+
42
+ // first, copy everything
43
+ await source.copy(target_base);
44
+
45
+ const location = this.get("location");
46
+ const client_location = join(location.client, location.static).path;
47
+ const mapper = text =>
48
+ defines.reduce((replaced, [key, substitution]) =>
49
+ replaced.replaceAll(key, substitution), text);
50
+
51
+ // then, copy and transform whitelisted paths using mapper
52
+ await Promise.all((await source.collect(filter)).map(async abs_path => {
53
+ const rel_path = join(directory, abs_path.debase(source));
54
+ if (directory.path === client_location && rel_path.path.endsWith(".css")) {
55
+ const contents = await abs_path.text();
56
+ const font_regex = /@font-face\s*\{.+?url\("(.+?\.woff2)"\).+?\}/gus;
57
+ this.fonts.push(...[...contents.matchAll(font_regex)].map(match => match[1]));
58
+ }
59
+ const target = await target_base.join(rel_path.debase(directory));
60
+ await target.directory.create();
61
+
62
+ defines.length > 0 && await target.write(mapper(await abs_path.text()));
63
+ }));
64
+ },
65
+ async compile(component) {
66
+ const { server, client, components } = this.get("location");
67
+
68
+ const compile = this.extensions[component.fullExtension]
69
+ ?? this.extensions[component.extension];
70
+ if (compile === undefined) {
71
+ const source = this.path.build.join(components);
72
+ const debased = `${component.path}`.replace(source, "");
73
+
74
+ const server_target = this.runpath(server, components, debased);
75
+ await server_target.directory.create();
76
+ await component.copy(server_target);
77
+
78
+ const client_target = this.runpath(client, components, debased);
79
+ await client_target.directory.create();
80
+ await component.copy(client_target);
81
+ } else {
82
+ // compile server components
83
+ await compile.server(component, this);
84
+
85
+ // compile client components
86
+ await compile.client(component, this);
87
+ }
88
+ },
89
+ register(extension, compile) {
90
+ this.extensions[extension] = compile;
91
+ },
92
+ runpath(...directories) {
93
+ return this.path.build.join(...directories);
94
+ },
95
+ target(name, handler) {
96
+ this.targets[name] = handler;
97
+ },
98
+ bind(extension, handler) {
99
+ this.bindings[extension] = handler;
100
+ },
101
+ done(fn) {
102
+ this.postbuild.push(fn);
103
+ },
104
+ };
105
+ };
@@ -0,0 +1,9 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Primate app</title>
5
+ <meta charset="utf-8" />
6
+ %head%
7
+ </head>
8
+ <body>%body%</body>
9
+ </html>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Error page</title>
5
+ <meta charset="utf-8" />
6
+ %head%
7
+ </head>
8
+ <body>
9
+ <h1>Error page</h1>
10
+ <p>
11
+ %body%
12
+ </p>
13
+ </body>
14
+ </html>
@@ -0,0 +1,179 @@
1
+ import config_filename from "#config-filename";
2
+ import log from "@primate/core/log";
3
+ import cascade from "@rcompat/async/cascade";
4
+ import Build from "@rcompat/build";
5
+ import dim from "@rcompat/cli/color/dim";
6
+ import collect from "@rcompat/fs/collect";
7
+ import join from "@rcompat/fs/join";
8
+ import exclude from "@rcompat/object/exclude";
9
+ import stringify from "@rcompat/object/stringify";
10
+ import manifest from "@rcompat/package/manifest";
11
+ import copy_includes from "./copy_includes.js";
12
+ import $router from "./router.js";
13
+
14
+ const html = /^.*.html$/u;
15
+
16
+ const pre = async (app, mode, target) => {
17
+ let target$ = target;
18
+ if (app.targets[target$] === undefined) {
19
+ throw new Error(`target ${dim(target)} does not exist`);
20
+ }
21
+ if (app.targets[target$].forward) {
22
+ target$ = app.targets[target$].forward;
23
+ }
24
+ app.build_target = target$;
25
+ log.system(`starting ${dim(target$)} build in ${dim(mode)} mode`);
26
+
27
+ app.build = new Build({
28
+ ...exclude(app.get("build"), ["includes", "index"]),
29
+ outdir: app.runpath(app.get("location.client")).path,
30
+ stdin: {
31
+ resolveDir: app.root.path,
32
+ },
33
+ }, mode);
34
+
35
+ app.server_build = ["routes", "types"];
36
+
37
+ // remove build directory in case exists
38
+ await app.path.build.remove();
39
+ await app.path.build.create();
40
+
41
+ await Promise.all(["server", "client", "pages", "components"]
42
+ .map(directory => app.runpath(directory).create()));
43
+
44
+ const router = await $router(app.path.routes);
45
+ const layout = { depth: router.depth("layout") };
46
+ app.set("layout", layout);
47
+
48
+ return { ...app, layout };
49
+ };
50
+
51
+ const js_re = /^.*.js$/u;
52
+ const write_directories = async (build_directory, app) => {
53
+ for (const name of app.server_build) {
54
+ const d = app.runpath(name);
55
+ const e = await Promise.all((await collect(d, js_re, { recursive: true }))
56
+ .map(async file => `${file}`.replace(d, _ => "")));
57
+ const files_js = `
58
+ const ${name} = [];
59
+ ${e.map((file , i) =>
60
+ `import * as ${name}${i} from "../${name}${file}";
61
+ ${name}.push(["${file.slice(1, -".js".length)}", ${name}${i}]);`,
62
+ ).join("\n")}
63
+ export default ${name};`;
64
+ await build_directory.join(`${name}.js`).write(files_js);
65
+ }
66
+ };
67
+
68
+ const write_components = async (build_directory, app) => {
69
+ const location = app.get("location");
70
+ const d2 = app.runpath(location.server, location.components);
71
+ const e = await Promise.all((await collect(d2, js_re, { recursive: true }))
72
+ .map(async file => `${file}`.replace(d2, _ => "")));
73
+ const components_js = `
74
+ const components = [];
75
+ ${e.map((component, i) =>
76
+ `import * as component${i} from "../server/components${component}";
77
+ components.push(["${component.slice(1, -".js".length)}", component${i}]);`,
78
+ ).join("\n")}
79
+
80
+ ${app.roots.map((root, i) => `
81
+ import * as root${i} from "${root}";
82
+ components.push(["${root.name}", root${i}]);
83
+ `).join("\n")}
84
+
85
+ export default components;`;
86
+ await build_directory.join("components.js").write(components_js);
87
+ };
88
+
89
+ const write_bootstrap = async (build_number, app, mode) => {
90
+ const build_start_script = `
91
+ import file from "@rcompat/fs/file";
92
+ import serve from "@primate/core/serve";
93
+ import config from "./${config_filename}";
94
+ const files = {};
95
+ ${app.server_build.map(name =>
96
+ `import ${name} from "./${build_number}/${name}.js";
97
+ files.${name} = ${name};`,
98
+ ).join("\n")}
99
+ import components from "./${build_number}/components.js";
100
+ import * as target from "./target.js";
101
+
102
+ await serve(file(import.meta.url).directory, {
103
+ ...target,
104
+ config,
105
+ files,
106
+ components,
107
+ mode: "${mode}",
108
+ });`;
109
+ await app.path.build.join("serve.js").write(build_start_script);
110
+ };
111
+
112
+ const post = async (app, mode, target) => {
113
+ const location = app.get("location");
114
+ const defaults = join(import.meta.dirname, "../defaults");
115
+
116
+ await Promise.all(["routes", "types", "components"].map(directory =>
117
+ app.stage(app.path[directory], location[directory])));
118
+
119
+ const directory = app.runpath(location.routes);
120
+ for (const path of await directory.collect()) {
121
+ await app.bindings[path.extension]
122
+ ?.(directory, path.debase(`${directory}/`));
123
+ }
124
+ // copy framework pages
125
+ await app.stage(defaults, location.pages, html);
126
+ // overwrite transformed pages to build
127
+ await app.stage(app.path.pages, location.pages, html);
128
+
129
+ // copy static files to build/server/static
130
+ await app.stage(app.path.static, join(location.server, location.static));
131
+
132
+ // copy static files to build/client/static
133
+ await app.stage(app.path.static, join(location.client, location.static));
134
+
135
+ // publish JavaScript and CSS files
136
+ const imports = await collect(app.path.static, /\.(?:css)$/u);
137
+ await Promise.all(imports.map(async file => {
138
+ const src = file.debase(app.path.static);
139
+ app.build.export(`import "./${location.static}${src}";`);
140
+ }));
141
+
142
+ // copy additional subdirectories to build/server
143
+ await copy_includes(app, location.server);
144
+
145
+ const components = await app.runpath(location.components).collect();
146
+
147
+ // from the build directory, compile to server and client
148
+ await Promise.all(components.map(component => app.compile(component)));
149
+
150
+ // start the build
151
+ await app.build.start();
152
+
153
+ // a target needs to create an `assets.js` that exports assets
154
+ await app.targets[app.build_target].target(app);
155
+
156
+ const build_number = crypto.randomUUID().slice(0, 8);
157
+ const build_directory = app.path.build.join(build_number);
158
+ // TODO: remove after rcompat automatically creates directories
159
+ await build_directory.create();
160
+
161
+ await write_components(build_directory, app);
162
+ await write_directories(build_directory, app);
163
+ await write_bootstrap(build_number, app, mode);
164
+
165
+ // copy config file
166
+ await app.root.join(config_filename).copy(app.path.build.join(config_filename));
167
+
168
+ const manifest_data = await manifest();
169
+ // create package.json
170
+ const package_json = "package.json";
171
+ await app.path.build.join(package_json).write(stringify(manifest_data));
172
+
173
+ log.system(`build written to ${dim(app.path.build)}`);
174
+
175
+ app.postbuild.forEach(fn => fn());
176
+ };
177
+
178
+ export default async (app, mode = "development", target = "web") =>
179
+ post(await (await cascade(app.modules.build))(await pre(app, mode, target)), mode, target);
@@ -0,0 +1,20 @@
1
+ import join from "@rcompat/fs/join";
2
+
3
+ export default async (app, type, post = () => undefined) => {
4
+ const includes = app.get("build.includes");
5
+ const reserved = Object.values(app.get("location"));
6
+
7
+ if (Array.isArray(includes)) {
8
+ await Promise.all(includes
9
+ .filter(include => !reserved.includes(include))
10
+ .filter(include => /^[^/]*$/u.test(include))
11
+ .map(async include => {
12
+ const path = app.root.join(include);
13
+ if (await path.exists()) {
14
+ const target = join(type, include);
15
+ await app.stage(path, target);
16
+ await post(target);
17
+ }
18
+ }));
19
+ }
20
+ };
@@ -0,0 +1,2 @@
1
+ export { default as init } from "./init.js";
2
+ export { default as build } from "./build.js";
@@ -0,0 +1,3 @@
1
+ import cascade from "@rcompat/async/cascade";
2
+
3
+ export default async app => (await cascade(app.modules.init))(app);
@@ -0,0 +1,32 @@
1
+ import double_route from "#error/double-route";
2
+ import optional_route from "#error/optional-route";
3
+ import rest_route from "#error/rest-route";
4
+ import Router from "@rcompat/fs/router";
5
+
6
+ const error_entries = Object.entries({
7
+ DoubleRoute: double_route,
8
+ OptionalRoute: optional_route,
9
+ RestRoute: rest_route,
10
+ });
11
+
12
+ export default async directory => {
13
+
14
+ try {
15
+ return await Router.load({
16
+ directory,
17
+ specials: {
18
+ guard: { recursive: true },
19
+ error: { recursive: false },
20
+ layout: { recursive: true },
21
+ },
22
+ predicate(route, request) {
23
+ return route.default[request.method.toLowerCase()] !== undefined;
24
+ },
25
+ });
26
+ } catch (error) {
27
+ error_entries.forEach(([key, value]) =>
28
+ error instanceof Router.Error[key] && value(error.route));
29
+ // rethrow original error
30
+ throw error;
31
+ }
32
+ };
@@ -0,0 +1,41 @@
1
+ import bye from "#bye";
2
+ import config from "#config";
3
+ import config_filename from "#config-filename";
4
+ import empty_config_file from "#error/empty-config-file";
5
+ import error_in_config_file from "#error/error-in-config-file";
6
+ import log from "@primate/core/log";
7
+ import tryreturn from "@rcompat/async/tryreturn";
8
+ import empty from "@rcompat/object/empty";
9
+ import override from "@rcompat/object/override";
10
+ import root from "@rcompat/package/root";
11
+ import platform from "@rcompat/platform";
12
+ import app from "./app.js";
13
+ import { build, init } from "./hook/exports.js";
14
+
15
+ const get_config = async project_root => {
16
+ const local_config = project_root.join(config_filename);
17
+ return await local_config.exists()
18
+ ? tryreturn(async _ => {
19
+ const imported = await local_config.import("default");
20
+
21
+ empty(imported) && empty_config_file(local_config);
22
+
23
+ return imported;
24
+ }).orelse(({ message }) =>
25
+ error_in_config_file(message, `${platform} ${local_config}`))
26
+ : config;
27
+ };
28
+
29
+ export default async (mode, target) => tryreturn(async _ => {
30
+ const package_root = await root();
31
+ const app_config = override(config, await get_config(package_root));
32
+ const $app = await app(package_root, app_config);
33
+ await build(await init($app), mode, target);
34
+ return true;
35
+ }).orelse(error => {
36
+ if (error.level === "error") {
37
+ bye();
38
+ return;
39
+ }
40
+ throw error;
41
+ });
@@ -0,0 +1,30 @@
1
+ import config_filename from "#config-filename";
2
+ import double_module from "#error/double-module";
3
+ import module_no_name from "#error/module-no-name";
4
+ import modules_array from "#error/modules-array";
5
+ import * as hooks from "./hook/exports.js";
6
+
7
+ const doubled = set => set.find((part, i, array) =>
8
+ array.filter((_, j) => i !== j).includes(part));
9
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
10
+ const load = (modules = []) => modules.map(module =>
11
+ [module, load(module.load?.() ?? [])]).flat();
12
+
13
+ export default async (root, modules) => {
14
+ !Array.isArray(modules) && modules_array("modules");
15
+
16
+ modules.some(({ name }, n) => name === undefined && module_no_name(n));
17
+
18
+ const names = modules.map(({ name }) => name);
19
+ new Set(names).size !== modules.length && double_module(
20
+ doubled(names), root.join(config_filename));
21
+
22
+ // collect modules
23
+ const loaded = load(modules).flat(2);
24
+
25
+ return {
26
+ names: loaded.map(module => module.name),
27
+ ...Object.fromEntries([...Object.keys(hooks), "context"]
28
+ .map(hook => [hook, filter(hook, loaded)])),
29
+ };
30
+ };
@@ -0,0 +1 @@
1
+ export { default as web } from "./web.js";
@@ -0,0 +1,81 @@
1
+ import collect from "@rcompat/fs/collect";
2
+
3
+ const html = /^.*.html$/u;
4
+
5
+ export default async app => {
6
+ const location = app.get("location");
7
+ const http = app.get("http");
8
+ const client = app.runpath(location.client);
9
+ const re = /app..*(?:js|css)$/u;
10
+ const $imports = (await client.collect(re, { recursive: false })).map((file, i) => {
11
+ const type = file.extension === ".css" ? "style" : "js";
12
+ const src = `${http.static.root}${file.debase(client).name}`;
13
+ const path = `./${file.debase(`${app.path.build}/`)}`;
14
+ return {
15
+ src,
16
+ path,
17
+ code: `await file(asset${i}).text()`,
18
+ type,
19
+ };
20
+ });
21
+ const d = app.runpath(location.pages);
22
+ const pages = await Promise.all((await collect(d, html, { recursive: true }))
23
+ .map(async file => `${file}`.replace(`${d}/`, _ => "")));
24
+ const pages_str = pages.map(page =>
25
+ `"${page}": await file("./${location.pages}/${page}").text(),`).join("\n");
26
+
27
+ const assets_scripts = `
28
+ import file from "@rcompat/fs/file";
29
+ import join from "@rcompat/fs/join";
30
+ import stringify from "@rcompat/object/stringify";
31
+ import crypto from "@rcompat/crypto";
32
+
33
+ const encoder = new TextEncoder();
34
+ const hash = async (data, algorithm = "sha-384") => {
35
+ const bytes = await crypto.subtle.digest(algorithm, encoder.encode(data));
36
+ const prefix = algorithm.replace("-", _ => "");
37
+ return \`\${prefix}-\${btoa(String.fromCharCode(...new Uint8Array(bytes)))}\`;
38
+ };
39
+
40
+ ${$imports.map(({ path }, i) =>
41
+ `const asset${i} = await join(import.meta.dirname, "${path}").text();`)
42
+ .join("\n ")}
43
+ const assets = [${$imports.map(($import, i) => `{
44
+ src: "${$import.src}",
45
+ code: asset${i},
46
+ type: "${$import.type}",
47
+ inline: false,
48
+ integrity: await hash(asset${i}),
49
+ }`).join(",\n ")}];
50
+
51
+ const imports = {
52
+ app: join("${http.static.root}", "${$imports.find($import =>
53
+ $import.src.includes("app") && $import.src.endsWith(".js")).src}").webpath(),
54
+ };
55
+ // importmap
56
+ assets.push({
57
+ inline: true,
58
+ code: stringify({ imports }),
59
+ type: "importmap",
60
+ integrity: await hash(stringify({ imports })),
61
+ });
62
+
63
+ const pages = {
64
+ ${pages_str}
65
+ };
66
+
67
+ const loader = {
68
+ page(name) {
69
+ return pages[name] ?? pages["${app.get("pages.app")}"];
70
+ },
71
+ asset(pathname) {
72
+ return assets.find(asset => asset.src === pathname);
73
+ },
74
+ };
75
+ const target = "web";
76
+
77
+ export { assets, loader, target };
78
+ `;
79
+ await app.path.build.join("target.js").write(assets_scripts);
80
+
81
+ };
@@ -0,0 +1 @@
1
+ export { default } from "#dispatch";
@@ -0,0 +1 @@
1
+ export { default } from "#log";
@@ -0,0 +1,5 @@
1
+ import print from "@rcompat/cli/print";
2
+ import dim from "@rcompat/cli/color/dim";
3
+ import yellow from "@rcompat/cli/color/yellow";
4
+
5
+ export default () => print(dim(yellow("~~ bye\n")));
@@ -0,0 +1 @@
1
+ export default "primate.config.js";
@@ -0,0 +1,48 @@
1
+ export default {
2
+ base: "/",
3
+ modules: [],
4
+ pages: {
5
+ app: "app.html",
6
+ error: "error.html",
7
+ },
8
+ log: {
9
+ level: "warn",
10
+ trace: false,
11
+ },
12
+ http: {
13
+ host: "localhost",
14
+ port: 6161,
15
+ csp: {},
16
+ static: {
17
+ root: "/",
18
+ },
19
+ },
20
+ request: {
21
+ body: {
22
+ parse: true,
23
+ },
24
+ },
25
+ location: {
26
+ // renderable components
27
+ components: "components",
28
+ // HTML pages
29
+ pages: "pages",
30
+ // hierarchical routes
31
+ routes: "routes",
32
+ // static assets
33
+ static: "static",
34
+ // runtime types
35
+ types: "types",
36
+ // build environment
37
+ build: "build",
38
+ // client build
39
+ client: "client",
40
+ // server build
41
+ server: "server",
42
+ },
43
+ build: {
44
+ name: "app",
45
+ includes: [],
46
+ excludes: [],
47
+ },
48
+ };