From 747b906dd7e8767f367f70bc54a4e87740f22e9d Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 28 Feb 2023 14:54:57 -0700 Subject: [PATCH] feat: filename.server$.ext --- README.md | 19 ++ examples/astro-basic/src/app/root.tsx | 3 + .../astro-basic/src/app/secret.server$.ts | 1 + package.json | 2 +- packages/bling/package.json | 2 +- packages/bling/src/babel.ts | 162 +++++++++++++----- packages/bling/src/vite.ts | 48 +++--- pnpm-lock.yaml | 2 +- 8 files changed, 168 insertions(+), 71 deletions(-) create mode 100644 examples/astro-basic/src/app/secret.server$.ts diff --git a/README.md b/README.md index 3c7f3c2..53c734c 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,25 @@ A function that can be called isomorphically from server or client side code to - The request object to be passed to the `fetch` call to the server function. - Can be used to add headers, signals, etc. +## Server-Only Files (`filename.server$.ext`) + +The `filename.server$.ext` pattern can be used to create server-side only files. These files will be removed from the client bundle. This is useful for things like server-side only imports, or server-side only code. It works with any file name and extension so long as `.server$.` is found in the resolved file pathname. + +When a server-only file is imported on the client, it will be provided the same exports, but stubbed with undefined values. Don't put anything sensitive in the exported variable name! 😜 + +```tsx +// secret.server$.ts` +export const secret = 'This is top secret!' +export const anotherSecret = '🤫 Shhh!' +``` + +Client output: + +```tsx +export const secret = undefined +export const anotherSecret = undefined +``` + ## `server$` (Coming Soon) The `server$` function can be used to scope any expression to the server-bundle only. This means that the expression will be removed from the client bundle. This is useful for things like server-side only imports, or server-side only code. diff --git a/examples/astro-basic/src/app/root.tsx b/examples/astro-basic/src/app/root.tsx index 3cb5598..0be467a 100644 --- a/examples/astro-basic/src/app/root.tsx +++ b/examples/astro-basic/src/app/root.tsx @@ -1,8 +1,11 @@ import { serverFn$ } from '@tanstack/bling' +import { secret } from './secret.server$' const sayHello = serverFn$(() => console.log('Hello world')) export function App() { + console.log('Do you know the secret?', secret) + return ( diff --git a/examples/astro-basic/src/app/secret.server$.ts b/examples/astro-basic/src/app/secret.server$.ts new file mode 100644 index 0000000..03cd799 --- /dev/null +++ b/examples/astro-basic/src/app/secret.server$.ts @@ -0,0 +1 @@ +export const secret = 'This is a secret!' diff --git a/package.json b/package.json index 7b5f417..a29756e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "repository": "https://github.com/tanstack/bling.git", "scripts": { "build": "ts-node scripts/build.ts", - "dev": "pnpm -rc --parallel exec 'pnpm dev'", + "dev": "pnpm -rc --filter \"./packages/**\" --parallel exec 'pnpm dev'", "test": "exit 0", "test:dev": "exit 0", "test:ci": "exit 0", diff --git a/packages/bling/package.json b/packages/bling/package.json index 2a0b591..4e27ff7 100644 --- a/packages/bling/package.json +++ b/packages/bling/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "concurrently": "^7.6.0", - "esbuild": "^0.16.3", + "esbuild": "^0.16.17", "esbuild-plugin-replace": "^1.3.0", "typescript": "4.9.4", "vitest": "^0.26.2" diff --git a/packages/bling/src/babel.ts b/packages/bling/src/babel.ts index 1d3c960..2804fe7 100644 --- a/packages/bling/src/babel.ts +++ b/packages/bling/src/babel.ts @@ -6,60 +6,77 @@ import crypto from 'crypto' import nodePath from 'path' +import * as esbuild from 'esbuild' const INLINE_SERVER_ROUTE_PREFIX = '/_m' -function transformServer({ types: t, template }) { - function getIdentifier(path) { - const parentPath = path.parentPath - if (parentPath.type === 'VariableDeclarator') { - const pp = parentPath - const name = pp.get('id') - return name.node.type === 'Identifier' ? name : null - } - if (parentPath.type === 'AssignmentExpression') { - const pp = parentPath - const name = pp.get('left') - return name.node.type === 'Identifier' ? name : null - } - if (path.node.type === 'ArrowFunctionExpression') { - return null - } - return path.node.id && path.node.id.type === 'Identifier' - ? path.get('id') - : null - } - function isIdentifierReferenced(ident) { - const b = ident.scope.getBinding(ident.node.name) - if (b && b.referenced) { - if (b.path.type === 'FunctionDeclaration') { - return !b.constantViolations - .concat(b.referencePaths) - .every((ref) => ref.findParent((p) => p === b.path)) - } - return true +export function compileServerFile$({ code }) { + let compiled = esbuild.buildSync({ + stdin: { + contents: code, + }, + write: false, + metafile: true, + platform: 'neutral', + format: 'esm', + // loader: { + // '.js': 'jsx', + // }, + logLevel: 'silent', + }) + + let exps + + for (let key in compiled.metafile.outputs) { + if (compiled.metafile.outputs[key].entryPoint) { + exps = compiled.metafile.outputs[key].exports } - return false } - function markFunction(path, state) { - const ident = getIdentifier(path) - if (ident && ident.node && isIdentifierReferenced(ident)) { - state.refs.add(ident) - } + + if (!exps) { + throw new Error('Could not find entry point to detect exports') } - function markImport(path, state) { - const local = path.get('local') - // if (isIdentifierReferenced(local)) { - state.refs.add(local) - // } + + console.log(exps) + + compiled = esbuild.buildSync({ + stdin: { + contents: `${exps + .map((key) => `export const ${key} = undefined`) + .join('\n')}`, + }, + write: false, + platform: 'neutral', + format: 'esm', + }) + + console.log(compiled.outputFiles[0].text) + + return { + code: compiled.outputFiles[0].text, } +} + +export function compileServerFn$({ code, compiler, id, ssr }) { + const compiledCode = compiler(code, id, (source: any, id: any) => ({ + plugins: [ + [ + transformServerFn$, + { + ssr, + root: process.cwd(), + minify: process.env.NODE_ENV === 'production', + }, + ], + ].filter(Boolean), + })) - function hashFn(str) { - return crypto - .createHash('shake256', { outputLength: 5 /* bytes = 10 hex digits*/ }) - .update(str) - .digest('hex') + return { + code: compiledCode, } +} + +export function transformServerFn$({ types: t, template }) { return { visitor: { Program: { @@ -366,4 +383,57 @@ function transformServer({ types: t, template }) { }, } } -export { transformServer as default } + +function getIdentifier(path) { + const parentPath = path.parentPath + if (parentPath.type === 'VariableDeclarator') { + const pp = parentPath + const name = pp.get('id') + return name.node.type === 'Identifier' ? name : null + } + if (parentPath.type === 'AssignmentExpression') { + const pp = parentPath + const name = pp.get('left') + return name.node.type === 'Identifier' ? name : null + } + if (path.node.type === 'ArrowFunctionExpression') { + return null + } + return path.node.id && path.node.id.type === 'Identifier' + ? path.get('id') + : null +} + +function isIdentifierReferenced(ident) { + const b = ident.scope.getBinding(ident.node.name) + if (b && b.referenced) { + if (b.path.type === 'FunctionDeclaration') { + return !b.constantViolations + .concat(b.referencePaths) + .every((ref) => ref.findParent((p) => p === b.path)) + } + return true + } + return false +} + +function markFunction(path, state) { + const ident = getIdentifier(path) + if (ident && ident.node && isIdentifierReferenced(ident)) { + state.refs.add(ident) + } +} + +function markImport(path, state) { + const local = path.get('local') + // if (isIdentifierReferenced(local)) { + state.refs.add(local) + // } +} + +function hashFn(str) { + return crypto + .createHash('shake256', { outputLength: 5 /* bytes = 10 hex digits*/ }) + .update(str) + .digest('hex') +} diff --git a/packages/bling/src/vite.ts b/packages/bling/src/vite.ts index a90ac88..cab4da8 100644 --- a/packages/bling/src/vite.ts +++ b/packages/bling/src/vite.ts @@ -1,7 +1,7 @@ import type { Plugin } from 'vite' import viteReact, { Options } from '@vitejs/plugin-react' import { fileURLToPath, pathToFileURL } from 'url' -import babel from './babel' +import { compileServerFile$, compileServerFn$ } from './babel' export function bling(opts?: { babel?: Options['babel'] }): Plugin { const options = opts ?? {} @@ -16,25 +16,31 @@ export function bling(opts?: { babel?: Options['babel'] }): Plugin { ? void 0 : transformOptions.ssr + let ssr = process.env.TEST_ENV === 'client' ? false : isSsr + const url = pathToFileURL(id) url.searchParams.delete('v') id = fileURLToPath(url).replace(/\\/g, '/') const babelOptions = - (fn: any) => - (...args: any[]) => { + (fn?: (source: any, id: any) => { plugins: any[] }) => + (source: any, id: any) => { const b: any = typeof options.babel === 'function' ? // @ts-ignore options.babel(...args) : options.babel ?? { plugins: [] } - const d = fn(...args) + const d = fn?.(source, id) return { - plugins: [...b.plugins, ...d.plugins], + plugins: [...b.plugins, ...(d?.plugins ?? [])], } } - let compiler = (code: string, id: string, fn: any) => { + let compiler = ( + code: string, + id: string, + fn?: (source: any, id: any) => { plugins: any[] } + ) => { let plugin = viteReact({ ...(options ?? {}), fastRefresh: false, @@ -45,25 +51,23 @@ export function bling(opts?: { babel?: Options['babel'] }): Plugin { return plugin[0].transform(code, id, transformOptions) } - let ssr = process.env.TEST_ENV === 'client' ? false : isSsr + if (url.pathname.includes('.server$.') && !ssr) { + const compiled = compileServerFile$({ + code, + }) + + return compiled.code + } if (code.includes('serverFn$(')) { - return compiler( + const compiled = compileServerFn$({ code, - id.replace(/\.ts$/, '.tsx').replace(/\.js$/, '.jsx'), - (source: any, id: any) => ({ - plugins: [ - [ - babel, - { - ssr, - root: process.cwd(), - minify: process.env.NODE_ENV === 'production', - }, - ], - ].filter(Boolean), - }) - ) + compiler, + ssr, + id: id.replace(/\.ts$/, '.tsx').replace(/\.js$/, '.jsx'), + }) + + return compiled.code } }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1db3409..e3828d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,7 +63,7 @@ importers: '@tanstack/bling': workspace:* '@vitejs/plugin-react': ^3.1.0 concurrently: ^7.6.0 - esbuild: ^0.16.3 + esbuild: ^0.16.17 esbuild-plugin-replace: ^1.3.0 typescript: 4.9.4 vitest: ^0.26.2