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:
Valerio Ageno
2024-06-20 19:40:14 +02:00
committed by GitHub
parent 6498905377
commit f37cb701b3
21 changed files with 323 additions and 64 deletions
+1
View File
@@ -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:
+19 -13
View File
@@ -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 -1
View File
@@ -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"
+2 -2
View File
@@ -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 -1
View File
@@ -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"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.1.4",
"version": "0.1.5",
"description": "",
"main": "src/index.js",
"packageManager": "pnpm@9.1.1",
@@ -9,8 +9,8 @@
"build": "turbo build",
"lint": "turbo lint",
"format": "turbo format",
"format:check": "turbo format:check",
"types": "turbo types",
"format:check": "turbo format:check",
"types": "turbo types",
"test": "turbo test"
},
"keywords": [],
@@ -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
+1
View File
@@ -0,0 +1 @@
# tuono-lazy-fn-vite-plugin
+50
View File
@@ -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"
}
}
+116
View File
@@ -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',
}),
)
+3 -2
View File
@@ -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"
},
+2 -1
View File
@@ -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(
+9 -1
View File
@@ -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>
)
}
+1
View File
@@ -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'
+36
View File
@@ -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
View File
@@ -4,7 +4,11 @@
"lint": {},
"format": {},
"format:check": {},
"types": {},
"types": {
"dependsOn": [
"^build"
]
},
"test": {},
"build": {
"outputs": [