mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 13:52:46 -07:00
feat: add support for client only lazy loading with dynamic (#331)
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export const ROUTES_FOLDER = './src/routes/'
|
export const ROUTES_FOLDER = './src/routes/'
|
||||||
export const LAYOUT_PATH_ID = '__layout'
|
export const LAYOUT_PATH_ID = '__layout'
|
||||||
export const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts'
|
export const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts'
|
||||||
|
export const DYNAMIC_FN = '__tuono__internal__lazyLoadRoute'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ROUTES_FOLDER,
|
ROUTES_FOLDER,
|
||||||
LAYOUT_PATH_ID,
|
LAYOUT_PATH_ID,
|
||||||
GENERATED_ROUTE_TREE,
|
GENERATED_ROUTE_TREE,
|
||||||
|
DYNAMIC_FN,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
|
|
||||||
import { sortRouteNodes } from './sort-route-nodes'
|
import { sortRouteNodes } from './sort-route-nodes'
|
||||||
@@ -168,7 +169,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
|||||||
const extension = node.filePath.endsWith('mdx') ? '.mdx' : ''
|
const extension = node.filePath.endsWith('mdx') ? '.mdx' : ''
|
||||||
return `const ${
|
return `const ${
|
||||||
node.variableName as string
|
node.variableName as string
|
||||||
}Import = dynamic(() => import('./${replaceBackslash(
|
}Import = ${DYNAMIC_FN}(() => import('./${replaceBackslash(
|
||||||
removeExt(
|
removeExt(
|
||||||
path.relative(
|
path.relative(
|
||||||
path.dirname(config.generatedRouteTree),
|
path.dirname(config.generatedRouteTree),
|
||||||
@@ -214,7 +215,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
|||||||
|
|
||||||
const routeImports = [
|
const routeImports = [
|
||||||
'// This file is auto-generated by Tuono',
|
'// This file is auto-generated by Tuono',
|
||||||
"import { createRoute, dynamic } from 'tuono'",
|
`import { createRoute, ${DYNAMIC_FN} } from 'tuono'`,
|
||||||
[
|
[
|
||||||
`import RootLayoutImport from './${replaceBackslash(
|
`import RootLayoutImport from './${replaceBackslash(
|
||||||
path.relative(
|
path.relative(
|
||||||
|
|||||||
+5
-3
@@ -1,11 +1,13 @@
|
|||||||
// This file is auto-generated by Tuono
|
// This file is auto-generated by Tuono
|
||||||
|
|
||||||
import { createRoute, dynamic } from 'tuono'
|
import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono'
|
||||||
|
|
||||||
import RootLayoutImport from './routes/__layout'
|
import RootLayoutImport from './routes/__layout'
|
||||||
|
|
||||||
const IndexImport = dynamic(() => import('./routes/index'))
|
const IndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
const PostscatchallImport = dynamic(
|
() => import('./routes/index'),
|
||||||
|
)
|
||||||
|
const PostscatchallImport = __tuono__internal__lazyLoadRoute(
|
||||||
() => import('./routes/posts/[...catch_all]'),
|
() => import('./routes/posts/[...catch_all]'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
// This file is auto-generated by Tuono
|
// This file is auto-generated by Tuono
|
||||||
|
|
||||||
import { createRoute, dynamic } from 'tuono'
|
import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono'
|
||||||
|
|
||||||
import RootLayoutImport from './routes/__layout'
|
import RootLayoutImport from './routes/__layout'
|
||||||
|
|
||||||
const AboutImport = dynamic(() => import('./routes/about.mdx'))
|
const AboutImport = __tuono__internal__lazyLoadRoute(
|
||||||
const IndexImport = dynamic(() => import('./routes/index'))
|
() => import('./routes/about.mdx'),
|
||||||
|
)
|
||||||
|
const IndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/index'),
|
||||||
|
)
|
||||||
|
|
||||||
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
||||||
|
|
||||||
|
|||||||
+19
-7
@@ -1,15 +1,27 @@
|
|||||||
// This file is auto-generated by Tuono
|
// This file is auto-generated by Tuono
|
||||||
|
|
||||||
import { createRoute, dynamic } from 'tuono'
|
import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono'
|
||||||
|
|
||||||
import RootLayoutImport from './routes/__layout'
|
import RootLayoutImport from './routes/__layout'
|
||||||
|
|
||||||
const PostslayoutImport = dynamic(() => import('./routes/posts/__layout'))
|
const PostslayoutImport = __tuono__internal__lazyLoadRoute(
|
||||||
const AboutImport = dynamic(() => import('./routes/about'))
|
() => import('./routes/posts/__layout'),
|
||||||
const IndexImport = dynamic(() => import('./routes/index'))
|
)
|
||||||
const PostspostImport = dynamic(() => import('./routes/posts/[post]'))
|
const AboutImport = __tuono__internal__lazyLoadRoute(
|
||||||
const PostsIndexImport = dynamic(() => import('./routes/posts/index'))
|
() => import('./routes/about'),
|
||||||
const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post'))
|
)
|
||||||
|
const IndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/index'),
|
||||||
|
)
|
||||||
|
const PostspostImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/posts/[post]'),
|
||||||
|
)
|
||||||
|
const PostsIndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/posts/index'),
|
||||||
|
)
|
||||||
|
const PostsMyPostImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/posts/my-post'),
|
||||||
|
)
|
||||||
|
|
||||||
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -1,12 +1,18 @@
|
|||||||
// This file is auto-generated by Tuono
|
// This file is auto-generated by Tuono
|
||||||
|
|
||||||
import { createRoute, dynamic } from 'tuono'
|
import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono'
|
||||||
|
|
||||||
import RootLayoutImport from './routes/__layout'
|
import RootLayoutImport from './routes/__layout'
|
||||||
|
|
||||||
const AboutImport = dynamic(() => import('./routes/about'))
|
const AboutImport = __tuono__internal__lazyLoadRoute(
|
||||||
const IndexImport = dynamic(() => import('./routes/index'))
|
() => import('./routes/about'),
|
||||||
const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post'))
|
)
|
||||||
|
const IndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/index'),
|
||||||
|
)
|
||||||
|
const PostsMyPostImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/posts/my-post'),
|
||||||
|
)
|
||||||
|
|
||||||
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -1,11 +1,15 @@
|
|||||||
// This file is auto-generated by Tuono
|
// This file is auto-generated by Tuono
|
||||||
|
|
||||||
import { createRoute, dynamic } from 'tuono'
|
import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono'
|
||||||
|
|
||||||
import RootLayoutImport from './routes/__layout'
|
import RootLayoutImport from './routes/__layout'
|
||||||
|
|
||||||
const AboutImport = dynamic(() => import('./routes/about'))
|
const AboutImport = __tuono__internal__lazyLoadRoute(
|
||||||
const IndexImport = dynamic(() => import('./routes/index'))
|
() => import('./routes/about'),
|
||||||
|
)
|
||||||
|
const IndexImport = __tuono__internal__lazyLoadRoute(
|
||||||
|
() => import('./routes/index'),
|
||||||
|
)
|
||||||
|
|
||||||
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport })
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# tuono-lazy-fn-vite-plugin
|
|
||||||
|
|
||||||
This is a vite plugin for [tuono](https://github.com/tuono-labs/tuono).
|
|
||||||
|
|
||||||
This package specifically handles the transpiling of the `dynamic` function
|
|
||||||
allowing custom componenents to be lazy loaded but also server side rendered.
|
|
||||||
|
|
||||||
Check [tuono](https://github.com/tuono-labs/tuono) for more.
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tuono-lazy-fn-vite-plugin",
|
|
||||||
"version": "0.17.0",
|
|
||||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
|
||||||
"homepage": "https://tuono.dev",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite build --watch",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
|
|
||||||
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
|
|
||||||
"types": "tsc --noEmit",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"test": "vitest run"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tuono-labs/tuono.git",
|
|
||||||
"directory": "packages/tuono-lazy-fn-vite-plugin"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Valerio Ageno",
|
|
||||||
"license": "MIT",
|
|
||||||
"type": "module",
|
|
||||||
"types": "dist/esm/index.d.ts",
|
|
||||||
"main": "dist/cjs/index.cjs",
|
|
||||||
"module": "dist/esm/index.js",
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"src",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": {
|
|
||||||
"types": "./dist/esm/index.d.ts",
|
|
||||||
"default": "./dist/esm/index.js"
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"types": "./dist/cjs/index.d.cts",
|
|
||||||
"default": "./dist/cjs/index.cjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"./package.json": "./package.json"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.24.4",
|
|
||||||
"@babel/types": "^7.24.0",
|
|
||||||
"vite": "^5.2.11"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tanstack/config": "0.7.13",
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"prettier": "^3.2.4",
|
|
||||||
"vitest": "2.1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const TUONO_DYNAMIC_FN_ID = 'dynamic'
|
|
||||||
export const TUONO_LAZY_FN_ID = '__tuono__internal__lazyLoadComponent'
|
|
||||||
export const TUONO_MAIN_PACKAGE = 'tuono'
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { transformSync } from '@babel/core'
|
|
||||||
import type { PluginItem as BabelPluginItem } from '@babel/core'
|
|
||||||
import * as BabelTypes from '@babel/types'
|
|
||||||
import { createFilter } from 'vite'
|
|
||||||
import type { Plugin as VitePlugin, Rollup, FilterPattern } from 'vite'
|
|
||||||
|
|
||||||
import {
|
|
||||||
TUONO_MAIN_PACKAGE,
|
|
||||||
TUONO_DYNAMIC_FN_ID,
|
|
||||||
TUONO_LAZY_FN_ID,
|
|
||||||
} from './constants'
|
|
||||||
import { isTuonoDynamicFnImported } from './utils'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [SERVER build]
|
|
||||||
* This plugin just removes the `dynamic` imported function from any tuono import
|
|
||||||
*/
|
|
||||||
const RemoveTuonoLazyImport: BabelPluginItem = {
|
|
||||||
name: 'remove-tuono-lazy-import-plugin',
|
|
||||||
visitor: {
|
|
||||||
ImportDeclaration: (path) => {
|
|
||||||
const importNode = path.node
|
|
||||||
if (importNode.source.value !== TUONO_MAIN_PACKAGE) return
|
|
||||||
|
|
||||||
path.traverse({
|
|
||||||
ImportSpecifier: (importSpecifierPath) => {
|
|
||||||
if (isTuonoDynamicFnImported(importSpecifierPath)) {
|
|
||||||
importSpecifierPath.remove()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// If there are no specifiers left after traverse
|
|
||||||
// remove the import to avoid unwanted side effects
|
|
||||||
if (importNode.specifiers.length === 0) {
|
|
||||||
path.remove()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [CLIENT build]
|
|
||||||
* This plugin replace the `dynamic` function with the `__tuono__internal__lazyLoadComponent` one
|
|
||||||
*/
|
|
||||||
const ReplaceTuonoLazyImport: BabelPluginItem = {
|
|
||||||
name: 'replace-tuono-lazy-import-plugin',
|
|
||||||
visitor: {
|
|
||||||
ImportSpecifier: (path) => {
|
|
||||||
if (
|
|
||||||
BabelTypes.isIdentifier(path.node.imported) &&
|
|
||||||
isTuonoDynamicFnImported(path)
|
|
||||||
) {
|
|
||||||
path.node.imported.name = TUONO_LAZY_FN_ID
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const turnLazyIntoStatic = {
|
|
||||||
VariableDeclaration: (
|
|
||||||
path: babel.NodePath<BabelTypes.VariableDeclaration>,
|
|
||||||
): void => {
|
|
||||||
path.node.declarations.forEach((variableDeclarationNode) => {
|
|
||||||
const init = variableDeclarationNode.init
|
|
||||||
|
|
||||||
if (
|
|
||||||
BabelTypes.isCallExpression(init) &&
|
|
||||||
// ensures that the method call is `TUONO_DYNAMIC_FN_ID`
|
|
||||||
BabelTypes.isIdentifier(init.callee, { name: TUONO_DYNAMIC_FN_ID }) &&
|
|
||||||
// import name must be an identifier
|
|
||||||
BabelTypes.isIdentifier(variableDeclarationNode.id) &&
|
|
||||||
// check that the first function parameter is an arrow function
|
|
||||||
BabelTypes.isArrowFunctionExpression(init.arguments[0])
|
|
||||||
) {
|
|
||||||
const cmpImportFn = init.arguments[0]
|
|
||||||
|
|
||||||
// ensures that the first parameter is a call expression (may be a block statement)
|
|
||||||
if (!BabelTypes.isCallExpression(cmpImportFn.body)) return
|
|
||||||
// ensures that the first parameter is a string literal (the import path)
|
|
||||||
if (!BabelTypes.isStringLiteral(cmpImportFn.body.arguments[0])) return
|
|
||||||
|
|
||||||
const importName = variableDeclarationNode.id.name
|
|
||||||
const importPath = cmpImportFn.body.arguments[0].value
|
|
||||||
|
|
||||||
if (importName && importPath) {
|
|
||||||
const importDeclaration = BabelTypes.importDeclaration(
|
|
||||||
[
|
|
||||||
BabelTypes.importDefaultSpecifier(
|
|
||||||
BabelTypes.identifier(importName),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
BabelTypes.stringLiteral(importPath),
|
|
||||||
)
|
|
||||||
|
|
||||||
path.replaceWith(importDeclaration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [SERVER build]
|
|
||||||
* This plugin statically imports the lazy loaded components
|
|
||||||
*/
|
|
||||||
const TurnLazyIntoStaticImport: BabelPluginItem = {
|
|
||||||
name: 'turn-lazy-into-static-import-plugin',
|
|
||||||
visitor: {
|
|
||||||
Program: (path) => {
|
|
||||||
path.traverse({
|
|
||||||
ImportSpecifier: (subPath) => {
|
|
||||||
if (isTuonoDynamicFnImported(subPath)) {
|
|
||||||
path.traverse(turnLazyIntoStatic)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TuonoLazyFnPluginOptions {
|
|
||||||
include: FilterPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TuonoLazyFnPlugin(
|
|
||||||
options: TuonoLazyFnPluginOptions,
|
|
||||||
): VitePlugin {
|
|
||||||
const { include } = options
|
|
||||||
|
|
||||||
const filter = createFilter(include)
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'vite-plugin-tuono-lazy-loading',
|
|
||||||
enforce: 'pre',
|
|
||||||
transform(code, id, opts): Rollup.TransformResult {
|
|
||||||
if (!filter(id)) return
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo we should exclude non tsx files from this transformation
|
|
||||||
* this might benefit build time avoiding running `includes` on non-tsx files.
|
|
||||||
* This can be executed using `_id` parameter
|
|
||||||
* which is the filepath that is being processed
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
code.includes(TUONO_DYNAMIC_FN_ID) &&
|
|
||||||
code.includes(TUONO_MAIN_PACKAGE)
|
|
||||||
) {
|
|
||||||
const plugins: Array<BabelPluginItem> = [
|
|
||||||
['@babel/plugin-syntax-jsx', {}],
|
|
||||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
|
||||||
]
|
|
||||||
|
|
||||||
if (opts?.ssr) {
|
|
||||||
plugins.push(RemoveTuonoLazyImport, TurnLazyIntoStaticImport)
|
|
||||||
} else {
|
|
||||||
plugins.push(ReplaceTuonoLazyImport)
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = transformSync(code, { plugins })
|
|
||||||
|
|
||||||
return res?.code
|
|
||||||
}
|
|
||||||
|
|
||||||
return code
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import {
|
|
||||||
isIdentifier,
|
|
||||||
isImportDeclaration,
|
|
||||||
isImportSpecifier,
|
|
||||||
} from '@babel/types'
|
|
||||||
|
|
||||||
import { TUONO_MAIN_PACKAGE, TUONO_DYNAMIC_FN_ID } from './constants'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By a given AST Node path returns true if the path involves an import specifier
|
|
||||||
* importing {@link TUONO_DYNAMIC_FN_ID}
|
|
||||||
*/
|
|
||||||
export const isTuonoDynamicFnImported = (path: babel.NodePath): boolean => {
|
|
||||||
// If the node isn't an import declaration there is no need to process it
|
|
||||||
if (!isImportDeclaration(path.parentPath?.node)) return false
|
|
||||||
|
|
||||||
// if the import doesn't import from 'tuono' we don't need to process it
|
|
||||||
if (path.parentPath.node.source.value !== TUONO_MAIN_PACKAGE) return false
|
|
||||||
|
|
||||||
// ensure that we are processing an import specifier
|
|
||||||
if (!isImportSpecifier(path.node)) return false
|
|
||||||
|
|
||||||
// finally check if the imported item is `TUONO_DYNAMIC_FN_ID`
|
|
||||||
return isIdentifier(path.node.imported, { name: TUONO_DYNAMIC_FN_ID })
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React, { Suspense, type JSX } from "react";
|
|
||||||
import { __tuono__internal__lazyLoadComponent as dynamic } from "tuono";
|
|
||||||
const DynamicComponent = dynamic(() => import("../components/DynamicComponent"));
|
|
||||||
const Loading = (): JSX.Element => <>Loading</>;
|
|
||||||
export default function IndexPage(): JSX.Element {
|
|
||||||
return <Suspense fallback={<Loading />}>
|
|
||||||
<DynamicComponent />
|
|
||||||
</Suspense>;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import React, { Suspense, type JSX } from "react";
|
|
||||||
import DynamicComponent from "../components/DynamicComponent";
|
|
||||||
const Loading = (): JSX.Element => <>Loading</>;
|
|
||||||
export default function IndexPage(): JSX.Element {
|
|
||||||
return <Suspense fallback={<Loading />}>
|
|
||||||
<DynamicComponent />
|
|
||||||
</Suspense>;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import React, { Suspense, type JSX } from "react";
|
|
||||||
import { dynamic } from "tuono";
|
|
||||||
|
|
||||||
const DynamicComponent = dynamic(() => import("../components/DynamicComponent"))
|
|
||||||
|
|
||||||
const Loading = (): JSX.Element => <>Loading</>
|
|
||||||
|
|
||||||
export default function IndexPage(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<DynamicComponent />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { createRoute } from 'tuono';
|
|
||||||
import { dynamic } from 'external-lib';
|
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
|
||||||
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { createRoute } from 'tuono';
|
|
||||||
import { dynamic } from 'external-lib';
|
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
|
||||||
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createRoute } from 'tuono'
|
|
||||||
import { dynamic } from 'external-lib'
|
|
||||||
|
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'))
|
|
||||||
const PokemonspokemonImport = dynamic(
|
|
||||||
() => import('./../src/routes/pokemons/[pokemon]'),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { createRoute, __tuono__internal__lazyLoadComponent as dynamic } from 'tuono';
|
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
|
||||||
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { createRoute } from 'tuono';
|
|
||||||
import IndexImport from "./../src/routes/index";
|
|
||||||
import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createRoute, dynamic } from 'tuono'
|
|
||||||
|
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'))
|
|
||||||
const PokemonspokemonImport = dynamic(
|
|
||||||
() => import('./../src/routes/pokemons/[pokemon]'),
|
|
||||||
)
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import fs from 'node:fs/promises'
|
|
||||||
import os from 'node:os'
|
|
||||||
|
|
||||||
import { it, expect, describe } from 'vitest'
|
|
||||||
import type { Plugin } from 'vite'
|
|
||||||
|
|
||||||
import { TuonoLazyFnPlugin } from '../src'
|
|
||||||
|
|
||||||
type ViteTransformHandler = Exclude<
|
|
||||||
Plugin['transform'],
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
||||||
Function | undefined
|
|
||||||
>['handler']
|
|
||||||
|
|
||||||
// Create a type-safe transform method
|
|
||||||
function getTransform(): (...args: Parameters<ViteTransformHandler>) => string {
|
|
||||||
/** @warning Keep in sync with {@link createBaseViteConfigFromTuonoConfig} */
|
|
||||||
const pluginFilesInclude = /\.(jsx|js|mdx|md|tsx|ts)$/
|
|
||||||
|
|
||||||
return TuonoLazyFnPlugin({ include: pluginFilesInclude }).transform as never
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('"dynamic" sources', async () => {
|
|
||||||
const folderNames = await fs.readdir(`${process.cwd()}/tests/sources`)
|
|
||||||
|
|
||||||
describe.each(folderNames)('%s', async (folderName) => {
|
|
||||||
const testDirPath = `${process.cwd()}/tests/sources/${folderName}`
|
|
||||||
|
|
||||||
const sourceFilePath = `${testDirPath}/source.tsx`
|
|
||||||
|
|
||||||
const sourceRaw = await fs.readFile(sourceFilePath, 'utf-8')
|
|
||||||
/**
|
|
||||||
* When adding `packages/lazy-fn-vite-plugin/tests/sources/dynamic-only` only
|
|
||||||
* the test involving that fixture were broken on Windows... but not the one in the other fixtures:
|
|
||||||
* - packages/lazy-fn-vite-plugin/tests/sources/vanilla
|
|
||||||
* - packages/lazy-fn-vite-plugin/tests/sources/external-dynamic
|
|
||||||
*
|
|
||||||
* Awkwardly this doesn't happen on `packages/fs-router-vite-plugin/tests/generator.spec.ts`
|
|
||||||
*
|
|
||||||
* Too much pain and sadness to investigate this right now.
|
|
||||||
* Might worth creating an utility function in the future if this happens again
|
|
||||||
*/
|
|
||||||
const source = sourceRaw.replace(new RegExp(os.EOL, 'g'), '\n')
|
|
||||||
|
|
||||||
it('should generate file for client', async () => {
|
|
||||||
const pluginTransform = getTransform()
|
|
||||||
const clientBundle = pluginTransform(source, sourceFilePath)
|
|
||||||
|
|
||||||
const expectedClientSrc = `${testDirPath}/client.expected.tsx`
|
|
||||||
|
|
||||||
await expect(clientBundle).toMatchFileSnapshot(
|
|
||||||
expectedClientSrc,
|
|
||||||
`${testDirPath} client build should be equal to ${expectedClientSrc}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should generate file for server', async () => {
|
|
||||||
const pluginTransform = getTransform()
|
|
||||||
const serverBundle = pluginTransform(source, sourceFilePath, {
|
|
||||||
ssr: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const expectedServerSrc = `${testDirPath}/server.expected.tsx`
|
|
||||||
|
|
||||||
await expect(serverBundle).toMatchFileSnapshot(
|
|
||||||
expectedServerSrc,
|
|
||||||
`${testDirPath} server build should be equal to ${expectedServerSrc}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('not valid file should not be processed', () => {
|
|
||||||
const notValidFiles: Array<string> = [
|
|
||||||
'src/styles/module.css',
|
|
||||||
'src/pages/file-without-ext',
|
|
||||||
'src/pages/file-with-invalid-ext',
|
|
||||||
'src/components/fileWithInvalidExt',
|
|
||||||
'src/components/sidebar/sidebar-link.module.css',
|
|
||||||
]
|
|
||||||
|
|
||||||
it.each(notValidFiles)('"%s"', (fileName) => {
|
|
||||||
const pluginTransform = getTransform()
|
|
||||||
|
|
||||||
const code = [
|
|
||||||
"import { createRoute, dynamic } from 'tuono'",
|
|
||||||
"const IndexImport = dynamic(() => import('./../src/routes/index'))",
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const result = pluginTransform(code, fileName)
|
|
||||||
|
|
||||||
expect(result).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"include": ["src", "tests", "vite.config.ts"],
|
|
||||||
"exclude": ["tests/sources"]
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
|
||||||
import { tanstackBuildConfig } from '@tanstack/config/build'
|
|
||||||
|
|
||||||
const config = defineConfig({})
|
|
||||||
|
|
||||||
export default mergeConfig(
|
|
||||||
config,
|
|
||||||
tanstackBuildConfig({
|
|
||||||
entry: './src/index.ts',
|
|
||||||
srcDir: './src',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import type { ReactElement } from 'react'
|
|
||||||
|
|
||||||
import type { RouteComponent } from './types'
|
|
||||||
|
|
||||||
type ImportFn = () => Promise<{ default: RouteComponent }>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to lazy load any component.
|
|
||||||
*
|
|
||||||
* The function acts exactly like React.lazy function but also renders the component on the server.
|
|
||||||
* If you want to just load the component client side use directly the react's lazy function.
|
|
||||||
*
|
|
||||||
* It can be wrapped within a React.Suspense component in order to handle its loading state.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const dynamic = (importFn: ImportFn): React.JSX.Element => {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* This function is just a placeholder. The real work is done by the bundler.
|
|
||||||
* The custom babel plugin will create two different bundles for the client and the server.
|
|
||||||
*
|
|
||||||
* The client will import the React's lazy function while the server will statically
|
|
||||||
* import the file.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* // User code
|
|
||||||
* import { dynamic } from 'tuono'
|
|
||||||
* const MyComponent = dynamic(() => import('./my-component'))
|
|
||||||
*
|
|
||||||
* // Client side generated code
|
|
||||||
* import { lazy } from 'react'
|
|
||||||
* const MyComponent = lazy(() => import('./my-component'))
|
|
||||||
*
|
|
||||||
* // Server side generated code
|
|
||||||
* import MyComponent from './my-component'
|
|
||||||
*
|
|
||||||
* Check the `lazy-fn-vite-plugin` package for more
|
|
||||||
*/
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const __tuono__internal__lazyLoadComponent = (
|
|
||||||
factory: ImportFn,
|
|
||||||
): RouteComponent => {
|
|
||||||
let LoadedComponent: RouteComponent | undefined
|
|
||||||
const LazyComponent = React.lazy(factory) as unknown as RouteComponent
|
|
||||||
|
|
||||||
const loadComponent = (): Promise<void> =>
|
|
||||||
factory().then((module) => {
|
|
||||||
LoadedComponent = module.default
|
|
||||||
})
|
|
||||||
|
|
||||||
const Component = (
|
|
||||||
props: React.ComponentProps<RouteComponent>,
|
|
||||||
): ReactElement =>
|
|
||||||
React.createElement(LoadedComponent || LazyComponent, props)
|
|
||||||
|
|
||||||
Component.preload = loadComponent
|
|
||||||
|
|
||||||
return Component
|
|
||||||
}
|
|
||||||
@@ -2,5 +2,5 @@ export { RouterProvider } from './components/RouterProvider'
|
|||||||
export { default as Link } from './components/Link'
|
export { default as Link } from './components/Link'
|
||||||
export { createRouter } from './router'
|
export { createRouter } from './router'
|
||||||
export { createRoute, createRootRoute } from './route'
|
export { createRoute, createRootRoute } from './route'
|
||||||
export { dynamic, __tuono__internal__lazyLoadComponent } from './dynamic'
|
|
||||||
export { useRouter } from './hooks/useRouter'
|
export { useRouter } from './hooks/useRouter'
|
||||||
|
export type { RouteProps, RouteComponent } from './types'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ComponentType as ReactComponentType, ReactNode } from 'react'
|
import type { ReactNode, ComponentType } from 'react'
|
||||||
|
|
||||||
export interface Segment {
|
export interface Segment {
|
||||||
type: 'pathname' | 'param' | 'wildcard'
|
type: 'pathname' | 'param' | 'wildcard'
|
||||||
@@ -23,6 +23,6 @@ export interface RouteProps<TData = unknown> {
|
|||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteComponent = ReactComponentType<RouteProps> & {
|
export type RouteComponent = ComponentType<RouteProps> & {
|
||||||
preload: () => void
|
preload: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@
|
|||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"tuono-fs-router-vite-plugin": "workspace:*",
|
"tuono-fs-router-vite-plugin": "workspace:*",
|
||||||
"tuono-lazy-fn-vite-plugin": "workspace:*",
|
|
||||||
"tuono-router": "workspace:*",
|
"tuono-router": "workspace:*",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"web-streams-polyfill": "^4.0.0"
|
"web-streams-polyfill": "^4.0.0"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { build, createServer, mergeConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import inject from '@rollup/plugin-inject'
|
import inject from '@rollup/plugin-inject'
|
||||||
import { TuonoFsRouterPlugin } from 'tuono-fs-router-vite-plugin'
|
import { TuonoFsRouterPlugin } from 'tuono-fs-router-vite-plugin'
|
||||||
import { TuonoLazyFnPlugin } from 'tuono-lazy-fn-vite-plugin'
|
|
||||||
|
|
||||||
import type { TuonoConfig } from '../config'
|
import type { TuonoConfig } from '../config'
|
||||||
|
|
||||||
@@ -67,7 +66,6 @@ function createBaseViteConfigFromTuonoConfig(
|
|||||||
react({ include: pluginFilesInclude }),
|
react({ include: pluginFilesInclude }),
|
||||||
|
|
||||||
TuonoFsRouterPlugin(),
|
TuonoFsRouterPlugin(),
|
||||||
TuonoLazyFnPlugin({ include: pluginFilesInclude }),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { lazy, createElement } from 'react'
|
||||||
|
import type { ReactElement } from 'react'
|
||||||
|
|
||||||
|
import type { RouteComponent } from 'tuono-router'
|
||||||
|
|
||||||
|
type ImportFn = () => Promise<{ default: RouteComponent }>
|
||||||
|
|
||||||
|
export const RouteLazyLoading = (factory: ImportFn): RouteComponent => {
|
||||||
|
let LoadedComponent: RouteComponent | undefined
|
||||||
|
const LazyComponent = lazy<RouteComponent>(factory)
|
||||||
|
|
||||||
|
const loadComponent = (): Promise<void> =>
|
||||||
|
factory().then((module) => {
|
||||||
|
LoadedComponent = module.default
|
||||||
|
})
|
||||||
|
|
||||||
|
const Component = (
|
||||||
|
props: React.ComponentProps<RouteComponent>,
|
||||||
|
): ReactElement => createElement(LoadedComponent || LazyComponent, props)
|
||||||
|
|
||||||
|
Component.preload = loadComponent
|
||||||
|
|
||||||
|
return Component
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* This component is heavily inspired by Next.js dynamic function
|
||||||
|
* Link: https://github.com/vercel/next.js/blob/1df81bcea62800198884438a2bb27ba14c9d506a/packages/next/src/shared/lib/dynamic.tsx
|
||||||
|
*/
|
||||||
|
import { lazy, Suspense, Fragment } from 'react'
|
||||||
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
|
const isServerSide = typeof window === 'undefined'
|
||||||
|
|
||||||
|
interface ComponentModule<T> {
|
||||||
|
default: React.ComponentType<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicOptions {
|
||||||
|
ssr?: boolean
|
||||||
|
loading?: React.ComponentType<unknown> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type Loader<T = object> = () => Promise<
|
||||||
|
React.ComponentType<T> | ComponentModule<T>
|
||||||
|
>
|
||||||
|
|
||||||
|
interface LoadableOptions<T> extends DynamicOptions {
|
||||||
|
loader: Loader<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadableFn = <T = object>(options: LoadableOptions<T>) => ComponentType<T>
|
||||||
|
|
||||||
|
const defaultLoaderOptions: LoadableOptions<object> = {
|
||||||
|
ssr: true,
|
||||||
|
loading: null,
|
||||||
|
loader: () => Promise.resolve(() => null),
|
||||||
|
}
|
||||||
|
|
||||||
|
function noSSR<T = object>(
|
||||||
|
LoadableInitializer: LoadableFn,
|
||||||
|
loadableOptions: LoadableOptions<T>,
|
||||||
|
): React.ComponentType<T> {
|
||||||
|
if (!isServerSide) {
|
||||||
|
return LoadableInitializer(loadableOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadableOptions.loading) return () => null
|
||||||
|
|
||||||
|
const Loading = loadableOptions.loading
|
||||||
|
// This will only be rendered on the server side
|
||||||
|
function NoSSRLoading(): React.JSX.Element {
|
||||||
|
return <Loading />
|
||||||
|
}
|
||||||
|
return NoSSRLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
function Loadable<T = object>(options: LoadableOptions<T>): ComponentType<T> {
|
||||||
|
const opts = { ...defaultLoaderOptions, ...options }
|
||||||
|
const Lazy = lazy(() => opts.loader().then())
|
||||||
|
const Loading = opts.loading
|
||||||
|
|
||||||
|
function LoadableComponent(props: T): React.JSX.Element {
|
||||||
|
const fallbackElement = Loading ? <Loading /> : null
|
||||||
|
|
||||||
|
const Wrap = Loading ? Suspense : Fragment
|
||||||
|
const wrapProps = Loading ? { fallback: fallbackElement } : {}
|
||||||
|
|
||||||
|
// TODO: In case ssr = false handle also the assets preloading
|
||||||
|
return (
|
||||||
|
<Wrap {...wrapProps}>
|
||||||
|
<Lazy {...props} />
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LoadableComponent.displayName = 'LoadableComponent'
|
||||||
|
|
||||||
|
return LoadableComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function lets you dynamically import a component.
|
||||||
|
* It uses [React.lazy()](https://react.dev/reference/react/lazy) with [Suspense](https://react.dev/reference/react/Suspense) under the hood.
|
||||||
|
*/
|
||||||
|
export function dynamic<T = object>(
|
||||||
|
importFn: Loader<T>,
|
||||||
|
opts?: DynamicOptions,
|
||||||
|
): ComponentType<T> {
|
||||||
|
if (typeof opts?.ssr === 'boolean' && !opts.ssr) {
|
||||||
|
return noSSR<T>(Loadable, { ...opts, loader: importFn })
|
||||||
|
}
|
||||||
|
return Loadable<T>({ ...opts, loader: importFn })
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ export {
|
|||||||
createRootRoute,
|
createRootRoute,
|
||||||
createRouter,
|
createRouter,
|
||||||
Link,
|
Link,
|
||||||
dynamic,
|
|
||||||
useRouter,
|
useRouter,
|
||||||
__tuono__internal__lazyLoadComponent,
|
|
||||||
} from 'tuono-router'
|
} from 'tuono-router'
|
||||||
|
|
||||||
|
export { RouteLazyLoading as __tuono__internal__lazyLoadRoute } from './dynamic/RouteLazyLoading'
|
||||||
|
export { dynamic } from './dynamic/dynamic'
|
||||||
|
|
||||||
export type { TuonoProps } from './types'
|
export type { TuonoProps } from './types'
|
||||||
|
|||||||
Generated
-28
@@ -186,9 +186,6 @@ importers:
|
|||||||
tuono-fs-router-vite-plugin:
|
tuono-fs-router-vite-plugin:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../tuono-fs-router-vite-plugin
|
version: link:../tuono-fs-router-vite-plugin
|
||||||
tuono-lazy-fn-vite-plugin:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../tuono-lazy-fn-vite-plugin
|
|
||||||
tuono-router:
|
tuono-router:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../tuono-router
|
version: link:../tuono-router
|
||||||
@@ -252,31 +249,6 @@ importers:
|
|||||||
specifier: 2.1.8
|
specifier: 2.1.8
|
||||||
version: 2.1.8(@types/node@22.10.6)(jsdom@25.0.1)(sugarss@4.0.1(postcss@8.5.0))
|
version: 2.1.8(@types/node@22.10.6)(jsdom@25.0.1)(sugarss@4.0.1(postcss@8.5.0))
|
||||||
|
|
||||||
packages/tuono-lazy-fn-vite-plugin:
|
|
||||||
dependencies:
|
|
||||||
'@babel/core':
|
|
||||||
specifier: ^7.24.4
|
|
||||||
version: 7.26.0
|
|
||||||
'@babel/types':
|
|
||||||
specifier: ^7.24.0
|
|
||||||
version: 7.26.3
|
|
||||||
vite:
|
|
||||||
specifier: ^5.2.11
|
|
||||||
version: 5.4.11(@types/node@22.10.6)(sugarss@4.0.1(postcss@8.5.0))
|
|
||||||
devDependencies:
|
|
||||||
'@tanstack/config':
|
|
||||||
specifier: 0.7.13
|
|
||||||
version: 0.7.13(@types/node@22.10.6)(esbuild@0.21.5)(rollup@4.25.0)(typescript@5.7.3)(vite@5.4.11(@types/node@22.10.6)(sugarss@4.0.1(postcss@8.5.0)))
|
|
||||||
'@types/babel__core':
|
|
||||||
specifier: ^7.20.5
|
|
||||||
version: 7.20.5
|
|
||||||
prettier:
|
|
||||||
specifier: ^3.2.4
|
|
||||||
version: 3.4.2
|
|
||||||
vitest:
|
|
||||||
specifier: 2.1.8
|
|
||||||
version: 2.1.8(@types/node@22.10.6)(jsdom@25.0.1)(sugarss@4.0.1(postcss@8.5.0))
|
|
||||||
|
|
||||||
packages/tuono-router:
|
packages/tuono-router:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-intersection-observer:
|
react-intersection-observer:
|
||||||
|
|||||||
Reference in New Issue
Block a user