2021-05-27 15:26:19 +02:00
|
|
|
import "./register_svelte/register_svelte.js";
|
2021-05-04 17:04:14 +02:00
|
|
|
|
2021-05-11 15:47:42 +02:00
|
|
|
import clearModule from "clear-module";
|
2021-05-03 19:29:22 +02:00
|
|
|
import config from "config";
|
2021-05-04 17:04:14 +02:00
|
|
|
import {promises as fs} from 'fs';
|
2021-03-24 21:41:13 +01:00
|
|
|
import path from "path";
|
|
|
|
import requireFromString from "require-from-string";
|
2021-05-03 19:29:22 +02:00
|
|
|
import {compile, preprocess} from "svelte/compiler";
|
|
|
|
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess.js";
|
|
|
|
|
2021-06-01 14:34:04 +02:00
|
|
|
import {publicUrl, routes} from "../common/Routing.js";
|
2021-05-03 19:29:22 +02:00
|
|
|
import {logger} from "../Logger.js";
|
|
|
|
import FileCache from "../utils/FileCache.js";
|
|
|
|
import ViewEngine from "./ViewEngine.js";
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
export default class SvelteViewEngine extends ViewEngine {
|
|
|
|
private readonly fileCache: FileCache = new FileCache();
|
2021-11-24 22:08:38 +01:00
|
|
|
private readonly reverseDependencyCache: Record<string, Set<string>> = {};
|
|
|
|
private readonly preprocessingCache: Record<string, string> = {};
|
2021-05-31 11:14:56 +02:00
|
|
|
private readonly cssCache: Record<string, string[] | undefined> = {};
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
public constructor(
|
2021-05-04 17:04:14 +02:00
|
|
|
targetDir: string,
|
2021-03-24 21:41:13 +01:00
|
|
|
...additionalViewPaths: string[]
|
|
|
|
) {
|
2021-05-04 17:04:14 +02:00
|
|
|
super(targetDir, 'views', 'svelte', true, ...additionalViewPaths);
|
|
|
|
}
|
2021-04-28 14:07:11 +02:00
|
|
|
|
2021-05-04 17:04:14 +02:00
|
|
|
public async onFileChange(file: string): Promise<void> {
|
2021-05-11 13:52:48 +02:00
|
|
|
delete this.preprocessingCache[this.toCanonicalName(file)];
|
2021-05-13 14:11:10 +02:00
|
|
|
await super.onFileChange(file);
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 17:04:14 +02:00
|
|
|
public async onFileRemove(file: string): Promise<void> {
|
|
|
|
const canonicalName = this.toCanonicalName(file);
|
2021-05-11 13:52:48 +02:00
|
|
|
delete this.preprocessingCache[canonicalName];
|
2021-11-24 22:08:38 +01:00
|
|
|
delete this.reverseDependencyCache[canonicalName];
|
|
|
|
Object.values(this.reverseDependencyCache).forEach(set => set.delete(canonicalName));
|
2021-05-04 17:04:14 +02:00
|
|
|
await super.onFileRemove(file);
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public async render(
|
|
|
|
file: string,
|
|
|
|
locals: Record<string, unknown>,
|
2021-04-28 14:53:46 +02:00
|
|
|
): Promise<string> {
|
2021-03-24 21:41:13 +01:00
|
|
|
const canonicalViewName = this.toCanonicalName(file);
|
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
const rootTemplateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html');
|
|
|
|
const rawOutput = await this.fileCache.get(rootTemplateFile, !config.get<boolean>('view.cache'));
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-11-28 16:41:46 +01:00
|
|
|
locals.isSsr = true;
|
2021-11-24 22:08:38 +01:00
|
|
|
const {
|
2021-03-24 21:41:13 +01:00
|
|
|
head,
|
|
|
|
html,
|
|
|
|
css,
|
2021-11-24 22:08:38 +01:00
|
|
|
} = await this.renderSvelteSsr(canonicalViewName, locals);
|
|
|
|
|
2021-11-28 16:41:46 +01:00
|
|
|
locals.isSsr = false;
|
2021-11-24 22:08:38 +01:00
|
|
|
const serializedLocals = JSON.stringify(locals, (key, value) => {
|
|
|
|
if (key.startsWith('_') || typeof value === 'function') return undefined;
|
|
|
|
return value;
|
2021-04-29 15:46:48 +02:00
|
|
|
});
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
// Replaces
|
2021-04-28 17:03:27 +02:00
|
|
|
const replaceMap: Record<string, string> = {
|
2021-04-28 11:00:51 +02:00
|
|
|
canonicalViewName: canonicalViewName,
|
2021-11-24 22:08:38 +01:00
|
|
|
locals: serializedLocals,
|
2021-04-28 11:00:51 +02:00
|
|
|
head: head,
|
|
|
|
html: html,
|
|
|
|
css: css,
|
2021-06-01 14:34:04 +02:00
|
|
|
|
|
|
|
routes: JSON.stringify(routes),
|
|
|
|
publicUrl: publicUrl,
|
2021-04-28 11:00:51 +02:00
|
|
|
};
|
2021-04-28 17:03:27 +02:00
|
|
|
return rawOutput.replace(
|
|
|
|
new RegExp(Object.keys(replaceMap).map(str => `%${str}%`).join('|'), 'g'),
|
2021-04-29 15:46:48 +02:00
|
|
|
(substring) => replaceMap[substring.slice(1, substring.length - 1)],
|
2021-04-28 17:03:27 +02:00
|
|
|
);
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public async preCompile(canonicalName: string, alsoCompileDependents: boolean): Promise<void> {
|
2021-05-31 11:14:56 +02:00
|
|
|
const targetFile = path.join(this.targetDir, canonicalName);
|
|
|
|
logger.info(canonicalName + ' > ', 'Pre-compiling', canonicalName, '->', targetFile);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
await this.preprocessSvelte(canonicalName);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
if (alsoCompileDependents && Object.keys(this.reverseDependencyCache).indexOf(canonicalName) >= 0) {
|
|
|
|
logger.info(canonicalName + ' > ', 'Pre-compiling dependents...');
|
|
|
|
for (const dependent of [...this.reverseDependencyCache[canonicalName]]) {
|
2021-03-24 21:41:13 +01:00
|
|
|
await this.preCompile(dependent, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
private async preprocessSvelte(canonicalName: string): Promise<string> {
|
2021-03-24 21:41:13 +01:00
|
|
|
// Cache
|
2021-05-11 13:52:48 +02:00
|
|
|
if (Object.keys(this.preprocessingCache).indexOf(canonicalName) >= 0) {
|
|
|
|
return this.preprocessingCache[canonicalName];
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
|
|
|
|
2021-05-13 14:11:10 +02:00
|
|
|
const file = await this.resolveFileFromCanonicalNameOrFail(canonicalName);
|
2021-05-11 13:52:48 +02:00
|
|
|
logger.info(canonicalName + ' > ', `Preprocessing ${file}`);
|
|
|
|
|
2021-03-24 21:41:13 +01:00
|
|
|
// mkdir output file dir
|
2021-05-11 13:52:48 +02:00
|
|
|
const outputFile = path.join(this.targetDir, canonicalName);
|
2021-05-04 17:04:14 +02:00
|
|
|
await fs.mkdir(path.dirname(outputFile), {recursive: true});
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
// Read source file if code was not already provided
|
2021-05-12 13:56:49 +02:00
|
|
|
const code = await this.fileCache.get(file, !config.get<boolean>('view.cache'));
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-05-11 13:52:48 +02:00
|
|
|
// Preprocess svelte
|
|
|
|
logger.info(canonicalName + ' > ', 'Svelte preprocessing');
|
2021-11-24 22:08:38 +01:00
|
|
|
const processed = await preprocess(
|
|
|
|
code,
|
2021-05-11 13:52:48 +02:00
|
|
|
sveltePreprocess({
|
|
|
|
typescript: {
|
2021-05-27 15:26:19 +02:00
|
|
|
tsconfigFile: 'src/assets/views/tsconfig.json',
|
2021-05-11 13:52:48 +02:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
filename: outputFile,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Write to output file
|
2021-11-24 22:08:38 +01:00
|
|
|
await fs.writeFile(outputFile, processed.code);
|
2021-05-11 13:52:48 +02:00
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
this.resolveAndCacheDependencies(processed.code, canonicalName);
|
2021-05-12 13:56:49 +02:00
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
return this.preprocessingCache[canonicalName] = processed.code;
|
2021-05-11 13:52:48 +02:00
|
|
|
}
|
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
private async renderSvelteSsr(canonicalName: string, locals: {[key: string]: unknown}): Promise<{
|
2021-03-24 21:41:13 +01:00
|
|
|
head: string,
|
2021-05-31 11:14:56 +02:00
|
|
|
css: string,
|
2021-03-24 21:41:13 +01:00
|
|
|
html: string,
|
|
|
|
}> {
|
2021-05-31 11:14:56 +02:00
|
|
|
const targetFile = path.join(this.targetDir, canonicalName);
|
2021-11-24 22:08:38 +01:00
|
|
|
const code = await this.fileCache.get(targetFile, !config.get<boolean>('view.cache'));
|
2021-05-31 11:14:56 +02:00
|
|
|
|
|
|
|
// Get dependencies css
|
|
|
|
const dependenciesCss: string[] = [];
|
2021-11-24 22:08:38 +01:00
|
|
|
for (const dependency of this.resolveAndCacheDependencies(code, canonicalName)) {
|
2021-11-25 00:25:29 +01:00
|
|
|
if (this.cssCache[dependency] === undefined || !config.get<boolean>('view.cache')) {
|
2021-11-24 22:08:38 +01:00
|
|
|
await this.renderSvelteSsr(dependency, locals);
|
2021-05-31 11:14:56 +02:00
|
|
|
}
|
|
|
|
const css = this.cssCache[dependency];
|
|
|
|
if (css === undefined) {
|
|
|
|
logger.error(typeof this.cssCache[dependency], !!this.cssCache[dependency]);
|
|
|
|
throw new Error(`Compiling ssr of ${dependency} didn't cache its css.`);
|
|
|
|
}
|
|
|
|
dependenciesCss.push(...css);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(canonicalName + ' > ', 'Compiling svelte ssr', targetFile);
|
|
|
|
|
2021-03-24 21:41:13 +01:00
|
|
|
// Svelte compile
|
2021-05-11 13:52:48 +02:00
|
|
|
const svelteSsr = compile(code, {
|
2021-03-24 21:41:13 +01:00
|
|
|
dev: config.get<boolean>('view.dev'),
|
|
|
|
generate: 'ssr',
|
|
|
|
format: 'cjs',
|
2021-05-31 11:14:56 +02:00
|
|
|
cssOutputFilename: targetFile + '.css',
|
2021-03-24 21:41:13 +01:00
|
|
|
});
|
|
|
|
|
2021-05-04 17:04:14 +02:00
|
|
|
// Load locals into locals store
|
2021-11-24 22:08:38 +01:00
|
|
|
const storesModule = await import(path.resolve(this.targetDir, "../ts/stores.js"));
|
|
|
|
storesModule.locals.set(locals);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-05-04 17:04:14 +02:00
|
|
|
// Load module and render
|
2021-05-31 11:14:56 +02:00
|
|
|
const moduleId = path.resolve(targetFile);
|
2021-05-11 15:47:42 +02:00
|
|
|
clearModule.single(moduleId);
|
2021-05-31 11:14:56 +02:00
|
|
|
const {
|
|
|
|
head,
|
|
|
|
css,
|
|
|
|
html,
|
|
|
|
} = requireFromString(svelteSsr.js.code, moduleId).default.render();
|
|
|
|
const cssFragments = css.code === '' ?
|
|
|
|
dependenciesCss :
|
|
|
|
[...dependenciesCss, css.code];
|
|
|
|
this.cssCache[canonicalName] = cssFragments;
|
|
|
|
return {
|
|
|
|
head,
|
|
|
|
css: [...new Set(cssFragments)].join(''),
|
|
|
|
html,
|
|
|
|
};
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
2021-05-12 13:54:54 +02:00
|
|
|
|
2021-11-24 22:08:38 +01:00
|
|
|
private resolveAndCacheDependencies(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.reverseDependencyCache)) {
|
|
|
|
this.reverseDependencyCache[dependency].delete(canonicalViewName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add new links to cache
|
|
|
|
for (const dependency of dependencies) {
|
|
|
|
if (Object.keys(this.reverseDependencyCache).indexOf(dependency) < 0) {
|
|
|
|
this.reverseDependencyCache[dependency] = new Set<string>();
|
2021-05-12 13:54:54 +02:00
|
|
|
}
|
2021-11-24 22:08:38 +01:00
|
|
|
this.reverseDependencyCache[dependency].add(canonicalViewName);
|
|
|
|
}
|
|
|
|
|
|
|
|
return dependencies;
|
2021-05-12 13:54:54 +02:00
|
|
|
}
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|