rsf-zero 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # rsf-zero
2
+
3
+ ## Setup
4
+
5
+ Add these scripts to your **package.json**:
6
+
7
+ ```
8
+ ...
9
+ "scripts": {
10
+ "dev": "rsf-zero dev",
11
+ "build": "rsf-zero build",
12
+ "start": "rsf-zero start",
13
+ },
14
+ ...
15
+ ```
16
+
17
+ Then create an **index.html**:
18
+ ```html
19
+ <!doctype html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="UTF-8" />
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
+ <title>RSF-Zero example</title>
25
+ </head>
26
+ <body>
27
+ <div id="root"></div>
28
+ <script type="module" src="/src/main.tsx"></script>
29
+ </body>
30
+ </html>
31
+ ```
32
+
33
+ And finally create a **src/main.tsx** file:
34
+ ```tsx
35
+ import { StrictMode } from "react";
36
+ import { createRoot } from "react-dom/client";
37
+
38
+ createRoot(document.getElementById("root")!).render(
39
+ <StrictMode>
40
+ RSF Zero up and running!
41
+ </StrictMode>,
42
+ );
43
+ ```
44
+
45
+
46
+ ## Advanced topics
47
+
48
+ ### Server function arguments
49
+
50
+ Server function calls are serialised and deserialised between client and server. Server function argument types must be serialisable.
51
+
52
+ **RSF Zero** uses [superjson](https://github.com/flightcontrolhq/superjson) under the hood, and the most common types are supported.
53
+ See the [superjson docs](https://github.com/flightcontrolhq/superjson?tab=readme-ov-file#parse) for more info.
54
+
55
+ ### Verbose logging
56
+ Prefix commands with `NODE_DEBUG=rsf-zero` to see debug logs, for example: `NODE_DEBUG=rsf-zero yarn build`.
@@ -0,0 +1,26 @@
1
+ import path from "path";
2
+ import { build as viteBuild } from "vite";
3
+ import viteReact from "@vitejs/plugin-react";
4
+ import { transformRsfForClientPlugin } from "../../transform/client/transformRsfForClientPlugin.js";
5
+ import { debug } from "../../debug.js";
6
+ export const buildClientFiles = async (rootDir, onActionFound) => {
7
+ let actionCounter = 0; // for logging
8
+ const _onActionFound = (action) => {
9
+ actionCounter++;
10
+ onActionFound(action);
11
+ };
12
+ const clientOutDir = path.join(rootDir, 'dist/client');
13
+ await viteBuild({
14
+ mode: 'production',
15
+ base: '/',
16
+ plugins: [
17
+ viteReact(),
18
+ transformRsfForClientPlugin(_onActionFound),
19
+ ],
20
+ build: {
21
+ emptyOutDir: true,
22
+ outDir: clientOutDir,
23
+ },
24
+ });
25
+ debug(`Built client with ${actionCounter} actions to ${clientOutDir}.`);
26
+ };
@@ -0,0 +1,89 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { createProgram, flattenDiagnosticMessageText, ModuleKind, } from "typescript";
4
+ import md5 from "md5";
5
+ import * as os from "node:os";
6
+ import { generateActionRegistryJs, generateActionRegistryTs, generateEmptyActionRegistryJs } from "../../transform/server/generateActionRegistry.js";
7
+ import { debug } from "../../debug.js";
8
+ export const buildServerFiles = (actions, rootDir) => {
9
+ const serverOutDir = path.join(rootDir, 'dist/server');
10
+ const srcDir = path.join(rootDir, 'src');
11
+ // Clear /dist/server before starting
12
+ if (fs.existsSync(serverOutDir)) {
13
+ fs.rmSync(serverOutDir, { recursive: true });
14
+ fs.mkdirSync(serverOutDir, { recursive: true });
15
+ }
16
+ const actionRegistryJsPath = path.join(serverOutDir, 'actionRegistry.js');
17
+ if (actions.length > 0) {
18
+ // Compile and output all standard files, e.g. those which are imported by the user's actions
19
+ buildStandardFiles(actions, serverOutDir, srcDir);
20
+ // Then generate a registry file which the server will load
21
+ buildRegistryFile(actions, srcDir, actionRegistryJsPath);
22
+ }
23
+ else {
24
+ // Create an empty registry file
25
+ buildEmptyRegistryFile(actionRegistryJsPath);
26
+ }
27
+ debug(`Built server with ${actions.length} actions to ${serverOutDir}.`);
28
+ };
29
+ const buildStandardFiles = (actions, serverOutDir, srcDir) => {
30
+ // Create tmp directory
31
+ const tmpDir = os.tmpdir();
32
+ const projectHash = md5(process.cwd());
33
+ const generatedRegistryFilePath = path.join(tmpDir, `rsf-zero-registry-${projectHash}.ts`);
34
+ const compilerOptions = {
35
+ strict: true,
36
+ module: ModuleKind.ESNext,
37
+ allowSyntheticDefaultImports: true,
38
+ skipLibCheck: true,
39
+ rewriteRelativeImportExtensions: true,
40
+ outDir: serverOutDir,
41
+ rootDir: srcDir,
42
+ };
43
+ // Generate registry ts file
44
+ const registryContent = generateActionRegistryTs(actions, generatedRegistryFilePath);
45
+ fs.writeFileSync(generatedRegistryFilePath, registryContent);
46
+ debug('wrote action registry ts file to: ' + generatedRegistryFilePath);
47
+ // Create TypeScript program with the temporary file as entry point
48
+ const program = createProgram([generatedRegistryFilePath], compilerOptions);
49
+ // Emit the compiled files
50
+ const emitResult = program.emit();
51
+ debug('Built server files to: ' + serverOutDir);
52
+ // Check for compilation errors
53
+ const allDiagnostics = program.getSemanticDiagnostics()
54
+ .concat(program.getSyntacticDiagnostics())
55
+ .concat(emitResult.diagnostics);
56
+ if (allDiagnostics.length > 0) {
57
+ allDiagnostics.forEach(diagnostic => {
58
+ if (diagnostic.file) {
59
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
60
+ const message = flattenDiagnosticMessageText(diagnostic.messageText, '\n');
61
+ console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
62
+ }
63
+ else {
64
+ console.error(flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
65
+ }
66
+ });
67
+ if (emitResult.emitSkipped) {
68
+ throw new Error('TypeScript compilation failed');
69
+ }
70
+ }
71
+ };
72
+ const buildRegistryFile = (actions, srcDir, actionRegistryJsPath) => {
73
+ // This file is what the server loads. It imports all the user's actions.
74
+ // It also exports a map of the action id to the action function.
75
+ //
76
+ // There are two ways to build this file, one is to place it in the user's src/ directory
77
+ // and use tsc to compile it and output it into dist. The other is to directly write it to
78
+ // dist as a javascript file.
79
+ //
80
+ // The first way means we pollute the user's src/ directory, which I don't like, so instead
81
+ // we do the direct option. A bit less ideal in this codebase, but a nicer DevEx for the user.
82
+ fs.writeFileSync(actionRegistryJsPath, generateActionRegistryJs(actions, srcDir));
83
+ debug('wrote action registry js file to: ' + actionRegistryJsPath);
84
+ };
85
+ const buildEmptyRegistryFile = (actionRegistryJsPath) => {
86
+ // We still need an empty file for the server
87
+ fs.writeFileSync(actionRegistryJsPath, generateEmptyActionRegistryJs());
88
+ debug('wrote empty action registry js file to: ' + actionRegistryJsPath);
89
+ };
@@ -0,0 +1,12 @@
1
+ import { resolveConfig } from 'vite';
2
+ import { buildServerFiles } from "./build/buildServerFiles.js";
3
+ import { buildClientFiles } from "./build/buildClientFiles.js";
4
+ export const build = async () => {
5
+ const rootDir = (await resolveConfig({}, 'build', 'production', 'production')).root;
6
+ // Map used for building the action registry
7
+ const actions = [];
8
+ // Build client files
9
+ await buildClientFiles(rootDir, (action) => actions.push(action));
10
+ // Build server files
11
+ buildServerFiles(actions, rootDir);
12
+ };
@@ -0,0 +1,32 @@
1
+ import { createServer as createViteServer } from "vite";
2
+ import fs from "fs";
3
+ import viteReact from "@vitejs/plugin-react";
4
+ import { transformRsfForClientPlugin } from "../../transform/client/transformRsfForClientPlugin.js";
5
+ import { debug } from "../../debug.js";
6
+ export const startVite = async ({ app, onActionFound }) => {
7
+ debug('starting dev vite');
8
+ const vite = await createViteServer({
9
+ server: { middlewareMode: true },
10
+ appType: "custom",
11
+ base: '/',
12
+ plugins: [
13
+ viteReact(),
14
+ transformRsfForClientPlugin(onActionFound),
15
+ ],
16
+ });
17
+ // Use vite's connect instance as middleware.
18
+ // This will handle serving client-side assets and HMR.
19
+ app.use(vite.middlewares);
20
+ // All other routes should be handled by Vite
21
+ // /{*splat} matches all routes including /
22
+ app.get("/{*splat}", async (req, res) => {
23
+ const templatePath = process.cwd() + "/index.html";
24
+ // 1. Read index.html
25
+ const template = fs.readFileSync(templatePath, "utf-8");
26
+ // 2. Apply Vite HTML transforms.
27
+ const html = await vite.transformIndexHtml(req.originalUrl, template);
28
+ // 3. Send the rendered HTML back.
29
+ res.status(200).set({ "Content-Type": "text/html" }).end(html);
30
+ });
31
+ return vite;
32
+ };
@@ -0,0 +1,30 @@
1
+ import express from "express";
2
+ import morgan from "morgan";
3
+ import "dotenv/config";
4
+ import { startVite } from "./dev/startVite.js";
5
+ import { createActionRoute } from "./start/createActionRoute.js";
6
+ import { debug } from "../debug.js";
7
+ export const dev = async () => {
8
+ const app = express();
9
+ const port = 3000;
10
+ app.use(morgan("dev"));
11
+ app.use(express.json());
12
+ // Dev RSF handler
13
+ // - create action route
14
+ const { add: addToActionRegistry } = createActionRoute(app);
15
+ // - in dev mode, Vite loads files on the fly, so we need to register actions handlers on the fly too
16
+ const onActionFound = async (action) => {
17
+ debug("Loading action handler: " + action.id);
18
+ const module = await vite.ssrLoadModule(`${action.sourceFilePath}`);
19
+ const actionFn = module[action.name];
20
+ addToActionRegistry(action.id, actionFn);
21
+ debug("Loaded action handler: " + action.name);
22
+ };
23
+ // Serve the frontend
24
+ // - transform any actions found for the frontend
25
+ // - and inform the onActionFound callback above
26
+ const vite = await startVite({ app, onActionFound });
27
+ app.listen(port, () => {
28
+ console.log(`Server is running at http://localhost:${port}`);
29
+ });
30
+ };
@@ -0,0 +1,54 @@
1
+ import { parse, stringify } from "superjson";
2
+ import { debug } from "../../debug.js";
3
+ export const createActionRoute = (app) => {
4
+ let actionRegistry = {};
5
+ const add = (actionId, actionFn) => {
6
+ debug("Registering server action: " + actionId);
7
+ actionRegistry[actionId] = actionFn;
8
+ };
9
+ const set = (newActionRegistry) => {
10
+ actionRegistry = newActionRegistry;
11
+ };
12
+ app.post("/actions/:actionId", async (req, res) => {
13
+ const { actionId } = req.params;
14
+ debug("Received request for action: ", actionId);
15
+ const actionFn = actionRegistry[actionId];
16
+ if (!actionFn) {
17
+ throw new Error('Server does not have this action registered: ' + actionId);
18
+ }
19
+ debug("Request body: ", req.body);
20
+ const serialisedFnArgs = req.body.serialisedFnArgs;
21
+ const fnArgs = parse(serialisedFnArgs);
22
+ // Call the action function
23
+ debug("Request fn args: ", fnArgs);
24
+ const actionFnReturnValue = await callActionFn(actionId, actionFn, fnArgs);
25
+ // Serialise the result
26
+ const serialisedFnResult = serialiseReturnValue(actionId, actionFnReturnValue);
27
+ res.json({ serialisedFnResult });
28
+ });
29
+ return {
30
+ add,
31
+ set,
32
+ };
33
+ };
34
+ const callActionFn = async (actionId, actionFn, fnArgs) => {
35
+ debug('Calling action: ' + actionId + ' with fn: ' + actionFn + ' with args: ', fnArgs);
36
+ try {
37
+ const actionFnReturnValue = await actionFn(...fnArgs);
38
+ debug('Action ' + actionId + ' return value: ', actionFnReturnValue);
39
+ return actionFnReturnValue;
40
+ }
41
+ catch (e) {
42
+ throw new Error('Error thrown in action handler ' + actionId + ': ' + e);
43
+ }
44
+ };
45
+ const serialiseReturnValue = (actionId, value) => {
46
+ try {
47
+ const serialised = stringify(value);
48
+ debug('Serialised action ' + actionId + ' return value: ', serialised);
49
+ return serialised;
50
+ }
51
+ catch (e) {
52
+ throw new Error('Error serialising ' + actionId + ' return value: ' + e);
53
+ }
54
+ };
@@ -0,0 +1,34 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import path from 'path';
10
+ import express from "express";
11
+ import morgan from "morgan";
12
+ import "dotenv/config";
13
+ import { createActionRoute } from "./start/createActionRoute.js";
14
+ export const start = async () => {
15
+ const app = express();
16
+ const port = 3000;
17
+ app.use(morgan("dev"));
18
+ app.use(express.json());
19
+ // Client
20
+ const staticPath = path.join(process.cwd(), 'dist/client/');
21
+ console.log('Serving static files from:', staticPath);
22
+ app.use(express.static('dist/client/'));
23
+ // Server
24
+ // - create action route
25
+ const { set: setActionRegistry } = createActionRoute(app);
26
+ // - load actionRegistry dynamically
27
+ const module = await import(__rewriteRelativeImportExtension(path.join(process.cwd(), 'dist/server/actionRegistry.js')));
28
+ const { actionRegistry } = module;
29
+ // - register action handlers
30
+ setActionRegistry(actionRegistry);
31
+ app.listen(port, () => {
32
+ console.log(`Server is running at http://localhost:${port}`);
33
+ });
34
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { dev } from "./cli/dev.js";
3
+ import { build } from "./cli/build.js";
4
+ import { start } from "./cli/start.js";
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+ if (command === 'dev') {
8
+ await dev();
9
+ }
10
+ else if (command === 'build') {
11
+ await build();
12
+ }
13
+ else if (command === 'start') {
14
+ await start();
15
+ }
16
+ else {
17
+ console.log('rsf-zero: Unknown command. Available commands: dev, build, start');
18
+ process.exit(1);
19
+ }
package/dist/debug.js ADDED
@@ -0,0 +1,5 @@
1
+ import { debuglog } from "node:util";
2
+ const log = debuglog('rsf-zero');
3
+ export const debug = (message, ...param) => {
4
+ log(message, ...param);
5
+ };
@@ -0,0 +1,22 @@
1
+ import { stringify, parse } from 'superjson';
2
+ export const createActionCaller = (actionName) => {
3
+ return async (...args) => {
4
+ try {
5
+ console.debug("caller called with args: " + args);
6
+ const result = await fetch("/actions/" + actionName, {
7
+ method: "POST",
8
+ body: JSON.stringify({ serialisedFnArgs: stringify(args) }),
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+ const parsedResult = await result.json();
14
+ const deserialisedFnResult = parse(parsedResult.serialisedFnResult);
15
+ console.debug("deserialisedFnResult: ", deserialisedFnResult);
16
+ return deserialisedFnResult;
17
+ }
18
+ catch (e) {
19
+ throw new Error('Error calling action ' + actionName + ': ' + e);
20
+ }
21
+ };
22
+ };
@@ -0,0 +1,19 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Converts any import e.g. absolute path to a relative one.
5
+ *
6
+ * Given `/User/x/my-file.ts, /User/x`
7
+ * Returns `./my-file.ts`
8
+ *
9
+ * Given `/User/x/my-file.ts, /User/somewhere-else`
10
+ * Returns `../x/my-file.ts`
11
+ *
12
+ * Can be passed an optional `replaceExtWith` param to replace the extension.
13
+ */
14
+ export const asRelativeImport = (targetPath, relativeToPath, replaceExtWith) => {
15
+ const relativeToDirPath = fs.lstatSync(relativeToPath).isDirectory() ? relativeToPath : path.dirname(relativeToPath);
16
+ const relative = path.relative(relativeToDirPath, targetPath);
17
+ // Add leading dot slash
18
+ return relative.startsWith('.') ? relative : './' + relative;
19
+ };
@@ -0,0 +1,20 @@
1
+ import { transformTopLevelRsf } from "./transformTopLevelRsf.js";
2
+ /**
3
+ * This plugin sees a file with 'use server' and transforms it to a client-side version.
4
+ *
5
+ * The client-side version has the original code replaced with an API call to the server for that action.
6
+ *
7
+ * This plugin also registers all actions found via the passed onActionFound function.
8
+ */
9
+ export const transformRsfForClientPlugin = (onActionFound) => {
10
+ return {
11
+ name: "vite-plugin-transform-rsf-for-client",
12
+ transform(code, id, options) {
13
+ if (options?.ssr) {
14
+ // plugin is running in the 'dev' command, and loading the module for the server
15
+ return;
16
+ }
17
+ return transformTopLevelRsf(id, code, onActionFound);
18
+ },
19
+ };
20
+ };
@@ -0,0 +1,64 @@
1
+ import * as swc from "@swc/core";
2
+ import { isTopLevelRsfFile } from "../isTopLevelRsfFile.js";
3
+ import { debug } from "../../debug.js";
4
+ import { getActionId } from "../getActionId.js";
5
+ /**
6
+ * Given a file that starts with 'use server', return a transformed client version of that file.
7
+ */
8
+ export const transformTopLevelRsf = (id, code, onActionFound) => {
9
+ if (!isTopLevelRsfFile(id, code)) {
10
+ return undefined;
11
+ }
12
+ debug("Transforming file for client: ", id);
13
+ const mod = swc.parseSync(code);
14
+ const exportNames = new Set();
15
+ for (const item of mod.body) {
16
+ if (item.type === 'ExportDeclaration') {
17
+ if (item.declaration.type === 'FunctionDeclaration') {
18
+ exportNames.add(item.declaration.identifier.value);
19
+ }
20
+ else if (item.declaration.type === 'ClassDeclaration') {
21
+ exportNames.add(item.declaration.identifier.value);
22
+ }
23
+ else if (item.declaration.type === 'VariableDeclaration') {
24
+ for (const d of item.declaration.declarations) {
25
+ if (d.id.type === 'Identifier') {
26
+ exportNames.add(d.id.value);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ else if (item.type === 'ExportNamedDeclaration') {
32
+ for (const s of item.specifiers) {
33
+ if (s.type === 'ExportSpecifier') {
34
+ exportNames.add(s.exported ? s.exported.value : s.orig.value);
35
+ }
36
+ }
37
+ }
38
+ else if (item.type === 'ExportDefaultExpression') {
39
+ exportNames.add('default');
40
+ }
41
+ else if (item.type === 'ExportDefaultDeclaration') {
42
+ exportNames.add('default');
43
+ }
44
+ }
45
+ let newCode = `
46
+ import { createActionCaller } from 'rsf-zero/client';`;
47
+ for (const exportName of exportNames) {
48
+ const actionId = getActionId(id, exportName);
49
+ onActionFound({
50
+ id: actionId,
51
+ sourceFilePath: id,
52
+ name: exportName,
53
+ });
54
+ if (exportName === 'default') {
55
+ newCode += `
56
+ export default createActionCaller('${actionId}');`;
57
+ }
58
+ else {
59
+ newCode += `
60
+ export const ${exportName} = createActionCaller('${actionId}');`;
61
+ }
62
+ }
63
+ return newCode;
64
+ };
@@ -0,0 +1,25 @@
1
+ export const collectExportNames = (mod) => {
2
+ const exportNames = new Set();
3
+ for (const item of mod.body) {
4
+ if (item.type === "ExportDeclaration") {
5
+ if (item.declaration.type === "FunctionDeclaration") {
6
+ exportNames.add(item.declaration.identifier.value);
7
+ }
8
+ else if (item.declaration.type === "VariableDeclaration") {
9
+ for (const d of item.declaration.declarations) {
10
+ if (d.id.type === "Identifier") {
11
+ exportNames.add(d.id.value);
12
+ }
13
+ }
14
+ }
15
+ }
16
+ else if (item.type === "ExportNamedDeclaration") {
17
+ for (const s of item.specifiers) {
18
+ if (s.type === "ExportSpecifier") {
19
+ exportNames.add(s.exported ? s.exported.value : s.orig.value);
20
+ }
21
+ }
22
+ }
23
+ }
24
+ return exportNames;
25
+ };
@@ -0,0 +1,10 @@
1
+ export const extname = (filePath) => {
2
+ const index = filePath.lastIndexOf(".");
3
+ if (index <= 0) {
4
+ return "";
5
+ }
6
+ if (["/", "."].includes(filePath[index - 1])) {
7
+ return "";
8
+ }
9
+ return filePath.slice(index);
10
+ };
@@ -0,0 +1,7 @@
1
+ import md5 from "md5";
2
+ import path from "path";
3
+ export const getActionId = (sourceFilePath, actionName) => {
4
+ const absolutePath = path.resolve(sourceFilePath);
5
+ const hash = md5(absolutePath);
6
+ return `${actionName}_${hash}`;
7
+ };
@@ -0,0 +1,8 @@
1
+ import { extname } from "./extname.js";
2
+ export const isTopLevelRsfFile = (id, code) => {
3
+ const ext = extname(id);
4
+ if (ext === ".ts" || ext === ".tsx") {
5
+ return code.startsWith(`'use server'`) || code.startsWith(`"use server"`);
6
+ }
7
+ return false;
8
+ };
@@ -0,0 +1,5 @@
1
+ import { extname } from "./extname.js";
2
+ export const replaceFileExt = (path, newExt) => {
3
+ const originalExt = extname(path);
4
+ return path.slice(0, -originalExt.length) + newExt;
5
+ };
@@ -0,0 +1,60 @@
1
+ import { asRelativeImport } from "../asRelativeImport.js";
2
+ import { replaceFileExt } from "../replaceFileExt.js";
3
+ /**
4
+ * Create a typescript file that imports all actions. This can then be passed to a tsc to be processed into dist js files.
5
+ */
6
+ export const generateActionRegistryTs = (actions, registryPath) => {
7
+ const actionsWithRelativePaths = actions.map(action => {
8
+ return {
9
+ ...action,
10
+ relativeSourceFilePath: asRelativeImport(action.sourceFilePath, registryPath),
11
+ };
12
+ });
13
+ let newCode = `// Generated by RSF Zero, do not modify
14
+ `;
15
+ for (const action of actionsWithRelativePaths) {
16
+ if (action.name === 'default') {
17
+ newCode += `import ${action.id} from '${action.relativeSourceFilePath}';
18
+ `;
19
+ }
20
+ else {
21
+ newCode += `import { ${action.name} as ${action.id} } from '${action.relativeSourceFilePath}';
22
+ `;
23
+ }
24
+ }
25
+ return newCode;
26
+ };
27
+ /**
28
+ * Create a javascript file that imports all actions. This is used by the start command to load action functions when the server starts.
29
+ */
30
+ export const generateActionRegistryJs = (actions, relativeToDir) => {
31
+ const actionsWithRelativePaths = actions.map(action => {
32
+ return {
33
+ ...action,
34
+ relativeSourceFilePathAsJs: replaceFileExt(asRelativeImport(action.sourceFilePath, relativeToDir), '.js'),
35
+ };
36
+ });
37
+ let newCode = `// Generated by RSF Zero, do not modify
38
+ `;
39
+ for (const action of actionsWithRelativePaths) {
40
+ if (action.name === 'default') {
41
+ newCode += `import ${action.id} from '${action.relativeSourceFilePathAsJs}';
42
+ `;
43
+ }
44
+ else {
45
+ newCode += `import { ${action.name} as ${action.id} } from '${action.relativeSourceFilePathAsJs}';
46
+ `;
47
+ }
48
+ }
49
+ newCode += `
50
+ export const actionRegistry = {
51
+ ${actionsWithRelativePaths.map(action => `"${action.id}": ${action.id},`).join('\n')}
52
+ };
53
+ `;
54
+ return newCode;
55
+ };
56
+ export const generateEmptyActionRegistryJs = () => {
57
+ return `// Generated by RSF Zero, do not modify
58
+ export const actionRegistry = {};
59
+ `;
60
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "rsf-zero",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "scripts": {
6
+ "build": "rm -rf ./dist && tsc",
7
+ "test": "vitest run",
8
+ "test:watch": "vitest"
9
+ },
10
+ "dependencies": {
11
+ "@swc/core": "^1.12.1",
12
+ "@vitejs/plugin-react": "^4.3.4",
13
+ "dotenv": "^17.2.1",
14
+ "express": "^5.1.0",
15
+ "morgan": "^1.10.0",
16
+ "superjson": "^2.2.2",
17
+ "vite": "^6.3.5"
18
+ },
19
+ "devDependencies": {
20
+ "@tsconfig/node22": "^22.0.2",
21
+ "@types/express": "^5.0.3",
22
+ "@types/md5": "^2",
23
+ "@types/morgan": "^1.9.10",
24
+ "@types/node": "^24.1.0",
25
+ "md5": "^2.3.0",
26
+ "typescript": "^5.8.3",
27
+ "vitest": "^3.2.4"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^19.0.0"
31
+ },
32
+ "exports": {
33
+ "./client": {
34
+ "types": "./dist/lib/client.d.ts",
35
+ "default": "./dist/lib/client.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "packageManager": "yarn@4.6.0",
42
+ "engines": {
43
+ "node": ">=22.14.0"
44
+ },
45
+ "bin": "dist/cli.js"
46
+ }