@primate/svelte 0.9.1 → 0.11.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 CHANGED
@@ -1,27 +1,6 @@
1
- # Primate Svelte module
1
+ # Svelte handler
2
2
 
3
- ## Install
3
+ This handler module supports SSR and hydration and serves Svelte components
4
+ with the .svelte extension.
4
5
 
5
- `npm i @primate/svelte`
6
-
7
- ## Configure
8
-
9
- Add to `primate.config.js`
10
-
11
- ```js
12
- import svelte from "@primate/svelte";
13
-
14
- export default {
15
- modules: [svelte()],
16
- };
17
- ```
18
-
19
- If you use a bundler, specify an array of entry points:
20
-
21
- ```js
22
- import svelte from "@primate/svelte";
23
-
24
- export default {
25
- modules: [svelte({entryPoints: ["EntryPage.svelte"]})],
26
- };
27
- ```
6
+ ## [Documentation](https://primatejs.com/modules/svelte)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@primate/svelte",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Primate Svelte module",
5
5
  "homepage": "https://primatejs.com/modules/svelte",
6
6
  "bugs": "https://github.com/primatejs/primate/issues",
7
7
  "license": "MIT",
8
8
  "files": [
9
- "src/**",
9
+ "src/**/*.js",
10
10
  "!src/**/*.spec.js"
11
11
  ],
12
12
  "repository": {
@@ -15,11 +15,11 @@
15
15
  "directory": "packages/svelte"
16
16
  },
17
17
  "dependencies": {
18
- "runtime-compat": "^0.17.0",
18
+ "runtime-compat": "^0.20.2",
19
19
  "svelte": "^3.59.1"
20
20
  },
21
21
  "peerDependencies": {
22
- "primate": "0.18"
22
+ "primate": "0.20"
23
23
  },
24
24
  "type": "module",
25
25
  "exports": "./src/exports.js"
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/module.js CHANGED
@@ -1,95 +1,211 @@
1
+ import {Path} from "runtime-compat/fs";
1
2
  import * as compiler from "svelte/compiler";
2
- import {File, Path} from "runtime-compat/fs";
3
+ import {tryreturn} from "runtime-compat/flow";
4
+ import errors from "./errors.js";
3
5
 
4
6
  const endings = {
5
7
  svelte: ".svelte",
6
8
  js: ".svelte.js",
7
9
  };
8
10
 
11
+ const hash = async (string, algorithm = "sha-256") => {
12
+ const base = 16;
13
+ const target_pad_length = 2;
14
+ const target_slice = 12;
15
+ const bytes = await crypto.subtle.digest(algorithm, encoder.encode(string));
16
+ return Array.from(new Uint8Array(bytes))
17
+ .map(byte => byte.toString(base).padStart(target_pad_length, "0"))
18
+ .join("")
19
+ .slice(0, target_slice);
20
+ };
21
+ const normalize = async path => `_${await hash(path)}`;
22
+
23
+ const rootname = "root";
24
+ const filename = `${rootname}.js`;
25
+
26
+ const options = {
27
+ server: {generate: "ssr", hydratable: true},
28
+ client: {generate: "dom", hydratable: true},
29
+ };
30
+
9
31
  const type = "module";
10
32
 
11
- const handler = path => (component, props = {}, {status = 200} = {}) =>
12
- async (app, headers) => {
13
- const {html: body} = (await import(`${path.join(`${component}.js`)}`))
14
- .default.render(props);
33
+ const load = async path =>
34
+ tryreturn(async () => (await import(`${path}.js`)).default)
35
+ .orelse(() => errors.MissingComponent.throw(path.name, path));
36
+
37
+ const make_component = base => async (name, props) =>
38
+ ({name, component: await load(base.join(name)), props});
15
39
 
16
- const name = component.slice(0, -endings.svelte.length);
40
+ const encoder = new TextEncoder();
17
41
 
18
- const src = new Path(app.config.http.static.root, app.config.dist);
19
- // create an entry point
20
- const entry = `import {${name} as Component} from "${src}.js";
21
- const props = JSON.parse(${JSON.stringify(JSON.stringify(props))});
22
- new Component({
42
+ const handler = ({path}) => (name, props = {}, {status = 200, page} = {}) =>
43
+ async (app, {layouts = [], as_layout} = {}) => {
44
+ const make = make_component(path);
45
+ if (as_layout) {
46
+ return make(name, props);
47
+ }
48
+ const headers = app.headers();
49
+ const components = (await Promise.all(layouts.map(layout =>
50
+ layout(app, {as_layout: true})
51
+ )))
52
+ /* set the actual page as the last component */
53
+ .concat(await make(name, props));
54
+
55
+ const data = components.map(component => component.props);
56
+
57
+ const root = app.paths.server.join(filename);
58
+ const imported = (await import(root)).default;
59
+ const {html} = imported.render({
60
+ components: components.map(({component}) => component),
61
+ data,
62
+ });
63
+
64
+ const names = (await Promise.all(components.map(component =>
65
+ normalize(component.name)))).join(", ");
66
+ const code = `
67
+ import {${rootname}, ${names}} from "app";
68
+ new ${rootname}({
23
69
  target: document.body,
24
70
  hydrate: true,
25
- props,
71
+ props: {
72
+ components: [${names}],
73
+ data: JSON.parse(${JSON.stringify(JSON.stringify(data))}),
74
+ },
26
75
  });
27
76
  `;
28
- const inline = true;
29
- const integrity = await app.publish({code: entry, type, inline});
77
+
78
+ const integrity = await app.publish({code, type, inline: true});
30
79
  // hacky, CSP should be a map that gets flattened in the end
31
80
  headers["Content-Security-Policy"] = headers["Content-Security-Policy"]
32
81
  .replace("script-src 'self' ", `script-src 'self' '${integrity}' `);
33
82
 
34
83
  // -> spread into new Response()
35
- return [await app.render({body}), {
84
+ return [await app.render({body: html, page}), {
36
85
  status,
37
86
  headers: {...headers, "Content-Type": "text/html"},
38
87
  }];
39
88
  };
40
89
 
90
+ const create_root = (length, data) => {
91
+ const n = length - 1;
92
+ const body = Array.from({length: n}, (_, i) => i - 1)
93
+ .reduceRight((child, _, i) => `
94
+ {#if components[${i + 1}]}
95
+ <svelte:component this={components[${i}]} ${data}={data[${i}]}>
96
+ ${child}
97
+ </svelte:component>
98
+ {:else}
99
+ <svelte:component this={components[${i}]} ${data}={data[${i}]} />
100
+ {/if}
101
+ `, `<svelte:component this={components[${n}]} ${data}={data[${n}]} />`);
102
+
103
+ return `
104
+ <script>
105
+ export let components;
106
+ export let data;
107
+ </script>
108
+ ${body}
109
+ `;
110
+ };
111
+
41
112
  const {svelte, js} = endings;
42
- export default ({directory, entryPoints} = {}) => ({
43
- name: "@primate/svelte",
44
- register(app, next) {
45
- app.register("svelte", handler(directory ?? app.paths.components));
46
- return next(app);
47
- },
48
- async compile(app, next) {
49
- const path = directory ?? app.paths.components;
50
- const options = {generate: "ssr", hydratable: true};
51
- const components = await path.list(filename => filename.endsWith(svelte));
52
- await Promise.all(components.map(async component => {
53
- const file = await component.file.read();
54
- const server = compiler.compile(file, options);
55
- const compiled = server.js.code.replaceAll(svelte, js);
56
- await File.write(`${component.path}.js`, compiled);
57
- }));
58
-
59
- return next(app);
60
- },
61
- async publish(app, next) {
62
- const _path = ["node_modules", "svelte", "package.json"];
63
- app.resolve(await Path.resolve().join(..._path).json(), "svelte");
64
-
65
- const path = directory ?? app.paths.components;
66
- const options = {
67
- generate: "dom",
68
- hydratable: true,
69
- };
70
- const components = await path.list(filename => filename.endsWith(svelte));
71
-
72
- await Promise.all(components.map(async component => {
73
- const file = await component.file.read();
74
- const client = compiler.compile(file, options);
75
- const css = client.css.code;
76
- const compiled = client.js.code.replaceAll(svelte, js);
77
-
78
- await app.publish({src: `${component.name}.js`, code: compiled, type});
79
- if (css !== null) {
80
- const name = `${component.name}.css`;
81
- await app.publish({src: name, code: css, type: "style"});
82
- app.bootstrap({type: "style", code: `import "./${name}";`});
113
+ export default ({
114
+ directory,
115
+ dynamicProps = "data",
116
+ } = {}) => {
117
+ const env = {};
118
+ return {
119
+ name: "@primate/svelte",
120
+ async init(app) {
121
+ const source = directory ?? app.paths.components;
122
+ env.copyJS = source !== app.paths.components;
123
+ env.copy = app.copy;
124
+ env.source = source;
125
+ env.components = await source.collect(/^.*.svelte$/u);
126
+ env.build = app.config.build.app;
127
+ env.paths = {
128
+ server: app.paths.server.join(env.build),
129
+ client: app.paths.client.join(env.build),
130
+ };
131
+ },
132
+ register(app, next) {
133
+ app.register("svelte", handler({path: env.paths.server}));
134
+ return next(app);
135
+ },
136
+ async compile(app, next) {
137
+ const target = env.paths.server;
138
+ await target.file.create();
139
+ // copy any JS files from an alternative components directory
140
+ if (env.copyJS) {
141
+ await app.copy(env.source, env.paths.server);
142
+ await app.copy(env.source, env.paths.client);
83
143
  }
84
- }));
85
144
 
86
- if (entryPoints !== undefined) {
87
- entryPoints.forEach(entry => {
88
- const name = entry.slice(0, -endings.svelte.length);
89
- const code = `export {default as ${name}} from "./${entry}.js";`;
145
+ await Promise.all(env.components.map(async component => {
146
+ const file = await component.file.read();
147
+ const server = compiler.compile(file, options.server);
148
+ const to = target.join(`${component.path}.js`.replace(env.source, ""));
149
+ await to.directory.file.create();
150
+ await to.file.write(server.js.code.replaceAll(svelte, js));
151
+ }));
152
+
153
+ const root = create_root(app.layoutDepth, dynamicProps);
154
+ const server = compiler.compile(root, options.server).js.code;
155
+ const to = app.paths.server.join(filename);
156
+ await to.file.write(server);
157
+
158
+ return next(app);
159
+ },
160
+ async publish(app, next) {
161
+ await app.import("svelte");
162
+ const {source, build} = env;
163
+
164
+ const target = env.paths.client;
165
+ await target.file.create();
166
+
167
+ await Promise.all(env.components.map(async component => {
168
+ const name = component.path.replace(`${source}/`, "");
169
+ const file = await component.file.read();
170
+ const client = compiler.compile(file, options.client);
171
+ {
172
+ {
173
+ const code = client.js.code.replaceAll(svelte, js);
174
+ const src = `${component.path}.js`.replace(`${source}`, build);
175
+ await app.publish({src, code, type});
176
+ }
177
+ const imported = await normalize(name);
178
+ app.bootstrap({
179
+ type: "script",
180
+ code: `export {default as ${imported}} from "./${name}.js";\n`,
181
+ });
182
+ }
183
+ if (client.css.code !== null) {
184
+ {
185
+ const {code} = client.css;
186
+ const src = `${component.path}.css`.replace(`${source}`, build);
187
+ await app.publish({src, code, type: "style"});
188
+ }
189
+ {
190
+ const src = `${component.path}.css`.replace(`${source}`, "");
191
+ app.bootstrap({type: "style", code: `import ".${src}";`});
192
+ }
193
+ }
194
+ }));
195
+
196
+ {
197
+ const root = create_root(app.layoutDepth, dynamicProps);
198
+ const client = compiler.compile(root, options.client);
199
+ const code = client.js.code.replaceAll(svelte, js);
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`;
90
205
  app.bootstrap({type: "script", code});
91
- });
92
- }
93
- return next(app);
94
- },
95
- });
206
+ }
207
+
208
+ return next(app);
209
+ },
210
+ };
211
+ };