Svelte: refactor many symbols and safe eval backend calls arguments

Also allow dumping function contents by outputing them directly
This commit is contained in:
Alice Gaudon 2021-04-29 15:46:48 +02:00
parent bd2b7e7579
commit e385986aca
4 changed files with 86 additions and 73 deletions

View File

@ -12,8 +12,9 @@ import {CssResult} from "svelte/types/compiler/interfaces";
import * as child_process from "child_process"; import * as child_process from "child_process";
import fs from "fs"; import fs from "fs";
const BACKEND_CODE_PREFIX = 'swaf.'; const BACKEND_CODE_PREFIX = 'locals.';
const COMPILED_SVELTE_EXTENSION = '.swafview'; const BACKEND_CODE_PREFIX_TEMPORARY_HOLDER = '$$locals$$';
const COMPILED_SVELTE_EXTENSION = '.precompiled';
export default class SvelteViewEngine extends ViewEngine { export default class SvelteViewEngine extends ViewEngine {
public static getPreCompileSeparator(canonicalViewName: string): string { public static getPreCompileSeparator(canonicalViewName: string): string {
@ -27,9 +28,9 @@ export default class SvelteViewEngine extends ViewEngine {
private readonly fileCache: FileCache = new FileCache(); private readonly fileCache: FileCache = new FileCache();
private readonly dependencyCache: Record<string, Set<string>> = {}; private readonly dependencyCache: Record<string, Set<string>> = {};
private readonly backendCodeCache: Record<string, { private readonly backendCallsCache: Record<string, {
backendReplacedCode: string, replacedBackendCall: string,
backendLines: string[], backendCalls: string[],
}> = {}; }> = {};
private rollup?: child_process.ChildProcess; private rollup?: child_process.ChildProcess;
@ -76,47 +77,48 @@ export default class SvelteViewEngine extends ViewEngine {
// Pre-compiled parts // Pre-compiled parts
const [ const [
backendLines, backendCalls,
head, head,
html, html,
css, css,
] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName)); ] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName));
const localMap: Record<string, unknown> = {}; const localMap: Record<string, unknown> = {};
backendLines.split('\n').forEach(line => { backendCalls.split('\n').forEach(code => {
const key = line.substring(1, line.indexOf(',') >= 0 ? line.indexOf(',') - 1 : line.length - 1); const key = code.substring(1, code.indexOf(',') >= 0 ? code.indexOf(',') - 1 : code.length - 1);
if (line.indexOf('[') >= 0) { if (code.indexOf('`[') >= 0) {
const args = line.substring(line.indexOf('[') + 1, line.length - 1) const args = code.substring(code.indexOf('`[') + 2, code.length - 2)
.split(/, *?/) .split(/, *?/)
.map(arg => { .map(arg => {
if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"'; if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"';
return arg; return arg;
}) })
.map(arg => JSON.parse(arg)); .map(arg => Function(`"use strict";const locals = arguments[0];return (${arg});`)(locals)); // Uses named parameter locals
const f = locals[key]; const f = locals[key];
if (typeof f !== 'function') throw new Error(key + ' is not a function.'); if (typeof f !== 'function') throw new Error(key + ' is not a function.');
localMap[`'${key}', ${JSON.stringify(args)}`] = f.call(locals, ...args); localMap[`'${key}', \`[${code.substring(code.indexOf('`[') + 2, code.length - 2)}]\``] = f.call(locals, ...args);
} else { } else {
localMap[`'${key}'`] = locals[key]; localMap[`'${key}'`] = locals[key];
} }
}); });
const props = JSON.stringify(localMap); const actualLocals = JSON.stringify(localMap, (key, value) => {
return typeof value === 'function' ?
value.toString() :
value;
});
// Replaces // Replaces
const replaceMap: Record<string, string> = { const replaceMap: Record<string, string> = {
canonicalViewName: canonicalViewName, canonicalViewName: canonicalViewName,
props: props, locals: actualLocals,
head: head, head: head,
html: html, html: html,
css: css, css: css,
}; };
return rawOutput.replace( return rawOutput.replace(
new RegExp(Object.keys(replaceMap).map(str => `%${str}%`).join('|'), 'g'), new RegExp(Object.keys(replaceMap).map(str => `%${str}%`).join('|'), 'g'),
(substring) => { (substring) => replaceMap[substring.slice(1, substring.length - 1)],
console.log(substring);
return replaceMap[substring.slice(1, substring.length - 1)];
},
); );
} }
@ -132,20 +134,20 @@ export default class SvelteViewEngine extends ViewEngine {
logger.info(canonicalName + ' > ', 'Pre-compiling', file, '->', intermediateFile); logger.info(canonicalName + ' > ', 'Pre-compiling', file, '->', intermediateFile);
const source = await this.fileCache.get(file, !config.get<boolean>('view.cache')); const source = await this.fileCache.get(file, !config.get<boolean>('view.cache'));
const allBackendLines: string[] = []; const allBackendCalls: string[] = [];
for (const dependency of this.resolveDependencies(source, canonicalName)) { for (const dependency of this.resolveDependencies(source, canonicalName)) {
allBackendLines.push(...(await this.replaceBackendCode(dependency)).backendLines); allBackendCalls.push(...(await this.replaceBackendCalls(dependency)).backendCalls);
} }
const {backendReplacedCode, backendLines} = await this.replaceBackendCode(canonicalName, source); const {replacedBackendCall, backendCalls} = await this.replaceBackendCalls(canonicalName, source);
allBackendLines.push(...backendLines); allBackendCalls.push(...backendCalls);
// Server Side Render (initial HTML and CSS, no-js) // Server Side Render (initial HTML and CSS, no-js)
const ssr = await this.compileSsr(canonicalName, intermediateFile, backendReplacedCode); const ssr = await this.compileSsr(canonicalName, intermediateFile, replacedBackendCall);
const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName); const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName);
const finalCode = [ const finalCode = [
[...new Set<string>(allBackendLines).values()].join('\n'), [...new Set<string>(allBackendCalls).values()].join('\n'),
ssr.head, ssr.head,
ssr.html, ssr.html,
ssr.css.code, ssr.css.code,
@ -186,13 +188,13 @@ export default class SvelteViewEngine extends ViewEngine {
return dependencies; return dependencies;
} }
private async replaceBackendCode(canonicalViewName: string, code?: string): Promise<{ private async replaceBackendCalls(canonicalViewName: string, code?: string): Promise<{
backendReplacedCode: string, replacedBackendCall: string,
backendLines: string[], backendCalls: string[],
}> { }> {
// Cache // Cache
if (Object.keys(this.backendCodeCache).indexOf(canonicalViewName) >= 0) { if (Object.keys(this.backendCallsCache).indexOf(canonicalViewName) >= 0) {
return this.backendCodeCache[canonicalViewName]; return this.backendCallsCache[canonicalViewName];
} }
// mkdir output file dir // mkdir output file dir
@ -206,19 +208,19 @@ export default class SvelteViewEngine extends ViewEngine {
} }
// Skip replace if there is no swaf export // Skip replace if there is no swaf export
if (!code.match(/export[ \n]+let[ \n]+swaf[ \n]*=[ \n]*{[ \n]*}/)) { if (!code.match(/export[ \n]+let[ \n]+locals[ \n]*=[ \n]*{[ \n]*}/)) {
const generated = { const generated = {
backendReplacedCode: code, replacedBackendCall: code,
backendLines: [], backendCalls: [],
}; };
await afs.writeFile(outputFile, generated.backendReplacedCode); await afs.writeFile(outputFile, generated.replacedBackendCall);
this.backendCodeCache[canonicalViewName] = generated; this.backendCallsCache[canonicalViewName] = generated;
return generated; return generated;
} }
let output = code; let output = code;
const backendLines = new Set<string>(); const backendCalls = new Set<string>();
let index = 0; let index = 0;
while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) { while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) {
@ -247,31 +249,32 @@ export default class SvelteViewEngine extends ViewEngine {
endIndex++; endIndex++;
} }
let backendLine = output.substring(startIndex, endIndex); let backendCall = output.substring(startIndex, endIndex);
if (backendLine.match(/([^()]+)\((.+?)\)/)) { if (backendCall.match(/([^()]+)\((.+?)\)/)) {
backendLine = backendLine.replace(/([^()]+)\((.+?)\)/, "'$1', [$2]"); backendCall = backendCall.replace(/([^()]+)\((.+?)\)/, "'$1', `[$2]`");
} else { } else {
backendLine = backendLine.replace(/([^()]+)/, "'$1'"); backendCall = backendCall.replace(/([^()]+)/, "'$1'");
} }
backendCalls.add(backendCall);
backendLines.add(backendLine);
output = output.substring(0, index) + output = output.substring(0, index) +
'swaf(' + backendLine + ')' + 'locals(' + backendCall.split(BACKEND_CODE_PREFIX).join(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER) + ')' +
output.substring(endIndex, output.length); output.substring(endIndex, output.length);
} }
output = output.split(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER).join(BACKEND_CODE_PREFIX);
const generated = { const generated = {
backendReplacedCode: output, replacedBackendCall: output,
backendLines: [...backendLines], backendCalls: [...backendCalls],
}; };
await afs.writeFile(outputFile, generated.backendReplacedCode); await afs.writeFile(outputFile, generated.replacedBackendCall);
this.backendCodeCache[canonicalViewName] = generated; this.backendCallsCache[canonicalViewName] = generated;
return generated; return generated;
} }
public async afterPreCompile(watch: boolean): Promise<void> { public async afterPreCompile(watch: boolean): Promise<void> {
await this.bundle(watch, ...Object.keys(this.backendCodeCache)); await this.bundle(watch, ...Object.keys(this.backendCallsCache));
} }
@ -280,12 +283,12 @@ export default class SvelteViewEngine extends ViewEngine {
} }
public async onFileChange(file: string): Promise<void> { public async onFileChange(file: string): Promise<void> {
delete this.backendCodeCache[this.toCanonicalName(file)]; delete this.backendCallsCache[this.toCanonicalName(file)];
} }
public async onFileRemove(file: string): Promise<void> { public async onFileRemove(file: string): Promise<void> {
const canonicalName = this.toCanonicalName(file); const canonicalName = this.toCanonicalName(file);
delete this.backendCodeCache[canonicalName]; delete this.backendCallsCache[canonicalName];
delete this.dependencyCache[canonicalName]; delete this.dependencyCache[canonicalName];
Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName)); Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName));
await this.stopRollup(); await this.stopRollup();
@ -322,14 +325,16 @@ export default class SvelteViewEngine extends ViewEngine {
const globals = ViewEngine.getGlobals(); const globals = ViewEngine.getGlobals();
delete require.cache[path.resolve(file)]; delete require.cache[path.resolve(file)];
return requireFromString(svelteSsr.js.code, file).default.render({ const localsFunction = (key: string, rawArgs?: string) => {
swaf: (key: string, args?: unknown[]) => { if (!rawArgs) return globals[key];
if (!args) return globals[key]; const args = Function(`"use strict";const locals = Object.assign(arguments[0], arguments[1]);return (${rawArgs});`)(localsFunction, globals) as string[];
const f = globals[key]; const f = globals[key];
if (typeof f !== 'function') throw new Error(key + ' is not a function.'); if (typeof f !== 'function') throw new Error(key + ' is not a function.');
return f.call(globals, ...args); return f.call(globals, ...args);
}, };
return requireFromString(svelteSsr.js.code, file).default.render({
locals: localsFunction,
}); });
} }

View File

@ -2,13 +2,14 @@
import HomeDep from "./home_dep.svelte"; import HomeDep from "./home_dep.svelte";
import Layout from "./layout.svelte"; import Layout from "./layout.svelte";
export let swaf = {}; export let locals = {};
let count = 5; let count = 5;
function handleClick() { function handleClick() {
count++; count++;
} }
let depTest; let depTest;
</script> </script>
@ -34,24 +35,24 @@
<button on:click={handleClick}>More hellos!!</button> <button on:click={handleClick}>More hellos!!</button>
<p>Direct access: {swaf.direct}</p> <p>Direct access: {locals.direct}</p>
{#if swaf.route('auth') === '/'} {#if locals.route('auth') === '/'}
We're home! We're home!
{:else} {:else}
We're somewhere else... {swaf.route('auth')} We're somewhere else... {locals.route('auth')}
{/if} {/if}
<p>The route to auth is {swaf.route('auth')}</p> <p>The route to auth is {locals.route('auth')}</p>
<p>\swaf.notcode</p> <p>\locals.notcode</p>
<p>{`{\\swaf.escaped}`}</p> <p>{`{\\locals.escaped}`}</p>
<div class="style-test"> <div class="style-test">
<p>Blue!</p> <p>Blue!</p>
</div> </div>
<HomeDep swaf={swaf} bind:depTest={depTest}/> <HomeDep locals={locals} bind:depTest={depTest}/>
<p>Dependency test: {depTest}</p> <p>Dependency test: {depTest}</p>

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
export let depTest = 'Success'; export let depTest = 'Success';
export let swaf = {}; export let locals = {};
let locallyDefinedVar = 'correct value';
</script> </script>
<style> <style>
@ -11,7 +13,13 @@
<p>Simple dep test</p> <p>Simple dep test</p>
<p>\locals.</p>
<p>\locals.</p>
<p>Nested swaf call: {swaf.direct}</p> <p>locals: {locals}</p>
<p>Nested swaf call: {swaf.route('auth')}</p> <p>\locals.route: {locals.route} <br> {locals.dump(typeof locals.route)}</p>
<p>Nested swaf call: {swaf.route('home')}</p> <p>\locals.dump: {locals.dump('bonjour')}</p>
<p>\locals.dump: {locals.dump(locals.app)}</p>
<p>\locals.dump: {locals.dump(NaN)}</p>
<p>\locals.route: {locals.route('auth')}</p>
<p>\locals.route: {locals.route('home')}</p>

View File

@ -7,17 +7,16 @@
<script type="module" defer> <script type="module" defer>
import View from '/js/%canonicalViewName%.js'; import View from '/js/%canonicalViewName%.js';
const props = %props%; const locals = %locals%;
new View({ new View({
hydrate: true, hydrate: true,
target: document.body, target: document.body,
props: { props: {
swaf: (key, args) => { locals: (key, args) => {
const line = args ? return locals[args ?
`'${key}', ${JSON.stringify(args)}` `'${key}', \`${args}\``
: `'${key}'`; : `'${key}'`];
return props[line];
}, },
}, },
}); });