Add svelte as a view engine to swaf #33
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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];
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user