Skip to content

Instantly share code, notes, and snippets.

@milahu
Last active November 22, 2020 12:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milahu/e1cd4aeedda84b77351cf84e10a97c07 to your computer and use it in GitHub Desktop.
Save milahu/e1cd4aeedda84b77351cf84e10a97c07 to your computer and use it in GitHub Desktop.
preval for svelte
// moved to https://github.com/milahu/svelte-preval
/*
preval for svelte
code is at line 250 :P
performant:
eval static expressions at compile time
allow shorter load times for client
readable:
avoid "magic numbers"
colocate "all the code" in one place
license = public domain
warranty = none
save file
sveltePreval.js
in your svelte project folder
next to the rollup.config.js file
install deps
npm i -D estree-walker lave astring
bundle size
acorn 96 KB // dependency of svelte
magic-string 20 KB // dependency of svelte
estree-walker 1 KB // yay to Rich Harris :D
lave 8 KB
astring 14 KB
edit rollup.config.js
import sveltePreval from './sveltePreval.js'
export default {
plugins: [
svelte({
preprocess: [
sveltePreval(),
],
}),
],
}
use in App.svelte
let testPreval = preval(() => ({
a: Math.PI * 0.5,
b: Math.sqrt(2),
}))
// --> testPreval.a = 1.5707963267948966
// unpack object properties to global scope
Object.assign(window, preval(() => ({
a: Math.PI * 0.5,
b: Math.sqrt(2),
})))
// --> a = 1.5707963267948966
let testPrevalFuncs = preval(()=>([
()=>'hello',
()=>('world'),
()=>{return 'foo'},
function(){return 'bar'},
]))
// --> testPrevalFuncs[0] = ()=>'hello'
run
npm run dev
see result in http://localhost:5000/build/bundle.js
let testPreval = {
"a": 1.5707963267948966,
"b": 1.4142135623730951
};
let testPrevalFuncs = [
() => "hello",
() => "world",
() => {
return "foo";
},
function () {
return "bar";
}
];
config "defaultOn"
set to false
to make preval "normally off"
and only enable in
<script preval>
or
<script preval="custom_preval_name">
config "funcName"
instead of "let x = preval(f)"
you can use "let x = custom_preval_name(f)"
local - by changing your App.svelte to
<script preval="local_preval_name">
let x = local_preval_name(() => ('result'))
</script>
global - by changing rollup.config.js to
preprocess: [
sveltePreval('custom_preval_name'),
],
import modules
yes you can use modules inside the preval code
but modules must be in commonJS format
that is, they must be compatible with node.js
so you can use
let testPreval = preval(() => {
const fs = require('fs')
return fs.readFileSync('input.txt').toString()
})
but this will NOT work [ES6 module format]
let testPreval = preval(() => {
import { moduleFunction } from 'moduleName'
// error: [!] (plugin svelte) SyntaxError: 'import' and 'export' may only appear at the top level
return moduleFunction('foo')
})
to use ES6 modules
you must transform them to commonJS format
npm i --save-dev @babel/core @babel/cli @babel/plugin-transform-modules-commonjs
./node_modules/@babel/cli/bin/babel.js --plugins @babel/plugin-transform-modules-commonjs src/moduleName.js > src/moduleName.cjs
and then use with
let testPreval = preval(() => {
const moduleName = require('./path/to/moduleName.cjs')
return moduleName.moduleFunction('foo')
})
TODO
support ES6 module format
transform require-d modules on the fly
to commonJS format, compatible with require()
see section "import modules"
eval more safe?
https://rollupjs.org/guide/en/#avoiding-eval
"rename eval" seems to break stuff
use meriyah JS parser?
pro: faster than acorn by factor 3
con: svelte is using acorn parser
--> + 100 KB bundle size
keep original line numbers after expansion?
source map?
allow to pass variables to preval
or: make preval aware of the previous script environment
assume other variables as static
let envVar = 'hello'
let prevalTest = preval(() => (envVar+' world'))
run before svelte js parser?
for now, svelte fails on parse errors
which per se is ok,
but "pre process" should run before svelte, no?
change <script type="x"> ?
expand this to use sweet.js macro system
https://jlongster.com/Stop-Writing-JavaScript-Compilers--Make-Macros-Instead
constexpr - evaluate expressions at compile time
https://gist.github.com/natefaubion/f4be4c8531ef45de87b4
write an html/css preprocessor
to replace all color formats to hex rgb
including NCS and HSV format
hex rgb is fastest to parse in browser
yay micro optimization
client side: support color theme switching
most basic: light mode vs dark mode
libraries
JS code parser, AST tree generator
https://github.com/acornjs/acorn # 100 KB + fast
https://github.com/mishoo/UglifyJS2 # slow
compare parser speed
https://cherow.github.io/cherow/performance/
http://turtlescript.github.cscott.net/benchmark/
https://marijnhaverbeke.nl/acorn/test/bench/index.html
parsers even faster than acorn
https://github.com/cherow/cherow [unmaintained]
fork projects:
https://github.com/meriyah/meriyah # 100 KB + super fast
faster than acorn by factor 3
https://meriyah.github.io/meriyah/performance/
https://github.com/buntis/buntis
AST walker = read only
https://github.com/Rich-Harris/estree-walker # 1 KB
https://github.com/davidbonnet/astravel # 7 kB
https://github.com/acornjs/acorn/tree/master/acorn-walk # 7 kB
AST transformer = read + write
https://github.com/substack/node-falafel # 90 kB
https://github.com/alloc/nebu # 120 kB
https://github.com/benjamn/recast # 500 kB
code generator = AST printer
https://github.com/estools/escodegen # slow
https://github.com/davidbonnet/astring # 14 KB + super fast
*/
// acorn = ast from javascript parser
// estree-walker = read-only AST tree walker
// lave = ast from live object serializer. eval in reverse
// astring = ast to javascript code generator
// magic-string = replace strings, keep index
import { parse as acorn_parse } from 'acorn'
import { walk as estree_walk } from 'estree-walker'
import lave from 'lave'
import { generate as astring_generate } from 'astring'
import magicString from 'magic-string'
const PREVAL_ATTR = 'preval'
const PREVAL_NAME = 'preval'
export default function sveltePreval(defaultOn = true, funcName = PREVAL_NAME) {
return {
script({ content, attributes /*, filename*/ })
{
// override global config per script
// <script preval="local_preval_name">
if (PREVAL_ATTR in attributes) {
if (attributes[PREVAL_ATTR] !== true) {
funcName = attributes[PREVAL_ATTR]
}
// true means "empty attribute" <script preval>
// --> keep default funcName
}
else if (defaultOn === false) {
return { code: content } // no change
}
const ast = acorn_parse(
content, {
// ecmaVersion: 10, // default in year 2019
sourceType: 'module',
})
let code = new magicString(content)
estree_walk( ast, {
enter: function ( node, parent, prop, index ) {
if (
node.type !== 'CallExpression'
|| node.callee.name !== funcName
)
{ return } // ignore this node
if (node.arguments.length !== 1) {
return console.error(`preval usage: ${funcName}(f)`)
}
const arg0Src = content.substring(
node.arguments[0].start, node.arguments[0].end)
const evalRes = eval('('+arg0Src+')()')
let evalAst = lave(evalRes).body[0].expression
let evalSrc = astring_generate(
evalAst, {
indent: '',
// sourceMap: sourceMapGenerator,
})
// lave BUG: function(){} --> eval("(function(){})")
// workaround: remove all eval("(....)")
if (evalSrc.indexOf('eval("(') !== -1) {
evalAst = acorn_parse(evalSrc) // lave's AST sucks?
// BUG? lave seems to give wrong start/end numbers
evalSrc = new magicString(evalSrc)
estree_walk( evalAst, {
enter: function ( node, parent, prop, index ) {
if (
node.type !== 'CallExpression'
|| node.callee.name !== 'eval'
|| node.arguments.length !== 1
)
{ return } // ignore this node
// patch the evalSrc
// patch the source
evalSrc.overwrite(
node.start, node.end,
node.arguments[0].value
)
}
})
evalSrc = evalSrc.toString()
}
// patch the source
code.overwrite( node.start, node.end, evalSrc )
}})
code = code.toString()
return { code }
}}}
@milahu
Copy link
Author

milahu commented Nov 22, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment