Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

svelte "support" lol #154

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions .eslintrc.js
@@ -0,0 +1,13 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}
2 changes: 1 addition & 1 deletion lib/helpers/raw.js
Expand Up @@ -13,7 +13,7 @@ var TerraformError = exports.TerraformError = require("../error").TerraformError
*/

var processors = exports.processors = {
"html": ["jade", "ejs", "md"],
"html": ["jade", "ejs", "md", "svelte"],
"css" : ["styl", "less", "scss", "sass"],
"js" : ["jsx", "cjs", "coffee"]
}
Expand Down
23 changes: 23 additions & 0 deletions lib/template/processors/svelte-esbuild-worker.js
@@ -0,0 +1,23 @@
var esbuild = require('esbuild');
var sveltePlugin = require('esbuild-svelte');

function init() {
return function (options) {
var plugin = sveltePlugin({
// TODO: This will cause CSS to be generated that we don't need (the server already served it).
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Svelte just merged support for css: 'none' sveltejs/svelte#7914

// I don't see a way to disable that entirely.
compilerOptions: {
hydratable: true,
css: true,
},
});

return esbuild.build(
Object.assign(options, {
plugins: [plugin],
})
);
};
}

module.exports = init;
130 changes: 130 additions & 0 deletions lib/template/processors/svelte.js
@@ -0,0 +1,130 @@
// We need this so Svelte components can import other Svelte files.
// Because the compiled component still contains require("./foo.svelte") calls.
require('svelte/register')({
extensions: ['.svelte'],
hydratable: true,
});

var path = require('path');
var requireFromString = require('require-from-string');
var rpc = require('sync-rpc');
var devalue = require('devalue');
var { compile } = require('svelte/compiler');
var TerraformError = require('../../error').TerraformError;

var esbuildClient = rpc(path.join(__dirname, 'svelte-esbuild-worker.js'));

module.exports = function (fileContents, options) {
return {
compile: function () {
var serverResult = compile(fileContents.toString(), {
generate: 'ssr',
format: 'cjs',
hydratable: true,
});

var componentModule = requireFromString(serverResult.js.code, options.filename, {
appendPaths: [],
});

var Component = componentModule.default;
var hasProgessiveFlag = componentModule.progressive;

// Progressive components cannot use exports such as `partial` because they don't work on the client.
if (hasProgessiveFlag) {
for (var i = 0; i < serverResult.vars.length; i++) {
var v = serverResult.vars[i];

// Exposing "public" would mean serializing the entire object to the client.
if (['partial', 'public'].includes(v.export_name)) {
throw new Error(`Cannot use "${v.export_name}" in progressive components`);
}
}
}

return function render(args) {
var rendered = Component.render(Object.assign({}, args, { enhanced: false }));

var clientScript = '';

// Compile the client script if needed.
if (hasProgessiveFlag) {
// E.g. `current`, but only if it is actually used.
var extraProps = {};

// For every exported component prop that is also a local, add it to the props.
// In other words: if a component uses a local such as `current` it will be serialized to the client.
serverResult.vars.forEach(function (v) {
// Exported component variable.
if (!v.module && v.export_name) {
if (
// Only expose these two for now, since I see value in both and they're trivial.
['current', 'environment'].includes(v.export_name) &&
Object.prototype.hasOwnProperty.call(args, v.export_name)
) {
extraProps[v.export_name] = args[v.export_name];
}
}
});

// TODO: In case we want to remove inline scripts and use external scripts,
// we can just generate a nanoid() and add that to the div
// instead of relying on the position (previousElementSibling) here.
var clientWrapper = `
import Component from ${devalue(options.filename)}

new Component({
// The <div> wrapper right before this script.
target: document.currentScript.previousElementSibling,
hydrate: true,
props: {
enhanced: true,
...(${devalue(extraProps)})
}
});
`;

clientScript = esbuildClient({
stdin: {
contents: clientWrapper,
resolveDir: path.dirname(options.filename),
sourcefile: 'harp-svelte-inline-wrapper.js',
},
bundle: true,
minify: true,
write: false,
}).outputFiles[0].text;
}

var styleBlock = '';

if (serverResult.css.code) {
styleBlock = `<style>${rendered.css.code}</style>`;
}

var scriptBlock = '';

if (clientScript) {
scriptBlock = `<script>${clientScript}</script>`;
}

return `<div style="display: contents;">${rendered.html}</div>${scriptBlock}${styleBlock}`;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this is probably still open to XSS because the devalue above in the inline script will run through esbuild making it kind of pointless. Depending on what esbuild does this might re-enable </script> inside any string. So either we need to replace that here or if we go 100% external scripts then no worries

};
},

parseError: function (error) {
console.error(error);
var arr = error.message.split('\n');
var path_arr = arr[0].split(':');

error.lineno = parseInt(error.lineno || path_arr[path_arr.length - 1] || -1);
error.message = arr[arr.length - 1];
error.source = 'Svelte';
error.dest = 'HTML';
error.filename = error.path || options.filename;
error.stack = fileContents.toString();

return new TerraformError(error);
},
};
};