-
-
Notifications
You must be signed in to change notification settings - Fork 709
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(start-vite-plugin): copy the TanStack Start specific plugin logi…
…c into its own standalone vite plugin (#1768) * copy the router-vite-plugin into start-vite-plugin * test: remove the tests we dont need * export config-name from generator * undo that last one * reduce this down * eliminate the router logic * undo the touches to the generator and cli * move this down here * eslint * chore: remove deps that are not needed * test: test for createServerFn being renamed during the destructured import * fix: handle destructured rename case * test: handle star imports * chore: push * chore: remove this
- Loading branch information
1 parent
92c6c2b
commit 1f35bcb
Showing
17 changed files
with
732 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// @ts-check | ||
|
||
/** @type {import('eslint').Linter.Config} */ | ||
const config = { | ||
ignorePatterns: ['test-files', 'snapshots', 'tests'], | ||
} | ||
|
||
module.exports = config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<img src="https://static.scarf.sh/a.png?x-pxid=d988eb79-b0fc-4a2b-8514-6a1ab932d188" /> | ||
|
||
# TanStack Start Vite Plugin | ||
|
||
See https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
{ | ||
"name": "@tanstack/start-vite-plugin", | ||
"version": "1.38.0", | ||
"description": "", | ||
"author": "Tanner Linsley", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/TanStack/router.git", | ||
"directory": "packages/start-vite-plugin" | ||
}, | ||
"homepage": "https://tanstack.com/router", | ||
"funding": { | ||
"type": "github", | ||
"url": "https://github.com/sponsors/tannerlinsley" | ||
}, | ||
"type": "module", | ||
"types": "dist/esm/index.d.ts", | ||
"main": "dist/cjs/index.cjs", | ||
"module": "dist/esm/index.js", | ||
"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" | ||
}, | ||
"sideEffects": false, | ||
"scripts": { | ||
"clean": "rimraf ./dist && rimraf ./coverage", | ||
"test": "pnpm test:eslint && pnpm test:types && pnpm test:build && pnpm test:unit", | ||
"test:unit": "vitest --typecheck --watch=false", | ||
"test:eslint": "eslint --ext .ts,.tsx ./src", | ||
"test:types": "tsc --noEmit", | ||
"test:build": "publint --strict", | ||
"build": "vite build" | ||
}, | ||
"keywords": [ | ||
"react", | ||
"location", | ||
"router", | ||
"routing", | ||
"async", | ||
"async router", | ||
"typescript" | ||
], | ||
"engines": { | ||
"node": ">=12" | ||
}, | ||
"files": [ | ||
"dist", | ||
"src/**" | ||
], | ||
"dependencies": { | ||
"@babel/core": "^7.23.7", | ||
"@babel/generator": "^7.23.6", | ||
"@babel/plugin-syntax-jsx": "^7.24.1", | ||
"@babel/plugin-syntax-typescript": "^7.24.1", | ||
"@babel/plugin-transform-react-jsx": "^7.23.4", | ||
"@babel/plugin-transform-typescript": "^7.24.1", | ||
"@babel/template": "^7.24.0", | ||
"@babel/traverse": "^7.24.1", | ||
"@babel/types": "^7.24.0", | ||
"@types/babel__core": "^7.20.5", | ||
"@types/babel__generator": "^7.6.8", | ||
"@types/babel__template": "^7.4.4", | ||
"@types/babel__traverse": "^7.20.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import * as babel from '@babel/core' | ||
|
||
export type CompileAstFn = (compileOpts: { | ||
code: string | ||
filename: string | ||
getBabelConfig: () => { plugins: Array<any> } | ||
}) => Promise<{ | ||
code: string | ||
map: any | ||
}> | ||
|
||
export function compileAst(makeOpts: { root: string }) { | ||
return async (opts: { | ||
code: string | ||
filename: string | ||
getBabelConfig: () => { plugins: Array<any> } | ||
}): Promise<{ | ||
code: string | ||
map: any | ||
}> => { | ||
const res = babel.transformSync(opts.code, { | ||
plugins: [ | ||
['@babel/plugin-syntax-jsx', {}], | ||
[ | ||
'@babel/plugin-syntax-typescript', | ||
{ | ||
isTSX: true, | ||
}, | ||
], | ||
...opts.getBabelConfig().plugins, | ||
], | ||
root: makeOpts.root, | ||
filename: opts.filename, | ||
sourceMaps: true, | ||
}) | ||
|
||
if (res?.code) { | ||
return { | ||
code: res.code, | ||
map: res.map, | ||
} | ||
} | ||
|
||
return { | ||
code: opts.code, | ||
map: null, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import * as t from '@babel/types' | ||
|
||
import type * as babel from '@babel/core' | ||
import type { CompileAstFn } from './ast' | ||
|
||
export async function createServerFnCompiler(opts: { | ||
code: string | ||
compile: CompileAstFn | ||
filename: string | ||
}) { | ||
return await opts.compile({ | ||
code: opts.code, | ||
filename: opts.filename, | ||
getBabelConfig: () => ({ | ||
plugins: [ | ||
[ | ||
{ | ||
visitor: { | ||
Program: { | ||
enter(programPath: babel.NodePath<t.Program>) { | ||
let identifierType: | ||
| 'ImportSpecifier' | ||
| 'ImportNamespaceSpecifier' = 'ImportSpecifier' | ||
|
||
let namespaceId = '' | ||
let serverFnId = 'createServerFn' | ||
|
||
programPath.traverse({ | ||
ImportDeclaration: (path) => { | ||
if (path.node.source.value !== '@tanstack/start') { | ||
return | ||
} | ||
|
||
path.node.specifiers.forEach((specifier) => { | ||
// handles a destructured import being renamed like "import { createServerFn as myCreateServerFn } from '@tanstack/start';" | ||
if ( | ||
specifier.type === 'ImportSpecifier' && | ||
specifier.imported.type === 'Identifier' | ||
) { | ||
if (specifier.imported.name === 'createServerFn') { | ||
serverFnId = specifier.local.name | ||
identifierType = 'ImportSpecifier' | ||
} | ||
} | ||
|
||
// handles a namespace import like "import * as TanStackStart from '@tanstack/start';" | ||
if (specifier.type === 'ImportNamespaceSpecifier') { | ||
identifierType = 'ImportNamespaceSpecifier' | ||
namespaceId = specifier.local.name | ||
serverFnId = `${specifier.local.name}.createServerFn` | ||
} | ||
}) | ||
}, | ||
CallExpression: (path) => { | ||
const importSpecifierCondition = | ||
path.node.callee.type === 'Identifier' && | ||
path.node.callee.name === serverFnId | ||
|
||
const importNamespaceSpecifierCondition = | ||
path.node.callee.type === 'MemberExpression' && | ||
path.node.callee.property.type === 'Identifier' && | ||
path.node.callee.property.name === 'createServerFn' | ||
|
||
const createServerFnEntryCondition = | ||
identifierType === 'ImportSpecifier' | ||
? importSpecifierCondition | ||
: importNamespaceSpecifierCondition | ||
|
||
if (createServerFnEntryCondition) { | ||
// If the function at createServerFn(_, MyFunc) doesn't have a | ||
// 'use server' directive at the top of the function scope, | ||
// then add it. | ||
const fn = path.node.arguments[1] | ||
|
||
if ( | ||
t.isFunctionExpression(fn) || | ||
t.isArrowFunctionExpression(fn) | ||
) { | ||
if (t.isBlockStatement(fn.body)) { | ||
const hasUseServerDirective = | ||
fn.body.directives.some((directive) => { | ||
return directive.value.value === 'use server' | ||
}) | ||
|
||
if (!hasUseServerDirective) { | ||
fn.body.directives.unshift( | ||
t.directive(t.directiveLiteral('use server')), | ||
) | ||
} | ||
} | ||
} else if ( | ||
t.isIdentifier(fn) || | ||
t.isCallExpression(fn) | ||
) { | ||
// A function was passed to createServerFn in the form of an | ||
// identifier or a call expression that returns a function. | ||
|
||
// We wrap the identifier/call expression in a function | ||
// expression that accepts the same arguments as the original | ||
// function with the "use server" directive at the top of the | ||
// function scope. | ||
|
||
const args = t.restElement(t.identifier('args')) | ||
|
||
// Annotate args with the type: | ||
// Parameters<Parameters<typeof createServerFn>[1]> | ||
|
||
args.typeAnnotation = t.tsTypeAnnotation( | ||
t.tsTypeReference( | ||
t.identifier('Parameters'), | ||
t.tsTypeParameterInstantiation([ | ||
t.tsIndexedAccessType( | ||
t.tsTypeReference( | ||
t.identifier('Parameters'), | ||
t.tsTypeParameterInstantiation([ | ||
t.tsTypeQuery(t.identifier(serverFnId)), | ||
]), | ||
), | ||
t.tsLiteralType(t.numericLiteral(1)), | ||
), | ||
]), | ||
), | ||
) | ||
|
||
const wrappedFn = t.arrowFunctionExpression( | ||
[args], | ||
t.blockStatement( | ||
[ | ||
t.returnStatement( | ||
t.callExpression( | ||
t.memberExpression( | ||
fn, | ||
t.identifier('apply'), | ||
), | ||
[ | ||
t.identifier('this'), | ||
t.identifier('args'), | ||
], | ||
), | ||
), | ||
], | ||
[t.directive(t.directiveLiteral('use server'))], | ||
), | ||
) | ||
|
||
path.node.arguments[1] = wrappedFn | ||
} | ||
} | ||
}, | ||
}) | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
root: process.cwd(), | ||
minify: process.env.NODE_ENV === 'production', | ||
}, | ||
], | ||
].filter(Boolean), | ||
}), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { fileURLToPath, pathToFileURL } from 'node:url' | ||
import { compileAst } from './ast' | ||
import { createServerFnCompiler } from './compilers' | ||
import type { Plugin } from 'vite' | ||
|
||
const debug = Boolean(process.env.TSR_VITE_DEBUG) | ||
|
||
export function TanStackStartVite(): Array<Plugin> { | ||
return [TanStackStartViteCreateServerFn()] | ||
} | ||
|
||
export function TanStackStartViteCreateServerFn(): Plugin { | ||
let ROOT: string = process.cwd() | ||
|
||
return { | ||
name: 'vite-plugin-tanstack-start-create-server-fn', | ||
enforce: 'pre', | ||
configResolved: async (config) => { | ||
ROOT = config.root | ||
}, | ||
async transform(code, id) { | ||
const url = pathToFileURL(id) | ||
url.searchParams.delete('v') | ||
id = fileURLToPath(url).replace(/\\/g, '/') | ||
|
||
const compile = compileAst({ | ||
root: ROOT, | ||
}) | ||
|
||
if (code.includes('createServerFn')) { | ||
if (code.includes('@react-refresh')) { | ||
throw new Error( | ||
`We detected that the '@vitejs/plugin-react' was passed before '@tanstack/start-vite-plugin'. Please make sure that '@tanstack/router-vite-plugin' is passed before '@vitejs/plugin-react' and try again: | ||
e.g. | ||
plugins: [ | ||
TanStackStartVite(), // Place this before viteReact() | ||
viteReact(), | ||
] | ||
`, | ||
) | ||
} | ||
|
||
if (debug) console.info('Handling createServerFn for id: ', id) | ||
const compiled = await createServerFnCompiler({ | ||
code, | ||
compile, | ||
filename: id, | ||
}) | ||
|
||
if (debug) console.info('') | ||
if (debug) console.info('Compiled Output') | ||
if (debug) console.info('') | ||
if (debug) console.info(compiled.code) | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
if (debug) console.info('') | ||
|
||
return compiled | ||
} | ||
|
||
return null | ||
}, | ||
} | ||
} |
Oops, something went wrong.