rowork 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/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/rowork.js +18 -0
- package/dashboard/app.css +234 -0
- package/dashboard/app.js +117 -0
- package/dashboard/index.html +67 -0
- package/dist/cli/errors.d.ts +15 -0
- package/dist/cli/errors.js +18 -0
- package/dist/cli/program.d.ts +19 -0
- package/dist/cli/program.js +59 -0
- package/dist/cli/registry.d.ts +37 -0
- package/dist/cli/registry.js +61 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.js +77 -0
- package/dist/commands/assets-setup.d.ts +18 -0
- package/dist/commands/assets-setup.js +136 -0
- package/dist/commands/assets.d.ts +2 -0
- package/dist/commands/assets.js +153 -0
- package/dist/commands/console.d.ts +4 -0
- package/dist/commands/console.js +141 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +50 -0
- package/dist/commands/dev-background.d.ts +3 -0
- package/dist/commands/dev-background.js +87 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +270 -0
- package/dist/commands/eject.d.ts +2 -0
- package/dist/commands/eject.js +136 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.js +54 -0
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.js +50 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +55 -0
- package/dist/commands/links.d.ts +32 -0
- package/dist/commands/links.js +152 -0
- package/dist/commands/make-event.d.ts +2 -0
- package/dist/commands/make-event.js +160 -0
- package/dist/commands/make-menu.d.ts +3 -0
- package/dist/commands/make-menu.js +36 -0
- package/dist/commands/make-stat.d.ts +2 -0
- package/dist/commands/make-stat.js +199 -0
- package/dist/commands/make-tool.d.ts +2 -0
- package/dist/commands/make-tool.js +124 -0
- package/dist/commands/make-ui.d.ts +3 -0
- package/dist/commands/make-ui.js +199 -0
- package/dist/commands/make.d.ts +21 -0
- package/dist/commands/make.js +181 -0
- package/dist/commands/next-steps.d.ts +5 -0
- package/dist/commands/next-steps.js +18 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +144 -0
- package/dist/commands/studio.d.ts +3 -0
- package/dist/commands/studio.js +45 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +173 -0
- package/dist/commands/wire.d.ts +2 -0
- package/dist/commands/wire.js +34 -0
- package/dist/core/agent-docs.d.ts +23 -0
- package/dist/core/agent-docs.js +151 -0
- package/dist/core/assets.d.ts +96 -0
- package/dist/core/assets.js +268 -0
- package/dist/core/background.d.ts +47 -0
- package/dist/core/background.js +149 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +62 -0
- package/dist/core/exec.d.ts +22 -0
- package/dist/core/exec.js +38 -0
- package/dist/core/format-generated.d.ts +12 -0
- package/dist/core/format-generated.js +28 -0
- package/dist/core/generate.d.ts +37 -0
- package/dist/core/generate.js +94 -0
- package/dist/core/github-release.d.ts +28 -0
- package/dist/core/github-release.js +38 -0
- package/dist/core/info.d.ts +77 -0
- package/dist/core/info.js +65 -0
- package/dist/core/modules.d.ts +32 -0
- package/dist/core/modules.js +265 -0
- package/dist/core/naming.d.ts +6 -0
- package/dist/core/naming.js +26 -0
- package/dist/core/open-cloud.d.ts +58 -0
- package/dist/core/open-cloud.js +170 -0
- package/dist/core/project-index.d.ts +26 -0
- package/dist/core/project-index.js +59 -0
- package/dist/core/rojo-edit.d.ts +18 -0
- package/dist/core/rojo-edit.js +53 -0
- package/dist/core/rokit-installer.d.ts +15 -0
- package/dist/core/rokit-installer.js +77 -0
- package/dist/core/scaffold.d.ts +44 -0
- package/dist/core/scaffold.js +163 -0
- package/dist/core/schema-edit.d.ts +46 -0
- package/dist/core/schema-edit.js +125 -0
- package/dist/core/source-scan.d.ts +24 -0
- package/dist/core/source-scan.js +84 -0
- package/dist/core/studio.d.ts +83 -0
- package/dist/core/studio.js +225 -0
- package/dist/core/toolchain.d.ts +31 -0
- package/dist/core/toolchain.js +98 -0
- package/dist/core/tsconfig-edit.d.ts +11 -0
- package/dist/core/tsconfig-edit.js +47 -0
- package/dist/core/ui-edit.d.ts +31 -0
- package/dist/core/ui-edit.js +89 -0
- package/dist/core/versions.d.ts +24 -0
- package/dist/core/versions.js +67 -0
- package/dist/dashboard/open.d.ts +3 -0
- package/dist/dashboard/open.js +18 -0
- package/dist/dashboard/server.d.ts +28 -0
- package/dist/dashboard/server.js +191 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.js +135 -0
- package/dist/modules/format.d.ts +3 -0
- package/dist/modules/format.js +28 -0
- package/dist/modules/index.d.ts +7 -0
- package/dist/modules/index.js +12 -0
- package/dist/modules/integrations.d.ts +8 -0
- package/dist/modules/integrations.js +54 -0
- package/dist/modules/leaderstats.d.ts +3 -0
- package/dist/modules/leaderstats.js +104 -0
- package/dist/modules/lint.d.ts +5 -0
- package/dist/modules/lint.js +43 -0
- package/dist/modules/networking.d.ts +23 -0
- package/dist/modules/networking.js +246 -0
- package/dist/modules/player-data.d.ts +19 -0
- package/dist/modules/player-data.js +176 -0
- package/dist/modules/types.d.ts +96 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/ui.d.ts +3 -0
- package/dist/modules/ui.js +67 -0
- package/dist/plugins/api.d.ts +127 -0
- package/dist/plugins/api.js +21 -0
- package/dist/plugins/loader.d.ts +20 -0
- package/dist/plugins/loader.js +128 -0
- package/dist/process/log-mux.d.ts +13 -0
- package/dist/process/log-mux.js +31 -0
- package/dist/process/supervisor.d.ts +48 -0
- package/dist/process/supervisor.js +141 -0
- package/dist/process/tree-kill.d.ts +25 -0
- package/dist/process/tree-kill.js +70 -0
- package/dist/templates/engine.d.ts +18 -0
- package/dist/templates/engine.js +54 -0
- package/dist/ui/logger.d.ts +5 -0
- package/dist/ui/logger.js +53 -0
- package/dist/ui/prompt.d.ts +9 -0
- package/dist/ui/prompt.js +24 -0
- package/package.json +69 -0
- package/templates/init/README.md.tmpl +37 -0
- package/templates/init/_gitignore.tmpl +23 -0
- package/templates/init/default.project.json.tmpl +57 -0
- package/templates/init/package.json.tmpl +12 -0
- package/templates/init/rokit.toml.tmpl +5 -0
- package/templates/init/src/client/controllers/ExampleController.ts.tmpl +12 -0
- package/templates/init/src/client/runtime.client.ts.tmpl +6 -0
- package/templates/init/src/server/runtime.server.ts.tmpl +12 -0
- package/templates/init/src/server/services/ExampleService.ts.tmpl +12 -0
- package/templates/init/src/shared/_gitkeep.tmpl +0 -0
- package/templates/init/tsconfig.json.tmpl +28 -0
- package/templates/integrations/data-replication/DataReplicationService.ts.tmpl +33 -0
- package/templates/integrations/data-replication/PlayerDataController.ts.tmpl +40 -0
- package/templates/integrations/data-replication/dataEvents.ts.tmpl +21 -0
- package/templates/make/component.ts.tmpl +8 -0
- package/templates/make/controller.ts.tmpl +6 -0
- package/templates/make/event-handler.ts.tmpl +22 -0
- package/templates/make/screen.tsx.tmpl +35 -0
- package/templates/make/service.ts.tmpl +6 -0
- package/templates/make/stat-service.ts.tmpl +26 -0
- package/templates/make/ui-button.tsx.tmpl +22 -0
- package/templates/make/ui-image.tsx.tmpl +11 -0
- package/templates/make/ui-label.tsx.tmpl +18 -0
- package/templates/make/ui-panel.tsx.tmpl +33 -0
- package/templates/make/ui-story.tsx.tmpl +13 -0
- package/templates/modules/format/prettierignore.tmpl +9 -0
- package/templates/modules/format/prettierrc.json.tmpl +4 -0
- package/templates/modules/leaderstats/LeaderstatsService.ts.tmpl +69 -0
- package/templates/modules/lint/eslint.config.mjs.tmpl +17 -0
- package/templates/modules/networking/client-network.ts.tmpl +9 -0
- package/templates/modules/networking/networking.ts.tmpl +22 -0
- package/templates/modules/networking/rate-limit.ts.tmpl +52 -0
- package/templates/modules/networking/server-network.ts.tmpl +20 -0
- package/templates/modules/player-data/PlayerData.ts.tmpl +15 -0
- package/templates/modules/player-data/PlayerDataService.ts.tmpl +127 -0
- package/templates/modules/ui/App.tsx.tmpl +19 -0
- package/templates/modules/ui/Button.story.tsx.tmpl +13 -0
- package/templates/modules/ui/Button.tsx.tmpl +21 -0
- package/templates/modules/ui/UiController.tsx.tmpl +21 -0
- package/templates/modules/ui/screens/HomeScreen.tsx.tmpl +20 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { syncAgentDocs } from "../core/agent-docs.js";
|
|
3
|
+
import { collectInfo } from "../core/info.js";
|
|
4
|
+
import { defineCommand } from "../plugins/api.js";
|
|
5
|
+
import { requireProject } from "./make.js";
|
|
6
|
+
export const infoCommand = defineCommand({
|
|
7
|
+
name: "info",
|
|
8
|
+
description: "Show the project, what is installed and every available command (--json for scripts and AIs).",
|
|
9
|
+
options: [{ flags: "--json", description: "print machine-readable JSON on stdout" }],
|
|
10
|
+
run(context) {
|
|
11
|
+
const info = collectInfo({
|
|
12
|
+
config: context.config,
|
|
13
|
+
projectRoot: context.projectRoot,
|
|
14
|
+
roworkVersion: context.roworkVersion,
|
|
15
|
+
});
|
|
16
|
+
if (context.options["json"] === true) {
|
|
17
|
+
process.stdout.write(`${JSON.stringify(info, undefined, 2)}\n`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { logger } = context;
|
|
21
|
+
logger.info(`${pc.bold("Rowork")} ${context.roworkVersion}`);
|
|
22
|
+
if (info.project === null) {
|
|
23
|
+
logger.info(pc.dim("Not inside a Rowork project. `rowork start` creates one."));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const dev = info.project.dev;
|
|
27
|
+
logger.info(`${pc.bold(info.project.name)} ${pc.dim(info.project.root)}`);
|
|
28
|
+
logger.info(` dev: ${dev === null ? "not running" : `running ${dev.mode === "foreground" ? "in a terminal" : "in the background"} (pid ${dev.pid}, port ${dev.port})`}`);
|
|
29
|
+
logger.blank();
|
|
30
|
+
logger.info(pc.bold("Modules"));
|
|
31
|
+
for (const module of info.modules) {
|
|
32
|
+
logger.info(` ${module.installed ? pc.green("installed") : pc.dim("available")} ${module.name}: ${module.description}`);
|
|
33
|
+
}
|
|
34
|
+
logger.blank();
|
|
35
|
+
logger.info(pc.dim("`rowork info --json` prints all of this, and every command, for scripts and AIs."));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const agentsSyncCommand = defineCommand({
|
|
39
|
+
name: "agents:sync",
|
|
40
|
+
description: "Create or refresh AGENTS.md (instructions for AIs and newcomers) from the project's current state.",
|
|
41
|
+
run(context) {
|
|
42
|
+
const { root, config } = requireProject(context, "agents:sync");
|
|
43
|
+
const touched = syncAgentDocs(root, config, context.roworkVersion);
|
|
44
|
+
if (touched.length === 0)
|
|
45
|
+
context.logger.info("AGENTS.md is already up to date.");
|
|
46
|
+
else
|
|
47
|
+
context.logger.success(`Updated ${touched.join(", ")}.`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=info.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { RoworkError } from "../cli/errors.js";
|
|
4
|
+
import { scaffoldProject } from "../core/scaffold.js";
|
|
5
|
+
import { defineCommand } from "../plugins/api.js";
|
|
6
|
+
import { isInteractive } from "../ui/prompt.js";
|
|
7
|
+
import { printNextSteps } from "./next-steps.js";
|
|
8
|
+
import { startCommand } from "./start.js";
|
|
9
|
+
export const initCommand = defineCommand({
|
|
10
|
+
name: "init",
|
|
11
|
+
guided: true,
|
|
12
|
+
description: "Create a ready-to-run Roblox project (roblox-ts + Flamework + Rojo).",
|
|
13
|
+
arguments: [
|
|
14
|
+
{ name: "name", description: "project name, also used as the directory name", required: false },
|
|
15
|
+
],
|
|
16
|
+
options: [
|
|
17
|
+
{ flags: "--path <dir>", description: "parent directory to create the project in" },
|
|
18
|
+
{ flags: "--no-install", description: "skip installing npm dependencies" },
|
|
19
|
+
{ flags: "--no-rokit", description: "skip installing the pinned Roblox toolchain" },
|
|
20
|
+
{ flags: "--install-rokit", description: "download and install Rokit if it is missing" },
|
|
21
|
+
{ flags: "--no-git", description: "skip git repository initialisation" },
|
|
22
|
+
{ flags: "--no-examples", description: "skip the example service and controller" },
|
|
23
|
+
{ flags: "--no-lint", description: "skip the linter (ESLint with the roblox-ts rules)" },
|
|
24
|
+
{ flags: "--no-format", description: "skip the formatter (Prettier)" },
|
|
25
|
+
{ flags: "-f, --force", description: "allow a target directory that is not empty" },
|
|
26
|
+
],
|
|
27
|
+
async run(context) {
|
|
28
|
+
const name = context.args["name"];
|
|
29
|
+
if (typeof name !== "string") {
|
|
30
|
+
// No name given: this is the guided version of the command.
|
|
31
|
+
if (isInteractive())
|
|
32
|
+
return startCommand.run(context);
|
|
33
|
+
throw new RoworkError("Missing project name.", {
|
|
34
|
+
hint: `Usage: rowork init <name>. In a terminal, ${pc.bold("rowork init")} alone starts the guided setup.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const result = await scaffoldProject({
|
|
38
|
+
name,
|
|
39
|
+
parent: typeof context.options["path"] === "string"
|
|
40
|
+
? resolve(context.cwd, context.options["path"])
|
|
41
|
+
: context.cwd,
|
|
42
|
+
install: context.options["install"] !== false,
|
|
43
|
+
lint: context.options["lint"] !== false,
|
|
44
|
+
format: context.options["format"] !== false,
|
|
45
|
+
rokit: context.options["rokit"] !== false,
|
|
46
|
+
installRokit: context.options["installRokit"] === true,
|
|
47
|
+
git: context.options["git"] !== false,
|
|
48
|
+
examples: context.options["examples"] !== false,
|
|
49
|
+
force: context.options["force"] === true,
|
|
50
|
+
roworkVersion: context.roworkVersion,
|
|
51
|
+
}, context.logger);
|
|
52
|
+
printNextSteps(context.logger, name, result);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type EventInfo, type StatInfo } from "../core/project-index.js";
|
|
2
|
+
import type { RoworkConfig } from "../plugins/api.js";
|
|
3
|
+
/** Turns typed names into the matching stats, or explains what exists. */
|
|
4
|
+
export declare function resolveStats(stats: StatInfo[], typed: string, what: string): StatInfo[];
|
|
5
|
+
export declare function resolveEvent(events: EventInfo[], typed: string): EventInfo;
|
|
6
|
+
export declare function askStatLink(root: string, config: RoworkConfig, message: string, single: boolean): Promise<string | undefined>;
|
|
7
|
+
export declare function askEventLink(root: string, config: RoworkConfig, message: string): Promise<string | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* A server-side handler for an event the client sends, wired to a saved value.
|
|
10
|
+
*
|
|
11
|
+
* The amount is fixed here and never read from the event: a client that could
|
|
12
|
+
* choose the amount could give itself anything. The comments say so, because the
|
|
13
|
+
* next person to edit this file is exactly the one tempted to "just use the
|
|
14
|
+
* amount the client sent".
|
|
15
|
+
*/
|
|
16
|
+
export declare function createEventHandler(options: {
|
|
17
|
+
root: string;
|
|
18
|
+
config: RoworkConfig;
|
|
19
|
+
event: {
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
stat: StatInfo;
|
|
23
|
+
}): {
|
|
24
|
+
path: string;
|
|
25
|
+
className: string;
|
|
26
|
+
};
|
|
27
|
+
/** Constructor injection for a service that uses saved values. */
|
|
28
|
+
export declare function serviceMembers(root: string, config: RoworkConfig, stats: StatInfo[]): {
|
|
29
|
+
imports: string;
|
|
30
|
+
members: string;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { RoworkError } from "../cli/errors.js";
|
|
2
|
+
import { generateFile, importPath } from "../core/generate.js";
|
|
3
|
+
import { listEvents, listStats, pascal, statServiceClass } from "../core/project-index.js";
|
|
4
|
+
import { toFieldName } from "../modules/player-data.js";
|
|
5
|
+
import { answered, prompts } from "../ui/prompt.js";
|
|
6
|
+
/**
|
|
7
|
+
* "Link it to ...?" for the guided `make:` commands.
|
|
8
|
+
*
|
|
9
|
+
* The person types the name of something that already exists, or leaves it empty
|
|
10
|
+
* for no link. Free text rather than a closed list, on purpose: it is faster for
|
|
11
|
+
* someone who knows the name, and what is available is printed in the question.
|
|
12
|
+
*/
|
|
13
|
+
async function askName(message, available) {
|
|
14
|
+
const raw = answered(await prompts.text({
|
|
15
|
+
message: `${message} Available: ${available.join(", ")}. Leave empty for none.`,
|
|
16
|
+
placeholder: available[0] ?? "",
|
|
17
|
+
defaultValue: "",
|
|
18
|
+
validate: (value) => {
|
|
19
|
+
const typed = (value ?? "").trim();
|
|
20
|
+
if (typed === "")
|
|
21
|
+
return undefined;
|
|
22
|
+
return typed.split(",").every((part) => available.includes(toFieldName(part))) ? undefined : `Pick from: ${available.join(", ")}`;
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
return raw.trim() === "" ? undefined : raw;
|
|
26
|
+
}
|
|
27
|
+
/** Turns typed names into the matching stats, or explains what exists. */
|
|
28
|
+
export function resolveStats(stats, typed, what) {
|
|
29
|
+
const wanted = typed.split(",").map((part) => toFieldName(part)).filter((part) => part !== "");
|
|
30
|
+
const found = wanted.map((name) => stats.find((stat) => stat.name === name));
|
|
31
|
+
const missing = wanted.filter((_, index) => found[index] === undefined);
|
|
32
|
+
if (missing.length > 0) {
|
|
33
|
+
throw new RoworkError(`No saved value called ${missing.map((name) => `\`${name}\``).join(", ")}.`, {
|
|
34
|
+
hint: stats.length === 0 ? `There are no saved values yet: create one with \`rowork make:stat\`, then ${what}.` : `Saved values: ${stats.map((stat) => stat.name).join(", ")}.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
export function resolveEvent(events, typed) {
|
|
40
|
+
const name = toFieldName(typed);
|
|
41
|
+
const event = events.find((candidate) => candidate.name === name && candidate.direction === "server");
|
|
42
|
+
if (event === undefined) {
|
|
43
|
+
const available = events.filter((candidate) => candidate.direction === "server").map((candidate) => candidate.name);
|
|
44
|
+
throw new RoworkError(`No event from the client called \`${name}\`.`, {
|
|
45
|
+
hint: available.length === 0 ? "There is none yet: create one with `rowork make:event`." : `Events the client sends: ${available.join(", ")}.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
export async function askStatLink(root, config, message, single) {
|
|
51
|
+
const stats = listStats(root, config);
|
|
52
|
+
if (stats.length === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
return askName(message, stats.map((stat) => stat.name)).then((typed) => (single && typed !== undefined ? typed.split(",")[0] : typed));
|
|
55
|
+
}
|
|
56
|
+
export async function askEventLink(root, config, message) {
|
|
57
|
+
const events = listEvents(root, config).filter((event) => event.direction === "server");
|
|
58
|
+
if (events.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
return askName(message, events.map((event) => event.name));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* How generated code reaches saved values: through a stat's own service when
|
|
64
|
+
* `make:stat` created one, otherwise through PlayerDataService.
|
|
65
|
+
*/
|
|
66
|
+
function accessFor(root, config, stats) {
|
|
67
|
+
const imports = [];
|
|
68
|
+
const ctor = [];
|
|
69
|
+
let usesData = false;
|
|
70
|
+
for (const stat of stats) {
|
|
71
|
+
const service = statServiceClass(root, config, stat.name);
|
|
72
|
+
if (service !== undefined) {
|
|
73
|
+
imports.push(`import { ${service} } from "./${service}";`);
|
|
74
|
+
ctor.push(`private readonly ${stat.name}: ${service}`);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
usesData = true;
|
|
78
|
+
}
|
|
79
|
+
if (usesData) {
|
|
80
|
+
imports.push('import { PlayerDataService } from "./PlayerDataService";');
|
|
81
|
+
ctor.push("private readonly playerData: PlayerDataService");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
imports,
|
|
85
|
+
ctor,
|
|
86
|
+
add: (stat) => statServiceClass(root, config, stat.name) !== undefined
|
|
87
|
+
? `this.${stat.name}.add(player, 1);`
|
|
88
|
+
: `this.playerData.update(player, (data) => {\n\t\t\t\tdata.${stat.name} += 1;\n\t\t\t\treturn data;\n\t\t\t});`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A server-side handler for an event the client sends, wired to a saved value.
|
|
93
|
+
*
|
|
94
|
+
* The amount is fixed here and never read from the event: a client that could
|
|
95
|
+
* choose the amount could give itself anything. The comments say so, because the
|
|
96
|
+
* next person to edit this file is exactly the one tempted to "just use the
|
|
97
|
+
* amount the client sent".
|
|
98
|
+
*/
|
|
99
|
+
export function createEventHandler(options) {
|
|
100
|
+
const { root, config, event, stat } = options;
|
|
101
|
+
const className = `${pascal(event.name)}Handler`;
|
|
102
|
+
const access = accessFor(root, config, [stat]);
|
|
103
|
+
const body = stat.type === "number"
|
|
104
|
+
? [
|
|
105
|
+
`\t\t\t// The amount is decided HERE, on the server. Do not use a number the client sent:`,
|
|
106
|
+
`\t\t\t// a player could send any amount and give themselves anything.`,
|
|
107
|
+
`\t\t\t${access.add(stat)}`,
|
|
108
|
+
].join("\n")
|
|
109
|
+
: `\t\t\t// TODO: change \`${stat.name}\` here, deciding the new value on the server. Never copy what the client sent.`;
|
|
110
|
+
const services = config.paths.services;
|
|
111
|
+
const written = generateFile({
|
|
112
|
+
projectRoot: root,
|
|
113
|
+
directory: services,
|
|
114
|
+
fileName: `${className}.ts`,
|
|
115
|
+
template: "event-handler",
|
|
116
|
+
variables: {
|
|
117
|
+
className,
|
|
118
|
+
event: event.name,
|
|
119
|
+
networkImport: importPath(root, services, `${config.paths.source}/server/network`),
|
|
120
|
+
imports: stat.type === "number" ? access.imports.join("\n") : "",
|
|
121
|
+
ctor: stat.type === "number" ? access.ctor.join(", ") : "",
|
|
122
|
+
body,
|
|
123
|
+
},
|
|
124
|
+
force: false,
|
|
125
|
+
});
|
|
126
|
+
return { path: written ?? `${services}/${className}.ts`, className };
|
|
127
|
+
}
|
|
128
|
+
/** A constructor, laid out the way Prettier does: one parameter property per line as soon as there are several. */
|
|
129
|
+
function constructorSource(params) {
|
|
130
|
+
if (params.length <= 1)
|
|
131
|
+
return `\tconstructor(${params.join("")}) {}`;
|
|
132
|
+
return `\tconstructor(\n${params.map((param) => `\t\t${param},`).join("\n")}\n\t) {}`;
|
|
133
|
+
}
|
|
134
|
+
/** Constructor injection for a service that uses saved values. */
|
|
135
|
+
export function serviceMembers(root, config, stats) {
|
|
136
|
+
if (stats.length === 0)
|
|
137
|
+
return { imports: "", members: "" };
|
|
138
|
+
const access = accessFor(root, config, stats);
|
|
139
|
+
// How to reach each value, written where the next reader will look.
|
|
140
|
+
const howTo = stats.map((stat) => {
|
|
141
|
+
const viaService = statServiceClass(root, config, stat.name) !== undefined;
|
|
142
|
+
if (!viaService)
|
|
143
|
+
return `\t// ${stat.name}: this.playerData.get(player)?.${stat.name}, or this.playerData.update(player, (data) => { ...; return data; })`;
|
|
144
|
+
return `\t// ${stat.name}: this.${stat.name}.get(player), this.${stat.name}.set(player, value)${stat.type === "number" ? `, this.${stat.name}.add(player, amount)` : ""}`;
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
// A blank line after the imports, as in a file written by hand.
|
|
148
|
+
imports: `${access.imports.join("\n")}\n`,
|
|
149
|
+
members: `\t// Saved values this service works with:\n${howTo.join("\n")}\n${constructorSource(access.ctor)}\n\n`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { RoworkError } from "../cli/errors.js";
|
|
3
|
+
import { resolveProjectPath } from "../core/config.js";
|
|
4
|
+
import { addEventToNetworking, addRateLimit } from "../core/schema-edit.js";
|
|
5
|
+
import { askEventParameters, checkParameters } from "../modules/networking.js";
|
|
6
|
+
import { toFieldName } from "../modules/player-data.js";
|
|
7
|
+
import { installModule } from "../core/modules.js";
|
|
8
|
+
import { networkingModule } from "../modules/networking.js";
|
|
9
|
+
import { defineCommand } from "../plugins/api.js";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { listStats, pascal } from "../core/project-index.js";
|
|
13
|
+
import { answered, prompts, requireInteractive } from "../ui/prompt.js";
|
|
14
|
+
import { askStatLink, createEventHandler, resolveStats } from "./links.js";
|
|
15
|
+
import { requireProject } from "./make.js";
|
|
16
|
+
const IDENTIFIER = /^[a-z][A-Za-z0-9]*$/;
|
|
17
|
+
export const makeEventCommand = defineCommand({
|
|
18
|
+
name: "make:event",
|
|
19
|
+
description: "Add an event (a typed message between client and server) to the networking file where it belongs.",
|
|
20
|
+
guided: true,
|
|
21
|
+
arguments: [{ name: "name", description: "the message, e.g. buyItem (omit it for the guided version)", required: false }],
|
|
22
|
+
options: [
|
|
23
|
+
{ flags: "--to <side>", description: "who receives it: server (default) or client" },
|
|
24
|
+
{ flags: "--args <list>", description: 'what it carries, e.g. "itemId: string, amount: number"' },
|
|
25
|
+
{ flags: "--link <value>", description: "a saved value the server changes when it receives this (a client event only)" },
|
|
26
|
+
],
|
|
27
|
+
async run(context) {
|
|
28
|
+
const { root, config } = requireProject(context, "make:event");
|
|
29
|
+
const missingModule = !(config.modules ?? []).includes("networking");
|
|
30
|
+
const given = context.args["name"];
|
|
31
|
+
if (missingModule && typeof given === "string") {
|
|
32
|
+
// Scripted: no question to ask. Say the one command that does both.
|
|
33
|
+
throw new RoworkError("There is no networking file to add an event to.", {
|
|
34
|
+
hint: `Run \`rowork add:networking --event "${given}:server()"\`: it installs typed networking and starts with that event.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
let rawName;
|
|
38
|
+
let side = "server";
|
|
39
|
+
let parameters = "";
|
|
40
|
+
let linkTyped;
|
|
41
|
+
if (typeof given === "string") {
|
|
42
|
+
rawName = given;
|
|
43
|
+
const to = context.options["to"];
|
|
44
|
+
if (to !== undefined) {
|
|
45
|
+
if (to !== "server" && to !== "client") {
|
|
46
|
+
throw new RoworkError(`Unknown side \`${String(to)}\`.`, { hint: "--to server (the client sends it) or --to client (the server sends it)." });
|
|
47
|
+
}
|
|
48
|
+
side = to;
|
|
49
|
+
}
|
|
50
|
+
parameters = checkParameters(typeof context.options["args"] === "string" ? context.options["args"] : "");
|
|
51
|
+
if (typeof context.options["link"] === "string")
|
|
52
|
+
linkTyped = context.options["link"];
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
requireInteractive("make:event", 'rowork make:event <name> --to server --args "itemId: string"');
|
|
56
|
+
rawName = answered(await prompts.text({
|
|
57
|
+
message: "Name of the message (anything you like, e.g. buy item)",
|
|
58
|
+
placeholder: "buy item",
|
|
59
|
+
validate: (value) => (IDENTIFIER.test(toFieldName(value ?? "")) ? undefined : "Use letters and digits, starting with a letter."),
|
|
60
|
+
}));
|
|
61
|
+
side = answered(await prompts.select({
|
|
62
|
+
message: `Who sends ${toFieldName(rawName)}?`,
|
|
63
|
+
options: [
|
|
64
|
+
{ value: "server", label: "The player's client, to the server", hint: "a button click, a purchase" },
|
|
65
|
+
{ value: "client", label: "The server, to the player(s)", hint: "a notification, a result" },
|
|
66
|
+
],
|
|
67
|
+
}));
|
|
68
|
+
parameters = await askEventParameters(toFieldName(rawName));
|
|
69
|
+
// "Link it to...?" only makes sense for what the client sends: that is what the server reacts to.
|
|
70
|
+
if (side === "server") {
|
|
71
|
+
linkTyped = await askStatLink(root, config, "Should the server change a saved value when it receives this? Type its name.", true);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const name = toFieldName(rawName);
|
|
75
|
+
if (!IDENTIFIER.test(name)) {
|
|
76
|
+
throw new RoworkError(`\`${rawName}\` cannot be used as a name.`, { hint: "Use letters and digits, starting with a letter." });
|
|
77
|
+
}
|
|
78
|
+
// A link is checked before anything is written.
|
|
79
|
+
let linkStat;
|
|
80
|
+
if (linkTyped !== undefined) {
|
|
81
|
+
if (side !== "server") {
|
|
82
|
+
throw new RoworkError("A link only applies to an event the client sends.", {
|
|
83
|
+
hint: "The server reacts to events from the client. Use --to server, or drop --link.",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
linkStat = resolveStats(listStats(root, config), linkTyped, "then link it")[0];
|
|
87
|
+
const handlerFile = join(config.paths.services, `${pascal(name)}Handler.ts`);
|
|
88
|
+
if (existsSync(join(root, handlerFile))) {
|
|
89
|
+
throw new RoworkError(`${handlerFile} already exists.`, { hint: "Pick another name for the event." });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const linkNow = () => {
|
|
93
|
+
if (linkStat === undefined)
|
|
94
|
+
return;
|
|
95
|
+
const handler = createEventHandler({ root, config, event: { name }, stat: linkStat });
|
|
96
|
+
context.logger.step(`${handler.path}: runs on the server when \`${name}\` arrives, linked to \`${linkStat.name}\``);
|
|
97
|
+
context.logger.info("It decides the amount on the server: do not use a number the client sent.");
|
|
98
|
+
};
|
|
99
|
+
// Guided and typed networking is not installed: offer to install it, starting with this event.
|
|
100
|
+
if (missingModule) {
|
|
101
|
+
const proceed = answered(await prompts.confirm({
|
|
102
|
+
message: `Events need the Typed networking module, which is not installed. Add it now, starting with \`${name}\`?`,
|
|
103
|
+
initialValue: true,
|
|
104
|
+
}));
|
|
105
|
+
if (!proceed) {
|
|
106
|
+
prompts.cancel("Nothing changed.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await installModule({ ...context, options: { event: [`${name}:${side}(${parameters})`] } }, networkingModule, root, false);
|
|
110
|
+
linkNow();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const file = `${config.paths.shared}/networking.ts`;
|
|
114
|
+
const path = resolveProjectPath(root, file);
|
|
115
|
+
let source;
|
|
116
|
+
try {
|
|
117
|
+
source = readFileSync(path, "utf8");
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new RoworkError(`Cannot read ${file}.`, { hint: "It is created by `rowork add:networking`." });
|
|
121
|
+
}
|
|
122
|
+
const newNetworking = addEventToNetworking(source, file, side, { name, parameters });
|
|
123
|
+
// An event the client sends is rate limited like the others: its line is added to the
|
|
124
|
+
// server's list. Computed before anything is written, like every edit here.
|
|
125
|
+
const serverFile = `${config.paths.source}/server/network.ts`;
|
|
126
|
+
let newServer;
|
|
127
|
+
let unprotected = false;
|
|
128
|
+
if (side === "server") {
|
|
129
|
+
try {
|
|
130
|
+
newServer = addRateLimit(readFileSync(resolveProjectPath(root, serverFile), "utf8"), name);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
newServer = undefined;
|
|
134
|
+
}
|
|
135
|
+
unprotected = newServer === undefined;
|
|
136
|
+
}
|
|
137
|
+
writeFileSync(path, newNetworking, "utf8");
|
|
138
|
+
context.logger.step(`${file}: added ${name}(${parameters})`);
|
|
139
|
+
if (newServer !== undefined) {
|
|
140
|
+
writeFileSync(resolveProjectPath(root, serverFile), newServer, "utf8");
|
|
141
|
+
context.logger.step(`${serverFile}: ${name} is rate limited per player`);
|
|
142
|
+
}
|
|
143
|
+
if (unprotected) {
|
|
144
|
+
context.logger.warn(`${serverFile} has no \`middleware\` list: \`${name}\` is NOT rate limited. Add \`${name}: [limit()]\` yourself, or a cheater can flood it.`);
|
|
145
|
+
}
|
|
146
|
+
linkNow();
|
|
147
|
+
context.logger.success(`Event \`${name}\` added (${side === "server" ? "client to server" : "server to client"}).`);
|
|
148
|
+
context.logger.blank();
|
|
149
|
+
const args = parameters === "" ? "" : "...";
|
|
150
|
+
if (side === "server") {
|
|
151
|
+
context.logger.info(`Server listens: Events.${name}.connect((player${args === "" ? "" : ", ..."}) => { ... });`);
|
|
152
|
+
context.logger.info(`Client sends: Events.${name}.fire(${args});`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
context.logger.info(`Client listens: Events.${name}.connect((${args}) => { ... });`);
|
|
156
|
+
context.logger.info(`Server sends: Events.${name}.fire(player${args === "" ? "" : ", ..."});`);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=make-event.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineCommand } from "../plugins/api.js";
|
|
2
|
+
import { answered, prompts, requireInteractive } from "../ui/prompt.js";
|
|
3
|
+
import { makeEventCommand } from "./make-event.js";
|
|
4
|
+
import { makeStatCommand } from "./make-stat.js";
|
|
5
|
+
import { makeScreenCommand, makeUiCommand } from "./make-ui.js";
|
|
6
|
+
import { makeComponentCommand, makeControllerCommand, makeServiceCommand, requireProject } from "./make.js";
|
|
7
|
+
/** Everything `rowork make` can create, in the order most people need it. */
|
|
8
|
+
const CHOICES = [
|
|
9
|
+
{ command: makeStatCommand, label: "Stat (saved value)", hint: "kills, coins, level: added to the player data everywhere it must be" },
|
|
10
|
+
{ command: makeEventCommand, label: "Event", hint: "a typed message between client and server" },
|
|
11
|
+
{ command: makeServiceCommand, label: "Service", hint: "server-side logic: data, rules, spawning" },
|
|
12
|
+
{ command: makeControllerCommand, label: "Controller", hint: "client-side logic: input, camera, effects" },
|
|
13
|
+
{ command: makeScreenCommand, label: "Screen", hint: "a full-screen layer of the interface (a ScreenGui), shown by App" },
|
|
14
|
+
{ command: makeUiCommand, label: "UI element", hint: "a button, text, panel or image, with its Studio preview" },
|
|
15
|
+
{ command: makeComponentCommand, label: "Component", hint: "behaviour for objects you tag in Studio: doors, pickups" },
|
|
16
|
+
];
|
|
17
|
+
export const makeCommand = defineCommand({
|
|
18
|
+
name: "make",
|
|
19
|
+
guided: true,
|
|
20
|
+
description: "Create a file in the right place and connect it where it is needed (choose from a list).",
|
|
21
|
+
async run(context) {
|
|
22
|
+
requireProject(context, "make");
|
|
23
|
+
requireInteractive("make", "rowork make:service <name>, make:controller <name>, make:component <name>, make:stat <name>, make:event <name>, make:screen <name> or make:ui <name>");
|
|
24
|
+
prompts.intro("rowork make");
|
|
25
|
+
const chosen = answered(await prompts.select({
|
|
26
|
+
message: "Which file do you want to create? (Rowork puts it in the right place and connects it)",
|
|
27
|
+
options: CHOICES.map((choice) => ({ value: choice.command.name, label: choice.label, hint: choice.hint })),
|
|
28
|
+
}));
|
|
29
|
+
const choice = CHOICES.find((candidate) => candidate.command.name === chosen);
|
|
30
|
+
if (choice === undefined)
|
|
31
|
+
return;
|
|
32
|
+
// No argument and no option: the chosen command runs its guided version.
|
|
33
|
+
await choice.command.run({ ...context, args: {}, options: {} });
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=make-menu.js.map
|