mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
Create babel plugin to handle lazy loading (#4)
* feat: create babel plugin to handle lazy loading * fix: update typescript CI to also run the tests in the pipelines * doc: update lazy loading component inline documentation * fix: typescript pipeline * fix: update CI typescript test command * fix: prevent duplicate pipelines on PR * feat: create separate vite plugin package * chore: refined lazy fn package * test: add test to lazy fn plugin * feat: update version to v0.1.5
This commit is contained in:
@@ -15,6 +15,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Build and test rust crates
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
@@ -12,18 +12,20 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
@@ -34,15 +36,19 @@ jobs:
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Test project
|
||||
run: pnpm test
|
||||
|
||||
fmt-lint-and-types:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: CI Checkout
|
||||
uses: actions/setup-node@v2
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
@@ -24,5 +24,5 @@ serde = { version = "1.0.202", features = ["derive"] }
|
||||
erased-serde = "0.4.5"
|
||||
serde_json = "1.0"
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.4"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.1.5"}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
homepage = "https://github.com/Valerioageno/tuono"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
@@ -36,6 +36,6 @@
|
||||
"vitest": "^1.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"turbo": "^2.0.3"
|
||||
"turbo": "^2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1 @@
|
||||
# tuono-lazy-fn-vite-plugin
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "tuono-lazy-fn-vite-plugin",
|
||||
"version": "0.1.5",
|
||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||
"types": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"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.11",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"prettier": "^3.2.4",
|
||||
"vitest": "^1.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import * as babel from '@babel/core'
|
||||
import type { PluginItem } from '@babel/core'
|
||||
|
||||
import * as t from '@babel/types'
|
||||
|
||||
import type {
|
||||
Identifier,
|
||||
ImportDeclaration,
|
||||
CallExpression,
|
||||
ArrowFunctionExpression,
|
||||
StringLiteral,
|
||||
} from '@babel/types'
|
||||
|
||||
/**
|
||||
* This plugin just removes the `lazy` imported function from any tuono import
|
||||
*/
|
||||
const RemoveTuonoLazyImport: PluginItem = {
|
||||
name: 'remove-tuono-lazy-import-plugin',
|
||||
visitor: {
|
||||
ImportSpecifier: (path) => {
|
||||
if ((path.node.imported as Identifier).name === 'lazy') {
|
||||
if (
|
||||
(path.parentPath.node as ImportDeclaration).source.value === 'tuono'
|
||||
) {
|
||||
path.remove()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Import { lazy } from 'react'
|
||||
*/
|
||||
const ImportReactLazy: PluginItem = {
|
||||
name: 'import-react-lazy-plugin',
|
||||
visitor: {
|
||||
Program: (path: any) => {
|
||||
let isReactImported = false
|
||||
|
||||
path.node.body.forEach((val: any) => {
|
||||
if (val.type === 'ImportDeclaration' && val.source.value === 'react') {
|
||||
isReactImported = true
|
||||
// TODO: Handle also here case of already imported react
|
||||
// Right now works just for the main routes file
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!isReactImported) {
|
||||
const importDeclaration = t.importDeclaration(
|
||||
[t.importSpecifier(t.identifier('lazy'), t.identifier('lazy'))],
|
||||
t.stringLiteral('react'),
|
||||
)
|
||||
path.unshiftContainer('body', importDeclaration)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* For the server side we need to statically import the lazy loaded components
|
||||
*/
|
||||
const TurnLazyToStaticImport: PluginItem = {
|
||||
name: 'turn-lazy-to-static-import-plugin',
|
||||
visitor: {
|
||||
VariableDeclaration: (path) => {
|
||||
path.node.declarations.forEach((el) => {
|
||||
const init = el.init as CallExpression
|
||||
if ((init.callee as Identifier).name === 'lazy') {
|
||||
const importName = (el.id as Identifier).name
|
||||
const importPath = (
|
||||
(
|
||||
(init.arguments[0] as ArrowFunctionExpression)
|
||||
.body as CallExpression
|
||||
).arguments[0] as StringLiteral
|
||||
).value
|
||||
|
||||
if (importName && importPath) {
|
||||
const importDeclaration = t.importDeclaration(
|
||||
[t.importDefaultSpecifier(t.identifier(importName))],
|
||||
t.stringLiteral(importPath),
|
||||
)
|
||||
|
||||
path.replaceWith(importDeclaration)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function LazyLoadingPlugin(): Plugin {
|
||||
return {
|
||||
name: 'vite-plugin-tuono-lazy-loading',
|
||||
enforce: 'pre',
|
||||
transform(code, _id, opts): string | undefined | null {
|
||||
if (code.includes('lazy') && code.includes('tuono')) {
|
||||
const res = babel.transformSync(code, {
|
||||
plugins: [
|
||||
['@babel/plugin-syntax-jsx', {}],
|
||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
||||
[RemoveTuonoLazyImport],
|
||||
[!opts?.ssr ? ImportReactLazy : []],
|
||||
[opts?.ssr ? TurnLazyToStaticImport : []],
|
||||
],
|
||||
sourceMaps: true,
|
||||
})
|
||||
|
||||
return res?.code
|
||||
}
|
||||
return code
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { it, expect, describe } from 'vitest'
|
||||
import { LazyLoadingPlugin } from '../src'
|
||||
|
||||
const SOURCE_CODE = `
|
||||
import { createRoute, lazy } from 'tuono'
|
||||
|
||||
const IndexImport = lazy(() => import('./../src/routes/index'))
|
||||
const PokemonspokemonImport = lazy(
|
||||
() => import('./../src/routes/pokemons/[pokemon]'),
|
||||
)
|
||||
`
|
||||
|
||||
const CLIENT_RESULT = `import { lazy } from "react";
|
||||
import { createRoute } from 'tuono';
|
||||
const IndexImport = lazy(() => import('./../src/routes/index'));
|
||||
const PokemonspokemonImport = lazy(() => import('./../src/routes/pokemons/[pokemon]'));`
|
||||
|
||||
const SERVER_RESULT = `import { createRoute } from 'tuono';
|
||||
import IndexImport from "./../src/routes/index";
|
||||
import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";`
|
||||
|
||||
describe('Transpile tuono source', () => {
|
||||
it('Into the client bundle', () => {
|
||||
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id')
|
||||
expect(bundle).toBe(CLIENT_RESULT)
|
||||
})
|
||||
|
||||
it('Into the server bundle', () => {
|
||||
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id', {
|
||||
ssr: true,
|
||||
})
|
||||
expect(bundle).toBe(SERVER_RESULT)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
@@ -9,7 +9,7 @@
|
||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||
"types": "tsc --noEmit",
|
||||
"test": "vitest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
@@ -88,6 +88,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"prettier": "^3.2.4",
|
||||
"tuono-lazy-fn-vite-plugin": "workspace:*",
|
||||
"vite": "^5.2.11",
|
||||
"zustand": "4.4.7"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { build, createServer, InlineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { ViteFsRouter } from './tuono-vite-plugin'
|
||||
import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin'
|
||||
|
||||
const BASE_CONFIG: InlineConfig = {
|
||||
root: '.tuono',
|
||||
@@ -8,7 +9,7 @@ const BASE_CONFIG: InlineConfig = {
|
||||
publicDir: '../public',
|
||||
cacheDir: 'cache',
|
||||
envDir: '../',
|
||||
plugins: [react(), ViteFsRouter()],
|
||||
plugins: [react(), ViteFsRouter(), LazyLoadingPlugin()],
|
||||
}
|
||||
|
||||
export function developmentSSRBundle() {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
removeGroups,
|
||||
removeUnderscores,
|
||||
removeLayoutSegments,
|
||||
removeTrailingUnderscores,
|
||||
} from './utils'
|
||||
|
||||
import type { Config, RouteNode, RouteSubNode } from './types'
|
||||
@@ -151,7 +150,6 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
const taskId = latestTask + 1
|
||||
latestTask = taskId
|
||||
|
||||
const start = Date.now()
|
||||
const { routeNodes: beforeRouteNodes, rustHandlersNodes } =
|
||||
await getRouteNodes(config)
|
||||
|
||||
@@ -232,7 +230,9 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
|
||||
const imports = [
|
||||
...sortedRouteNodes.map((node) => {
|
||||
return `import ${node.variableName}Import from './${replaceBackslash(
|
||||
return `const ${
|
||||
node.variableName
|
||||
}Import = lazy(() => import('./${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
@@ -240,7 +240,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}'`
|
||||
)}'))`
|
||||
}),
|
||||
].join('\n')
|
||||
|
||||
@@ -253,9 +253,6 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
const createRouteUpdates = [
|
||||
sortedRouteNodes
|
||||
.map((node) => {
|
||||
const loaderNode = routePiecesByPath[node.routePath]?.loader
|
||||
const lazyComponentNode = routePiecesByPath[node.routePath]?.lazy
|
||||
|
||||
return [
|
||||
`const ${node.variableName}Route = ${node.variableName}.update({
|
||||
${[
|
||||
@@ -267,30 +264,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(',')}
|
||||
} as any)`,
|
||||
// Verify if needed
|
||||
loaderNode
|
||||
? `.updateLoader({ loader: lazyFn(() => import('./${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, loaderNode.filePath),
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}'), 'loader') })`
|
||||
: '',
|
||||
lazyComponentNode
|
||||
? `.lazy(() => import('./${replaceBackslash(
|
||||
removeExt(
|
||||
path.relative(
|
||||
path.dirname(config.generatedRouteTree),
|
||||
path.resolve(config.folderName, lazyComponentNode.filePath),
|
||||
),
|
||||
false,
|
||||
),
|
||||
)}').then((d) => d.Route))`
|
||||
: '',
|
||||
})`,
|
||||
].join('')
|
||||
})
|
||||
.join('\n\n'),
|
||||
@@ -298,7 +272,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
||||
|
||||
const routeImports = [
|
||||
'// This file is auto-generated by Tuono',
|
||||
"import { createRoute } from 'tuono'",
|
||||
"import { createRoute, lazy } from 'tuono'",
|
||||
[
|
||||
`import RootImport from './${replaceBackslash(
|
||||
path.relative(
|
||||
|
||||
@@ -4,8 +4,16 @@ import {
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
lazy,
|
||||
} from './router'
|
||||
|
||||
export type { TuonoProps } from './types'
|
||||
|
||||
export { createRoute, createRootRoute, createRouter, Link, RouterProvider }
|
||||
export {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
lazy,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import type { Route } from '../route'
|
||||
import { useServerSideProps } from '../hooks/useServerSideProps'
|
||||
|
||||
@@ -19,12 +20,19 @@ export const RouteMatch = ({
|
||||
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
||||
|
||||
if (!route.isRoot) {
|
||||
return route.options.getParentRoute().component({
|
||||
children: route.options.component({ data, isLoading }),
|
||||
data,
|
||||
isLoading,
|
||||
})
|
||||
const Root = route.options.getParentRoute()
|
||||
return (
|
||||
<Root.component data={data} isLoading={isLoading}>
|
||||
<React.Suspense>
|
||||
<route.options.component data={data} isLoading={isLoading} />
|
||||
</React.Suspense>
|
||||
</Root.component>
|
||||
)
|
||||
}
|
||||
|
||||
return route.options.component({ data, isLoading })
|
||||
return (
|
||||
<React.Suspense>
|
||||
<route.options.component data={data} isLoading={isLoading} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export { RouterProvider } from './components/RouterProvider'
|
||||
export { default as Link } from './components/Link'
|
||||
export { createRouter } from './router'
|
||||
export { createRoute, createRootRoute } from './route'
|
||||
export { lazy } from './lazy'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* 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 lazy = (importFn: () => JSX.Element): 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 { lazy } from 'tuono'
|
||||
* const MyComponent = lazy(() => 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'
|
||||
*
|
||||
*/
|
||||
return <></>
|
||||
}
|
||||
+5
-1
@@ -4,7 +4,11 @@
|
||||
"lint": {},
|
||||
"format": {},
|
||||
"format:check": {},
|
||||
"types": {},
|
||||
"types": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test": {},
|
||||
"build": {
|
||||
"outputs": [
|
||||
|
||||
Reference in New Issue
Block a user