diff --git a/packages/vite-plugin-rsc/README.md b/packages/vite-plugin-rsc/README.md
new file mode 100644
index 0000000..a381dbe
--- /dev/null
+++ b/packages/vite-plugin-rsc/README.md
@@ -0,0 +1,7 @@
+# @impalajs/vite-plugin-extract-server-components
+
+
+
+
+
+
diff --git a/packages/vite-plugin-rsc/package.json b/packages/vite-plugin-rsc/package.json
new file mode 100644
index 0000000..af76e77
--- /dev/null
+++ b/packages/vite-plugin-rsc/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@impalajs/vite-plugin-extract-server-components",
+ "version": "0.0.6",
+ "description": "",
+ "scripts": {
+ "build": "tsup src/plugin.ts --format esm --dts --clean"
+ },
+ "module": "./dist/plugin.mjs",
+ "types": "./dist/plugin.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/plugin.d.ts",
+ "import": "./dist/plugin.mjs"
+ }
+ },
+ "keywords": [],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "tsup": "^6.7.0"
+ },
+ "peerDependencies": {
+ "vite": ">=4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
\ No newline at end of file
diff --git a/packages/vite-plugin-rsc/src/plugin.ts b/packages/vite-plugin-rsc/src/plugin.ts
new file mode 100644
index 0000000..f6018bd
--- /dev/null
+++ b/packages/vite-plugin-rsc/src/plugin.ts
@@ -0,0 +1,212 @@
+import { Plugin, ResolvedConfig, Manifest } from "vite";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+import { existsSync, readFileSync } from "node:fs";
+interface ASTNode {
+ type: string;
+ start: number;
+ end: number;
+ body?: Array;
+ id?: ASTNode;
+ expression?: ASTNode;
+ declaration?: ASTNode;
+ declarations?: Array;
+ name?: string;
+ specifiers?: Array;
+
+ value?: string;
+ exported?: ASTNode;
+}
+
+/**
+ * Checks if the node has a string literal at the top level that matches the statement
+ */
+const hasPragma = (ast: ASTNode, statement: string) =>
+ ast.body?.some((node) => {
+ return (
+ node.type === "ExpressionStatement" &&
+ node.expression?.type === "Literal" &&
+ node.expression.value === statement
+ );
+ });
+
+/**
+ * Finds all the named and default exports of a module
+ */
+
+const getExports = (ast: ASTNode) => {
+ const exports: Array = [];
+ ast.body?.forEach((node) => {
+ if (node.type === "ExportDefaultDeclaration") {
+ exports.push("default");
+ }
+ if (node.type === "ExportNamedDeclaration") {
+ if (node.declaration?.type === "VariableDeclaration") {
+ node.declaration?.declarations?.forEach((declaration) => {
+ const name = declaration?.id?.name;
+ if (name) {
+ exports.push(name);
+ }
+ });
+ return;
+ }
+
+ if (node.declaration?.type === "FunctionDeclaration") {
+ const name = node.declaration?.id?.name;
+ if (name) {
+ exports.push(name);
+ }
+ return;
+ }
+
+ if (node.specifiers?.length) {
+ node.specifiers.forEach((specifier) => {
+ const name = specifier?.exported?.name;
+ if (name) {
+ exports.push(name);
+ }
+ });
+ }
+ }
+ });
+ return exports;
+};
+
+export default function plugin({
+ serverDist = "dist/server",
+ clientDist = "dist/static",
+}: {
+ serverDist?: string;
+ clientDist?: string;
+}): Plugin {
+ const clientPragma = "use client";
+ // const serverPragma = "use server";
+ let externals = new Set();
+ let config: ResolvedConfig;
+ let isSsr: boolean;
+ let isBuild: boolean;
+ let manifest: Manifest = {};
+
+ const bundleMap = new Map();
+ const clientModuleId = "virtual:client-bundle-map";
+ const resolvedClientModuleId = "\0" + clientModuleId;
+ const serverModuleId = "virtual:server-bundle-map";
+ const resolvedServerModuleId = "\0" + serverModuleId;
+
+ const clientBundleMapFilename = "client-bundle-map.json";
+ const serverBundleMapFilename = "server-bundle-map.json";
+
+ return {
+ name: "vite-plugin-extract-server-components",
+
+ config(config) {
+ config.build ||= {};
+ if (config.build.ssr) {
+ const manifestPath = path.join(
+ config.root || "",
+ clientDist,
+ "manifest.json"
+ );
+ if (existsSync(manifestPath)) {
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
+ }
+ config.build.rollupOptions ||= {};
+ config.build.rollupOptions.external = (id) => externals.has(id);
+ }
+ },
+
+ configResolved(resolvedConfig) {
+ config = resolvedConfig;
+ isSsr = !!config.build.ssr;
+ isBuild = config.command === "build";
+ },
+ resolveId(id, source) {
+ if (id === clientModuleId) {
+ return resolvedClientModuleId;
+ }
+ if (id === serverModuleId) {
+ return resolvedServerModuleId;
+ }
+ },
+ load(id) {
+ if (id === resolvedClientModuleId) {
+ return `export default ${JSON.stringify(
+ // Yes the client bundle map is in the server dist because
+ // it's the SSR build that generates the client bundle map
+ path.join(config.root || "", serverDist, clientBundleMapFilename)
+ )}`;
+ }
+ if (id === resolvedServerModuleId) {
+ return `export default ${JSON.stringify(
+ path.join(config.root || "", clientDist, serverBundleMapFilename)
+ )}`;
+ }
+ },
+ transform(code, id) {
+ // Short circuit if the file doesn't have the literal string
+ if (!code?.includes(clientPragma)) {
+ return;
+ }
+ // Check properly for the pragma
+ const ast = this.parse(code, { sourceType: "module" });
+ const localId = path.relative(config.root || "", id);
+ if (hasPragma(ast, clientPragma)) {
+ if (isSsr) {
+ const bundlePath = pathToFileURL(
+ path.join(config.root, clientDist, manifest[localId].file)
+ );
+ externals.add(bundlePath.href);
+ if (manifest[localId]) {
+ const exports = getExports(ast);
+ const exportProxies = exports
+ .map((name) => {
+ const symbolName = `${manifest[localId].file}#${name}`;
+ bundleMap.set(symbolName, {
+ id: symbolName,
+ chunks: [],
+ name,
+ async: true,
+ });
+ const localName = name === "default" ? "DefaultExport" : name;
+
+ return `
+ import { ${
+ name === "default" ? "default as DefaultExport" : name
+ } } from ${JSON.stringify(
+ bundlePath.href
+ )};${localName}.$$typeof = Symbol.for("react.client.reference");${localName}.$$id=${JSON.stringify(
+ symbolName
+ )}; export ${
+ name === "default" ? "default DefaultExport" : `{ ${name} }`
+ } `;
+ })
+ .join("\n");
+ return {
+ code: exportProxies,
+ map: { mappings: "" },
+ };
+ }
+ } else {
+ this.emitFile({
+ type: "chunk",
+ id,
+ preserveSignature: "allow-extension",
+ });
+ }
+ }
+
+ // todo, work out how to handle server only code
+ // if (hasPragma(ast, serverPragma)) {
+ // }
+ },
+ generateBundle() {
+ if (isBuild && isSsr) {
+ this.emitFile({
+ type: "asset",
+ fileName: serverBundleMapFilename,
+ source: JSON.stringify(Object.fromEntries(bundleMap)),
+ });
+ }
+ },
+ };
+}
diff --git a/packages/vite-plugin-rsc/tsconfig.json b/packages/vite-plugin-rsc/tsconfig.json
new file mode 100644
index 0000000..5ae90c5
--- /dev/null
+++ b/packages/vite-plugin-rsc/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file