diff --git a/Cargo.toml b/Cargo.toml index a9654ee..a59d6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,12 @@ axum = "0.7.4" # Rocket dependencies rocket = "0.5.0-rc.2" +# Salvo dependencies +salvo = { version = "0.68.3", features = ["serve-static"] } + +serde_json = "1.0.118" +tracing = "0.1" +tracing-subscriber = "0.3" env_logger = "0.9.0" [[example]] @@ -90,3 +96,7 @@ path = "examples/webpack-react/server.rs" [[example]] name = "rspack-react" path = "examples/rspack-react/server.rs" + +[[example]] +name="vite-svelte" +path="examples/vite-svelte/backend/main.rs" \ No newline at end of file diff --git a/examples/vite-svelte/Cargo.toml b/examples/vite-svelte/Cargo.toml new file mode 100644 index 0000000..4f52a49 --- /dev/null +++ b/examples/vite-svelte/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "svelte-salvo-ssr" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "svelte-salvo-ssr" +path = "backend/main.rs" + +[dependencies] +salvo = { version = "0.68.3", features = ["serve-static"] } +serde_json = "1.0.118" +ssr_rs = "0.5.4" +tokio = { version = "1", features = ["macros"] } +tracing = "0.1" +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/examples/vite-svelte/README.md b/examples/vite-svelte/README.md new file mode 100644 index 0000000..7b83373 --- /dev/null +++ b/examples/vite-svelte/README.md @@ -0,0 +1,21 @@ +# Svelte-Salvo-SSR-template +It is a template for Svelte SSR with Salvo-rs and Vite. + +1. Install npm dependencies: +```sh +pnpm install +``` + +2. Use vite to build svelte client JS and CSS: +```sh +pnpx vite build --config vite.client.config.js +``` + +3. Use vite to build svelte SSR JS: +```sh +pnpx vite build --config vite.ssr.config.js +``` +4. Run Rust server: +```sh +cargo run +``` diff --git a/examples/vite-svelte/backend/main.rs b/examples/vite-svelte/backend/main.rs new file mode 100644 index 0000000..48ce172 --- /dev/null +++ b/examples/vite-svelte/backend/main.rs @@ -0,0 +1,85 @@ +use salvo::prelude::*; +use ssr_rs::Ssr; +use std::cell::RefCell; +use std::fs::read_to_string; +use std::path::Path; + +thread_local! { + static SSR: RefCell> = RefCell::new( + Ssr::from( + read_to_string(Path::new("./dist/server/server.js").to_str().unwrap()).unwrap(), + "" + ).unwrap_or_else(|err| { + eprintln!("Failed to initialize SSR: {}", err); + std::process::exit(1); + }) + ) +} + +#[handler] +async fn index(res: &mut Response) { + let result = SSR.with(|ssr| { + let mut ssr = ssr.borrow_mut(); + ssr.render_to_string(None).unwrap_or_else(|err| { + eprintln!("Error rendering to string: {}", err); + String::new() + }) + }); + + if result.is_empty() { + eprintln!("Rendered result is empty"); + res.status_code(StatusCode::INTERNAL_SERVER_ERROR); + res.render(Text::Plain("Internal Server Error")); + return; + } + + //println!("Rendered result: {}", result); // For debugging + + let result: serde_json::Value = match serde_json::from_str(&result) { + Ok(val) => val, + Err(err) => { + eprintln!("Failed to parse JSON: {}", err); + res.status_code(StatusCode::INTERNAL_SERVER_ERROR); + res.render(Text::Plain("Internal Server Error")); + return; + } + }; + + let html = result["html"].as_str().unwrap_or(""); + let css = result["css"].as_str().unwrap_or(""); + + let full_html = format!( + r#" + + + + + + +
{}
+ + + "#, + css, html + ); + res.render(Text::Html(full_html)); +} + +#[tokio::main] +async fn main() { + Ssr::create_platform(); + let router = Router::new() + .push(Router::with_path("/client/<**path>").get(StaticDir::new(["./dist/client"]))) + .push( + Router::with_path("/client/assets/<**path>") + .get(StaticDir::new(["./dist/assets/client"])), + ) + .push(Router::with_path("/").get(index)); + + let acceptor = TcpListener::new("127.0.0.1:8080").bind().await; + + tracing_subscriber::fmt().init(); + tracing::info!("Listening on http://{:?}", acceptor.local_addr()); + + Server::new(acceptor).serve(router).await; +} diff --git a/examples/vite-svelte/frontend/App.svelte b/examples/vite-svelte/frontend/App.svelte new file mode 100644 index 0000000..1f31354 --- /dev/null +++ b/examples/vite-svelte/frontend/App.svelte @@ -0,0 +1,47 @@ + + +
+
+ + + + + + +
+

Vite + Svelte

+ +
+ +
+ +

+ Check out SvelteKit, the official Svelte app framework powered by Vite! +

+ +

+ Click on the Vite and Svelte logos to learn more +

+
+ + diff --git a/examples/vite-svelte/frontend/app.css b/examples/vite-svelte/frontend/app.css new file mode 100644 index 0000000..617f5e9 --- /dev/null +++ b/examples/vite-svelte/frontend/app.css @@ -0,0 +1,79 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/vite-svelte/frontend/assets/svelte.svg b/examples/vite-svelte/frontend/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/examples/vite-svelte/frontend/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-svelte/frontend/head.js b/examples/vite-svelte/frontend/head.js new file mode 100644 index 0000000..f690331 --- /dev/null +++ b/examples/vite-svelte/frontend/head.js @@ -0,0 +1,6 @@ +export function buildHead(head, css) { + return ` + ${head} + + `; + } \ No newline at end of file diff --git a/examples/vite-svelte/frontend/lib/Counter.svelte b/examples/vite-svelte/frontend/lib/Counter.svelte new file mode 100644 index 0000000..e45f903 --- /dev/null +++ b/examples/vite-svelte/frontend/lib/Counter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/examples/vite-svelte/frontend/main.js b/examples/vite-svelte/frontend/main.js new file mode 100644 index 0000000..b2b3c4c --- /dev/null +++ b/examples/vite-svelte/frontend/main.js @@ -0,0 +1,8 @@ +import App from './App.svelte'; + +const app = new App({ + target: document.querySelector('#svelte-app'), + hydrate: true +}); + +export default app; diff --git a/examples/vite-svelte/frontend/server.js b/examples/vite-svelte/frontend/server.js new file mode 100644 index 0000000..2ba0fc2 --- /dev/null +++ b/examples/vite-svelte/frontend/server.js @@ -0,0 +1,6 @@ +import App from './App.svelte'; + +export function render() { + const { html, css } = App.render(); + return JSON.stringify({ html, css: css.code }); +} diff --git a/examples/vite-svelte/frontend/vite-env.d.ts b/examples/vite-svelte/frontend/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/examples/vite-svelte/frontend/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/vite-svelte/index.html b/examples/vite-svelte/index.html new file mode 100644 index 0000000..dbbfa2a --- /dev/null +++ b/examples/vite-svelte/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + + +
+ + + diff --git a/examples/vite-svelte/jsconfig.json b/examples/vite-svelte/jsconfig.json new file mode 100644 index 0000000..588de60 --- /dev/null +++ b/examples/vite-svelte/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["frontend/**/*.d.ts", "frontend/**/*.js", "frontend/**/*.svelte"] +} diff --git a/examples/vite-svelte/package.json b/examples/vite-svelte/package.json new file mode 100644 index 0000000..6dc5886 --- /dev/null +++ b/examples/vite-svelte/package.json @@ -0,0 +1,16 @@ +{ + "name": "my-vue-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && vite build --config vite.config.ssr.js", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "svelte": "^4.2.18", + "vite": "^5.3.1" + } +} diff --git a/examples/vite-svelte/public/vite.svg b/examples/vite-svelte/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/vite-svelte/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/vite-svelte/svelte.config.js b/examples/vite-svelte/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/examples/vite-svelte/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/examples/vite-svelte/vite.client.config.js b/examples/vite-svelte/vite.client.config.js new file mode 100644 index 0000000..960c9b6 --- /dev/null +++ b/examples/vite-svelte/vite.client.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + base: '/client/', + plugins: [svelte({ + compilerOptions: { + hydratable: true + } + })], + build: { + outDir: 'dist/client', + emptyOutDir: true, + rollupOptions: { + input: './frontend/main.js', + output: { + format: 'esm', + entryFileNames: '[name].js', + chunkFileNames: '[name]-[hash].js', + assetFileNames: 'assets/[name][extname]', + }, + } + } +}) diff --git a/examples/vite-svelte/vite.ssr.config.js b/examples/vite-svelte/vite.ssr.config.js new file mode 100644 index 0000000..3002b99 --- /dev/null +++ b/examples/vite-svelte/vite.ssr.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + base: '/client/', + plugins: [svelte()], + build: { + ssr: true, + outDir: 'dist/server', + emptyOutDir: true, + rollupOptions: { + input: './frontend/server.js', + output: { + format: 'iife', + entryFileNames: '[name].js', + chunkFileNames: '[name]-[hash].js', + } + } + }, + ssr: { + noExternal: true, + } +}) diff --git a/tests/assets/svelte-4-iife.js b/tests/assets/svelte-4-iife.js new file mode 100644 index 0000000..9f85b2f --- /dev/null +++ b/tests/assets/svelte-4-iife.js @@ -0,0 +1,63 @@ +(function(exports) { + "use strict"; + function run(fn) { + return fn(); + } + function blank_object() { + return /* @__PURE__ */ Object.create(null); + } + function run_all(fns) { + fns.forEach(run); + } + let current_component; + function set_current_component(component) { + current_component = component; + } + let on_destroy; + function create_ssr_component(fn) { + function $$render(result, props, bindings, slots, context) { + const parent_component = current_component; + const $$ = { + on_destroy, + context: new Map(context || (parent_component ? parent_component.$$.context : [])), + // these will be immediately discarded + on_mount: [], + before_update: [], + after_update: [], + callbacks: blank_object() + }; + set_current_component({ $$ }); + const html = fn(result, props, bindings, slots); + set_current_component(parent_component); + return html; + } + return { + render: (props = {}, { $$slots = {}, context = /* @__PURE__ */ new Map() } = {}) => { + on_destroy = []; + const result = { title: "", head: "", css: /* @__PURE__ */ new Set() }; + const html = $$render(result, props, {}, $$slots, context); + run_all(on_destroy); + return { + html, + css: { + code: Array.from(result.css).map((css) => css.code).join("\n"), + map: null + // TODO + }, + head: result.title + result.head + }; + }, + $$render + }; + } + const App = create_ssr_component(($$result, $$props, $$bindings, slots) => { + return `
`; + }); + function render() { + const { html } = App.render(); + return html; + } + exports.render = render; + Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); + return exports; +})({}); diff --git a/tests/svelte.rs b/tests/svelte.rs new file mode 100644 index 0000000..c27bd41 --- /dev/null +++ b/tests/svelte.rs @@ -0,0 +1,24 @@ +use ssr_rs::Ssr; +use std::fs::read_to_string; +use std::sync::Once; + +static INIT: Once = Once::new(); + +fn prepare() { + INIT.call_once(|| { + Ssr::create_platform(); + }) +} + +#[test] +fn renders_svelte_exported_as_iife() { + prepare(); + + let source = read_to_string("./tests/assets/svelte-4-iife.js").unwrap(); + + let mut js = Ssr::from(source, "").unwrap(); + + let html = js.render_to_string(None).unwrap(); + + assert_eq!(html, "
"); +}