diff --git a/.changeset/red-drinks-attend.md b/.changeset/red-drinks-attend.md new file mode 100644 index 0000000..421deb0 --- /dev/null +++ b/.changeset/red-drinks-attend.md @@ -0,0 +1,5 @@ +--- +"@leanweb/fullstack": minor +--- + +Add views and props intellisense plugin diff --git a/packages/core/src/runtime/Render.ts b/packages/core/src/runtime/Render.ts index 5fceba0..59de006 100644 --- a/packages/core/src/runtime/Render.ts +++ b/packages/core/src/runtime/Render.ts @@ -15,4 +15,4 @@ export function unsafeRenderToString( return internal.unsafeRenderToString(componentOrOutput, props); } -export {SSRComponentExport, resolveComponent, makeFactory} from './internal/render.js' \ No newline at end of file +export {SSRComponentExport, resolveComponent, makeFactory, Views} from './internal/render.js' \ No newline at end of file diff --git a/packages/core/src/runtime/internal/render.ts b/packages/core/src/runtime/internal/render.ts index e8363b2..eff9c55 100644 --- a/packages/core/src/runtime/internal/render.ts +++ b/packages/core/src/runtime/internal/render.ts @@ -77,17 +77,22 @@ export function resolveComponent( return isLazy(entry) ? entry().then((_) => _.default) : entry.default; } +export interface Views { + // [k:string]: SSRComponentProps +} + export function makeFactory>( f: (name: string) => T ) { - return ( - name: string, - props?: SSRComponentProps + return ( + name: K, + props?: V[K] ): T extends Promise ? Promise : string => { + // @ts-expect-error const output = f(name); // @ts-expect-error return output instanceof Promise - ? output.then((_) => unsafeRenderToString(_, props)) - : unsafeRenderToString(output, props); + ? output.then((_) => unsafeRenderToString(_, props as SSRComponentProps)) + : unsafeRenderToString(output, props as SSRComponentProps); }; } diff --git a/packages/core/src/vite/Plugin.ts b/packages/core/src/vite/Plugin.ts index 71b959a..c6df8e3 100644 --- a/packages/core/src/vite/Plugin.ts +++ b/packages/core/src/vite/Plugin.ts @@ -32,6 +32,8 @@ import * as Island from "./Island.js"; import { devServer } from "./devServer/DevServer.js"; import * as AssetRef from "./devServer/assetRef/AssetRef.js"; import { previewServer } from "./preview/Server.js"; +import {views as pluginViews} from './views.js' +import { dedent } from "ts-dedent"; // import { compressFile } from "./Compress.js"; // interface Manifest { @@ -611,6 +613,7 @@ export default function fullstack(userConfig?: Options) { // We need island preprocessing to run before our inner build during production build pluginIsland, pluginBuild, + pluginViews, pluginPreview, svelte(svelteOptions), ]; diff --git a/packages/core/src/vite/views.ts b/packages/core/src/vite/views.ts new file mode 100644 index 0000000..e474036 --- /dev/null +++ b/packages/core/src/vite/views.ts @@ -0,0 +1,107 @@ +import path from "node:path"; +import fs from "node:fs"; +import type { Plugin } from "vite"; +import { globSync as glob } from "glob"; +import { dedent } from "ts-dedent"; + +const cwd = process.cwd(); + +const outDir = cwd; +const outFile = path.join(outDir, "views.d.ts"); + +const prefix = path.join(cwd, "src/views/"); + +let cache = new Set(); + +const write = (views: string[]) => { + fs.writeFileSync( + outFile, + dedent` + import {ComponentProps} from 'svelte'; + + ${views + .map((file, i) => `import $${i} from "${relative_path(outDir, file)}";`) + .join("\n")} + + declare module "@leanweb/fullstack/runtime/Render" { + interface Views { + ${views + .map((file, i) => { + const file_ = file.replace(prefix, ""); + const { dir, name } = path.parse(file_); + const key = path.join(dir, name); + const value = `ComponentProps<$${i}>`; + return [`"${key}": ${value}`, `"${file_}": ${value}`]; + }) + .flat() + .join(",\n")} + } + } + ` + ); +}; + +export const views: Plugin = { + name: "fullstack:views", + config() { + return { + server: { + watch: { + ignored: [outFile], + }, + }, + }; + }, + buildStart() { + const files = glob("**/*.svelte", { + cwd, + absolute: true, + root: "/src/views", + }); + + write(files); + + cache = new Set(files); + }, + configureServer(server) { + return () => { + server.watcher.on("all", (e, file) => { + if (!file.endsWith(".svelte")) return; + + if (e == "unlink") { + cache.delete(file); + } + + if (e == "add") { + cache.add(file); + } + + write([...cache]); + }); + }; + }, +}; + +function posixify(str: string) { + return str.replace(/\\/g, "/"); +} + +/** + * Like `path.join`, but posixified and with a leading `./` if necessary + */ +function join_relative(...str: string[]) { + let result = posixify(path.join(...str)); + if (!result.startsWith(".")) { + result = `./${result}`; + } + return result; +} + +/** + * Like `path.relative`, but always posixified and with a leading `./` if necessary. + * Useful for JS imports so the path can safely reside inside of `node_modules`. + * Otherwise paths could be falsely interpreted as package paths. + */ +function relative_path(from: string, to: string) { + return join_relative(path.relative(from, to)); +} diff --git a/playground/basic/.gitignore b/playground/basic/.gitignore index 9039326..9a763c0 100644 --- a/playground/basic/.gitignore +++ b/playground/basic/.gitignore @@ -2,4 +2,5 @@ node_modules .env public/assets -env.d.ts \ No newline at end of file +env.d.ts +views.d.ts \ No newline at end of file diff --git a/playground/basic/fullstack-env.d.ts b/playground/basic/fullstack-env.d.ts index a505337..0a4e5e3 100644 --- a/playground/basic/fullstack-env.d.ts +++ b/playground/basic/fullstack-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// /// /// /// diff --git a/playground/basic/package.json b/playground/basic/package.json index f3351b6..b0db80d 100644 --- a/playground/basic/package.json +++ b/playground/basic/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "tsc && vite build", "preview": "vite preview" }, "keywords": [], diff --git a/playground/basic/src/entry.ts b/playground/basic/src/entry.ts index cfbc7af..9aa6444 100644 --- a/playground/basic/src/entry.ts +++ b/playground/basic/src/entry.ts @@ -11,6 +11,24 @@ import Async from "./views/async.svelte?ssr"; import Home from "./views/home.svelte?ssr"; // import About from "./views/about.svx?ssr"; +import { + makeFactory, + resolveComponent, + type SSRComponentExport, +} from "@leanweb/fullstack/runtime/Render"; + +const components = import.meta.glob( + "./views/**/*.svelte", + { query: { ssr: true }, eager: true } +); + +export const render = makeFactory((name) => { + return resolveComponent(`./views/${name}.svelte`, components); +}); + +console.log(render('home', {})); + + type SessionData = { userId: string; }; diff --git a/playground/basic/src/views/home.svelte b/playground/basic/src/views/home.svelte index e2f90d4..908d545 100644 --- a/playground/basic/src/views/home.svelte +++ b/playground/basic/src/views/home.svelte @@ -1,5 +1,6 @@ diff --git a/playground/basic/src/views/inner.svelte b/playground/basic/src/views/inner.svelte index 93f2f29..ffce95e 100644 --- a/playground/basic/src/views/inner.svelte +++ b/playground/basic/src/views/inner.svelte @@ -10,6 +10,6 @@ Document -