From dd9525ef87d34c0ee4620813c6d1d2e20180e003 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sat, 20 Apr 2024 18:57:41 +0900 Subject: [PATCH] chore: setup --- packages/server-action/README.md | 13 ++ packages/server-action/package.json | 37 +++++ packages/server-action/src/client.ts | 2 + packages/server-action/src/index.ts | 0 packages/server-action/src/plugin.ts | 209 ++++++++++++++++++++++++++ packages/server-action/src/server.ts | 25 +++ packages/server-action/tsconfig.json | 7 + packages/server-action/tsup.config.ts | 7 + pnpm-lock.yaml | 12 ++ 9 files changed, 312 insertions(+) create mode 100644 packages/server-action/README.md create mode 100644 packages/server-action/package.json create mode 100644 packages/server-action/src/client.ts create mode 100644 packages/server-action/src/index.ts create mode 100644 packages/server-action/src/plugin.ts create mode 100644 packages/server-action/src/server.ts create mode 100644 packages/server-action/tsconfig.json create mode 100644 packages/server-action/tsup.config.ts diff --git a/packages/server-action/README.md b/packages/server-action/README.md new file mode 100644 index 000000000..a53283b20 --- /dev/null +++ b/packages/server-action/README.md @@ -0,0 +1,13 @@ +# server-action + +## todo + +- extract from https://github.com/hi-ogawa/vite-environment-examples/blob/main/examples/vue-ssr-extra/src/features/server-action/server.ts +- action side effects + - redirect + - revalidate +- progressive enhancements +- react / vue agnostics? +- server action handler +- context +- custom serialization diff --git a/packages/server-action/package.json b/packages/server-action/package.json new file mode 100644 index 000000000..50e13a867 --- /dev/null +++ b/packages/server-action/package.json @@ -0,0 +1,37 @@ +{ + "name": "@hiogawa/server-action", + "version": "0.0.0-pre.0", + "homepage": "https://github.com/hi-ogawa/vite-plugins/tree/main/packages/server-action", + "repository": { + "type": "git", + "url": "https://github.com/hi-ogawa/vite-plugins", + "directory": "packages/server-action" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "tsup --watch", + "build": "tsup --clean", + "release": "pnpm publish --no-git-checks --access public" + }, + "dependencies": { + "fast-glob": "^3.3.2", + "magic-string": "^0.30.8" + }, + "peerDependencies": { + "vite": "*" + } +} diff --git a/packages/server-action/src/client.ts b/packages/server-action/src/client.ts new file mode 100644 index 000000000..8a173c36e --- /dev/null +++ b/packages/server-action/src/client.ts @@ -0,0 +1,2 @@ +// call server +// diff --git a/packages/server-action/src/index.ts b/packages/server-action/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server-action/src/plugin.ts b/packages/server-action/src/plugin.ts new file mode 100644 index 000000000..f67ba88d2 --- /dev/null +++ b/packages/server-action/src/plugin.ts @@ -0,0 +1,209 @@ +import FastGlob from "fast-glob"; +import MagicString from "magic-string"; +import fs from "node:fs"; +import { parseAstAsync, type Plugin, type PluginOption, type ResolvedConfig } from "vite"; + +const USE_SERVER_RE = /^("use server"|'use server')/; + +export function vitePluginServerAction({ + clientRuntime, + serverRuntime, + entries, +}: { + clientRuntime: string; + serverRuntime: string; + entries: string[], +}): PluginOption { + let config: ResolvedConfig; + + const basePlugin: Plugin = { + name: vitePluginServerAction.name + ":base", + configResolved(config_) { + config = config_; + }, + } + + const transformPlugin: Plugin = { + name: vitePluginServerAction.name + ":transform", + async transform(code, id, options) { + if (USE_SERVER_RE.test(code)) { + const { writable, exportNames } = await parseExports(code) + if (!options?.ssr) { + const outCode = [ + `import { createServerReference as $$create } from "${clientRuntime}";`, + ...[...exportNames].map( + (name) => `export const ${name} = $$create("${id}", "${name}");`, + ), + ].join("\n"); + return { code: outCode, map: null }; + } else { + const footer = [ + code, + `import { registerServerReference as $$register } from "${serverRuntime}";`, + ...[...exportNames].map( + (name) => `${name} = $$register(${name}, "${id}", "${name}");`, + ), + ].join("\n"); + writable.append(footer); + return { code: writable.toString(), map: writable.generateMap() }; + } + } + return; + }, + }; + + const virtualServerReference = createVirtualPlugin( + "server-action/references", + async function () { + const files = await FastGlob(entries, { + cwd: config.root, + absolute: true, + }) + const ids: string[] = []; + for (const file of files) { + const code = await fs.promises.readFile(file, "utf-8"); + if (USE_SERVER_RE.test(code)) { + ids.push(file); + } + } + return [ + "export default {", + ...ids.map((id) => `"${id}": () => import("${id}"),\n`), + "}", + ].join("\n"); + }, + ); + + return [ + basePlugin, + transformPlugin, + virtualServerReference, + vitePluginSilenceDirectiveBuildWarning(), + ]; +} + +function createVirtualPlugin(name: string, load: Plugin["load"]) { + name = "virtual:" + name; + return { + name, + resolveId(source, _importer, _options) { + return source === name ? "\0" + name : undefined; + }, + load(id, options) { + if (id === "\0" + name) { + return (load as any).apply(this, [id, options]); + } + }, + } satisfies Plugin; +} + +function vitePluginSilenceDirectiveBuildWarning(): Plugin { + return { + name: vitePluginSilenceDirectiveBuildWarning.name, + apply: "build", + enforce: "post", + config: (config, _env) => ({ + build: { + rollupOptions: { + onwarn(warning, defaultHandler) { + if ( + warning.code === "SOURCEMAP_ERROR" && + warning.message.includes("(1:0)") + ) { + return; + } + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message.includes(`"use client"`) || + warning.message.includes(`"use server"`)) + ) { + return; + } + if (config.build?.rollupOptions?.onwarn) { + config.build.rollupOptions.onwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }, + }, + }, + }), + }; +} + +async function parseExports(code: string) { + const ast = await parseAstAsync(code); + const writable = new MagicString(code); // replace "const" with "let" + const exportNames = new Set(); + + for (const node of ast.body) { + // named exports + if (node.type === "ExportNamedDeclaration") { + if (node.declaration) { + if ( + node.declaration.type === "FunctionDeclaration" || + node.declaration.type === "ClassDeclaration" + ) { + /** + * export function foo() {} + */ + exportNames.add(node.declaration.id.name); + } else if (node.declaration.type === "VariableDeclaration") { + /** + * export const foo = 1, bar = 2 + */ + // replace "const" to "let" + if (node.declaration.kind === "const") { + const { start } = node.declaration as any; + writable.remove(start, start + 5); + writable.appendLeft(start, "let"); + } + for (const decl of node.declaration.declarations) { + if (decl.id.type === "Identifier") { + exportNames.add(decl.id.name); + } else { + console.error(parseExports.name, "unsupported code", decl); + } + } + } + } else { + /** + * export { foo, bar } from './foo' + * export { foo, bar as car } + */ + for (const spec of node.specifiers) { + exportNames.add(spec.exported.name); + } + } + } + + // default export + if (node.type === "ExportDefaultDeclaration") { + if ( + (node.declaration.type === "FunctionDeclaration" || + node.declaration.type === "ClassExpression") && + node.declaration.id + ) { + /** + * export default function foo() {} + * export default class A {} + */ + exportNames.add("default"); + } else { + /** + * export default () => {} + */ + exportNames.add("default"); + } + } + + /** + * export * from './foo' + */ + if (node.type === "ExportAllDeclaration") { + console.error(parseExports.name, "unsupported code", node); + } + } + + return { exportNames, writable }; +} diff --git a/packages/server-action/src/server.ts b/packages/server-action/src/server.ts new file mode 100644 index 000000000..3d293f511 --- /dev/null +++ b/packages/server-action/src/server.ts @@ -0,0 +1,25 @@ +import { tinyassert } from "@hiogawa/utils" + +export function serverActionHandler({ request }: { request: Request; }) { + request; +} + +export async function decodeActionRequest(request: Request) { + request; +} + +export function importServerAction(id: string, name: string) { + import(/* @vite-ignore */ id); + import("virtual:server-action/references" as string); +} + +export async function importServerReference(id: string) { + if (import.meta.env.DEV) { + return import(/* @vite-ignore */ id); + } else { + const mod = await import("virtual:server-action/references" as string); + const dyn = mod.default[id]; + tinyassert(dyn); + return dyn(); + } +} diff --git a/packages/server-action/tsconfig.json b/packages/server-action/tsconfig.json new file mode 100644 index 000000000..189593f4b --- /dev/null +++ b/packages/server-action/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "tsup.config.ts"], + "compilerOptions": { + "types": ["vite/client"] + } +} diff --git a/packages/server-action/tsup.config.ts b/packages/server-action/tsup.config.ts new file mode 100644 index 000000000..cb9c84eb6 --- /dev/null +++ b/packages/server-action/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76d3970df..8794f9ff0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,18 @@ importers: specifier: ^5.2.3 version: 5.2.3(@types/node@20.11.28) + packages/server-action: + dependencies: + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + magic-string: + specifier: ^0.30.8 + version: 0.30.8 + vite: + specifier: ^5.2.3 + version: 5.2.3(@types/node@20.11.28) + packages/vite-glob-routes: dependencies: vite: