Crate standalone fs router package (#11)

* refactor: moved fs-router to standalone package

* chore: remove useless split source function

* feat: add simple test cases

* fix: README formatting

* fix: remove useless RouteSubNode

* feat: update version to v0.4.4
This commit is contained in:
Valerio Ageno
2024-07-08 21:39:00 +02:00
committed by GitHub
parent bc7ffc2246
commit e4d4a75b88
31 changed files with 261 additions and 724 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.4.3"
version = "0.4.4"
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.4.3"
version = "0.4.4"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
@@ -31,7 +31,7 @@ regex = "1.10.5"
either = "1.13.0"
tower-http = {version = "0.5.2", features = ["fs"]}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.3"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.4"}
# Match the same version used by axum
tokio-tungstenite = "0.21.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.4.3"
version = "0.4.4"
edition = "2021"
description = "The react/rust fullstack framework"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
+2 -1
View File
@@ -13,7 +13,8 @@
},
"workspaces": [
"tuono",
"tuono-lazy-fn-vite-plugin"
"tuono-lazy-fn-vite-plugin",
"tuono-fs-router-vite-plugin"
],
"author": "Valerio Ageno",
"license": "MIT",
+12
View File
@@ -0,0 +1,12 @@
# tuono-fs-router-vite-plugin
This is a vite plugin for [tuono](https://github.com/Valerioageno/tuono).
This package specifically handles the file system based routing.
Check [tuono](https://github.com/Valerioageno/tuono) for more.
## Credits
This plugin is strongly inspired by the [@tanstack/router](https://tanstack.com/router/latest)
route generator plugin.
@@ -0,0 +1,51 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.4.4",
"description": "Plugin for the tuono's file system router. 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:watch": "vitest",
"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",
"prettier": "^3.2.4",
"vite": "^5.2.11"
},
"devDependencies": {
"@tanstack/config": "^0.7.11",
"@types/babel__core": "^7.20.5",
"vitest": "^1.5.2"
}
}
@@ -0,0 +1,3 @@
export const ROUTES_FOLDER = './src/routes/'
export const ROOT_PATH_ID = '__root'
export const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts'
@@ -1,4 +1,3 @@
import * as fs from 'fs'
import * as fsp from 'fs/promises'
import path from 'path'
@@ -15,14 +14,11 @@ import {
removeLayoutSegments,
} from './utils'
import type { Config, RouteNode, RouteSubNode } from './types'
import type { Config, RouteNode } from './types'
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
import { format } from 'prettier'
const ROUTES_FOLDER = './src/routes/'
const ROOT_PATH_ID = '__root'
const GENERATED_ROUTE_TREE = './.tuono/routeTree.gen.ts'
let latestTask = 0
const defaultConfig: Config = {
@@ -168,8 +164,6 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
(d): string => d.routePath,
]).filter((d) => ![`/${ROOT_PATH_ID}`].includes(d.routePath || ''))
const routePiecesByPath: Record<string, RouteSubNode> = {}
// Loop over the flat list of routeNodes and
// build up a tree based on the routeNodes' routePath
const routeNodes: RouteNode[] = []
@@ -202,10 +196,6 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
return
}
if (node.isLayout && !node.children?.length) {
return
}
const route = `${node.variableName}Route`
if (node.children?.length) {
@@ -0,0 +1,47 @@
import { routeGenerator } from './generator'
import { normalize } from 'path'
import type { Plugin } from 'vite'
const ROUTES_DIRECTORY_PATH = './src/routes'
let lock = false
export default function RouterGenerator(): Plugin {
const generate = async (): Promise<void> => {
if (lock) return
lock = true
try {
await routeGenerator()
} catch (err) {
console.error(err)
} finally {
lock = false
}
}
const handleFile = async (file: string): Promise<void> => {
const filePath = normalize(file)
if (filePath.startsWith(ROUTES_DIRECTORY_PATH)) {
await generate()
}
}
return {
name: 'vite-plugin-tuono-fs-router',
configResolved: async (): Promise<void> => {
await generate()
},
watchChange: async (
file: string,
context: { event: string },
): Promise<void> => {
if (['create', 'update', 'delete'].includes(context.event)) {
await handleFile(file)
}
},
}
}
@@ -11,17 +11,6 @@ export interface RouteNode {
variableName?: string
}
/**
* @deprecated
*/
export interface RouteSubNode {
component?: RouteNode
errorComponent?: RouteNode
pendingComponent?: RouteNode
loader?: RouteNode
lazy?: RouteNode
}
export interface Config {
folderName: string
generatedRouteTree: string
@@ -0,0 +1,43 @@
import fs from 'fs/promises'
import { describe, it, expect } from 'vitest'
import { routeGenerator } from '../src/generator'
function makeFolderDir(folder: string) {
return process.cwd() + `/tests/generator/${folder}`
}
async function getRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
return await fs.readFile(dir + '/routeTree.gen.ts', 'utf-8')
}
async function getExpectedRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
const location = dir + '/routeTree.expected.ts'
return await fs.readFile(location, 'utf-8')
}
describe('generator works', async () => {
const folderNames = await fs.readdir(process.cwd() + '/tests/generator')
it.each(folderNames.map((folder) => [folder]))(
'should wire-up the routes for a "%s" tree',
async (folderName) => {
const currentFolder = `${process.cwd()}/tests/generator/${folderName}`
await routeGenerator({
folderName: `${currentFolder}/routes`,
generatedRouteTree: `${currentFolder}/routeTree.gen.ts`,
})
const [expectedRouteTree, generatedRouteTree] = await Promise.all([
getExpectedRouteTreeFileText(folderName),
getRouteTreeFileText(folderName),
])
console.log(generatedRouteTree)
expect(generatedRouteTree).equal(expectedRouteTree)
},
)
})
@@ -0,0 +1,40 @@
// This file is auto-generated by Tuono
import { createRoute, dynamic } from 'tuono'
import RootImport from './routes/__root'
const AboutImport = dynamic(() => import('./routes/about'))
const IndexImport = dynamic(() => import('./routes/index'))
const PostsMyPostImport = dynamic(() => import('./routes/posts/my-post'))
const rootRoute = createRoute({ isRoot: true, component: RootImport })
const About = createRoute({ component: AboutImport })
const Index = createRoute({ component: IndexImport })
const PostsMyPost = createRoute({ component: PostsMyPostImport })
// Create/Update Routes
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
})
const PostsMyPostRoute = PostsMyPost.update({
path: '/posts/my-post',
getParentRoute: () => rootRoute,
})
// Create and export the route tree
export const routeTree = rootRoute.addChildren([
IndexRoute,
AboutRoute,
PostsMyPostRoute,
])
@@ -0,0 +1,29 @@
// This file is auto-generated by Tuono
import { createRoute, dynamic } from 'tuono'
import RootImport from './routes/__root'
const AboutImport = dynamic(() => import('./routes/about'))
const IndexImport = dynamic(() => import('./routes/index'))
const rootRoute = createRoute({ isRoot: true, component: RootImport })
const About = createRoute({ component: AboutImport })
const Index = createRoute({ component: IndexImport })
// Create/Update Routes
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
})
// Create and export the route tree
export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute])
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.4.3",
"version": "0.4.4",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.4.3",
"version": "0.4.4",
"description": "The react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
@@ -88,7 +88,7 @@
"@types/node": "^20.12.7",
"@vitejs/plugin-react-swc": "^3.7.0",
"fast-text-encoding": "^1.0.6",
"prettier": "^3.2.4",
"tuono-fs-router-vite-plugin": "workspace:*",
"tuono-lazy-fn-vite-plugin": "workspace:*",
"vite": "^5.2.11",
"zustand": "4.4.7"
@@ -100,6 +100,7 @@
"@types/babel__traverse": "^7.20.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"prettier": "^3.2.4",
"jsdom": "^24.0.0",
"vitest": "^1.5.2"
},
+1 -1
View File
@@ -1,6 +1,6 @@
import { build, createServer, InlineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { ViteFsRouter } from './tuono-vite-plugin'
import ViteFsRouter from 'tuono-fs-router-vite-plugin'
import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin'
const BASE_CONFIG: InlineConfig = {
@@ -1,21 +0,0 @@
/**
* Overview:
*
* src/routes = Routes entry point
* src/routes/layout.tsx = Shared layout
* src/routes/index.tsx = xyz.com
* src/routes/about.tsx = xyz.com/about
* src/routes/about-loading.tsx = xyz.com/about - while loading data
* src/routes/about/index.tsx = xyz.com/about
* src/routes/posts/[slug].tsx = xyz.com/posts/my-lovely-post
* src/routes/posts/[...params].tsx = xyz.com/posts/my-lovely-post/commend-id-304
* src/routes/404.tsx = Not found
*
* public/ = Public files
*
* All the routes are lazy loaded!
*/
import { routeGenerator } from './generator'
export { routeGenerator }
@@ -1,282 +0,0 @@
import * as babel from '@babel/core'
import * as t from '@babel/types'
import { eliminateUnreferencedIdentifiers } from './eliminateUnreferencedIdentifiers'
import { SPLIT_PREFIX } from './constants'
import type { BabelFileResult } from '@babel/core'
type SplitModulesById = Record<
string,
{ id: string; node: t.FunctionExpression }
>
interface State {
filename: string
opts: {
minify: boolean
root: string
}
imported: Record<string, boolean>
refs: Set<any>
serverIndex: number
splitIndex: number
splitModulesById: SplitModulesById
}
interface MakeCompileFnArgs {
root: string
}
interface MakeCompileFnReturnArgs {
code: string
filename: string
getBabelConfig: () => { plugins: any[] }
}
interface CompileOutput {
code: string
map: BabelFileResult['map']
}
type SplitNodeType = (typeof splitNodeTypes)[number]
export const makeCompile = ({
root,
}: MakeCompileFnArgs): ((args: MakeCompileFnReturnArgs) => CompileOutput) => {
return ({ code, getBabelConfig, filename }) => {
const res = babel.transformSync(code, {
plugins: [
['@babel/plugin-syntax-jsx', {}],
['@babel/plugin-syntax-typescript', { isTSX: true }],
...getBabelConfig().plugins,
],
root,
filename,
sourceMaps: true,
})
if (res?.code) {
return {
code: res.code,
map: res.map,
}
}
return {
code,
map: null,
}
}
}
interface SplitFileFnArgs {
code: string
compile: (args: MakeCompileFnReturnArgs) => CompileOutput
filename: string
}
// NOTE: We don't need the loader
const splitNodeTypes = ['component', 'loader'] as const
// Reusable function to get literal value or resolve variable to literal
function resolveIdentifier(path: any, node: any) {
if (t.isIdentifier(node)) {
const binding = path.scope.getBinding(node.name)
if (binding) {
const declarator = binding.path.node
if (t.isObjectExpression(declarator.init)) {
return declarator.init
} else if (t.isFunctionDeclaration(declarator.init)) {
return declarator.init
}
}
return undefined
}
return node
}
export const splitFile = async ({
code,
compile,
filename,
}: SplitFileFnArgs): Promise<CompileOutput> => {
return compile({
code,
filename,
getBabelConfig: () => ({
plugins: [
[
{
visitor: {
Program: {
enter(
programPath: babel.NodePath<t.Program>,
state: State,
): void {
const splitNodesByType: Record<
SplitNodeType,
t.Node | undefined
> = {
component: undefined,
loader: undefined,
}
// Find the node
// NOTE: I'd use a more NextJS approach.
// All the "default exported" fns are the page.
programPath.traverse(
{
CallExpression: (path) => {
if (path.node.callee.type === 'Identifier') {
// TODO: Update this to support just "default exported" fns
if (path.node.callee.name === 'createFileRoute') {
if (
path.parentPath.node.type === 'CallExpression'
) {
const options = resolveIdentifier(
path,
path.parentPath.node.arguments[0],
)
if (t.isObjectExpression(options)) {
options.properties.forEach((prop) => {
if (t.isObjectProperty(prop)) {
splitNodeTypes.forEach((type) => {
if (t.isIdentifier(prop.key)) {
if (prop.key.name === 'type') {
splitNodesByType[type] = prop.value
}
}
})
}
})
// Remove all of the options
options.properties = []
}
}
}
}
},
},
state,
)
// TODO: We don't need to iterate since we don't support the loader
splitNodeTypes.forEach((splitType) => {
let splitNode = splitNodesByType[splitType]
if (!splitNode) {
return
}
while (t.isIdentifier(splitNode)) {
const binding = programPath.scope.getBinding(
splitNode.name,
)
splitNode = binding?.path.node
}
// Add the node to the program
if (splitNode) {
if (t.isFunctionDeclaration(splitNode)) {
programPath.pushContainer(
'body',
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(splitType),
t.functionExpression(
splitNode.id || null,
splitNode.params,
splitNode.body,
splitNode.generator,
splitNode.async,
),
),
]),
)
} else if (
t.isFunctionExpression(splitNode) ||
t.isArrowFunctionExpression(splitNode)
) {
programPath.pushContainer(
'body',
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(splitType),
splitType as any,
),
]),
)
} else if (t.isImportSpecifier(splitNode)) {
programPath.pushContainer(
'body',
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(splitType),
splitNode.local,
),
]),
)
} else {
throw new Error(
`Unexpected splitNode type ${splitNode.type}`,
)
}
}
// If the splitNode exists at the top of the program
// then we need to remove that copy
programPath.node.body = programPath.node.body.filter(
(node) => {
return node !== splitNode
},
)
// Export the node
programPath.pushContainer('body', [
t.exportNamedDeclaration(null, [
t.exportSpecifier(
t.identifier(splitType),
t.identifier(splitType),
),
]),
])
})
// Convert exports to imports from the original file
programPath.traverse({
ExportNamedDeclaration(path) {
// e.g. export const x = 1 or export { x }
// becomes
// import { x } from '${opts.id}'
if (path.node.declaration) {
if (t.isVariableDeclaration(path.node.declaration)) {
path.replaceWith(
t.importDeclaration(
path.node.declaration.declarations.map((decl) =>
t.importSpecifier(
t.identifier((decl.id as any).name),
t.identifier((decl.id as any).name),
),
),
t.stringLiteral(
filename.split(`?${SPLIT_PREFIX}`)[0] as string,
),
),
)
}
}
},
})
eliminateUnreferencedIdentifiers(programPath)
},
},
},
},
],
],
}),
})
}
@@ -1 +0,0 @@
export const SPLIT_PREFIX = 'sp-'
@@ -1,298 +0,0 @@
// Copied from https://github.com/pcattori/vite-env-only/blob/main/src/dce.ts
// Adapted with some minor changes for the purpose of this project
import * as t from '@babel/types'
import type { types as BabelTypes } from '@babel/core'
import type { NodePath } from '@babel/traverse'
type IdentifierPath = NodePath<BabelTypes.Identifier>
// export function findReferencedIdentifiers(
// programPath: NodePath<BabelTypes.Program>,
// ): Set<IdentifierPath> {
// const refs = new Set<IdentifierPath>()
// function markFunction(
// path: NodePath<
// | BabelTypes.FunctionDeclaration
// | BabelTypes.FunctionExpression
// | BabelTypes.ArrowFunctionExpression
// >,
// ) {
// const ident = getIdentifier(path)
// if (ident?.node && isIdentifierReferenced(ident)) {
// refs.add(ident)
// }
// }
// function markImport(
// path: NodePath<
// | BabelTypes.ImportSpecifier
// | BabelTypes.ImportDefaultSpecifier
// | BabelTypes.ImportNamespaceSpecifier
// >,
// ) {
// const local = path.get('local')
// if (isIdentifierReferenced(local)) {
// refs.add(local)
// }
// }
// programPath.traverse({
// VariableDeclarator(path) {
// if (path.node.id.type === 'Identifier') {
// const local = path.get('id') as NodePath<BabelTypes.Identifier>
// if (isIdentifierReferenced(local)) {
// refs.add(local)
// }
// } else if (path.node.id.type === 'ObjectPattern') {
// const pattern = path.get('id') as NodePath<BabelTypes.ObjectPattern>
// const properties = pattern.get('properties')
// properties.forEach((p) => {
// const local = p.get(
// p.node.type === 'ObjectProperty'
// ? 'value'
// : p.node.type === 'RestElement'
// ? 'argument'
// : (function () {
// throw new Error('invariant')
// })(),
// ) as NodePath<BabelTypes.Identifier>
// if (isIdentifierReferenced(local)) {
// refs.add(local)
// }
// })
// } else if (path.node.id.type === 'ArrayPattern') {
// const pattern = path.get('id') as NodePath<BabelTypes.ArrayPattern>
// const elements = pattern.get('elements')
// elements.forEach((e) => {
// let local: NodePath<BabelTypes.Identifier>
// if (e.node?.type === 'Identifier') {
// local = e as NodePath<BabelTypes.Identifier>
// } else if (e.node?.type === 'RestElement') {
// local = e.get('argument') as NodePath<BabelTypes.Identifier>
// } else {
// return
// }
// if (isIdentifierReferenced(local)) {
// refs.add(local)
// }
// })
// }
// },
// FunctionDeclaration: markFunction,
// FunctionExpression: markFunction,
// ArrowFunctionExpression: markFunction,
// ImportSpecifier: markImport,
// ImportDefaultSpecifier: markImport,
// ImportNamespaceSpecifier: markImport,
// })
// return refs
// }
/**
* @param refs - If provided, only these identifiers will be considered for removal.
*/
export const eliminateUnreferencedIdentifiers = (
programPath: NodePath<BabelTypes.Program>,
refs?: Set<IdentifierPath>,
): void => {
let referencesRemovedInThisPass: number
const shouldBeRemoved = (ident: IdentifierPath): boolean => {
if (isIdentifierReferenced(ident)) return false
if (!refs) return true
return refs.has(ident)
}
const sweepFunction = (
path: NodePath<
| BabelTypes.FunctionDeclaration
| BabelTypes.FunctionExpression
| BabelTypes.ArrowFunctionExpression
>,
): void => {
const identifier = getIdentifier(path)
if (identifier?.node && shouldBeRemoved(identifier)) {
++referencesRemovedInThisPass
if (
t.isAssignmentExpression(path.parentPath.node) ||
t.isVariableDeclarator(path.parentPath.node)
) {
path.parentPath.remove()
} else {
path.remove()
}
}
}
const sweepImport = (
path: NodePath<
| BabelTypes.ImportSpecifier
| BabelTypes.ImportDefaultSpecifier
| BabelTypes.ImportNamespaceSpecifier
>,
): void => {
const local = path.get('local')
if (shouldBeRemoved(local)) {
++referencesRemovedInThisPass
path.remove()
if (
(path.parent as BabelTypes.ImportDeclaration).specifiers.length === 0
) {
path.parentPath.remove()
}
}
}
// Traverse again to remove unused references. This happens at least once,
// then repeats until no more references are removed.
do {
referencesRemovedInThisPass = 0
programPath.scope.crawl()
programPath.traverse({
VariableDeclarator(path: any) {
if (path.node.id.type === 'Identifier') {
const local = path.get('id') as NodePath<BabelTypes.Identifier>
if (shouldBeRemoved(local)) {
++referencesRemovedInThisPass
path.remove()
}
} else if (path.node.id.type === 'ObjectPattern') {
const pattern = path.get('id') as NodePath<BabelTypes.ObjectPattern>
const beforeCount = referencesRemovedInThisPass
const properties = pattern.get('properties')
properties.forEach((property) => {
const local = property.get(
property.node.type === 'ObjectProperty'
? 'value'
: // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
property.node.type === 'RestElement'
? 'argument'
: (function () {
throw new Error('invariant')
})(),
) as NodePath<BabelTypes.Identifier>
if (shouldBeRemoved(local)) {
++referencesRemovedInThisPass
property.remove()
}
})
if (
beforeCount !== referencesRemovedInThisPass &&
pattern.get('properties').length < 1
) {
path.remove()
}
} else if (path.node.id.type === 'ArrayPattern') {
const pattern = path.get('id') as NodePath<BabelTypes.ArrayPattern>
let hasRemoved = false as boolean
pattern.get('elements').forEach((element, index) => {
// if (!element) return // Skip holes in the pattern
let identifierPath: NodePath<BabelTypes.Identifier>
if (t.isIdentifier(element.node)) {
identifierPath = element as NodePath<BabelTypes.Identifier>
} else if (t.isRestElement(element.node)) {
identifierPath = element.get(
'argument',
) as NodePath<BabelTypes.Identifier>
} else {
// For now, ignore other types like AssignmentPattern
return
}
if (shouldBeRemoved(identifierPath)) {
hasRemoved = true
pattern.node.elements[index] = null // Remove the element by setting it to null
}
})
// If any elements were removed and no elements are left, remove the entire declaration
if (
hasRemoved &&
pattern.node.elements.every((element) => element === null)
) {
path.remove()
++referencesRemovedInThisPass
}
}
},
FunctionDeclaration: sweepFunction,
FunctionExpression: sweepFunction,
ArrowFunctionExpression: sweepFunction,
ImportSpecifier: sweepImport,
ImportDefaultSpecifier: sweepImport,
ImportNamespaceSpecifier: sweepImport,
})
} while (referencesRemovedInThisPass)
}
function getIdentifier(
path: NodePath<
| BabelTypes.FunctionDeclaration
| BabelTypes.FunctionExpression
| BabelTypes.ArrowFunctionExpression
>,
): NodePath<BabelTypes.Identifier> | null {
const parentPath = path.parentPath
if (parentPath.type === 'VariableDeclarator') {
const variablePath = parentPath as NodePath<BabelTypes.VariableDeclarator>
const name = variablePath.get('id')
return name.node.type === 'Identifier'
? (name as NodePath<BabelTypes.Identifier>)
: null
}
if (parentPath.type === 'AssignmentExpression') {
const variablePath = parentPath as NodePath<BabelTypes.AssignmentExpression>
const name = variablePath.get('left')
return name.node.type === 'Identifier'
? (name as NodePath<BabelTypes.Identifier>)
: null
}
if (path.node.type === 'ArrowFunctionExpression') {
return null
}
if (path.node.type === 'FunctionExpression') {
return null
}
return path.node.id && path.node.id.type === 'Identifier'
? (path.get('id') as NodePath<BabelTypes.Identifier>)
: null
}
function isIdentifierReferenced(
ident: NodePath<BabelTypes.Identifier>,
): boolean {
const binding = ident.scope.getBinding(ident.node.name)
if (binding?.referenced) {
// Functions can reference themselves, so we need to check if there's a
// binding outside the function scope or not.
if (binding.path.type === 'FunctionDeclaration') {
return !binding.constantViolations
.concat(binding.referencePaths)
// Check that every reference is contained within the function:
.every((ref) => ref.findParent((parent) => parent === binding.path))
}
return true
}
return false
}
@@ -1,90 +0,0 @@
import { fileURLToPath, pathToFileURL } from 'url'
import { routeGenerator } from '../routes-generator'
import { normalize } from 'path'
// eslint-disable-next-line sort-imports
import { makeCompile, splitFile } from './compiler'
import { SPLIT_PREFIX } from './constants'
import type { Plugin } from 'vite'
const ROUTES_DIRECTORY_PATH = './src/routes'
let lock = false
export function RouterGenerator(): Plugin {
const generate = async (): Promise<void> => {
if (lock) return
lock = true
try {
// TODO: generator function
await routeGenerator()
} catch (err) {
} finally {
lock = false
}
}
const handleFile = async (file: string): Promise<void> => {
const filePath = normalize(file)
if (filePath.startsWith(ROUTES_DIRECTORY_PATH)) {
// TODO: generator function
await generate()
}
}
return {
name: 'vite-plugin-tuono-fs-router-generator',
configResolved: async (): Promise<void> => {
await generate()
},
watchChange: async (
file: string,
context: { event: string },
): Promise<void> => {
if (['create', 'update', 'delete'].includes(context.event)) {
await handleFile(file)
}
},
}
}
export function RouterCodeSplitter(): Plugin {
const ROOT: string = process.cwd()
return {
name: 'vite-plugin-tuono-fs-router-code-splitter',
enforce: 'pre',
resolveId(source): string | null {
if (source.startsWith(SPLIT_PREFIX + ':')) {
return source.replace(SPLIT_PREFIX + ':', '')
}
return null
},
async transform(code, id): Promise<any> {
const url = pathToFileURL(id)
url.searchParams.delete('v')
id = fileURLToPath(url).replace(/\\/g, '/')
const compile = makeCompile({ root: ROOT })
if (id.includes(SPLIT_PREFIX)) {
const compiled = await splitFile({
code,
compile,
filename: id,
})
return compiled
}
return null
},
}
}
export function ViteFsRouter(): Plugin[] {
return [RouterGenerator(), RouterCodeSplitter()]
}