@primate/solid 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/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # Solid
2
+
3
+ This handler module supports SSR and serves SolidJS (JSX) components with the
4
+ .jsx extension.
5
+
6
+ ## [Documentation](https://primatejs.com/modules/solid)
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@primate/solid",
3
+ "version": "0.0.1",
4
+ "description": "Primate Solid module",
5
+ "homepage": "https://primatejs.com/modules/solid",
6
+ "bugs": "https://github.com/primatejs/primate/issues",
7
+ "license": "MIT",
8
+ "files": [
9
+ "src/**/*.js",
10
+ "!src/**/*.spec.js"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/primatejs/primate",
15
+ "directory": "packages/solid"
16
+ },
17
+ "dependencies": {
18
+ "babel-preset-solid": "^1.7.7",
19
+ "runtime-compat": "^0.25.1",
20
+ "solid-js": "^1.7.11"
21
+ },
22
+ "peerDependencies": {
23
+ "primate": "0.21"
24
+ },
25
+ "type": "module",
26
+ "exports": "./src/exports.js"
27
+ }
@@ -0,0 +1,20 @@
1
+ import {generateHydrationScript} from "solid-js/web";
2
+ import rootname from "./rootname.js";
3
+ import liveview from "./liveview.js";
4
+
5
+ const {groups: {code: hydration_script}} = generateHydrationScript()
6
+ .match(/^<script>(?<code>.*?)<\/script>/u);
7
+
8
+ export default ({names, data}, options) => `
9
+ import * as components from "app";
10
+ import {hydrate_solid} from "app";
11
+
12
+ window._$HY = { events: [], completed: new WeakSet(), r: {} };
13
+
14
+ ${hydration_script}
15
+
16
+ const root = hydrate_solid(() => components.${rootname}({
17
+ components: [${names.map(name => `components.${name}`).join(", ")}],
18
+ data: JSON.parse(${JSON.stringify(JSON.stringify(data))}),
19
+ }), globalThis.window.document.body);
20
+ ${options.liveview ? liveview : ""}`;
@@ -0,0 +1,16 @@
1
+ export default (length, data) => {
2
+ const n = length - 1;
3
+ const body = Array.from({length: n}, (_, i) => i - 1)
4
+ .reduceRight((child, _, i) => `components[${i + 1}] !== undefined
5
+ ? createComponent(components[${i}], {${data}: data[${i}],
6
+ children: ${child}})
7
+ : createComponent(components[${i}], {${data}: data[${i}]})
8
+ `, `createComponent(components[${n}], {${data}: data[${n}]})`);
9
+
10
+ return `
11
+ import {createComponent} from "solid-js/web";
12
+ export default function Root({components, data} = {}) {
13
+ return ${body};
14
+ };
15
+ `;
16
+ };
@@ -0,0 +1,22 @@
1
+ export default (length, data) => {
2
+ const n = length - 1;
3
+ const body = Array.from({length: n}, (_, i) => i - 1)
4
+ .reduceRight((child, _, i) => `<Show
5
+ when={components[${i + 1}] !== undefined}
6
+ fallback={
7
+ <>
8
+ <Dynamic component={components[${i}]} ${data}={data[${i}]} />
9
+ </>
10
+ }
11
+ >
12
+ <Dynamic component={components[${i}]} ${data}={data[${i}]}>${child}</Dynamic>
13
+ </Show>
14
+ `, `<Dynamic component={components[${n}]} ${data}={data[${n}]} />`);
15
+
16
+ return `
17
+ import {Dynamic, Show} from "solid-js/web";
18
+ export default function Root({components, data} = {}) {
19
+ return ${body};
20
+ };
21
+ `;
22
+ };
@@ -0,0 +1,5 @@
1
+ export {default as client} from "./client.js";
2
+ export {default as hydrate} from "./hydrate.js";
3
+ export {default as create_root} from "./create-root.js";
4
+ export {default as create_root2} from "./create-root2.js";
5
+ export {default as rootname} from "./rootname.js";
@@ -0,0 +1 @@
1
+ export default "export {hydrate as hydrate_solid} from \"solid-js/web\";";
@@ -0,0 +1,12 @@
1
+ import rootname from "./rootname.js";
2
+
3
+ export default `
4
+ const {liveview} = components;
5
+ window.addEventListener("DOMContentLoaded", _ => liveview((props, update) => {
6
+ root.render(
7
+ createElement(components.${rootname}, {
8
+ components: props.names.map(name => components[name]),
9
+ data: props.data,
10
+ })
11
+ );
12
+ }));`;
@@ -0,0 +1 @@
1
+ export default "solid_root";
package/src/errors.js ADDED
@@ -0,0 +1,6 @@
1
+ import {Path} from "runtime-compat/fs";
2
+ import {Logger} from "primate";
3
+
4
+ const json = await new Path(import.meta.url).up(1).join("errors.json").json();
5
+
6
+ export default Logger.err(json.errors, json.module);
package/src/exports.js ADDED
@@ -0,0 +1 @@
1
+ export {default} from "./module.js";
package/src/module.js ADDED
@@ -0,0 +1,211 @@
1
+ import crypto from "runtime-compat/crypto";
2
+ import {Path} from "runtime-compat/fs";
3
+ import {Response, Status, MediaType} from "runtime-compat/http";
4
+ import {transformAsync} from "@babel/core";
5
+ import solid from "babel-preset-solid";
6
+ import {renderToString} from "solid-js/web";
7
+ import {tryreturn} from "runtime-compat/async";
8
+ import errors from "./errors.js";
9
+ import {client, create_root, create_root2, rootname, hydrate} from "./client/exports.js";
10
+
11
+ // do not hard-depend on node
12
+ const packager = import.meta.runtime?.packager ?? "package.json";
13
+ const library = import.meta.runtime?.library ?? "node_modules";
14
+
15
+ const encoder = new TextEncoder();
16
+ const hash = async (string, algorithm = "sha-256") => {
17
+ const base = 16;
18
+ const target_pad_length = 2;
19
+ const target_slice = 12;
20
+ const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
21
+ return Array.from(new Uint8Array(bytes))
22
+ .map(byte => byte.toString(base).padStart(target_pad_length, "0"))
23
+ .join("")
24
+ .slice(0, target_slice);
25
+ };
26
+ const normalize = async path => `solid_${await hash(path)}`;
27
+
28
+ const import$ = async (module, app, copy_dependency) => {
29
+ const {build: {modules}, http: {static: {root}}} = app.config;
30
+ const parts = module.split("/");
31
+ const path = [library, ...parts];
32
+ const pkg = await Path.resolve().join(...path, packager).json();
33
+ if (copy_dependency) {
34
+ const dependency = Path.resolve().join(...path);
35
+ const to = app.build.paths.client.join(modules, ...parts);
36
+ await dependency.file.copy(`${to}`);
37
+ }
38
+
39
+ const entry = pkg.exports["."].browser.import;
40
+ app.importmaps[module] = new Path(root, modules, module, entry).path;
41
+ };
42
+
43
+ const filename = `${rootname}.js`;
44
+ const type = "module";
45
+
46
+ const load = async path =>
47
+ tryreturn(async () => (await import(`${path}.js`)).default)
48
+ .orelse(_ => errors.MissingComponent.throw(path.name, path));
49
+
50
+ const make_component = base => async (name, props) =>
51
+ ({name, component: await load(base.join(name)), props});
52
+
53
+ const handler = (name, props = {}, {status = Status.OK} = {}) =>
54
+ async (app, {layouts = [], as_layout} = {}, request) => {
55
+ const options = {
56
+ liveview: app.liveview !== undefined,
57
+ };
58
+ const {headers} = request;
59
+ const {paths} = app.build;
60
+ const make = make_component(paths.server.join(app.config.build.app));
61
+ if (as_layout) {
62
+ return make(name, props);
63
+ }
64
+ const components = (await Promise.all(layouts.map(layout =>
65
+ layout(app, {as_layout: true}, request)
66
+ )))
67
+ /* set the actual page as the last component */
68
+ .concat(await make(name, props));
69
+
70
+ const data = components.map(component => component.props);
71
+ const names = await Promise.all(components.map(component =>
72
+ normalize(component.name)));
73
+ if (options.liveview && headers.get(app.liveview.header) !== undefined) {
74
+ return new Response(JSON.stringify({names, data}), {
75
+ status,
76
+ headers: {...await app.headers(),
77
+ "Content-Type": MediaType.APPLICATION_JSON},
78
+ });
79
+ }
80
+
81
+ const root = paths.server.join(filename);
82
+ const imported = (await import(root)).default;
83
+ const body = renderToString(() => imported({
84
+ components: components.map(({component}) => component),
85
+ data,
86
+ }));
87
+
88
+ const code = client({names, data}, options);
89
+
90
+ await app.publish({code, type, inline: true});
91
+ // needs to be called before app.render
92
+ const headers$ = await app.headers();
93
+
94
+ return new Response(await app.render({body}), {
95
+ status,
96
+ headers: {...headers$, "Content-Type": MediaType.TEXT_HTML},
97
+ });
98
+ };
99
+
100
+ const options = {
101
+ server: [solid, {
102
+ generate: "ssr",
103
+ hydratable: true,
104
+ }],
105
+ client: [solid, {
106
+ generate: "dom",
107
+ hydratable: true,
108
+ }],
109
+ };
110
+
111
+ const endings = {
112
+ solid: ".solid",
113
+ js: ".solid.js",
114
+ };
115
+
116
+ const transform = {
117
+ async server(file) {
118
+ return (await transformAsync(file, {presets: [options.server]})).code;
119
+ },
120
+ async client(file) {
121
+ return (await transformAsync(file, {presets: [options.client]})).code;
122
+ },
123
+ };
124
+
125
+ export default ({
126
+ dynamicProps = "data",
127
+ extension = "jsx",
128
+ } = {}) => {
129
+ const env = {};
130
+
131
+ return {
132
+ name: "@primate/solid",
133
+ init(app, next) {
134
+ env.source = app.build.paths.components;
135
+
136
+ return next(app);
137
+ },
138
+ register(app, next) {
139
+ app.register("solid", handler);
140
+ return next(app);
141
+ },
142
+ async compile(app, next) {
143
+ // copy components to build/components
144
+ await app.copy(app.paths.components, env.source, /^.*.(?:solid)$/u);
145
+ const components = await env.source.collect(/^.*.solid$/u);
146
+ const target = app.build.paths.server.join(app.config.build.app);
147
+ await target.file.create();
148
+
149
+ await Promise.all(components.map(async component => {
150
+ const file = await component.file.read();
151
+ const code = await transform.server(file);
152
+ const to = target.join(`${component.path}.js`.replace(env.source, ""));
153
+ await to.directory.file.create();
154
+ await to.file.write(code.replaceAll(endings.solid, endings.js));
155
+ }));
156
+
157
+ const root = await transform.server(create_root2(app.layoutDepth,
158
+ dynamicProps));
159
+ const to = app.build.paths.server.join(filename);
160
+ await to.file.write(root);
161
+
162
+ return next(app);
163
+ },
164
+ async publish(app, next) {
165
+ const target = app.build.paths.client.join(app.config.build.app);
166
+ await target.file.create();
167
+
168
+ // create client components
169
+ const components = await env.source.collect(/^.*.solid/u);
170
+ await Promise.all(components.map(async component => {
171
+ const name = component.path.replace(`${env.source}/`, "");
172
+ const file = await component.file.read();
173
+ const code = await transform.client(file);
174
+ const build = app.config.build.app;
175
+ const {path} = component;
176
+
177
+ {
178
+ {
179
+ const src = `${path}.js`.replace(`${env.source}`, _ => build);
180
+ await app.publish({src,
181
+ code: code.replaceAll(endings.solid, endings.js),
182
+ type});
183
+ }
184
+ const imported = await normalize(name);
185
+ app.bootstrap({
186
+ type: "script",
187
+ code: `export {default as ${imported}} from "./${name}.js";\n`,
188
+ });
189
+ }
190
+ }));
191
+
192
+ await import$("solid-js", app, true);
193
+ await import$("solid-js/web", app);
194
+
195
+ app.bootstrap({type: "script", code: hydrate});
196
+
197
+ {
198
+ const code = await transform.client(create_root2(app.layoutDepth,
199
+ dynamicProps));
200
+ const src = new Path(app.config.http.static.root, filename);
201
+ await app.publish({src, code, type});
202
+ }
203
+ {
204
+ const code = `export {default as ${rootname}} from "../${filename}";\n`;
205
+ app.bootstrap({type: "script", code});
206
+ }
207
+
208
+ return next(app);
209
+ },
210
+ };
211
+ };