diff --git a/packages/tuono-fs-router-vite-plugin/src/constants.ts b/packages/tuono-fs-router-vite-plugin/src/constants.ts index 2df09828..7b0718e0 100644 --- a/packages/tuono-fs-router-vite-plugin/src/constants.ts +++ b/packages/tuono-fs-router-vite-plugin/src/constants.ts @@ -1,3 +1,4 @@ export const ROUTES_FOLDER = './src/routes/' export const LAYOUT_PATH_ID = '__layout' export const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts' +export const DYNAMIC_FN = '__tuono__internal__lazyLoadRoute' diff --git a/packages/tuono-fs-router-vite-plugin/src/generator.ts b/packages/tuono-fs-router-vite-plugin/src/generator.ts index 53e5f5b7..225835c3 100644 --- a/packages/tuono-fs-router-vite-plugin/src/generator.ts +++ b/packages/tuono-fs-router-vite-plugin/src/generator.ts @@ -22,6 +22,7 @@ import { ROUTES_FOLDER, LAYOUT_PATH_ID, GENERATED_ROUTE_TREE, + DYNAMIC_FN, } from './constants' import { sortRouteNodes } from './sort-route-nodes' @@ -168,7 +169,7 @@ export async function routeGenerator(config = defaultConfig): Promise { const extension = node.filePath.endsWith('mdx') ? '.mdx' : '' return `const ${ node.variableName as string - }Import = dynamic(() => import('./${replaceBackslash( + }Import = ${DYNAMIC_FN}(() => import('./${replaceBackslash( removeExt( path.relative( path.dirname(config.generatedRouteTree), @@ -214,7 +215,7 @@ export async function routeGenerator(config = defaultConfig): Promise { const routeImports = [ '// This file is auto-generated by Tuono', - "import { createRoute, dynamic } from 'tuono'", + `import { createRoute, ${DYNAMIC_FN} } from 'tuono'`, [ `import RootLayoutImport from './${replaceBackslash( path.relative( diff --git a/packages/tuono-fs-router-vite-plugin/tests/generator/catch_all/routeTree.expected.ts b/packages/tuono-fs-router-vite-plugin/tests/generator/catch_all/routeTree.expected.ts index 7910bdd7..29cb3294 100644 --- a/packages/tuono-fs-router-vite-plugin/tests/generator/catch_all/routeTree.expected.ts +++ b/packages/tuono-fs-router-vite-plugin/tests/generator/catch_all/routeTree.expected.ts @@ -1,11 +1,13 @@ // This file is auto-generated by Tuono -import { createRoute, dynamic } from 'tuono' +import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono' import RootLayoutImport from './routes/__layout' -const IndexImport = dynamic(() => import('./routes/index')) -const PostscatchallImport = dynamic( +const IndexImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/index'), +) +const PostscatchallImport = __tuono__internal__lazyLoadRoute( () => import('./routes/posts/[...catch_all]'), ) diff --git a/packages/tuono-fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts b/packages/tuono-fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts index 6ee4883a..e4741257 100644 --- a/packages/tuono-fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts +++ b/packages/tuono-fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts @@ -1,11 +1,15 @@ // This file is auto-generated by Tuono -import { createRoute, dynamic } from 'tuono' +import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono' import RootLayoutImport from './routes/__layout' -const AboutImport = dynamic(() => import('./routes/about.mdx')) -const IndexImport = dynamic(() => import('./routes/index')) +const AboutImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/about.mdx'), +) +const IndexImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/index'), +) const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport }) diff --git a/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts b/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts index e17e1af6..d686da5e 100644 --- a/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts +++ b/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level-root-dynamic/routeTree.expected.ts @@ -1,15 +1,27 @@ // This file is auto-generated by Tuono -import { createRoute, dynamic } from 'tuono' +import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono' import RootLayoutImport from './routes/__layout' -const PostslayoutImport = dynamic(() => import('./routes/posts/__layout')) -const AboutImport = dynamic(() => import('./routes/about')) -const IndexImport = dynamic(() => import('./routes/index')) -const PostspostImport = dynamic(() => import('./routes/posts/[post]')) -const PostsIndexImport = dynamic(() => import('./routes/posts/index')) -const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post')) +const PostslayoutImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/posts/__layout'), +) +const AboutImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/about'), +) +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 }) diff --git a/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts b/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts index f91e1f6a..2190ffbb 100644 --- a/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts +++ b/packages/tuono-fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts @@ -1,12 +1,18 @@ // This file is auto-generated by Tuono -import { createRoute, dynamic } from 'tuono' +import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono' import RootLayoutImport from './routes/__layout' -const AboutImport = dynamic(() => import('./routes/about')) -const IndexImport = dynamic(() => import('./routes/index')) -const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post')) +const AboutImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/about'), +) +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 }) diff --git a/packages/tuono-fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts b/packages/tuono-fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts index 5e276f01..3db42380 100644 --- a/packages/tuono-fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts +++ b/packages/tuono-fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts @@ -1,11 +1,15 @@ // This file is auto-generated by Tuono -import { createRoute, dynamic } from 'tuono' +import { createRoute, __tuono__internal__lazyLoadRoute } from 'tuono' import RootLayoutImport from './routes/__layout' -const AboutImport = dynamic(() => import('./routes/about')) -const IndexImport = dynamic(() => import('./routes/index')) +const AboutImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/about'), +) +const IndexImport = __tuono__internal__lazyLoadRoute( + () => import('./routes/index'), +) const rootRoute = createRoute({ isRoot: true, component: RootLayoutImport }) diff --git a/packages/tuono-lazy-fn-vite-plugin/README.md b/packages/tuono-lazy-fn-vite-plugin/README.md deleted file mode 100644 index 76135f94..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/README.md +++ /dev/null @@ -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. diff --git a/packages/tuono-lazy-fn-vite-plugin/package.json b/packages/tuono-lazy-fn-vite-plugin/package.json deleted file mode 100644 index 661595d2..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/tuono-lazy-fn-vite-plugin/src/constants.ts b/packages/tuono-lazy-fn-vite-plugin/src/constants.ts deleted file mode 100644 index bed282fe..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/src/constants.ts +++ /dev/null @@ -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' diff --git a/packages/tuono-lazy-fn-vite-plugin/src/index.ts b/packages/tuono-lazy-fn-vite-plugin/src/index.ts deleted file mode 100644 index b34eb6ae..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/src/index.ts +++ /dev/null @@ -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, - ): 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 = [ - ['@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 - }, - } -} diff --git a/packages/tuono-lazy-fn-vite-plugin/src/utils.ts b/packages/tuono-lazy-fn-vite-plugin/src/utils.ts deleted file mode 100644 index 4e3390fc..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/src/utils.ts +++ /dev/null @@ -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 }) -} diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/client.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/client.expected.tsx deleted file mode 100644 index 87cbe459..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/client.expected.tsx +++ /dev/null @@ -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 }> - - ; -} \ No newline at end of file diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/server.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/server.expected.tsx deleted file mode 100644 index 195d781f..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/server.expected.tsx +++ /dev/null @@ -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 }> - - ; -} \ No newline at end of file diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/source.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/source.tsx deleted file mode 100644 index 98981ad8..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/dynamic-only/source.tsx +++ /dev/null @@ -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 ( - }> - - - ); -} \ No newline at end of file diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/client.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/client.expected.tsx deleted file mode 100644 index 2f76de16..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/client.expected.tsx +++ /dev/null @@ -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]')); diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/server.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/server.expected.tsx deleted file mode 100644 index 2f76de16..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/server.expected.tsx +++ /dev/null @@ -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]')); diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/source.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/source.tsx deleted file mode 100644 index f3062fee..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/external-dynamic/source.tsx +++ /dev/null @@ -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]'), -) diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/client.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/client.expected.tsx deleted file mode 100644 index 978c3aa1..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/client.expected.tsx +++ /dev/null @@ -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]')); diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/server.expected.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/server.expected.tsx deleted file mode 100644 index 00aa40c8..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/server.expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createRoute } from 'tuono'; -import IndexImport from "./../src/routes/index"; -import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]"; diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/source.tsx b/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/source.tsx deleted file mode 100644 index 0f912545..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/sources/vanilla/source.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createRoute, dynamic } from 'tuono' - -const IndexImport = dynamic(() => import('./../src/routes/index')) -const PokemonspokemonImport = dynamic( - () => import('./../src/routes/pokemons/[pokemon]'), -) diff --git a/packages/tuono-lazy-fn-vite-plugin/tests/transpileSource.test.ts b/packages/tuono-lazy-fn-vite-plugin/tests/transpileSource.test.ts deleted file mode 100644 index ffdbc177..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tests/transpileSource.test.ts +++ /dev/null @@ -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) => 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 = [ - '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() - }) -}) diff --git a/packages/tuono-lazy-fn-vite-plugin/tsconfig.json b/packages/tuono-lazy-fn-vite-plugin/tsconfig.json deleted file mode 100644 index a30a085c..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src", "tests", "vite.config.ts"], - "exclude": ["tests/sources"] -} diff --git a/packages/tuono-lazy-fn-vite-plugin/vite.config.ts b/packages/tuono-lazy-fn-vite-plugin/vite.config.ts deleted file mode 100644 index c09134a3..00000000 --- a/packages/tuono-lazy-fn-vite-plugin/vite.config.ts +++ /dev/null @@ -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', - }), -) diff --git a/packages/tuono-router/src/dynamic.tsx b/packages/tuono-router/src/dynamic.tsx deleted file mode 100644 index 9ff7ac99..00000000 --- a/packages/tuono-router/src/dynamic.tsx +++ /dev/null @@ -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 => - factory().then((module) => { - LoadedComponent = module.default - }) - - const Component = ( - props: React.ComponentProps, - ): ReactElement => - React.createElement(LoadedComponent || LazyComponent, props) - - Component.preload = loadComponent - - return Component -} diff --git a/packages/tuono-router/src/index.ts b/packages/tuono-router/src/index.ts index fdb28559..8a76e565 100644 --- a/packages/tuono-router/src/index.ts +++ b/packages/tuono-router/src/index.ts @@ -2,5 +2,5 @@ export { RouterProvider } from './components/RouterProvider' export { default as Link } from './components/Link' export { createRouter } from './router' export { createRoute, createRootRoute } from './route' -export { dynamic, __tuono__internal__lazyLoadComponent } from './dynamic' export { useRouter } from './hooks/useRouter' +export type { RouteProps, RouteComponent } from './types' diff --git a/packages/tuono-router/src/types.ts b/packages/tuono-router/src/types.ts index f6d0c2b7..40fc5c63 100644 --- a/packages/tuono-router/src/types.ts +++ b/packages/tuono-router/src/types.ts @@ -1,4 +1,4 @@ -import type { ComponentType as ReactComponentType, ReactNode } from 'react' +import type { ReactNode, ComponentType } from 'react' export interface Segment { type: 'pathname' | 'param' | 'wildcard' @@ -23,6 +23,6 @@ export interface RouteProps { children?: ReactNode } -export type RouteComponent = ReactComponentType & { +export type RouteComponent = ComponentType & { preload: () => void } diff --git a/packages/tuono/package.json b/packages/tuono/package.json index 8cff5d58..bb577edc 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -99,7 +99,6 @@ "@vitejs/plugin-react-swc": "^3.7.0", "fast-text-encoding": "^1.0.6", "tuono-fs-router-vite-plugin": "workspace:*", - "tuono-lazy-fn-vite-plugin": "workspace:*", "tuono-router": "workspace:*", "vite": "^5.2.11", "web-streams-polyfill": "^4.0.0" diff --git a/packages/tuono/src/build/index.ts b/packages/tuono/src/build/index.ts index 72345d4a..9781b4d6 100644 --- a/packages/tuono/src/build/index.ts +++ b/packages/tuono/src/build/index.ts @@ -3,7 +3,6 @@ import { build, createServer, mergeConfig } from 'vite' import react from '@vitejs/plugin-react-swc' import inject from '@rollup/plugin-inject' import { TuonoFsRouterPlugin } from 'tuono-fs-router-vite-plugin' -import { TuonoLazyFnPlugin } from 'tuono-lazy-fn-vite-plugin' import type { TuonoConfig } from '../config' @@ -67,7 +66,6 @@ function createBaseViteConfigFromTuonoConfig( react({ include: pluginFilesInclude }), TuonoFsRouterPlugin(), - TuonoLazyFnPlugin({ include: pluginFilesInclude }), ], } diff --git a/packages/tuono/src/dynamic/RouteLazyLoading.tsx b/packages/tuono/src/dynamic/RouteLazyLoading.tsx new file mode 100644 index 00000000..3fb82a60 --- /dev/null +++ b/packages/tuono/src/dynamic/RouteLazyLoading.tsx @@ -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(factory) + + const loadComponent = (): Promise => + factory().then((module) => { + LoadedComponent = module.default + }) + + const Component = ( + props: React.ComponentProps, + ): ReactElement => createElement(LoadedComponent || LazyComponent, props) + + Component.preload = loadComponent + + return Component +} diff --git a/packages/tuono/src/dynamic/dynamic.tsx b/packages/tuono/src/dynamic/dynamic.tsx new file mode 100644 index 00000000..022f0b49 --- /dev/null +++ b/packages/tuono/src/dynamic/dynamic.tsx @@ -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 { + default: React.ComponentType +} + +interface DynamicOptions { + ssr?: boolean + loading?: React.ComponentType | null +} + +type Loader = () => Promise< + React.ComponentType | ComponentModule +> + +interface LoadableOptions extends DynamicOptions { + loader: Loader +} + +type LoadableFn = (options: LoadableOptions) => ComponentType + +const defaultLoaderOptions: LoadableOptions = { + ssr: true, + loading: null, + loader: () => Promise.resolve(() => null), +} + +function noSSR( + LoadableInitializer: LoadableFn, + loadableOptions: LoadableOptions, +): React.ComponentType { + 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 + } + return NoSSRLoading +} + +function Loadable(options: LoadableOptions): ComponentType { + const opts = { ...defaultLoaderOptions, ...options } + const Lazy = lazy(() => opts.loader().then()) + const Loading = opts.loading + + function LoadableComponent(props: T): React.JSX.Element { + const fallbackElement = Loading ? : null + + const Wrap = Loading ? Suspense : Fragment + const wrapProps = Loading ? { fallback: fallbackElement } : {} + + // TODO: In case ssr = false handle also the assets preloading + return ( + + + + ) + } + 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( + importFn: Loader, + opts?: DynamicOptions, +): ComponentType { + if (typeof opts?.ssr === 'boolean' && !opts.ssr) { + return noSSR(Loadable, { ...opts, loader: importFn }) + } + return Loadable({ ...opts, loader: importFn }) +} diff --git a/packages/tuono/src/index.ts b/packages/tuono/src/index.ts index 11e47498..93525d99 100644 --- a/packages/tuono/src/index.ts +++ b/packages/tuono/src/index.ts @@ -3,9 +3,10 @@ export { createRootRoute, createRouter, Link, - dynamic, useRouter, - __tuono__internal__lazyLoadComponent, } from 'tuono-router' +export { RouteLazyLoading as __tuono__internal__lazyLoadRoute } from './dynamic/RouteLazyLoading' +export { dynamic } from './dynamic/dynamic' + export type { TuonoProps } from './types' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1d19de3..187f817a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,9 +186,6 @@ importers: tuono-fs-router-vite-plugin: specifier: workspace:* version: link:../tuono-fs-router-vite-plugin - tuono-lazy-fn-vite-plugin: - specifier: workspace:* - version: link:../tuono-lazy-fn-vite-plugin tuono-router: specifier: workspace:* version: link:../tuono-router @@ -252,31 +249,6 @@ importers: 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-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: dependencies: react-intersection-observer: