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 +56 -0
- package/dist/cli/build/buildClientFiles.js +26 -0
- package/dist/cli/build/buildServerFiles.js +89 -0
- package/dist/cli/build.js +12 -0
- package/dist/cli/dev/startVite.js +32 -0
- package/dist/cli/dev.js +30 -0
- package/dist/cli/start/createActionRoute.js +54 -0
- package/dist/cli/start.js +34 -0
- package/dist/cli.js +19 -0
- package/dist/debug.js +5 -0
- package/dist/lib/client.js +22 -0
- package/dist/transform/asRelativeImport.js +19 -0
- package/dist/transform/client/transformRsfForClientPlugin.js +20 -0
- package/dist/transform/client/transformTopLevelRsf.js +64 -0
- package/dist/transform/collectExportNames.js +25 -0
- package/dist/transform/extname.js +10 -0
- package/dist/transform/getActionId.js +7 -0
- package/dist/transform/isTopLevelRsfFile.js +8 -0
- package/dist/transform/replaceFileExt.js +5 -0
- package/dist/transform/server/generateActionRegistry.js +60 -0
- package/dist/types.js +1 -0
- package/package.json +46 -0
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
|
+
};
|
package/dist/cli/dev.js
ADDED
@@ -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,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,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
|
+
}
|