Make svelte work

This commit is contained in:
Alice Gaudon 2021-03-24 16:07:50 +01:00
parent 5f0e11efed
commit 29fa5f4e38
14 changed files with 750 additions and 96 deletions

View File

@ -48,6 +48,7 @@
view: { view: {
cache: false, cache: false,
enable_asset_cache: false, enable_asset_cache: false,
dev: true,
}, },
magic_link: { magic_link: {
validity_period: 20, validity_period: 20,

View File

@ -21,5 +21,6 @@
view: { view: {
cache: true, cache: true,
enable_asset_cache: true, enable_asset_cache: true,
dev: false,
}, },
} }

View File

@ -18,11 +18,13 @@
"prepare-sources": "node scripts/prepare-sources.js", "prepare-sources": "node scripts/prepare-sources.js",
"compile": "yarn clean && tsc", "compile": "yarn clean && tsc",
"build": "yarn prepare-sources && yarn compile && node scripts/dist.js", "build": "yarn prepare-sources && yarn compile && node scripts/dist.js",
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"maildev\"", "dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon -i public\" \"maildev\"",
"lint": "eslint .", "lint": "eslint .",
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish" "release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@sveltejs/eslint-config": "sveltejs/eslint-config", "@sveltejs/eslint-config": "sveltejs/eslint-config",
"@tsconfig/svelte": "^1.0.10", "@tsconfig/svelte": "^1.0.10",
"@types/compression": "^1.7.0", "@types/compression": "^1.7.0",
@ -61,6 +63,11 @@
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"nodemon": "^2.0.6", "nodemon": "^2.0.6",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2",
"rollup": "^2.42.3",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.32.8", "sass": "^1.32.8",
"supertest": "^6.0.0", "supertest": "^6.0.0",
"svelte": "^3.35.0", "svelte": "^3.35.0",

View File

@ -76,7 +76,7 @@ export default class TestApp extends Application {
// Dynamic views and routes // Dynamic views and routes
this.use(new NunjucksComponent(['test/views', 'views'])); this.use(new NunjucksComponent(['test/views', 'views']));
this.use(new FrontendToolsComponent('public', 'views', 'build')); this.use(new FrontendToolsComponent('public', 'build'));
this.use(new PreviousUrlComponent()); this.use(new PreviousUrlComponent());
// Services // Services

View File

@ -1,4 +1,4 @@
import {Router} from "express"; import {Express, Router} from "express";
import * as fs from "fs"; import * as fs from "fs";
import path from "path"; import path from "path";
import config from "config"; import config from "config";
@ -6,53 +6,102 @@ import ApplicationComponent from "../ApplicationComponent";
import {logger} from "../Logger"; import {logger} from "../Logger";
import {ServerError} from "../HttpError"; import {ServerError} from "../HttpError";
import * as crypto from "crypto"; import * as crypto from "crypto";
import {compile, preprocess} from "svelte/compiler";
import requireFromString from "require-from-string"; import requireFromString from "require-from-string";
import "svelte/register";
import {compile, preprocess} from "svelte/compiler";
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess"; import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess";
import chokidar from "chokidar"; import chokidar from "chokidar";
import {CssResult} from "svelte/types/compiler/interfaces"; import {CssResult} from "svelte/types/compiler/interfaces";
import {PreRenderedChunk, rollup, RollupBuild, RollupOptions, RollupWatcher, RollupWatchOptions, watch} from "rollup";
import svelte from "rollup-plugin-svelte";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import {terser} from "rollup-plugin-terser";
import cssOnlyRollupPlugin from "rollup-plugin-css-only";
import livereloadRollupPlugin from "rollup-plugin-livereload";
const BACKEND_CODE_PREFIX = 'swaf.'; const BACKEND_CODE_PREFIX = 'swaf.';
const COMPILED_SVELTE_EXTENSION = '.swafview'; const COMPILED_SVELTE_EXTENSION = '.swafview';
export default class FrontendToolsComponent extends ApplicationComponent { export default class FrontendToolsComponent extends ApplicationComponent {
public static getSveltePreCompileSeparator(file: string): string { public static getSveltePreCompileSeparator(canonicalViewName: string): string {
return '\n---' + crypto.createHash('sha1').update(path.basename(path.resolve(file))).digest('base64') + '---\n'; return '\n---' +
crypto.createHash('sha1')
.update(path.basename(path.resolve(canonicalViewName)))
.digest('base64') +
'---\n';
} }
private content: Map<string, string> = new Map(); private content: Map<string, string> = new Map();
private readonly viewPaths: string[];
private readonly svelteDevViewsPath: string;
private readonly dependencyCache: Record<string, Set<string>> = {};
private readonly backendCodeCache: Record<string, {
backendReplacedCode: string,
backendLines: string[],
}> = {};
private readonly fileCache: Record<string, string> = {};
private rollup?: RollupBuild | RollupWatcher;
public constructor( public constructor(
private readonly publicAssetsPath: string, private readonly publicAssetsPath: string,
private readonly svelteViewsPath: string,
private readonly svelteOutputPath: string, private readonly svelteOutputPath: string,
...viewPaths: string[]
) { ) {
super(); super();
this.viewPaths = [
...viewPaths.map(p => path.resolve(p)),
path.resolve(__dirname, '../../../views'),
path.resolve(__dirname, '../../views'),
path.resolve(__dirname, '../views'),
];
this.svelteDevViewsPath = path.resolve('views');
if (!fs.existsSync(svelteOutputPath)) { if (!fs.existsSync(svelteOutputPath)) {
fs.mkdirSync(svelteOutputPath); fs.mkdirSync(svelteOutputPath);
} }
} }
public async start(): Promise<void> { public async start(app: Express): Promise<void> {
await this.cachePublicAssets(); await this.cachePublicAssets();
await this.preCompileSvelteViews(); await this.preCompileSvelteViews();
const watcher = chokidar.watch(this.svelteViewsPath, {persistent: true}); const watcher = chokidar.watch(this.svelteDevViewsPath, {persistent: true});
watcher.on('ready', () => { watcher.on('ready', () => {
logger.debug('Watching svelte assets for changes'); logger.debug('Watching svelte assets for changes');
watcher.on('add', (path) => { watcher.on('add', (path) => {
this.preCompileSvelte(path) if (path.endsWith('.svelte')) {
.catch(logger.error); this.resetBundle()
.then(() => this.preCompileSvelte(path, true))
.then(() => this.bundle(...Object.keys(this.backendCodeCache)))
.catch(err => logger.error(err));
}
}); });
watcher.on('change', (path) => { watcher.on('change', (path) => {
this.preCompileSvelte(path) if (path.endsWith('.svelte')) {
.catch(logger.error); delete this.backendCodeCache[this.toCanonicalViewName(path)];
this.preCompileSvelte(path, true)
.then(() => this.bundle(...Object.keys(this.backendCodeCache)))
.catch(err => logger.error(err));
}
}); });
}); });
app.engine('svelte', (path, options, callback) => {
this.renderSvelte(path, options as Record<string, unknown>, callback)
.catch(err => callback(err));
});
app.set('views', this.viewPaths);
app.set('view engine', 'svelte');
}
public async stop(): Promise<void> {
await this.resetBundle();
} }
private async cachePublicAssets(): Promise<void> { private async cachePublicAssets(): Promise<void> {
@ -66,11 +115,12 @@ export default class FrontendToolsComponent extends ApplicationComponent {
private async preCompileSvelteViews(): Promise<void> { private async preCompileSvelteViews(): Promise<void> {
logger.info('Pre-compiling svelte views...'); logger.info('Pre-compiling svelte views...');
await this.forEachFileInDirRecursively(this.svelteViewsPath, async file => { await this.forEachFileInDirRecursively(this.svelteDevViewsPath, async file => {
if (file.endsWith('.svelte')) { if (file.endsWith('.svelte')) {
await this.preCompileSvelte(file); await this.preCompileSvelte(file);
} }
}); });
await this.bundle(...Object.keys(this.backendCodeCache));
} }
public async handle(router: Router): Promise<void> { public async handle(router: Router): Promise<void> {
@ -124,8 +174,12 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}); });
} }
private async preCompileSvelte(file: string): Promise<void> { private async preCompileSvelte(file: string, alsoCompileDependents: boolean = false): Promise<void> {
logger.debug('Pre-compiling', file); file = path.relative('.', file);
const canonicalViewName = this.toCanonicalViewName(file);
const intermediateFile = path.join(this.svelteOutputPath, canonicalViewName);
logger.debug(canonicalViewName + ' > ', 'Pre-compiling', file, '->', intermediateFile);
const source = await new Promise<string>((resolve, reject) => { const source = await new Promise<string>((resolve, reject) => {
fs.readFile(file, (err, data) => { fs.readFile(file, (err, data) => {
if (err) return reject(err); if (err) return reject(err);
@ -134,54 +188,127 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}); });
}); });
const {backendReplacedCode, backendLines} = this.replaceBackendCode(source); const allBackendLines: string[] = [];
for (const dependency of this.resolveDependencies(source, canonicalViewName)) {
allBackendLines.push(...(await this.replaceBackendCode(dependency)).backendLines);
}
const preprocessed = await this.preprocessSvelte(backendReplacedCode, file); const {backendReplacedCode, backendLines} = await this.replaceBackendCode(canonicalViewName, source);
allBackendLines.push(...backendLines);
const preprocessedCode = await this.preProcessSvelte(backendReplacedCode, intermediateFile, canonicalViewName);
// Server Side Render (initial HTML, no-js) // Server Side Render (initial HTML, no-js)
const ssr = this.compileSvelteSsr(preprocessed.code, file, preprocessed.sourcemap); const ssr = this.compileSvelteSsr(preprocessedCode, intermediateFile, canonicalViewName);
// Actual svelte const separator = FrontendToolsComponent.getSveltePreCompileSeparator(canonicalViewName);
const svelte = this.compileSvelteJS(preprocessed.code, preprocessed.sourcemap);
const separator = FrontendToolsComponent.getSveltePreCompileSeparator(file);
const finalCode = [ const finalCode = [
[...backendLines.values()].join('\n'), [...new Set<string>(allBackendLines).values()].join('\n'),
ssr.head, ssr.head,
ssr.html, ssr.html,
ssr.css.code, ssr.css.code,
ssr.css.map,
svelte.code,
svelte.map,
].join(separator); ].join(separator);
const newFile = path.join(this.svelteOutputPath, path.basename(file) + COMPILED_SVELTE_EXTENSION); const swafViewFile = path.join(this.svelteOutputPath, canonicalViewName + COMPILED_SVELTE_EXTENSION);
await new Promise<void>((resolve, reject) => fs.writeFile(newFile, finalCode, err => { await new Promise<void>((resolve, reject) => {
fs.mkdir(path.dirname(swafViewFile), {recursive: true}, (err) => {
if (err) return reject(err);
resolve();
});
});
await new Promise<void>((resolve, reject) => fs.writeFile(swafViewFile, finalCode, err => {
if (err) return reject(err); if (err) return reject(err);
resolve(); resolve();
})); }));
if (alsoCompileDependents && Object.keys(this.dependencyCache).indexOf(canonicalViewName) >= 0) {
logger.debug(canonicalViewName + ' > ', 'Compiling dependents...');
for (const dependent of [...this.dependencyCache[canonicalViewName]]) {
await this.preCompileSvelte(await this.resolveViewFromCanonicalName(dependent), true);
}
}
} }
private replaceBackendCode(code: string): { private resolveDependencies(source: string, canonicalViewName: string): string[] {
const dependencies: string[] = [];
for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) {
dependencies.push(path.join(path.dirname(canonicalViewName), match[1]));
}
// Clear existing links from cache
for (const dependency of Object.keys(this.dependencyCache)) {
this.dependencyCache[dependency].delete(canonicalViewName);
}
// Add new links to cache
for (const dependency of dependencies) {
if (Object.keys(this.dependencyCache).indexOf(dependency) < 0) {
this.dependencyCache[dependency] = new Set<string>();
}
this.dependencyCache[dependency].add(canonicalViewName);
}
return dependencies;
}
private async replaceBackendCode(canonicalViewName: string, code?: string): Promise<{
backendReplacedCode: string, backendReplacedCode: string,
backendLines: string[], backendLines: string[],
} { }> {
// Cache
if (Object.keys(this.backendCodeCache).indexOf(canonicalViewName) >= 0) {
return this.backendCodeCache[canonicalViewName];
}
const outputFile = path.join(this.svelteOutputPath, canonicalViewName);
await new Promise<void>((resolve, reject) => {
fs.mkdir(path.dirname(outputFile), {recursive: true}, (err) => {
if (err) return reject(err);
resolve();
});
});
// Read source file if code was not already provided
if (!code) {
const file = await this.resolveViewFromCanonicalName(canonicalViewName);
code = await new Promise<string>((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) return reject(err);
resolve(data.toString());
});
});
}
// Skip replace if there is no swaf export
if (code.indexOf(`export let swaf = {};`) < 0) { if (code.indexOf(`export let swaf = {};`) < 0) {
return { const generated = {
backendReplacedCode: code, backendReplacedCode: code,
backendLines: [], backendLines: [],
}; };
await new Promise<void>((resolve, reject) => {
fs.writeFile(outputFile, generated.backendReplacedCode, (err) => {
if (err) return reject(err);
resolve();
});
});
this.backendCodeCache[canonicalViewName] = generated;
return generated;
} }
let output = code;
const backendLines = new Set<string>(); const backendLines = new Set<string>();
let index = 0; let index = 0;
while ((index = code.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) { while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) {
// Escaping // Escaping
if (index > 0 && code[index - 1] === '\\') { if (index > 0 && output[index - 1] === '\\') {
const isEscapingEscaped = index > 1 && code[index - 2] === '\\'; const isEscapingEscaped: boolean = index > 1 && output[index - 2] === '\\';
code = code.substring(0, index - 1 - (isEscapingEscaped ? 1 : 0)) + output = output.substring(0, index - 1 - (isEscapingEscaped ? 1 : 0)) +
code.substring(index, code.length); output.substring(index, output.length);
continue; continue;
} }
@ -189,38 +316,50 @@ export default class FrontendToolsComponent extends ApplicationComponent {
let endIndex = startIndex; let endIndex = startIndex;
let struct = 0; let struct = 0;
while (endIndex < code.length) { while (endIndex < output.length) {
if (['(', '[', '{'].indexOf(code[endIndex]) >= 0) struct++; if (['(', '[', '{'].indexOf(output[endIndex]) >= 0) struct++;
if ([')', ']', '}'].indexOf(code[endIndex]) >= 0) { if ([')', ']', '}'].indexOf(output[endIndex]) >= 0) {
struct--; struct--;
if (struct <= 0) { if (struct <= 0) {
if (struct === 0) endIndex++; if (struct === 0) endIndex++;
break; break;
} }
} }
if ([' ', '\n', '<'].indexOf(code[endIndex]) >= 0 && struct === 0) break; if ([' ', '\n', '<'].indexOf(output[endIndex]) >= 0 && struct === 0) break;
endIndex++; endIndex++;
} }
const backendLine = code.substring(startIndex, endIndex); let backendLine = output.substring(startIndex, endIndex);
if (backendLine.match(/([^()]+)\((.+?)\)/)) {
backendLine = backendLine.replace(/([^()]+)\((.+?)\)/, "'$1', [$2]");
} else {
backendLine = backendLine.replace(/([^()]+)/, "'$1'");
}
backendLines.add(backendLine); backendLines.add(backendLine);
code = code.substring(0, index) + output = output.substring(0, index) +
'swaf(`' + backendLine.replace(/([^\\])`/, '$1\\`') + '`)' + 'swaf(' + backendLine + ')' +
code.substring(endIndex, code.length); output.substring(endIndex, output.length);
} }
logger.silly('Replaced backend code'); const generated = {
backendReplacedCode: output,
return {
backendReplacedCode: code,
backendLines: [...backendLines], backendLines: [...backendLines],
}; };
await new Promise<void>((resolve, reject) => {
fs.writeFile(outputFile, generated.backendReplacedCode, (err) => {
if (err) return reject(err);
resolve();
});
});
this.backendCodeCache[canonicalViewName] = generated;
return generated;
} }
private async preprocessSvelte(code: string, filename: string): Promise<{ private async preProcessSvelte(code: string, filename: string, canonicalViewName: string): Promise<string> {
code: string, logger.debug(canonicalViewName + ' > ', 'Preprocessing svelte', filename);
sourcemap?: SourceMap,
}> {
const preprocessed = await preprocess( const preprocessed = await preprocess(
code, code,
sveltePreprocess({ sveltePreprocess({
@ -233,42 +372,249 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}, },
); );
return { return preprocessed.code;
code: preprocessed.code,
sourcemap: preprocessed.map as (SourceMap | undefined),
};
} }
private compileSvelteSsr(code: string, filename: string, sourcemap?: SourceMap): { private compileSvelteSsr(code: string, filename: string, canonicalViewName: string): {
head: string, head: string,
css: CssResult, css: CssResult,
html: string, html: string,
} { } {
logger.debug(canonicalViewName + ' > ', 'Compiling svelte ssr', filename);
const svelteSsr = compile(code, { const svelteSsr = compile(code, {
dev: false, dev: config.get<boolean>('view.dev'),
generate: 'ssr', generate: 'ssr',
format: 'cjs', format: 'cjs',
sourcemap: sourcemap,
cssOutputFilename: filename + '.css', cssOutputFilename: filename + '.css',
}); });
const locals = this.getGlobals();
return requireFromString(svelteSsr.js.code, filename).default.render({ return requireFromString(svelteSsr.js.code, filename).default.render({
swaf: () => 'undefined', swaf: (key: string, args?: unknown[]) => {
if (!args) return locals[key];
const f = locals[key];
if (typeof f !== 'function') throw new Error(key + ' is not a function.');
return f.call(locals, ...args);
},
}); });
} }
private compileSvelteJS(code: string, sourcemap?: SourceMap): { private async bundle(...canonicalViewNames: string[]): Promise<void> {
code: string, logger.debug('Bundling');
map: SourceMap,
} { const production = !config.get<boolean>('view.dev');
const compiled = compile(code, { const options: RollupOptions | RollupWatchOptions = {
dev: false, input: canonicalViewNames.map(name => path.join(this.svelteOutputPath, name)),
hydratable: true, output: {
sourcemap: sourcemap, sourcemap: true,
format: 'es',
name: 'bundle',
dir: path.join(this.publicAssetsPath, 'js'),
entryFileNames: (chunkInfo: PreRenderedChunk): string => {
const name = chunkInfo.facadeModuleId ?
path.relative(this.svelteOutputPath, chunkInfo.facadeModuleId) :
chunkInfo.name;
return name + '.js';
},
},
plugins: [
svelte({
preprocess: sveltePreprocess({
typescript: {
tsconfigFile: 'tsconfig.views.json',
},
}),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
hydratable: true,
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
cssOnlyRollupPlugin({output: 'bundle.css'}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
],
watch: {
buildDelay: 1000,
clearScreen: false,
},
};
if (production) {
// If we're building for production (npm run build
// instead of npm run dev), minify
options.plugins?.push(terser());
} else {
// Watch the `public` directory and refresh the
// browser on changes when not in production
const plugin = livereloadRollupPlugin('public');
options.plugins?.push(plugin);
}
for (const name of canonicalViewNames) {
await new Promise<void>((resolve, reject) => {
fs.mkdir(path.dirname(path.join(this.publicAssetsPath, 'js', name)), {recursive: true}, err => {
if (err) return reject(err);
resolve();
});
});
}
if (production) {
if (!this.rollup) {
this.rollup = await rollup(options);
await this.rollup.write({
format: 'es',
dir: path.join(this.publicAssetsPath, 'js'),
});
}
} else {
if (!this.rollup) {
this.rollup = watch(options);
this.rollup.on('event', (event) => {
if (event.code === 'ERROR' || event.code === 'BUNDLE_END') {
event.result?.close().catch(err => logger.error(err));
logger.debug('Bundled from watch');
}
});
}
}
}
private async resetBundle(): Promise<void> {
if (this.rollup) {
logger.debug('Stopping rollup...');
await this.rollup.close();
this.rollup = undefined;
}
}
private async renderSvelte(
file: string,
locals: Record<string, unknown>,
callback: (err: Error | null, rendered?: string) => void,
): Promise<void> {
const canonicalViewName = this.toCanonicalViewName(file);
const actualFile = path.join(this.svelteOutputPath, canonicalViewName + COMPILED_SVELTE_EXTENSION);
if (!config.get<boolean>('view.enable_asset_cache')) delete this.fileCache[actualFile];
const view = await this.getFileContentsFromCache(actualFile);
const templateFile = await this.resolveViewFromCanonicalName('layouts/svelte_layout.html');
if (!config.get<boolean>('view.enable_asset_cache')) delete this.fileCache[templateFile];
let output = await this.getFileContentsFromCache(templateFile);
const [
backendLines,
head,
html,
css,
] = view.split(FrontendToolsComponent.getSveltePreCompileSeparator(canonicalViewName));
locals = Object.assign(this.getGlobals(), locals);
const localMap: Record<string, unknown> = {};
backendLines.split('\n').forEach(line => {
const key = line.substring(1, line.indexOf(',') >= 0 ? line.indexOf(',') - 1 : line.length - 1);
if (line.indexOf('[') >= 0) {
const args = line.substring(line.indexOf('[') + 1, line.length - 1)
.split(/, *?/)
.map(arg => {
if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"';
return arg;
})
.map(arg => JSON.parse(arg));
const f = locals[key];
if (typeof f !== 'function') throw new Error(key + ' is not a function.');
localMap[`'${key}', ${JSON.stringify(args)}`] = f.call(locals, ...args);
} else {
localMap[`'${key}'`] = locals[key];
}
}); });
const props = JSON.stringify(localMap);
output = output.replace('%canonicalViewName%', canonicalViewName);
output = output.replace('%props%', props);
output = output.replace('%head%', head);
output = output.replace('%html%', html);
output = output.replace('%css%', css);
callback(null, output);
}
private async getFileContentsFromCache(file: string): Promise<string> {
if (!this.fileCache[file]) {
this.fileCache[file] = await new Promise<string>((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) return reject(err);
resolve(data.toString());
});
});
}
return this.fileCache[file];
}
private toCanonicalViewName(file: string): string {
const resolvedFilePath = path.resolve(file);
let canonicalViewName: string | null = null;
for (const viewPath of this.viewPaths) {
if (resolvedFilePath.startsWith(viewPath)) {
canonicalViewName = resolvedFilePath.substring(viewPath.length + 1);
}
}
if (!canonicalViewName) throw new Error('view ' + file + ' not found');
return canonicalViewName;
}
private async resolveViewFromCanonicalName(canonicalName: string): Promise<string> {
for (const viewPath of this.viewPaths) {
const tryPath = path.join(viewPath, canonicalName);
if (await new Promise<boolean>((resolve, reject) => {
fs.stat(tryPath, (err) => {
if (err == null) {
resolve(true);
} else if (err.code === 'ENOENT') {
resolve(false);
} else {
reject(err);
}
});
})) {
return tryPath;
}
}
throw new Error('View not found from canonical name ' + canonicalName);
}
/**
* TODO: add a way to add locals from anywhere
*/
private getGlobals(): Record<string, unknown> {
return { return {
code: compiled.js.code, route: (name: string) => 'unimplemented route ' + name,
map: compiled.js.map, direct: 'access',
}; };
} }
} }

View File

@ -48,8 +48,12 @@ export default class NunjucksComponent extends ApplicationComponent {
.addFilter('hex', (v: number) => { .addFilter('hex', (v: number) => {
return v.toString(16); return v.toString(16);
}); });
this.environment.express(app);
app.set('view engine', 'njk'); app.engine('njk', (path, options, callback) => {
this.environment?.render(path, options, (err, res) => {
callback(err, res || undefined);
});
});
} }
public async init(_router: Router): Promise<void> { public async init(_router: Router): Promise<void> {

8
src/types/RollupPluginCssOnly.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module "rollup-plugin-css-only" {
import {Plugin} from "rollup";
export default function cssOnlyRollupPlugin(options: CssOnlyPluginOptions): Plugin;
export type CssOnlyPluginOptions = {
output: string;
};
}

4
src/types/RollupPluginLivereload.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "rollup-plugin-livereload" {
import {Plugin} from "rollup";
export default function livereloadRollupPlugin(path: string): Plugin;
}

View File

@ -2,7 +2,7 @@
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "public/js", "outDir": "public/js",
"rootDir": "./views", "rootDir": "./build",
"target": "ES6", "target": "ES6",
"strict": true, "strict": true,
"lib": [ "lib": [

View File

@ -1,29 +1,37 @@
<script lang="ts"> <script>
import HomeDep from "./home_dep.svelte";
import Layout from "./layout.svelte";
export let swaf = {}; export let swaf = {};
let count: number; let count = 5;
function handleClick(): void { function handleClick() {
count++; count++;
} }
let depTest;
</script> </script>
<style lang="scss"> <style lang="scss">
p:last-of-type { p:last-of-type {
color: blueviolet; color: blueviolet;
} }
.style-test { .style-test {
p { p {
color: blue; color: blue;
} }
} }
</style> </style>
<Layout title="Home"/>
<h1>Hello {count}!</h1> <h1>Hello {count}!</h1>
<button on:click={handleClick}>More hellos!!</button> <button on:click={handleClick}>More hellos!!</button>
<p>Direct access: {swaf.direct}</p>
{#if swaf.route('auth') === '/'} {#if swaf.route('auth') === '/'}
We're home! We're home!
{:else} {:else}
@ -32,10 +40,14 @@
<p>The route to auth is {swaf.route('auth')}</p> <p>The route to auth is {swaf.route('auth')}</p>
<p>\$$.notcode</p> <p>\swaf.notcode</p>
<p>{`{\\$$.escaped}`}</p> <p>{`{\\swaf.escaped}`}</p>
<div class="style-test"> <div class="style-test">
<p>Blue!</p> <p>Blue!</p>
</div> </div>
<HomeDep swaf={swaf} bind:depTest={depTest}/>
<p>Dependency test: {depTest}</p>

17
views/home_dep.svelte Normal file
View File

@ -0,0 +1,17 @@
<script lang="ts">
export let depTest = 'Success';
export let swaf = {};
</script>
<style>
p {
color: brown;
}
</style>
<p>Simple dep test</p>
<p>Nested swaf call: {swaf.direct}</p>
<p>Nested swaf call: {swaf.route('auth')}</p>
<p>Nested swaf call: {swaf.route('home')}</p>

23
views/layout.svelte Normal file
View File

@ -0,0 +1,23 @@
<script>
export let title = undefined;
export let description = undefined;
export let refresh_after = undefined;
</script>
<svelte:head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>{title || 'Undefined title'}</title>
{#if description}
<meta name="description" content={description}>
{/if}
<link rel="shortcut icon" type="image/png" href="/img/logox1024.png">
<link rel="shortcut icon" type="image/png" href="/img/logox128.png">
<link rel="shortcut icon" type="image/svg" href="/img/logo.svg">
{#if refresh_after}
<meta http-equiv="refresh" content={refresh_after}>
{/if}
</svelte:head>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
%head%
<style>%css%</style>
<script type="module" defer>
import View from '/js/%canonicalViewName%.js';
const props = %props%;
new View({
hydrate: true,
target: document.body,
props: {
swaf: (key, args) => {
const line = args ?
`'${key}', ${JSON.stringify(args)}`
: `'${key}'`;
return props[line];
},
},
});
</script>
</head>
<body>
%html%
</body>
</html>

220
yarn.lock
View File

@ -9,7 +9,7 @@
dependencies: dependencies:
"@babel/highlight" "^7.10.4" "@babel/highlight" "^7.10.4"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13":
version "7.12.13" version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
@ -552,6 +552,48 @@
resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4" resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4"
integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ== integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==
"@rollup/plugin-commonjs@^17.1.0":
version "17.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d"
integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==
dependencies:
"@rollup/pluginutils" "^3.1.0"
commondir "^1.0.1"
estree-walker "^2.0.1"
glob "^7.1.6"
is-reference "^1.2.1"
magic-string "^0.25.7"
resolve "^1.17.0"
"@rollup/plugin-node-resolve@^11.2.0":
version "11.2.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60"
integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==
dependencies:
"@rollup/pluginutils" "^3.1.0"
"@types/resolve" "1.17.1"
builtin-modules "^3.1.0"
deepmerge "^4.2.2"
is-module "^1.0.0"
resolve "^1.19.0"
"@rollup/pluginutils@4":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.0.tgz#0dcc61c780e39257554feb7f77207dceca13c838"
integrity sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
dependencies:
"@types/estree" "0.0.39"
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@sindresorhus/is@^0.14.0": "@sindresorhus/is@^0.14.0":
version "0.14.0" version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@ -671,6 +713,16 @@
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
"@types/estree@*":
version "0.0.47"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/express-serve-static-core@^4.17.18": "@types/express-serve-static-core@^4.17.18":
version "4.17.19" version "4.17.19"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d"
@ -853,6 +905,13 @@
resolved "https://registry.yarnpkg.com/@types/require-from-string/-/require-from-string-1.2.0.tgz#c18cfc8a2c1a0259e5841d1fef2b5e9d01c64242" resolved "https://registry.yarnpkg.com/@types/require-from-string/-/require-from-string-1.2.0.tgz#c18cfc8a2c1a0259e5841d1fef2b5e9d01c64242"
integrity sha512-5vE9WoOOC9/DoD3Zj53UISpM+5tSvh8k0mL4fe2zFI6vO715/W4IQ3EdVUrWVMrFi1/NZhyMvm2iKsDFkEGddQ== integrity sha512-5vE9WoOOC9/DoD3Zj53UISpM+5tSvh8k0mL4fe2zFI6vO715/W4IQ3EdVUrWVMrFi1/NZhyMvm2iKsDFkEGddQ==
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
dependencies:
"@types/node" "*"
"@types/sass@^1.16.0": "@types/sass@^1.16.0":
version "1.16.0" version "1.16.0"
resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.16.0.tgz#b41ac1c17fa68ffb57d43e2360486ef526b3d57d" resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.16.0.tgz#b41ac1c17fa68ffb57d43e2360486ef526b3d57d"
@ -1547,6 +1606,11 @@ buffer-from@1.x, buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
builtin-modules@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==
bytes@3.0.0: bytes@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -1727,7 +1791,7 @@ cheerio@^1.0.0-rc.3:
parse5 "^6.0.1" parse5 "^6.0.1"
parse5-htmlparser2-tree-adapter "^6.0.1" parse5-htmlparser2-tree-adapter "^6.0.1"
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.4.1, chokidar@^3.5.1: "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.2.2, chokidar@^3.4.1, chokidar@^3.5.0, chokidar@^3.5.1:
version "3.5.1" version "3.5.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
@ -1887,6 +1951,11 @@ commander@^5.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
component-bind@1.0.0: component-bind@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
@ -2816,6 +2885,21 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
estree-walker@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
estree-walker@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
esutils@^2.0.2: esutils@^2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@ -3861,6 +3945,11 @@ is-installed-globally@^0.3.1:
global-dirs "^2.0.1" global-dirs "^2.0.1"
is-path-inside "^3.0.1" is-path-inside "^3.0.1"
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
is-negative-zero@^2.0.1: is-negative-zero@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
@ -3910,6 +3999,13 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
is-reference@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
dependencies:
"@types/estree" "*"
is-regex@^1.1.2: is-regex@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
@ -4400,7 +4496,7 @@ jest-watcher@^26.6.2:
jest-util "^26.6.2" jest-util "^26.6.2"
string-length "^4.0.1" string-length "^4.0.1"
jest-worker@^26.6.2: jest-worker@^26.2.1, jest-worker@^26.6.2:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
@ -4638,6 +4734,21 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
livereload-js@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-3.3.2.tgz#c88b009c6e466b15b91faa26fd7c99d620e12651"
integrity sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==
livereload@^0.9.1:
version "0.9.3"
resolved "https://registry.yarnpkg.com/livereload/-/livereload-0.9.3.tgz#a714816375ed52471408bede8b49b2ee6a0c55b1"
integrity sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==
dependencies:
chokidar "^3.5.0"
livereload-js "^3.3.1"
opts ">= 1.2.0"
ws "^7.4.3"
load-json-file@^1.0.0: load-json-file@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -4740,6 +4851,13 @@ lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" yallist "^4.0.0"
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
dependencies:
sourcemap-codec "^1.4.4"
maildev@^1.1.0: maildev@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/maildev/-/maildev-1.1.0.tgz#8b6977f244373be00112c942ae15dd32f5c225c9" resolved "https://registry.yarnpkg.com/maildev/-/maildev-1.1.0.tgz#8b6977f244373be00112c942ae15dd32f5c225c9"
@ -5705,6 +5823,11 @@ optionator@^0.9.1:
type-check "^0.4.0" type-check "^0.4.0"
word-wrap "^1.2.3" word-wrap "^1.2.3"
"opts@>= 1.2.0":
version "2.0.2"
resolved "https://registry.yarnpkg.com/opts/-/opts-2.0.2.tgz#a17e189fbbfee171da559edd8a42423bc5993ce1"
integrity sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==
p-cancelable@^1.0.0: p-cancelable@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
@ -5922,7 +6045,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
version "2.2.3" version "2.2.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d"
integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==
@ -6088,6 +6211,13 @@ random-bytes@~1.0.0:
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
dependencies:
safe-buffer "^5.1.0"
range-parser@~1.2.1: range-parser@~1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -6351,6 +6481,11 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
require-relative@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
resolve-cwd@^3.0.0: resolve-cwd@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@ -6373,7 +6508,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.10.0, resolve@^1.10.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1: resolve@^1.10.0, resolve@^1.10.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0:
version "1.20.0" version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -6412,6 +6547,52 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
rollup-plugin-css-only@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz#6a701cc5b051c6b3f0961e69b108a9a118e1b1df"
integrity sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==
dependencies:
"@rollup/pluginutils" "4"
rollup-plugin-livereload@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.0.tgz#d3928d74e8cf2ae4286c5dd46b770fd3f3b82313"
integrity sha512-oC/8NqumGYuphkqrfszOHUUIwzKsaHBICw6QRwT5uD07gvePTS+HW+GFwu6f9K8W02CUuTvtIM9AWJrbj4wE1A==
dependencies:
livereload "^0.9.1"
rollup-plugin-svelte@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz#d45f2b92b1014be4eb46b55aa033fb9a9c65f04d"
integrity sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==
dependencies:
require-relative "^0.8.7"
rollup-pluginutils "^2.8.2"
rollup-plugin-terser@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
dependencies:
"@babel/code-frame" "^7.10.4"
jest-worker "^26.2.1"
serialize-javascript "^4.0.0"
terser "^5.0.0"
rollup-pluginutils@^2.8.2:
version "2.8.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
dependencies:
estree-walker "^0.6.1"
rollup@^2.42.3:
version "2.45.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.45.2.tgz#8fb85917c9f35605720e92328f3ccbfba6f78b48"
integrity sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ==
optionalDependencies:
fsevents "~2.3.1"
rsvp@^4.8.4: rsvp@^4.8.4:
version "4.8.5" version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@ -6441,7 +6622,7 @@ safe-buffer@5.2.0:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -6548,6 +6729,13 @@ send@0.17.1:
range-parser "~1.2.1" range-parser "~1.2.1"
statuses "~1.5.0" statuses "~1.5.0"
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
dependencies:
randombytes "^2.1.0"
serve-static@1.14.1: serve-static@1.14.1:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
@ -6753,7 +6941,7 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0" source-map-url "^0.4.0"
urix "^0.1.0" urix "^0.1.0"
source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6: source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.19:
version "0.5.19" version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@ -6783,11 +6971,16 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3: source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3" version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spawn-command@^0.0.2-1: spawn-command@^0.0.2-1:
version "0.0.2-1" version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
@ -7170,6 +7363,15 @@ terminal-link@^2.0.0:
ansi-escapes "^4.2.1" ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0" supports-hyperlinks "^2.0.0"
terser@^5.0.0:
version "5.6.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
dependencies:
commander "^2.20.0"
source-map "~0.7.2"
source-map-support "~0.5.19"
test-exclude@^6.0.0: test-exclude@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@ -7763,7 +7965,7 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2" signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5" typedarray-to-buffer "^3.1.5"
ws@^7.2.3, ws@^7.4.4: ws@^7.2.3, ws@^7.4.3, ws@^7.4.4:
version "7.4.5" version "7.4.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1"
integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==