vitrify 0.3.0 → 0.4.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/dist/app-urls.js +1 -1
- package/dist/bin/cli.js +26 -4
- package/dist/bin/dev.js +59 -27
- package/dist/frameworks/vue/fastify-ssr-plugin.js +67 -16
- package/dist/frameworks/vue/server.js +9 -4
- package/dist/helpers/collect-css-ssr.js +57 -0
- package/dist/index.js +255 -69
- package/dist/plugins/quasar.js +9 -4
- package/dist/types/bin/build.d.ts +2 -2
- package/dist/types/bin/dev.d.ts +39 -3
- package/dist/types/frameworks/vue/fastify-ssr-plugin.d.ts +6 -3
- package/dist/types/frameworks/vue/server.d.ts +9 -5
- package/dist/types/helpers/collect-css-ssr.d.ts +10 -0
- package/dist/types/helpers/routes.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/plugins/index.d.ts +1 -1
- package/dist/types/vitrify-config.d.ts +3 -4
- package/package.json +32 -32
- package/src/node/app-urls.ts +1 -1
- package/src/node/bin/build.ts +2 -2
- package/src/node/bin/cli.ts +33 -5
- package/src/node/bin/dev.ts +92 -34
- package/src/node/frameworks/vue/fastify-ssr-plugin.ts +80 -18
- package/src/node/frameworks/vue/server.ts +22 -8
- package/src/node/helpers/collect-css-ssr.ts +77 -0
- package/src/node/index.ts +285 -81
- package/src/node/plugins/index.ts +1 -1
- package/src/node/plugins/quasar.ts +9 -4
- package/src/node/vitrify-config.ts +7 -4
- package/src/vite/fastify/entry.ts +11 -0
- package/src/vite/fastify/server.ts +10 -0
- package/src/vite/vue/index.html +1 -0
- package/src/vite/vue/main.ts +0 -1
- package/src/vite/vue/ssr/app.ts +25 -0
- package/src/vite/vue/ssr/entry-server.ts +13 -1
- package/src/vite/vue/ssr/server.ts +23 -14
package/dist/app-urls.js
CHANGED
|
@@ -14,7 +14,7 @@ export const getSrcDir = (appDir) => new URL('src/', appDir);
|
|
|
14
14
|
export const getCwd = () => new URL(`file://${process.cwd()}/`);
|
|
15
15
|
export const parsePath = (path, basePath) => {
|
|
16
16
|
if (path) {
|
|
17
|
-
if (path.slice(-1) !== '/')
|
|
17
|
+
if (!path.includes('.') && path.slice(-1) !== '/')
|
|
18
18
|
path += '/';
|
|
19
19
|
if (path.startsWith('.')) {
|
|
20
20
|
return new URL(path, basePath);
|
package/dist/bin/cli.js
CHANGED
|
@@ -36,6 +36,13 @@ cli
|
|
|
36
36
|
outDir: new URL('spa/', baseOutDir).pathname
|
|
37
37
|
});
|
|
38
38
|
break;
|
|
39
|
+
case 'fastify':
|
|
40
|
+
await build({
|
|
41
|
+
ssr: 'fastify',
|
|
42
|
+
...args,
|
|
43
|
+
outDir: new URL('server/', baseOutDir).pathname
|
|
44
|
+
});
|
|
45
|
+
break;
|
|
39
46
|
case 'ssr':
|
|
40
47
|
await build({
|
|
41
48
|
ssr: 'client',
|
|
@@ -80,28 +87,43 @@ cli
|
|
|
80
87
|
.option('-m, --mode [mode]', 'Development server mode', { default: 'csr' })
|
|
81
88
|
.option('--host [host]', 'Specify which IP addresses the server should listen on', { default: '127.0.0.1' })
|
|
82
89
|
.option('--appDir [appDir]', 'Application directory')
|
|
90
|
+
.option('--app [app]', 'Fastify app instance path')
|
|
83
91
|
.option('--publicDir [publicDir]', 'Public directory')
|
|
84
92
|
.action(async (options) => {
|
|
85
93
|
let server;
|
|
86
|
-
let
|
|
94
|
+
let config;
|
|
87
95
|
if (options.host === true) {
|
|
88
96
|
options.host = '0.0.0.0';
|
|
89
97
|
}
|
|
90
98
|
const { createServer } = await import('./dev.js');
|
|
91
99
|
const cwd = (await import('../app-urls.js')).getCwd();
|
|
100
|
+
let app;
|
|
101
|
+
const appPath = parsePath(options.app, cwd)?.pathname;
|
|
102
|
+
if (appPath) {
|
|
103
|
+
app = await import(appPath);
|
|
104
|
+
}
|
|
92
105
|
switch (options.mode) {
|
|
93
106
|
case 'ssr':
|
|
94
107
|
;
|
|
95
|
-
({ server,
|
|
108
|
+
({ server, config } = await createServer({
|
|
96
109
|
mode: 'ssr',
|
|
97
110
|
host: options.host,
|
|
98
111
|
appDir: parsePath(options.appDir, cwd),
|
|
99
112
|
publicDir: parsePath(options.publicDir, cwd)
|
|
100
113
|
}));
|
|
101
114
|
break;
|
|
115
|
+
case 'fastify':
|
|
116
|
+
;
|
|
117
|
+
({ server, config } = await createServer({
|
|
118
|
+
mode: 'fastify',
|
|
119
|
+
host: options.host,
|
|
120
|
+
appDir: parsePath(options.appDir, cwd),
|
|
121
|
+
publicDir: parsePath(options.publicDir, cwd)
|
|
122
|
+
}));
|
|
123
|
+
break;
|
|
102
124
|
default:
|
|
103
125
|
;
|
|
104
|
-
({ server,
|
|
126
|
+
({ server, config } = await createServer({
|
|
105
127
|
host: options.host,
|
|
106
128
|
appDir: parsePath(options.appDir, cwd),
|
|
107
129
|
publicDir: parsePath(options.publicDir, cwd)
|
|
@@ -109,7 +131,7 @@ cli
|
|
|
109
131
|
break;
|
|
110
132
|
}
|
|
111
133
|
console.log('Dev server running at:');
|
|
112
|
-
printHttpServerUrls(server,
|
|
134
|
+
printHttpServerUrls(server, config);
|
|
113
135
|
});
|
|
114
136
|
cli.command('test').action(async (options) => {
|
|
115
137
|
const { test } = await import('./test.js');
|
package/dist/bin/dev.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { searchForWorkspaceRoot } from 'vite';
|
|
2
2
|
import { baseConfig } from '../index.js';
|
|
3
3
|
import fastify from 'fastify';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const { getAppDir, getCliDir, getCwd } = await import('../app-urls.js');
|
|
7
|
-
const cwd = getCwd();
|
|
4
|
+
export async function createVitrifyDevServer({ port = 3000, logLevel = 'info', mode = 'csr', framework = 'vue', host, appDir, publicDir }) {
|
|
5
|
+
const { getAppDir, getCliDir, getCliViteDir, getCwd } = await import('../app-urls.js');
|
|
8
6
|
const cliDir = getCliDir();
|
|
9
7
|
if (!appDir)
|
|
10
8
|
appDir = getAppDir();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
let config = {};
|
|
10
|
+
let ssrMode;
|
|
11
|
+
if (mode === 'ssr')
|
|
12
|
+
ssrMode = 'server';
|
|
13
|
+
if (mode === 'fastify')
|
|
14
|
+
ssrMode = 'fastify';
|
|
15
|
+
config = await baseConfig({
|
|
16
|
+
framework,
|
|
17
|
+
ssr: ssrMode,
|
|
17
18
|
command: 'dev',
|
|
18
19
|
mode: 'development',
|
|
19
20
|
appDir,
|
|
@@ -21,12 +22,16 @@ export async function createServer({ port = 3000, logLevel = 'info', mode = 'csr
|
|
|
21
22
|
});
|
|
22
23
|
config.logLevel = logLevel;
|
|
23
24
|
config.server = {
|
|
25
|
+
https: config.server?.https,
|
|
24
26
|
port,
|
|
25
|
-
middlewareMode: mode === 'ssr' ? 'ssr' : undefined,
|
|
27
|
+
// middlewareMode: mode === 'ssr' ? 'ssr' : undefined,
|
|
28
|
+
middlewareMode: mode !== 'csr' ? 'ssr' : false,
|
|
26
29
|
fs: {
|
|
27
30
|
allow: [
|
|
28
31
|
searchForWorkspaceRoot(process.cwd()),
|
|
29
|
-
|
|
32
|
+
...(Array.isArray(appDir)
|
|
33
|
+
? appDir.map((dir) => searchForWorkspaceRoot(dir.pathname))
|
|
34
|
+
: [searchForWorkspaceRoot(appDir.pathname)]),
|
|
30
35
|
searchForWorkspaceRoot(cliDir.pathname)
|
|
31
36
|
// appDir.pathname,
|
|
32
37
|
]
|
|
@@ -39,29 +44,56 @@ export async function createServer({ port = 3000, logLevel = 'info', mode = 'csr
|
|
|
39
44
|
},
|
|
40
45
|
host
|
|
41
46
|
};
|
|
42
|
-
const
|
|
47
|
+
const vitrifyDevServer = await (await import('vite')).createServer({
|
|
43
48
|
configFile: false,
|
|
44
49
|
...config
|
|
45
50
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
return vitrifyDevServer;
|
|
52
|
+
}
|
|
53
|
+
export async function createServer({ port = 3000, logLevel = 'info', mode = 'csr', framework = 'vue', host, appDir, publicDir }) {
|
|
54
|
+
const { getAppDir, getCliDir, getCliViteDir, getCwd } = await import('../app-urls.js');
|
|
55
|
+
const cliDir = getCliDir();
|
|
56
|
+
const vite = await createVitrifyDevServer({
|
|
57
|
+
port,
|
|
58
|
+
logLevel,
|
|
59
|
+
mode,
|
|
60
|
+
framework,
|
|
61
|
+
host,
|
|
62
|
+
appDir,
|
|
63
|
+
publicDir
|
|
64
|
+
});
|
|
65
|
+
let entryUrl;
|
|
66
|
+
let setup;
|
|
50
67
|
let server;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
console.log(`Development mode: ${mode}`);
|
|
69
|
+
if (['ssr', 'fastify'].includes(mode)) {
|
|
70
|
+
const entryUrl = mode === 'fastify'
|
|
71
|
+
? new URL('src/vite/fastify/entry.ts', cliDir).pathname
|
|
72
|
+
: new URL(`src/vite/${framework}/ssr/entry-server.ts`, cliDir).pathname;
|
|
73
|
+
({ setup } = await vite.ssrLoadModule(entryUrl));
|
|
74
|
+
const app = fastify({
|
|
75
|
+
https: typeof vite.config.server.https === 'object'
|
|
76
|
+
? vite.config.server.https
|
|
77
|
+
: {}
|
|
78
|
+
});
|
|
79
|
+
if (setup) {
|
|
80
|
+
await setup({
|
|
81
|
+
fastify: app
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// await app.register(fastifySsrPlugin, {
|
|
85
|
+
// appDir,
|
|
86
|
+
// vitrifyDir: new URL('../..', import.meta.url),
|
|
87
|
+
// mode: 'development'
|
|
88
|
+
// })
|
|
89
|
+
await app.listen({
|
|
90
|
+
port: Number(port || 3000),
|
|
91
|
+
host
|
|
59
92
|
});
|
|
60
|
-
await app.listen(port || 3000, host);
|
|
61
93
|
server = app.server;
|
|
62
94
|
}
|
|
63
95
|
else {
|
|
64
96
|
server = (await vite.listen()).httpServer;
|
|
65
97
|
}
|
|
66
|
-
return { server, vite };
|
|
98
|
+
return { server, config: vite.config };
|
|
67
99
|
}
|
|
@@ -1,22 +1,66 @@
|
|
|
1
|
-
import fastifyStatic from 'fastify
|
|
1
|
+
import fastifyStatic from '@fastify/static';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import { componentsModules, collectCss } from '../../helpers/collect-css-ssr.js';
|
|
3
4
|
const fastifySsrPlugin = async (fastify, options, done) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
options.vitrifyDir =
|
|
6
|
+
options.vitrifyDir || new URL('../../..', import.meta.url);
|
|
7
|
+
const frameworkDir = new URL('vite/vue/', options.vitrifyDir);
|
|
8
|
+
options.baseUrl = options.baseUrl || '/';
|
|
9
|
+
if (options.baseUrl.charAt(options.baseUrl.length - 1) !== '/' ||
|
|
10
|
+
options.baseUrl.charAt(0) !== '/')
|
|
11
|
+
throw new Error('baseUrl should start and end with a /');
|
|
12
|
+
if (options.mode === 'development') {
|
|
13
|
+
if (!options.vitrifyDir)
|
|
14
|
+
throw new Error('Option vitrifyDir cannot be undefined');
|
|
15
|
+
// if (!options.vite) throw new Error('Option vite cannot be undefined')
|
|
16
|
+
// const { resolve } = await import('import-meta-resolve')
|
|
17
|
+
// const cliDir = new URL('../', await resolve('vitrify', import.meta.url))
|
|
18
|
+
options.appDir = options.appDir || new URL('../../..', import.meta.url);
|
|
19
|
+
const { createServer, searchForWorkspaceRoot } = await import('vite');
|
|
20
|
+
const { baseConfig } = await import('vitrify');
|
|
21
|
+
const cliDir = options.vitrifyDir;
|
|
22
|
+
const config = await baseConfig({
|
|
23
|
+
ssr: 'server',
|
|
24
|
+
command: 'dev',
|
|
25
|
+
mode: 'development',
|
|
26
|
+
appDir: options.appDir,
|
|
27
|
+
publicDir: options.publicDir || new URL('public', options.appDir)
|
|
28
|
+
});
|
|
29
|
+
config.server = {
|
|
30
|
+
middlewareMode: true,
|
|
31
|
+
fs: {
|
|
32
|
+
allow: [
|
|
33
|
+
searchForWorkspaceRoot(process.cwd()),
|
|
34
|
+
searchForWorkspaceRoot(options.appDir.pathname),
|
|
35
|
+
searchForWorkspaceRoot(cliDir.pathname)
|
|
36
|
+
// appDir.pathname,
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
watch: {
|
|
40
|
+
// During tests we edit the files too fast and sometimes chokidar
|
|
41
|
+
// misses change events, so enforce polling for consistency
|
|
42
|
+
usePolling: true,
|
|
43
|
+
interval: 100
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const vite = await createServer({
|
|
47
|
+
configFile: false,
|
|
48
|
+
...config
|
|
49
|
+
});
|
|
50
|
+
console.log('Dev mode');
|
|
51
|
+
const middie = (await import('@fastify/middie')).default;
|
|
8
52
|
await fastify.register(middie);
|
|
9
|
-
fastify.use(
|
|
10
|
-
fastify.get(
|
|
53
|
+
fastify.use(vite.middlewares);
|
|
54
|
+
fastify.get(`${options.baseUrl}*`, async (req, res) => {
|
|
11
55
|
try {
|
|
12
56
|
const url = req.raw.url;
|
|
13
57
|
const ssrContext = {
|
|
14
58
|
req,
|
|
15
59
|
res
|
|
16
60
|
};
|
|
17
|
-
const template = readFileSync(new URL('index.html',
|
|
18
|
-
const entryUrl = new URL('ssr/entry-server.ts',
|
|
19
|
-
const render = (await
|
|
61
|
+
const template = readFileSync(new URL('index.html', frameworkDir)).toString();
|
|
62
|
+
const entryUrl = new URL('ssr/entry-server.ts', frameworkDir).pathname;
|
|
63
|
+
const render = (await vite.ssrLoadModule(entryUrl)).render;
|
|
20
64
|
let manifest;
|
|
21
65
|
// TODO: https://github.com/vitejs/vite/issues/2282
|
|
22
66
|
try {
|
|
@@ -25,11 +69,18 @@ const fastifySsrPlugin = async (fastify, options, done) => {
|
|
|
25
69
|
catch (e) {
|
|
26
70
|
manifest = {};
|
|
27
71
|
}
|
|
72
|
+
const cssModules = [entryUrl];
|
|
73
|
+
// // @ts-ignore
|
|
74
|
+
// if (options.vite?.config.vitrify!.globalCss)
|
|
75
|
+
// cssModules.push(...options.vite?.config.vitrify.globalCss)
|
|
76
|
+
const matchedModules = componentsModules(cssModules, vite);
|
|
77
|
+
const css = collectCss(matchedModules);
|
|
28
78
|
const [appHtml, preloadLinks] = await render(url, manifest, ssrContext);
|
|
29
79
|
const html = template
|
|
30
80
|
.replace(`<!--preload-links-->`, preloadLinks)
|
|
31
81
|
.replace(`<!--app-html-->`, appHtml)
|
|
32
|
-
.replace('<!--product-name-->', options.productName || 'Product name')
|
|
82
|
+
.replace('<!--product-name-->', options.productName || 'Product name')
|
|
83
|
+
.replace('<!--dev-ssr-css-->', css);
|
|
33
84
|
res.code(200);
|
|
34
85
|
res.type('text/html');
|
|
35
86
|
res.send(html);
|
|
@@ -37,14 +88,14 @@ const fastifySsrPlugin = async (fastify, options, done) => {
|
|
|
37
88
|
}
|
|
38
89
|
catch (e) {
|
|
39
90
|
console.error(e.stack);
|
|
40
|
-
|
|
91
|
+
vite && vite.ssrFixStacktrace(e);
|
|
41
92
|
res.code(500);
|
|
42
93
|
res.send(e.stack);
|
|
43
94
|
}
|
|
44
95
|
});
|
|
45
96
|
}
|
|
46
97
|
else {
|
|
47
|
-
options.
|
|
98
|
+
options.appDir = options.appDir || new URL('../../..', import.meta.url);
|
|
48
99
|
fastify.register(fastifyStatic, {
|
|
49
100
|
root: new URL('./dist/ssr/client', options.appDir).pathname,
|
|
50
101
|
wildcard: false,
|
|
@@ -52,7 +103,7 @@ const fastifySsrPlugin = async (fastify, options, done) => {
|
|
|
52
103
|
prefix: options.baseUrl
|
|
53
104
|
});
|
|
54
105
|
fastify.get(`${options.baseUrl}*`, async (req, res) => {
|
|
55
|
-
const url = req.raw.url;
|
|
106
|
+
const url = req.raw.url?.replace(options.baseUrl, '/');
|
|
56
107
|
const provide = options.provide ? await options.provide(req, res) : {};
|
|
57
108
|
const ssrContext = {
|
|
58
109
|
req,
|
|
@@ -69,8 +120,8 @@ const fastifySsrPlugin = async (fastify, options, done) => {
|
|
|
69
120
|
let html = template
|
|
70
121
|
.replace(`<!--preload-links-->`, preloadLinks)
|
|
71
122
|
.replace(`<!--app-html-->`, appHtml);
|
|
72
|
-
if (options.
|
|
73
|
-
for (const ssrFunction of options.
|
|
123
|
+
if (options.onRendered?.length) {
|
|
124
|
+
for (const ssrFunction of options.onRendered) {
|
|
74
125
|
html = ssrFunction(html, ssrContext);
|
|
75
126
|
}
|
|
76
127
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import fastify from 'fastify';
|
|
2
|
-
|
|
3
|
-
export const createApp = ({ setup, appDir, baseUrl, onRenderedHooks }) => {
|
|
2
|
+
export const createApp = ({ onSetup, appDir, baseUrl, onRendered, fastifySsrPlugin, vitrifyDir, mode }) => {
|
|
4
3
|
const app = fastify({
|
|
5
4
|
logger: true
|
|
6
5
|
});
|
|
7
6
|
app.register(fastifySsrPlugin, {
|
|
8
7
|
baseUrl,
|
|
9
8
|
appDir,
|
|
10
|
-
|
|
9
|
+
onRendered,
|
|
10
|
+
vitrifyDir,
|
|
11
|
+
mode
|
|
11
12
|
});
|
|
12
|
-
|
|
13
|
+
// if (onSetup?.length) {
|
|
14
|
+
// for (const setup of onSetup) {
|
|
15
|
+
// setup(app)
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
13
18
|
return app;
|
|
14
19
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect SSR CSS for Vite
|
|
3
|
+
*/
|
|
4
|
+
export const componentsModules = (components, vite) => {
|
|
5
|
+
const matchedModules = new Set();
|
|
6
|
+
components.forEach((component) => {
|
|
7
|
+
const modules = vite.moduleGraph.getModulesByFile(component);
|
|
8
|
+
modules?.forEach((mod) => matchedModules.add(mod));
|
|
9
|
+
});
|
|
10
|
+
return matchedModules;
|
|
11
|
+
};
|
|
12
|
+
export const collectCss = (mods, styles = new Map(), checkedComponents = new Set()) => {
|
|
13
|
+
for (const mod of mods) {
|
|
14
|
+
if ((mod.file?.endsWith('.scss') ||
|
|
15
|
+
mod.file?.endsWith('.css') ||
|
|
16
|
+
mod.id?.includes('vue&type=style')) &&
|
|
17
|
+
mod.ssrModule) {
|
|
18
|
+
styles.set(mod.url, mod.ssrModule.default);
|
|
19
|
+
}
|
|
20
|
+
if (mod.importedModules.size > 0 && !checkedComponents.has(mod.id)) {
|
|
21
|
+
checkedComponents.add(mod.id);
|
|
22
|
+
collectCss(mod.importedModules, styles, checkedComponents);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let result = '';
|
|
26
|
+
styles.forEach((content, id) => {
|
|
27
|
+
const styleTag = `<style type="text/css" vite-module-id="${hashCode(id)}">${content}</style>`;
|
|
28
|
+
result = result.concat(styleTag);
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Client listener to detect updated modules through HMR, and remove the initial styled attached to the head
|
|
34
|
+
*/
|
|
35
|
+
export const removeCssHotReloaded = () => {
|
|
36
|
+
if (import.meta.hot) {
|
|
37
|
+
import.meta.hot.on('vite:beforeUpdate', (module) => {
|
|
38
|
+
module.updates.forEach((update) => {
|
|
39
|
+
const moduleStyle = document.querySelector(`[vite-module-id="${hashCode(update.acceptedPath)}"]`);
|
|
40
|
+
if (moduleStyle) {
|
|
41
|
+
moduleStyle.remove();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const hashCode = (moduleId) => {
|
|
48
|
+
let hash = 0, i, chr;
|
|
49
|
+
if (moduleId.length === 0)
|
|
50
|
+
return hash;
|
|
51
|
+
for (i = 0; i < moduleId.length; i++) {
|
|
52
|
+
chr = moduleId.charCodeAt(i);
|
|
53
|
+
hash = (hash << 5) - hash + chr;
|
|
54
|
+
hash |= 0; // Convert to 32bit integer
|
|
55
|
+
}
|
|
56
|
+
return hash;
|
|
57
|
+
};
|