From e4d4a75b88a2843af130a417c465378652554636 Mon Sep 17 00:00:00 2001 From: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:39:00 +0200 Subject: [PATCH] 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 --- crates/tuono/Cargo.toml | 2 +- crates/tuono_lib/Cargo.toml | 4 +- crates/tuono_lib_macros/Cargo.toml | 2 +- package.json | 3 +- packages/fs-router-vite-plugin/README.md | 12 + packages/fs-router-vite-plugin/package.json | 51 +++ .../fs-router-vite-plugin/src/constants.ts | 3 + .../src}/generator.ts | 14 +- packages/fs-router-vite-plugin/src/index.ts | 47 +++ .../src}/types.ts | 11 - .../src}/utils.ts | 0 .../tests/generator.spec.ts | 43 +++ .../multi-level/routeTree.expected.ts | 40 +++ .../generator/multi-level/routes/__root.tsx | 1 + .../generator/multi-level/routes/about.tsx | 1 + .../generator/multi-level/routes/index.tsx | 1 + .../multi-level/routes/posts/my-post.tsx | 1 + .../single-level/routeTree.expected.ts | 29 ++ .../generator/single-level/routes/__root.tsx | 1 + .../generator/single-level/routes/about.tsx | 1 + .../generator/single-level/routes/index.tsx | 1 + packages/fs-router-vite-plugin/tsconfig.json | 4 + packages/fs-router-vite-plugin/vite.config.ts | 12 + packages/lazy-fn-vite-plugin/package.json | 2 +- packages/tuono/package.json | 5 +- packages/tuono/src/build/index.ts | 2 +- .../tuono/src/build/routes-generator/index.ts | 21 -- .../src/build/tuono-vite-plugin/compiler.ts | 282 ----------------- .../src/build/tuono-vite-plugin/constants.ts | 1 - .../eliminateUnreferencedIdentifiers.ts | 298 ------------------ .../src/build/tuono-vite-plugin/index.ts | 90 ------ 31 files changed, 261 insertions(+), 724 deletions(-) create mode 100644 packages/fs-router-vite-plugin/README.md create mode 100644 packages/fs-router-vite-plugin/package.json create mode 100644 packages/fs-router-vite-plugin/src/constants.ts rename packages/{tuono/src/build/routes-generator => fs-router-vite-plugin/src}/generator.ts (95%) create mode 100644 packages/fs-router-vite-plugin/src/index.ts rename packages/{tuono/src/build/routes-generator => fs-router-vite-plugin/src}/types.ts (63%) rename packages/{tuono/src/build/routes-generator => fs-router-vite-plugin/src}/utils.ts (100%) create mode 100644 packages/fs-router-vite-plugin/tests/generator.spec.ts create mode 100644 packages/fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts create mode 100644 packages/fs-router-vite-plugin/tests/generator/multi-level/routes/__root.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/multi-level/routes/about.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/multi-level/routes/index.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/multi-level/routes/posts/my-post.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts create mode 100644 packages/fs-router-vite-plugin/tests/generator/single-level/routes/__root.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/single-level/routes/about.tsx create mode 100644 packages/fs-router-vite-plugin/tests/generator/single-level/routes/index.tsx create mode 100644 packages/fs-router-vite-plugin/tsconfig.json create mode 100644 packages/fs-router-vite-plugin/vite.config.ts delete mode 100644 packages/tuono/src/build/routes-generator/index.ts delete mode 100644 packages/tuono/src/build/tuono-vite-plugin/compiler.ts delete mode 100644 packages/tuono/src/build/tuono-vite-plugin/constants.ts delete mode 100644 packages/tuono/src/build/tuono-vite-plugin/eliminateUnreferencedIdentifiers.ts delete mode 100644 packages/tuono/src/build/tuono-vite-plugin/index.ts diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 25f8255f..7930885c 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.4.3" +version = "0.4.4" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index cb1c3813..16c84a91 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.4.3" +version = "0.4.4" edition = "2021" authors = ["V. Ageno "] 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"] } diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index 9d22cdf1..e04b847c 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -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"] diff --git a/package.json b/package.json index 5b8e8b0c..4033a42f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/fs-router-vite-plugin/README.md b/packages/fs-router-vite-plugin/README.md new file mode 100644 index 00000000..3a846741 --- /dev/null +++ b/packages/fs-router-vite-plugin/README.md @@ -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. diff --git a/packages/fs-router-vite-plugin/package.json b/packages/fs-router-vite-plugin/package.json new file mode 100644 index 00000000..394ea755 --- /dev/null +++ b/packages/fs-router-vite-plugin/package.json @@ -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" + } +} diff --git a/packages/fs-router-vite-plugin/src/constants.ts b/packages/fs-router-vite-plugin/src/constants.ts new file mode 100644 index 00000000..c2ed960e --- /dev/null +++ b/packages/fs-router-vite-plugin/src/constants.ts @@ -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' diff --git a/packages/tuono/src/build/routes-generator/generator.ts b/packages/fs-router-vite-plugin/src/generator.ts similarity index 95% rename from packages/tuono/src/build/routes-generator/generator.ts rename to packages/fs-router-vite-plugin/src/generator.ts index 66d654cf..4af951a9 100644 --- a/packages/tuono/src/build/routes-generator/generator.ts +++ b/packages/fs-router-vite-plugin/src/generator.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 { (d): string => d.routePath, ]).filter((d) => ![`/${ROOT_PATH_ID}`].includes(d.routePath || '')) - const routePiecesByPath: Record = {} - // 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 { return } - if (node.isLayout && !node.children?.length) { - return - } - const route = `${node.variableName}Route` if (node.children?.length) { diff --git a/packages/fs-router-vite-plugin/src/index.ts b/packages/fs-router-vite-plugin/src/index.ts new file mode 100644 index 00000000..4b1ade18 --- /dev/null +++ b/packages/fs-router-vite-plugin/src/index.ts @@ -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 => { + if (lock) return + lock = true + + try { + await routeGenerator() + } catch (err) { + console.error(err) + } finally { + lock = false + } + } + + const handleFile = async (file: string): Promise => { + const filePath = normalize(file) + + if (filePath.startsWith(ROUTES_DIRECTORY_PATH)) { + await generate() + } + } + + return { + name: 'vite-plugin-tuono-fs-router', + configResolved: async (): Promise => { + await generate() + }, + watchChange: async ( + file: string, + context: { event: string }, + ): Promise => { + if (['create', 'update', 'delete'].includes(context.event)) { + await handleFile(file) + } + }, + } +} diff --git a/packages/tuono/src/build/routes-generator/types.ts b/packages/fs-router-vite-plugin/src/types.ts similarity index 63% rename from packages/tuono/src/build/routes-generator/types.ts rename to packages/fs-router-vite-plugin/src/types.ts index 77d18743..56fe3cbe 100644 --- a/packages/tuono/src/build/routes-generator/types.ts +++ b/packages/fs-router-vite-plugin/src/types.ts @@ -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 diff --git a/packages/tuono/src/build/routes-generator/utils.ts b/packages/fs-router-vite-plugin/src/utils.ts similarity index 100% rename from packages/tuono/src/build/routes-generator/utils.ts rename to packages/fs-router-vite-plugin/src/utils.ts diff --git a/packages/fs-router-vite-plugin/tests/generator.spec.ts b/packages/fs-router-vite-plugin/tests/generator.spec.ts new file mode 100644 index 00000000..ca310cbb --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator.spec.ts @@ -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) + }, + ) +}) diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts b/packages/fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts new file mode 100644 index 00000000..72301dad --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level/routeTree.expected.ts @@ -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, +]) diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/__root.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/__root.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/__root.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/about.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/about.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/about.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/index.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/index.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/index.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/posts/my-post.tsx b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/posts/my-post.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/multi-level/routes/posts/my-post.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts b/packages/fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts new file mode 100644 index 00000000..23b22805 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/single-level/routeTree.expected.ts @@ -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]) diff --git a/packages/fs-router-vite-plugin/tests/generator/single-level/routes/__root.tsx b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/__root.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/__root.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/single-level/routes/about.tsx b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/about.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/about.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/single-level/routes/index.tsx b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/index.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/single-level/routes/index.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tsconfig.json b/packages/fs-router-vite-plugin/tsconfig.json new file mode 100644 index 00000000..b36d1bf9 --- /dev/null +++ b/packages/fs-router-vite-plugin/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "vite.config.ts"] +} diff --git a/packages/fs-router-vite-plugin/vite.config.ts b/packages/fs-router-vite-plugin/vite.config.ts new file mode 100644 index 00000000..c09134a3 --- /dev/null +++ b/packages/fs-router-vite-plugin/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', + }), +) diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index 36934fde..8a1578c4 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -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", diff --git a/packages/tuono/package.json b/packages/tuono/package.json index f818043a..265905ce 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -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" }, diff --git a/packages/tuono/src/build/index.ts b/packages/tuono/src/build/index.ts index 69af3db5..29d3da12 100644 --- a/packages/tuono/src/build/index.ts +++ b/packages/tuono/src/build/index.ts @@ -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 = { diff --git a/packages/tuono/src/build/routes-generator/index.ts b/packages/tuono/src/build/routes-generator/index.ts deleted file mode 100644 index 75e568f3..00000000 --- a/packages/tuono/src/build/routes-generator/index.ts +++ /dev/null @@ -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 } diff --git a/packages/tuono/src/build/tuono-vite-plugin/compiler.ts b/packages/tuono/src/build/tuono-vite-plugin/compiler.ts deleted file mode 100644 index df19f9dc..00000000 --- a/packages/tuono/src/build/tuono-vite-plugin/compiler.ts +++ /dev/null @@ -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 - refs: Set - 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 => { - return compile({ - code, - filename, - getBabelConfig: () => ({ - plugins: [ - [ - { - visitor: { - Program: { - enter( - programPath: babel.NodePath, - 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) - }, - }, - }, - }, - ], - ], - }), - }) -} diff --git a/packages/tuono/src/build/tuono-vite-plugin/constants.ts b/packages/tuono/src/build/tuono-vite-plugin/constants.ts deleted file mode 100644 index a617de7e..00000000 --- a/packages/tuono/src/build/tuono-vite-plugin/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SPLIT_PREFIX = 'sp-' diff --git a/packages/tuono/src/build/tuono-vite-plugin/eliminateUnreferencedIdentifiers.ts b/packages/tuono/src/build/tuono-vite-plugin/eliminateUnreferencedIdentifiers.ts deleted file mode 100644 index 73f963cc..00000000 --- a/packages/tuono/src/build/tuono-vite-plugin/eliminateUnreferencedIdentifiers.ts +++ /dev/null @@ -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 - -// export function findReferencedIdentifiers( -// programPath: NodePath, -// ): Set { -// const refs = new Set() - -// 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 -// if (isIdentifierReferenced(local)) { -// refs.add(local) -// } -// } else if (path.node.id.type === 'ObjectPattern') { -// const pattern = path.get('id') as NodePath - -// 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 -// if (isIdentifierReferenced(local)) { -// refs.add(local) -// } -// }) -// } else if (path.node.id.type === 'ArrayPattern') { -// const pattern = path.get('id') as NodePath - -// const elements = pattern.get('elements') -// elements.forEach((e) => { -// let local: NodePath -// if (e.node?.type === 'Identifier') { -// local = e as NodePath -// } else if (e.node?.type === 'RestElement') { -// local = e.get('argument') as NodePath -// } 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, - refs?: Set, -): 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 - if (shouldBeRemoved(local)) { - ++referencesRemovedInThisPass - path.remove() - } - } else if (path.node.id.type === 'ObjectPattern') { - const pattern = path.get('id') as NodePath - - 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 - - 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 - - let hasRemoved = false as boolean - - pattern.get('elements').forEach((element, index) => { - // if (!element) return // Skip holes in the pattern - - let identifierPath: NodePath - - if (t.isIdentifier(element.node)) { - identifierPath = element as NodePath - } else if (t.isRestElement(element.node)) { - identifierPath = element.get( - 'argument', - ) as NodePath - } 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 | null { - const parentPath = path.parentPath - if (parentPath.type === 'VariableDeclarator') { - const variablePath = parentPath as NodePath - const name = variablePath.get('id') - return name.node.type === 'Identifier' - ? (name as NodePath) - : null - } - - if (parentPath.type === 'AssignmentExpression') { - const variablePath = parentPath as NodePath - const name = variablePath.get('left') - return name.node.type === 'Identifier' - ? (name as NodePath) - : 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) - : null -} - -function isIdentifierReferenced( - ident: NodePath, -): 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 -} diff --git a/packages/tuono/src/build/tuono-vite-plugin/index.ts b/packages/tuono/src/build/tuono-vite-plugin/index.ts deleted file mode 100644 index f40d7fad..00000000 --- a/packages/tuono/src/build/tuono-vite-plugin/index.ts +++ /dev/null @@ -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 => { - if (lock) return - lock = true - - try { - // TODO: generator function - await routeGenerator() - } catch (err) { - } finally { - lock = false - } - } - - const handleFile = async (file: string): Promise => { - 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 => { - await generate() - }, - watchChange: async ( - file: string, - context: { event: string }, - ): Promise => { - 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 { - 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()] -}