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-03-24 21:41:13 +01:00
|
|
|
import crypto from "crypto";
|
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
|
|
|
|
2021-05-12 13:55:47 +02:00
|
|
|
const BACKEND_CODE_PREFIX = '$locals.';
|
2021-04-29 15:46:48 +02:00
|
|
|
const BACKEND_CODE_PREFIX_TEMPORARY_HOLDER = '$$locals$$';
|
|
|
|
const COMPILED_SVELTE_EXTENSION = '.precompiled';
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
export default class SvelteViewEngine extends ViewEngine {
|
|
|
|
public static getPreCompileSeparator(canonicalViewName: string): string {
|
|
|
|
return '\n---' +
|
|
|
|
crypto.createHash('sha1')
|
|
|
|
.update(path.basename(path.resolve(canonicalViewName)))
|
|
|
|
.digest('base64') +
|
|
|
|
'---\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private readonly fileCache: FileCache = new FileCache();
|
|
|
|
private readonly dependencyCache: Record<string, Set<string>> = {};
|
2021-05-11 13:52:48 +02:00
|
|
|
private readonly preprocessingCache: Record<string, PreprocessingCacheEntry> = {};
|
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-05-04 17:04:14 +02:00
|
|
|
delete this.dependencyCache[canonicalName];
|
|
|
|
Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName));
|
|
|
|
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);
|
|
|
|
|
|
|
|
// View
|
2021-05-04 17:04:14 +02:00
|
|
|
const actualFile = path.join(this.targetDir, canonicalViewName + COMPILED_SVELTE_EXTENSION);
|
2021-04-27 15:08:41 +02:00
|
|
|
const view = await this.fileCache.get(actualFile, !config.get<boolean>('view.cache'));
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
// Root template
|
2021-05-13 14:11:10 +02:00
|
|
|
const templateFile = await this.resolveFileFromCanonicalNameOrFail('layouts/svelte_layout.html');
|
2021-04-28 11:00:51 +02:00
|
|
|
const rawOutput = await this.fileCache.get(templateFile, !config.get<boolean>('view.cache'));
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
// Pre-compiled parts
|
|
|
|
const [
|
2021-04-29 15:46:48 +02:00
|
|
|
backendCalls,
|
2021-03-24 21:41:13 +01:00
|
|
|
head,
|
|
|
|
html,
|
|
|
|
css,
|
|
|
|
] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName));
|
|
|
|
|
2021-05-12 13:54:54 +02:00
|
|
|
const localMap: Record<string, unknown> = this.compileBackendCalls(backendCalls.split('\n'), locals, false);
|
2021-04-29 15:46:48 +02:00
|
|
|
const actualLocals = JSON.stringify(localMap, (key, value) => {
|
2021-05-31 11:16:46 +02:00
|
|
|
if (key.startsWith('_')) return undefined;
|
2021-04-29 15:46:48 +02:00
|
|
|
return typeof value === 'function' ?
|
|
|
|
value.toString() :
|
|
|
|
value;
|
|
|
|
});
|
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-04-29 15:46:48 +02:00
|
|
|
locals: actualLocals,
|
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-05-31 11:14:56 +02:00
|
|
|
const {backendCalls} = await this.preprocess(canonicalName);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
// Server Side Render (initial HTML and CSS, no-js)
|
2021-05-31 11:14:56 +02:00
|
|
|
const ssr = await this.compileSsr(canonicalName);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName);
|
|
|
|
const finalCode = [
|
2021-05-31 11:14:56 +02:00
|
|
|
[...new Set<string>(backendCalls).values()].join('\n'),
|
2021-03-24 21:41:13 +01:00
|
|
|
ssr.head,
|
|
|
|
ssr.html,
|
2021-05-31 11:14:56 +02:00
|
|
|
ssr.css,
|
2021-03-24 21:41:13 +01:00
|
|
|
].join(separator);
|
|
|
|
|
2021-05-04 17:04:14 +02:00
|
|
|
const swafViewFile = path.join(this.targetDir, canonicalName + COMPILED_SVELTE_EXTENSION);
|
|
|
|
await fs.mkdir(path.dirname(swafViewFile), {recursive: true});
|
|
|
|
await fs.writeFile(swafViewFile, finalCode);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
if (alsoCompileDependents && Object.keys(this.dependencyCache).indexOf(canonicalName) >= 0) {
|
2021-03-26 10:56:32 +01:00
|
|
|
logger.info(canonicalName + ' > ', 'Compiling dependents...');
|
2021-03-24 21:41:13 +01:00
|
|
|
for (const dependent of [...this.dependencyCache[canonicalName]]) {
|
|
|
|
await this.preCompile(dependent, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-05-12 13:56:49 +02:00
|
|
|
private async preprocess(canonicalName: string): Promise<PreprocessingCacheEntry> {
|
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
|
|
|
// Replace backend calls
|
|
|
|
const replacedBackendCalls = await this.replaceBackendCalls(canonicalName, code);
|
|
|
|
|
|
|
|
// Preprocess svelte
|
|
|
|
logger.info(canonicalName + ' > ', 'Svelte preprocessing');
|
|
|
|
const preprocessed = await preprocess(
|
|
|
|
replacedBackendCalls.code,
|
|
|
|
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
|
|
|
|
await fs.writeFile(outputFile, preprocessed.code);
|
|
|
|
|
2021-05-12 13:56:49 +02:00
|
|
|
// Preprocess dependencies
|
|
|
|
const backendCalls: string[] = replacedBackendCalls.backendCalls;
|
|
|
|
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
|
|
|
backendCalls.push(...(await this.preprocess(dependency)).backendCalls);
|
|
|
|
}
|
|
|
|
|
2021-05-11 13:52:48 +02:00
|
|
|
return this.preprocessingCache[canonicalName] = {
|
2021-05-12 13:56:49 +02:00
|
|
|
backendCalls: backendCalls,
|
2021-05-11 13:52:48 +02:00
|
|
|
code: preprocessed.code,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private async replaceBackendCalls(canonicalName: string, code: string): Promise<PreprocessingCacheEntry> {
|
|
|
|
logger.info(canonicalName + ' > ', 'Replacing backend calls');
|
|
|
|
|
2021-03-24 21:41:13 +01:00
|
|
|
// Skip replace if there is no swaf export
|
2021-05-31 11:18:04 +02:00
|
|
|
if (!code.match(/import[ \n]+{[ \n]*locals[ \n]*}[ \n]+from[ \n]+["'](\.\.\/)+ts\/stores(\.js)?["']/)) {
|
2021-05-11 13:52:48 +02:00
|
|
|
return {
|
2021-04-29 15:46:48 +02:00
|
|
|
backendCalls: [],
|
2021-05-11 13:52:48 +02:00
|
|
|
code: code,
|
2021-03-24 21:41:13 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let output = code;
|
2021-04-29 15:46:48 +02:00
|
|
|
const backendCalls = new Set<string>();
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
let index = 0;
|
|
|
|
while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) {
|
|
|
|
// Escaping
|
|
|
|
if (index > 0 && output[index - 1] === '\\') {
|
|
|
|
const isEscapingEscaped: boolean = index > 1 && output[index - 2] === '\\';
|
|
|
|
output = output.substring(0, index - 1 - (isEscapingEscaped ? 1 : 0)) +
|
|
|
|
output.substring(index, output.length);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const startIndex = index + BACKEND_CODE_PREFIX.length;
|
|
|
|
let endIndex = startIndex;
|
|
|
|
let struct = 0;
|
|
|
|
|
|
|
|
while (endIndex < output.length) {
|
|
|
|
if (['(', '[', '{'].indexOf(output[endIndex]) >= 0) struct++;
|
|
|
|
if ([')', ']', '}'].indexOf(output[endIndex]) >= 0) {
|
|
|
|
struct--;
|
|
|
|
if (struct <= 0) {
|
|
|
|
if (struct === 0) endIndex++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-05-31 15:34:37 +02:00
|
|
|
if ([' ', '\n', '<', '.', '\'', '"', '?', ','].indexOf(output[endIndex]) >= 0 && struct === 0) break;
|
2021-03-24 21:41:13 +01:00
|
|
|
endIndex++;
|
|
|
|
}
|
|
|
|
|
2021-04-29 15:46:48 +02:00
|
|
|
let backendCall = output.substring(startIndex, endIndex);
|
2021-05-13 17:38:48 +02:00
|
|
|
if (backendCall.match(/([^()]+)\((.*?)\)/)) {
|
|
|
|
backendCall = backendCall.replace(/([^()]+)\((.*?)\)/, "'$1', `[$2]`");
|
2021-03-24 21:41:13 +01:00
|
|
|
} else {
|
2021-05-04 17:04:14 +02:00
|
|
|
backendCall = backendCall.replace(/([^()]+)(\(\))?/, "'$1'");
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
2021-04-29 15:46:48 +02:00
|
|
|
backendCalls.add(backendCall);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
|
|
|
output = output.substring(0, index) +
|
2021-05-12 13:55:47 +02:00
|
|
|
'$locals(' + backendCall.split(BACKEND_CODE_PREFIX).join(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER) + ')' +
|
2021-03-24 21:41:13 +01:00
|
|
|
output.substring(endIndex, output.length);
|
|
|
|
}
|
2021-04-29 15:46:48 +02:00
|
|
|
output = output.split(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER).join(BACKEND_CODE_PREFIX);
|
2021-03-24 21:41:13 +01:00
|
|
|
|
2021-05-11 13:52:48 +02:00
|
|
|
return {
|
2021-04-29 15:46:48 +02:00
|
|
|
backendCalls: [...backendCalls],
|
2021-05-11 13:52:48 +02:00
|
|
|
code: output,
|
2021-03-24 21:41:13 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-05-31 11:14:56 +02:00
|
|
|
private async compileSsr(canonicalName: string): 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);
|
|
|
|
const {backendCalls, code} = await this.preprocess(canonicalName);
|
|
|
|
|
|
|
|
// Get dependencies css
|
|
|
|
const dependenciesCss: string[] = [];
|
|
|
|
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
|
|
|
if (this.cssCache[dependency] === undefined) {
|
|
|
|
await this.compileSsr(dependency);
|
|
|
|
}
|
|
|
|
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-05-27 15:26:19 +02:00
|
|
|
const localsModulePath = "../../intermediates/assets/ts/stores.js";
|
2021-05-04 17:04:14 +02:00
|
|
|
const localsModule = await import(localsModulePath);
|
2021-05-13 16:03:59 +02:00
|
|
|
const locals = this.getGlobals().get();
|
2021-05-12 13:54:54 +02:00
|
|
|
const localMap = this.compileBackendCalls(backendCalls, locals, true);
|
2021-05-11 15:13:32 +02:00
|
|
|
localsModule.locals.set((key: string, args: string) => {
|
2021-05-12 13:54:54 +02:00
|
|
|
return localMap[args ?
|
2021-05-11 15:13:32 +02:00
|
|
|
`'${key}', \`${args}\``
|
|
|
|
: `'${key}'`];
|
|
|
|
});
|
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
|
|
|
|
|
|
|
private compileBackendCalls(
|
|
|
|
backendCalls: string[],
|
|
|
|
locals: Record<string, unknown>,
|
|
|
|
isPreRender: boolean,
|
|
|
|
): Record<string, unknown> {
|
|
|
|
locals = {...locals, isPreRender};
|
|
|
|
|
|
|
|
const localMap: Record<string, unknown> = {};
|
|
|
|
backendCalls.forEach(code => {
|
|
|
|
const key = code.substring(1, code.indexOf(',') >= 0 ? code.indexOf(',') - 1 : code.length - 1);
|
|
|
|
if (code.indexOf('`[') >= 0) {
|
|
|
|
const args = code.substring(code.indexOf('`[') + 2, code.length - 2)
|
|
|
|
.split(/, *?/)
|
|
|
|
.map(arg => {
|
|
|
|
if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"';
|
|
|
|
return arg;
|
|
|
|
})
|
2021-05-13 17:38:48 +02:00
|
|
|
.filter(arg => arg.length > 0)
|
|
|
|
.map(arg => {
|
|
|
|
return Function(`"use strict";const $locals = arguments[0];return (${arg});`)(locals);
|
|
|
|
}); // Uses named parameter locals
|
2021-05-12 13:54:54 +02:00
|
|
|
|
|
|
|
const f = locals[key];
|
|
|
|
if (typeof f !== 'function') throw new Error(key + ' is not a function.');
|
|
|
|
localMap[`'${key}', \`[${code.substring(code.indexOf('`[') + 2, code.length - 2)}]\``] = f.call(locals, ...args);
|
|
|
|
} else {
|
|
|
|
localMap[`'${key}'`] = locals[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return localMap;
|
|
|
|
}
|
2021-03-24 21:41:13 +01:00
|
|
|
}
|
|
|
|
|
2021-05-11 13:52:48 +02:00
|
|
|
type PreprocessingCacheEntry = {
|
|
|
|
backendCalls: string[],
|
|
|
|
code: string,
|
|
|
|
};
|