Skip to content

Commit

Permalink
feat: Svelte 5 SSR support - experimental
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobrosenberg committed Dec 29, 2024
1 parent eaeafca commit 4c8d98c
Show file tree
Hide file tree
Showing 41 changed files with 244 additions and 116 deletions.
4 changes: 3 additions & 1 deletion examples/starter-basic/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export default defineConfig({
clearScreen: false,
resolve: { alias: { '@': resolve('src') } },
plugins: [
routify({ render: { ssr: { enable: production }, ssg: { enable: production } } }),
routify({
render: { ssr: production, ssg: production },
}),
svelte({
compilerOptions: {
dev: !production,
Expand Down
4 changes: 2 additions & 2 deletions examples/starter-svelte-5/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mount } from 'svelte'
import { hydrate } from 'svelte'
import App from './App.svelte'

mount(App, {
hydrate(App, {
target: document.body,
})
2 changes: 2 additions & 0 deletions examples/starter-svelte-5/src/routes/hello-world.svelte
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
<h1>Hello world!</h1>

<a href="/">Back</a>
7 changes: 6 additions & 1 deletion examples/starter-svelte-5/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ export default defineConfig({
clearScreen: false,

plugins: [
routify({}),
routify({
render: {
ssg: !!production,
ssr: !!production,
},
}),
svelte({
compilerOptions: {
dev: !production,
Expand Down
9 changes: 8 additions & 1 deletion lib/buildtime/RoutifyBuildtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { filemapperPlugin } from './plugins/filemapper/index.js'
import { metaFromFilePlugin } from './plugins/metaFromFile/index.js'
import { namedModulePlugin } from './plugins/namedModule/index.js'
import { watcherPlugin } from './plugins/watcher/index.js'
import { hashObj, resolvePlugins, split, writeFileIfDifferent } from './utils.js'
import {
getSvelteVersion,
hashObj,
resolvePlugins,
split,
writeFileIfDifferent,
} from './utils.js'
import { configent } from 'configent'
import { metaSplitPlugin } from './plugins/metaSplit/index.js'
import { metaPersistPlugin } from './plugins/metaPersist/index.js'
Expand Down Expand Up @@ -52,6 +58,7 @@ const getDefaults = () => ({
omitDirFromPathPlugin,
themesPlugin,
],
svelteApi: getSvelteVersion().startsWith('5') ? 5 : 4,
watch: false,
})

Expand Down
3 changes: 2 additions & 1 deletion lib/buildtime/plugins/exporter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ export const exportSitemap = async rootNode => {
* @param {RelativePaths} relativePaths
*/
export const exportRender = async (instance, relativePaths) => {
const svelteVersion = instance.options.svelteApi
const body = `
import * as module from '${relativePaths.rootComponent}'
import { renderModule } from '@roxi/routify/tools'
import { renderModule } from '@roxi/routify/tools/ssr${svelteVersion}.js'
import { map } from './route-map.js'
export const render = url => renderModule(module, { url, routesMap: map })`
Expand Down
20 changes: 20 additions & 0 deletions lib/buildtime/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import fse from 'fs-extra'
import { fileURLToPath } from 'url'
import { dirname, resolve, relative } from 'path'
import { mkdirSync, existsSync, readFileSync } from 'fs'
import { createRequire } from 'module'

const require = createRequire(import.meta.url) // Create a CommonJS-like `require`

export const relativeUnix = (path, path2) => relative(path, path2).replace(/\\/g, '/')

Expand Down Expand Up @@ -143,3 +146,20 @@ export const hashObj = val => {
*/
export const deepSet = (t, p, ...v) =>
v.length > 1 ? (t[p] = t[p] || {}) && deepSet(t[p], ...v) : (t[p] = v[0])

export function getSvelteVersion() {
try {
// Resolve the project root dynamically
const projectRoot = process.cwd()

// Resolve the `svelte/package.json` relative to the project root
const sveltePath = require.resolve('svelte/package.json', {
paths: [projectRoot],
})
const packageJson = JSON.parse(fse.readFileSync(sveltePath, 'utf-8'))
return packageJson.version
} catch (e) {
console.error('Error resolving Svelte version:', e)
return null
}
}
1 change: 0 additions & 1 deletion lib/extra/tools/index.js

This file was deleted.

2 changes: 1 addition & 1 deletion lib/extra/tools/ssr.js → lib/extra/tools/ssr4.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const polyfillFetch = async () => {
}

/**
* Returns a statically rendered Routify app
* Returns a statically rendered Routify app - uses Svelte 3 - 4
* @param {(SvelteComponentDev|{default: SvelteComponentDev}) & {load: (url:string)=>Promise<any>}} module App.svelte
* @param {string | string[] | import('../../runtime').PreloadOptions=} urlOrOptions one or multiple urls separated by ";<routerName>="
* @returns {Promise<{
Expand Down
47 changes: 47 additions & 0 deletions lib/extra/tools/ssr5.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render } from 'svelte/server'
import { preloadUrl } from '../../runtime/index.js'

const polyfillFetch = async () => {
const fetch = await import('node-fetch')

// @ts-ignore
globalThis.fetch = fetch.default
// @ts-ignore
globalThis.Headers = fetch.Headers
// @ts-ignore
globalThis.Request = fetch.Request
// @ts-ignore
globalThis.Response = fetch.Response
}

/**
* Returns a statically rendered Routify app - uses Svelte 5
* @param {(SvelteComponentDev|{default: SvelteComponentDev}) & {load: (url:string)=>Promise<any>}} module App.svelte
* @param {string | string[] | import('../../runtime').PreloadOptions=} urlOrOptions one or multiple urls separated by ";<routerName>="
* @returns {Promise<{
* html: string,
* status: number,
* css: { code: string, map: string },
* head: '',
* error: string,
* maxage: number,
* props: Object.<string, string>,
* redirect: string
* }>}
*/
export const renderModule = async (module, urlOrOptions) => {
await polyfillFetch()
// const render = module.default?.render || module['render']

const url = urlOrOptions.url || urlOrOptions
const load = module.load ? await module.load(url) : {}

const preloadUrlLoad = await preloadUrl(urlOrOptions)

return {
status: 200,
...(await render(module.default)),
...load,
...preloadUrlLoad?.flat()[0],
}
}
2 changes: 1 addition & 1 deletion lib/extra/vite-plugin/assets/cjsRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const render = async (path = '/') => {
const html = template
.replace('<!--ssr:html-->', output.html)
.replace('<!--ssr:head-->', output.head)
.replace('<!--ssr:css-->', '<style>' + output.css.code + '</style>')
.replace('<!--ssr:css-->', '<style>' + output.css?.code || '' + '</style>')
return html
}

Expand Down
2 changes: 1 addition & 1 deletion lib/extra/vite-plugin/assets/esmRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const render = async (path = '/') => {
const html = template
.replace('<!--ssr:html-->', output.html)
.replace('<!--ssr:head-->', output.head)
.replace('<!--ssr:css-->', '<style>' + output.css.code + '</style>')
.replace('<!--ssr:css-->', '<style>' + output.css?.code || '' + '</style>')
return { ...output, html }
}

Expand Down
5 changes: 4 additions & 1 deletion lib/extra/vite-plugin/devServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export const devServer = (server, options) => () =>
const html = template
.replace('<!--ssr:html-->', output.html)
.replace('<!--ssr:head-->', output.head)
.replace('<!--ssr:css-->', '<style>' + output.css.code + '</style>')
.replace(
'<!--ssr:css-->',
'<style>' + output.css?.code || '' + '</style>',
)

res.setHeader('Content-Type', 'text/html')

Expand Down
7 changes: 5 additions & 2 deletions lib/extra/vite-plugin/vite-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function RoutifyPlugin(input = {}) {
isSsr = !!cfg.build?.ssr
if (isSsr) {
// cfg.ssr.noExternal = true
cfg.ssr.target = 'webworker'
// cfg.ssr.target = 'webworker' // disabled for Svelte 5
}

options.routifyDir ||= './.routify'
Expand All @@ -54,7 +54,10 @@ export default function RoutifyPlugin(input = {}) {
},
},
build: {
ssr: cfg.build?.ssr === true ? `${options.routifyDir}/render.js` : cfg.build?.ssr,
ssr:
cfg.build?.ssr === true
? `${options.routifyDir}/render.js`
: cfg.build?.ssr,
outDir: `${options.outDir}/${cfg.build?.ssr ? 'server' : 'client'}`,
},
envPrefix: ['VITE_', 'ROUTIFY_SSR_ENABLE'],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"types": "./typings/lib/extra/tools/ssr.d.ts",
"default": "./lib/extra/tools/index.js"
},
"./tools/*": "./lib/extra/tools/*",
"./components/*": "./lib/runtime/components/*"
},
"scripts": {
Expand Down Expand Up @@ -98,7 +99,7 @@
"rollup": "^3.3.0",
"rollup-plugin-svelte": "^7.1.0",
"semantic-release": "^17.4.7",
"spank": "^2.0.0",
"spank": "^2.1.0",
"spassr": "^2.6.0",
"svelte-esm-loader": "^0.0.3",
"tree-kill": "^1.2.2",
Expand Down
1 change: 1 addition & 0 deletions types/typedef.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
* @prop {string[]} filemapper.fallbackFiles defaults to ['_reset.svelte']
* @prop {(string|RegExp)[]} extensions defaults to ['.svelte', '.html', '.md', '.svx'],
* @prop {string[]|Object[]} plugins
* @prop {4|5} svelteApi defaults to 4
* @prop {boolean} watch rebuild Routify routes on changes
* @prop {ThemeConfig} themes
*/
Expand Down
1 change: 0 additions & 1 deletion typings/lib/buildtime/plugins/filemapper/lib/File.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// <reference types="node" />
export class File {
constructor(path: any);
path: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export function createRoot404(rootNode: import("../../../../../common/RNode.js").RNode<any>, routifyDir: any): void;
export function createRoot404(rootNode: RNode, routifyDir: any): void;
6 changes: 3 additions & 3 deletions typings/lib/buildtime/plugins/themes/themes.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export function themes({ instance }: {
instance: any;
}): Promise<void>;
export function createThemedRootNodes(instance: RoutifyBuildtime, config: import('./utils.js').ThemeConfig): void;
export function createThemedRootNodes(instance: RoutifyBuildtime, config: import("./utils.js").ThemeConfig): void;
export function createThemedRootNode(instance: RoutifyBuildtime, name: any, themePreferenceGroups: (string[] | string)[], rootNodeName: any): void;
export function nodeMatchesThemes(node: any, themes: any): any;
export function copyNodeToTheme(rootNode: any): (node: any) => void;
export function copyNode(node: import("../../../common/RNode.js").RNode<any>, rootNode: any, location?: string): void;
export function tagNodeThemes(node: import("../../../common/RNode.js").RNode<any>): void;
export function copyNode(node: RNode, rootNode: any, location?: string): void;
export function tagNodeThemes(node: RNode): void;
2 changes: 1 addition & 1 deletion typings/lib/buildtime/plugins/themes/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type ThemeUserPresetObject = {
*/
rootNodes?: string[];
};
export type ThemeUserPreset = (string | string[])[] | ThemeUserPresetObject;
export type ThemeUserPreset = ThemeUserHierarchy | ThemeUserPresetObject;
export type ThemePreset = {
/**
* - The normalized hierarchy for themes.
Expand Down
1 change: 1 addition & 0 deletions typings/lib/buildtime/utils.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export function getSvelteVersion(): any;
export function relativeUnix(path: any, path2: any): string;
export function createDirname(meta: any): string;
export function stringifyWithEscape(obj: any): string;
Expand Down
8 changes: 4 additions & 4 deletions typings/lib/common/RNode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class RNode<InstanceType extends import("./Routify").Routify<any>> {
*/
constructor(name: string, module: ReservedCmpProps | string, instance: InstanceType);
/** @type {InstanceType['NodeType']} */
parent: InstanceType['NodeType'];
parent: InstanceType["NodeType"];
/** @type {Object.<string, any>} */
meta: {
[x: string]: any;
Expand All @@ -23,7 +23,7 @@ export class RNode<InstanceType extends import("./Routify").Routify<any>> {
storage: Map<any, any>;
};
/** @param {InstanceType['NodeConstructor']['prototype']} child */
appendChild(child: InstanceType['NodeConstructor']['prototype']): void;
appendChild(child: InstanceType["NodeConstructor"]["prototype"]): void;
/**
* Creates a new child node
* Same as `node.appendChild(instance.createNode('my-node'))`
Expand Down Expand Up @@ -81,9 +81,9 @@ export class RNode<InstanceType extends import("./Routify").Routify<any>> {
* Example: /home -> /home/main -> /home/main/index
* @param {'children'|'navigableChildren'} childType
*/
getDefaults(childType?: 'children' | 'navigableChildren'): any[];
getDefaults(childType?: "children" | "navigableChildren"): any[];
/** @returns {InstanceType['NodeConstructor']['prototype']} */
toJSON(): InstanceType['NodeConstructor']['prototype'];
toJSON(): InstanceType["NodeConstructor"]["prototype"];
/** @returns {string} */
get path(): string;
}
10 changes: 4 additions & 6 deletions typings/lib/common/Routify.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@ export class Routify<NodeConstructor extends typeof RNode<any>> {
/** @type {typeof RNode<any>} */
NodeConstructor: typeof RNode<any>;
/** @type {NodeConstructor['prototype']} */
NodeType: NodeConstructor['prototype'];
NodeType: NodeConstructor["prototype"];
/** @type {NodeConstructor['prototype'][]} */
nodeIndex: NodeConstructor['prototype'][];
nodeIndex: NodeConstructor["prototype"][];
/** @type {Object<string, NodeConstructor['prototype']>} */
rootNodes: {
[x: string]: NodeConstructor['prototype'];
[x: string]: NodeConstructor["prototype"];
};
utils: UrlParamUtils;
/**
* @param {string=} name relative path for the node
* @param {any|string=} module svelte component
* @returns {this['NodeType']}
*/
createNode(name?: string | undefined, module?: (any | string) | undefined): this['NodeType'];
createNode(name?: string | undefined, module?: (any | string) | undefined): this["NodeType"];
}
import { RNode } from './RNode.js';
declare var NodeConstructor: typeof RNode<any>;
import { UrlParamUtils } from '../runtime/Instance/UrlParamUtils.js';
export {};
2 changes: 1 addition & 1 deletion typings/lib/common/helpers.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function assignNode(target: RNodeRuntime, ...sources: RNodeRuntime[]): RNodeRuntime;
export function findNearestParent(node: RNodeRuntime, callback: (arg0: RNodeRuntime['parent']) => any): RNodeRuntime['parent'] | undefined;
export function findNearestParent(node: RNodeRuntime, callback: (arg0: RNodeRuntime["parent"]) => any): RNodeRuntime["parent"] | undefined;
export function getDistance(parentNode: RNodeRuntime, childNode: RNodeRuntime): number | undefined;
4 changes: 2 additions & 2 deletions typings/lib/common/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export function deepAssign<T extends {}, T2 extends {}>(receiver: T, ...sources:
export function sortPlugins(plugins: RoutifyBuildtimePlugin[]): RoutifyBuildtimePlugin[];
export function isObjectOrArray(v: any): boolean;
export function normalizePlugins(plugins: RoutifyBuildtimePlugin[]): RoutifyBuildtimePlugin[];
export function mockRoutes<T extends import("../buildtime/RoutifyBuildtime").RoutifyBuildtime | import("../runtime/Instance/RoutifyRuntime").RoutifyRuntime>(instance: T, routes: {
export function mockRoutes<T extends RoutifyBuildtime | RoutifyRuntime>(instance: T, routes: {
[x: string]: any;
}): import("../buildtime/RNodeBuildtime").RNodeBuildtime | import("../runtime/Instance/RNodeRuntime").RNodeRuntime;
export function addPropsToComp<Component extends typeof import("svelte/internal").SvelteComponentDev>(Comp: Component, props: {
[x: string]: any;
}): Component;
export function next<T extends import("svelte/store").Readable<V>, V>(store: T, wanted?: V | ((wanted: V) => boolean), strict?: boolean | undefined): Promise<V>;
export function next<T extends import("svelte/store").Readable<V>, V>(store: T, wanted?: (((wanted: V) => boolean) | V) | undefined, strict?: boolean | undefined): Promise<V>;
export function throttle(fn: any, hash: any): Promise<void>;
export function lazySet(store: any, value: any): any;
export function jsonClone(obj: any): any;
Expand Down
2 changes: 1 addition & 1 deletion typings/lib/extra/express-plugin/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export type Options = {
/**
* Vite config
*/
viteServerConfig?: import('vite').InlineConfig;
viteServerConfig?: import("vite").InlineConfig;
};
20 changes: 20 additions & 0 deletions typings/lib/extra/tools/ssr4.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function dynamicImport(specifier: any): Promise<any>;
export function renderModule(module: (SvelteComponentDev | {
default: SvelteComponentDev;
}) & {
load: (url: string) => Promise<any>;
}, urlOrOptions?: (string | string[] | import("../../runtime").PreloadOptions) | undefined): Promise<{
html: string;
status: number;
css: {
code: string;
map: string;
};
head: "";
error: string;
maxage: number;
props: {
[x: string]: string;
};
redirect: string;
}>;
19 changes: 19 additions & 0 deletions typings/lib/extra/tools/ssr5.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function renderModule(module: (SvelteComponentDev | {
default: SvelteComponentDev;
}) & {
load: (url: string) => Promise<any>;
}, urlOrOptions?: (string | string[] | import("../../runtime").PreloadOptions) | undefined): Promise<{
html: string;
status: number;
css: {
code: string;
map: string;
};
head: "";
error: string;
maxage: number;
props: {
[x: string]: string;
};
redirect: string;
}>;
Loading

0 comments on commit 4c8d98c

Please sign in to comment.