├── src ├── lib │ ├── b.js │ └── a.js └── app.js ├── .vscode └── settings.json ├── README.md ├── deno.json └── server.js /src/lib/b.js: -------------------------------------------------------------------------------- 1 | const b = () => { 2 | console.log({ b }); 3 | }; 4 | 5 | export default b; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/a.js: -------------------------------------------------------------------------------- 1 | import b from './b.js'; 2 | 3 | const a = () => { 4 | console.log({ a }); 5 | console.log({ b }); 6 | }; 7 | 8 | export default a; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vultra (vue ultra) 2 | 3 | > [!WARNING]\ 4 | > This project does not require build steps, bundling or compiling, you probably 5 | > don't want to be here — how did you get here? 6 | 7 | - No build 8 | - No bundle 9 | - Isomorphic ESM 10 | - Typechecking with JSDOC, esm.sh + Deno LSP 11 | - `modulepreload` HTTP headers 12 | - Minify js + strip comments in client side code 13 | - Dynamic styling with [Fstyle](https://github.com/jamesdiacono/Fstyle) 14 | - Server and client side routing (vue-router) 15 | 16 | --- 17 | 18 | To start: `deno task dev` 19 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "fake-tag": "https://esm.sh/v133/fake-tag@5.0.0/es2022/fake-tag.mjs", 4 | "fstyle": "https://james.diacono.com.au/files/fstyle.js", 5 | "vue": "https://esm.sh/v133/vue@3.3.7/es2022/vue.mjs", 6 | "vue/server-renderer": "https://esm.sh/v133/vue@3.3.7/es2022/server-renderer.js", 7 | "vue-router": "https://esm.sh/v133/vue-router@4.2.5/X-ZS92dWU/es2022/vue-router.mjs" 8 | }, 9 | "tasks": { 10 | "dev": "deno run -A --watch server.js", 11 | "start": "deno run -A server.js" 12 | }, 13 | "compilerOptions": { 14 | "lib": ["deno.ns", "dom", "dom.iterable", "dom.asynciterable"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // required until Deno v2 4 | // https://github.com/denoland/deno/issues/13367 5 | import 'data:text/javascript,delete globalThis.window;'; 6 | 7 | import { renderToWebStream } from 'vue/server-renderer'; 8 | import { createRouter, createMemoryHistory } from 'vue-router'; 9 | import app, { routes } from './src/app.js'; 10 | 11 | // minify js 12 | import minify from 'npm:@node-minify/core'; 13 | import uglifyjs from 'npm:@node-minify/uglify-js'; 14 | 15 | // preload headers 16 | import resolveLinkRelations from 'npm:modulepreload-link-relations/resolveLinkRelations.mjs'; 17 | import formatLinkHeaderRelations from 'npm:modulepreload-link-relations/formatLinkHeaderRelations.mjs'; 18 | 19 | // use importmap from deno.json 20 | const config = await Deno.readTextFile('./deno.json'); 21 | const importmap = { imports: JSON.parse(config).imports }; 22 | 23 | const router = createRouter({ 24 | history: createMemoryHistory(), 25 | routes, 26 | }); 27 | app.use(router); 28 | app.provide('importmap', JSON.stringify(importmap)); 29 | 30 | const appPath = './src'; 31 | 32 | Deno.serve(async (request) => { 33 | const url = new URL(request.url, 'http://localhost'); 34 | 35 | // quick js file server 36 | const static_path = './src'; 37 | const filePath = static_path + url.pathname; 38 | let file; 39 | try { 40 | file = await Deno.readTextFile(filePath); 41 | } catch { 42 | // ignore 43 | } 44 | if (file) { 45 | // @ts-ignore missing types from minify lib 46 | file = await minify({ 47 | compressor: uglifyjs, 48 | content: file, 49 | }); 50 | // build modulepreload headers 51 | const linkRelations = await resolveLinkRelations({ 52 | appPath, 53 | url: filePath.replace(appPath, ''), 54 | }); 55 | 56 | /** @type {Object.} */ 57 | const headers = { 58 | 'content-type': 'text/javascript', 59 | }; 60 | if (linkRelations) { 61 | headers.link = formatLinkHeaderRelations(linkRelations); 62 | } 63 | return new Response(file, { headers }); 64 | } 65 | 66 | if (url.pathname === '/favicon.ico') { 67 | return new Response(null, { status: 404 }); 68 | } 69 | 70 | await router.push(new URL(request.url).pathname); 71 | 72 | const stream = renderToWebStream(app); 73 | 74 | return new Response(stream, { 75 | headers: { 76 | 'content-type': 'text/html; charset=utf-8', 77 | }, 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | globalThis.__VUE_OPTIONS_API__ = true; 4 | globalThis.__VUE_PROD_DEVTOOLS__ = false; 5 | 6 | // modulepreload test 7 | import a from './lib/a.js'; 8 | console.log({ a }); 9 | 10 | import fstyle from 'fstyle'; 11 | 12 | import { 13 | h, 14 | shallowRef, 15 | createSSRApp, 16 | inject, 17 | ref, 18 | watchEffect, 19 | onUnmounted, 20 | readonly, 21 | } from 'vue'; 22 | 23 | import { 24 | createRouter, 25 | createWebHistory, 26 | RouterView, 27 | RouterLink, 28 | } from 'vue-router'; 29 | 30 | import css from 'fake-tag'; 31 | 32 | /** @type {Object.} */ 33 | const styles = {}; 34 | 35 | const context = fstyle.context({ 36 | intern: true, 37 | /** 38 | * @param {object} x 39 | * @param {string} x.class 40 | * @param {string} x.statements 41 | */ 42 | insert: (x) => { 43 | styles[x.class] = x.statements; 44 | }, 45 | }); 46 | 47 | /** 48 | * @param {any} styler 49 | * @returns {any} 50 | */ 51 | function use_fstyle(styler) { 52 | const classes = ref([]); 53 | /** @type {any} */ 54 | let handle; 55 | watchEffect(function watcher() { 56 | const requireable = styler(); 57 | const new_handle = context.require(requireable); 58 | if (handle !== undefined) { 59 | handle.release(); 60 | } 61 | classes.value = new_handle.classes; 62 | handle = new_handle; 63 | }); 64 | onUnmounted(function () { 65 | handle.release(); 66 | }); 67 | return readonly(classes); 68 | } 69 | 70 | const demo_styler = fstyle.css(function demo() { 71 | return css` 72 | .[] { 73 | color: pink; 74 | font-size: 200%; 75 | } 76 | 77 | @media (max-width: 600px) {.[] { 78 | color: red; 79 | }}`; 80 | }); 81 | 82 | const body_styler = fstyle.css(function demo() { 83 | return css` 84 | .[] { 85 | background: black; 86 | color: white; 87 | }`; 88 | }); 89 | 90 | const nav_styler = fstyle.css(function nav() { 91 | return css` 92 | .[] { 93 | margin: 1em 0; 94 | } 95 | 96 | .[] a { 97 | margin-right: 0.5em; 98 | }`; 99 | }); 100 | 101 | export const routes = [ 102 | { 103 | path: '/', 104 | component: h('h1', 'HOME'), 105 | }, 106 | { 107 | path: '/about', 108 | component: h('h1', 'ABOUT'), 109 | }, 110 | ]; 111 | 112 | /** 113 | * @param {string} importmap - JSON stringified importmap 114 | */ 115 | const ImportMapScript = (importmap) => { 116 | return h('script', { 117 | name: 'importmap', 118 | type: 'importmap', 119 | innerHTML: importmap, 120 | }); 121 | }; 122 | 123 | const HydrateScript = () => { 124 | return h('script', { 125 | name: 'script', 126 | type: 'module', 127 | src: `/app.js`, 128 | }); 129 | }; 130 | 131 | const app = { 132 | setup() { 133 | const importmap = inject('importmap'); 134 | 135 | const count = shallowRef(0); 136 | const body_classes = use_fstyle(() => body_styler()); 137 | const demo_classes = use_fstyle(() => demo_styler()); 138 | const nav_classes = use_fstyle(() => nav_styler()); 139 | 140 | function up() { 141 | count.value++; 142 | } 143 | 144 | function down() { 145 | count.value--; 146 | } 147 | 148 | return function render() { 149 | return h('html', { lang: 'en' }, [ 150 | h('head', [ 151 | h('title', 'VULTRA'), 152 | ImportMapScript(importmap), 153 | HydrateScript(), 154 | h('meta', { 155 | name: 'viewport', 156 | content: 'width=320, initial-scale=1', 157 | }), 158 | h('style', { name: 'fstyle' }, Object.values(styles).join('\n')), 159 | ]), 160 | h('body', { class: body_classes.value }, [ 161 | h('h2', { class: demo_classes.value }, 'count: ' + count.value), 162 | h('button', { onClick: up }, 'Up'), 163 | h('button', { onClick: down }, 'Down'), 164 | h('nav', { class: nav_classes.value }, [ 165 | h(RouterLink, { to: '/' }, () => 'Home'), 166 | h(RouterLink, { to: '/about' }, () => 'About'), 167 | ]), 168 | h(RouterView), 169 | ]), 170 | ]); 171 | }; 172 | }, 173 | }; 174 | 175 | const ultraApp = createSSRApp(app); 176 | 177 | export default ultraApp; 178 | 179 | if (typeof document !== 'undefined') { 180 | const router = createRouter({ 181 | history: createWebHistory(), 182 | routes, 183 | }); 184 | ultraApp.use(router); 185 | ultraApp.provide('importmap', document.scripts.namedItem('importmap')); 186 | // @ts-ignore document 187 | router.isReady().then(() => ultraApp.mount(document)); 188 | } 189 | --------------------------------------------------------------------------------