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,32 @@
|
|
|
1
|
+
import { coreIntegrations } from "../modules/integrations.js";
|
|
2
|
+
import type { ModuleDefinition } from "../modules/types.js";
|
|
3
|
+
import type { CommandContext, RoworkConfig } from "../plugins/api.js";
|
|
4
|
+
export declare function installedModules(context: CommandContext): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Installs one module into the current project.
|
|
7
|
+
*
|
|
8
|
+
* Checks come first and writes come last: a module that cannot be installed
|
|
9
|
+
* (already there, missing prerequisite, a file it would overwrite) must leave
|
|
10
|
+
* the project exactly as it was.
|
|
11
|
+
*/
|
|
12
|
+
export declare function installModule(context: CommandContext, definition: ModuleDefinition, root: string, guided: boolean): Promise<void>;
|
|
13
|
+
/** Integrations whose modules are all installed and that have not been applied yet. */
|
|
14
|
+
export declare function pendingIntegrations(config: RoworkConfig): typeof coreIntegrations;
|
|
15
|
+
/**
|
|
16
|
+
* Generates the glue between modules that are now all installed.
|
|
17
|
+
*
|
|
18
|
+
* A failure never undoes the module that triggered it: the integration is
|
|
19
|
+
* skipped with a warning and stays pending, so `rowork wire` can retry.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyPendingIntegrations(context: CommandContext, root: string, options?: {
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Installs several modules in dependency order, for `rowork start`.
|
|
26
|
+
*
|
|
27
|
+
* A module that needs another one brings it along. One failing does not stop
|
|
28
|
+
* the others: the project already exists and is usable, so it is reported and
|
|
29
|
+
* can be retried with `rowork add`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installModulesByName(context: CommandContext, root: string, names: string[]): Promise<string[]>;
|
|
32
|
+
//# sourceMappingURL=modules.d.ts.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { RoworkError } from "../cli/errors.js";
|
|
4
|
+
import { coreModules } from "../modules/index.js";
|
|
5
|
+
import { coreIntegrations } from "../modules/integrations.js";
|
|
6
|
+
import { syncAgentDocs } from "./agent-docs.js";
|
|
7
|
+
import { addNodeModuleScopes } from "./rojo-edit.js";
|
|
8
|
+
import { setCompilerOptions } from "./tsconfig-edit.js";
|
|
9
|
+
import { CONFIG_FILENAME, loadConfig, resolveProjectPath } from "./config.js";
|
|
10
|
+
import { run as runBinary } from "./exec.js";
|
|
11
|
+
import { ensureFlameworkPath, generateFile, importPath } from "./generate.js";
|
|
12
|
+
import { findExecutable } from "./toolchain.js";
|
|
13
|
+
/** Prefix a module uses to say "the import path to this project file". */
|
|
14
|
+
const IMPORT_MARKER = "@import:";
|
|
15
|
+
export function installedModules(context) {
|
|
16
|
+
return context.config?.modules ?? [];
|
|
17
|
+
}
|
|
18
|
+
/** Adds a module's name to `modules` in rowork.json, keeping the file readable. */
|
|
19
|
+
function recordModule(projectRoot, name) {
|
|
20
|
+
const file = join(projectRoot, CONFIG_FILENAME);
|
|
21
|
+
const config = JSON.parse(readFileSync(file, "utf8"));
|
|
22
|
+
config.modules = [...new Set([...(config.modules ?? []), name])].sort();
|
|
23
|
+
writeFileSync(file, `${JSON.stringify(config, undefined, 2)}\n`, "utf8");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Installs one module into the current project.
|
|
27
|
+
*
|
|
28
|
+
* Checks come first and writes come last: a module that cannot be installed
|
|
29
|
+
* (already there, missing prerequisite, a file it would overwrite) must leave
|
|
30
|
+
* the project exactly as it was.
|
|
31
|
+
*/
|
|
32
|
+
export async function installModule(context, definition, root, guided) {
|
|
33
|
+
const config = context.config;
|
|
34
|
+
if (config === undefined)
|
|
35
|
+
throw new RoworkError("No project.");
|
|
36
|
+
const { logger } = context;
|
|
37
|
+
if (installedModules(context).includes(definition.name)) {
|
|
38
|
+
throw new RoworkError(`The ${definition.title} module is already installed.`, {
|
|
39
|
+
hint: "Its files are yours to edit. Adding it again would overwrite your changes.",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
for (const required of definition.requires ?? []) {
|
|
43
|
+
if (!installedModules(context).includes(required)) {
|
|
44
|
+
throw new RoworkError(`The ${definition.title} module needs the \`${required}\` module first.`, {
|
|
45
|
+
hint: `Run \`rowork add:${required}\`, then try again.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const plan = await definition.plan({ guided, options: context.options, config, projectRoot: root });
|
|
50
|
+
const files = resolveFiles(plan, root);
|
|
51
|
+
assertNoClash(files, root);
|
|
52
|
+
// tsconfig.json is computed now, so a conflict stops the install before anything is written.
|
|
53
|
+
let newTsconfig;
|
|
54
|
+
if (plan.compilerOptions !== undefined) {
|
|
55
|
+
const path = join(root, "tsconfig.json");
|
|
56
|
+
let current;
|
|
57
|
+
try {
|
|
58
|
+
current = readFileSync(path, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new RoworkError("Cannot read tsconfig.json.", { hint: "This module needs to set compiler options there." });
|
|
62
|
+
}
|
|
63
|
+
newTsconfig = { path, source: setCompilerOptions(current, "tsconfig.json", plan.compilerOptions) };
|
|
64
|
+
}
|
|
65
|
+
// Same for default.project.json: computed now, written with the rest.
|
|
66
|
+
let newProject;
|
|
67
|
+
if (plan.nodeModuleScopes !== undefined) {
|
|
68
|
+
const path = join(root, config.paths.rojoProject);
|
|
69
|
+
let current;
|
|
70
|
+
try {
|
|
71
|
+
current = readFileSync(path, "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new RoworkError(`Cannot read ${config.paths.rojoProject}.`, { hint: "This module needs to map a package folder there." });
|
|
75
|
+
}
|
|
76
|
+
newProject = { path, source: addNodeModuleScopes(current, config.paths.rojoProject, plan.nodeModuleScopes) };
|
|
77
|
+
}
|
|
78
|
+
logger.info(`Adding the ${definition.title} module`);
|
|
79
|
+
if (definition.dependencies !== undefined && context.options["install"] !== false) {
|
|
80
|
+
if (findExecutable("npm", root) === undefined) {
|
|
81
|
+
throw new RoworkError("npm was not found on your PATH.", { hint: "Install Node.js." });
|
|
82
|
+
}
|
|
83
|
+
logger.step(`installing ${definition.dependencies.join(", ")} (npm)`);
|
|
84
|
+
logger.blank();
|
|
85
|
+
await runBinary("npm", ["install", ...definition.dependencies], { cwd: root });
|
|
86
|
+
logger.blank();
|
|
87
|
+
}
|
|
88
|
+
writePlan(logger, root, config, plan, files, "modules");
|
|
89
|
+
if (newTsconfig !== undefined && newTsconfig.source !== readFileSync(newTsconfig.path, "utf8")) {
|
|
90
|
+
writeFileSync(newTsconfig.path, newTsconfig.source, "utf8");
|
|
91
|
+
logger.step(`tsconfig.json: set ${Object.keys(plan.compilerOptions ?? {}).join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
if (newProject !== undefined && newProject.source !== readFileSync(newProject.path, "utf8")) {
|
|
94
|
+
writeFileSync(newProject.path, newProject.source, "utf8");
|
|
95
|
+
logger.step(`${config.paths.rojoProject}: mapped ${(plan.nodeModuleScopes ?? []).join(", ")} into the game`);
|
|
96
|
+
}
|
|
97
|
+
recordModule(root, definition.name);
|
|
98
|
+
if (definition.postInstall !== undefined && context.options["install"] !== false) {
|
|
99
|
+
try {
|
|
100
|
+
await definition.postInstall({ logger, projectRoot: root, options: context.options });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.warn(`${definition.title}: a last step did not finish (${error instanceof Error ? error.message : String(error)}). The module itself is installed.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Glue between this module and the ones already there, whatever the order.
|
|
107
|
+
await applyPendingIntegrations(context, root);
|
|
108
|
+
// The project's AGENTS.md lists installed modules and how to use them.
|
|
109
|
+
const touched = syncAgentDocs(root, loadConfig(root), context.roworkVersion);
|
|
110
|
+
if (touched.length > 0)
|
|
111
|
+
logger.step(`updated ${touched.join(", ")}`);
|
|
112
|
+
logger.success(`${definition.title} module added`);
|
|
113
|
+
logger.blank();
|
|
114
|
+
for (const note of plan.notes)
|
|
115
|
+
logger.info(note);
|
|
116
|
+
}
|
|
117
|
+
/** Turns `@import:` markers into real relative import paths. */
|
|
118
|
+
function resolveFiles(plan, root) {
|
|
119
|
+
return plan.files.map((file) => ({
|
|
120
|
+
...file,
|
|
121
|
+
variables: Object.fromEntries(Object.entries(file.variables).map(([key, value]) => [
|
|
122
|
+
key,
|
|
123
|
+
value.startsWith(IMPORT_MARKER)
|
|
124
|
+
? importPath(root, file.directory, value.slice(IMPORT_MARKER.length))
|
|
125
|
+
: value,
|
|
126
|
+
])),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function clashesOf(files, root) {
|
|
130
|
+
return files
|
|
131
|
+
.map((file) => join(file.directory, file.fileName))
|
|
132
|
+
.filter((path) => existsSync(resolveProjectPath(root, path)));
|
|
133
|
+
}
|
|
134
|
+
function assertNoClash(files, root) {
|
|
135
|
+
const clashes = clashesOf(files, root);
|
|
136
|
+
if (clashes.length > 0) {
|
|
137
|
+
throw new RoworkError(`Would overwrite: ${clashes.join(", ")}.`, {
|
|
138
|
+
hint: "Move or rename those files, then run the command again.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function writePlan(logger, root, config, plan, files, templateRoot) {
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const written = generateFile({
|
|
145
|
+
projectRoot: root,
|
|
146
|
+
directory: file.directory,
|
|
147
|
+
fileName: file.fileName,
|
|
148
|
+
template: file.template,
|
|
149
|
+
templateRoot,
|
|
150
|
+
variables: file.variables,
|
|
151
|
+
force: false,
|
|
152
|
+
});
|
|
153
|
+
if (written !== undefined)
|
|
154
|
+
logger.step(written);
|
|
155
|
+
}
|
|
156
|
+
for (const entry of plan.register) {
|
|
157
|
+
const runtime = resolveProjectPath(root, join(config.paths.source, entry.side, `runtime.${entry.side}.ts`));
|
|
158
|
+
if (ensureFlameworkPath(runtime, entry.directory, logger)) {
|
|
159
|
+
logger.step(`registered ${entry.directory} in runtime.${entry.side}.ts`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
addScripts(logger, root, plan.scripts);
|
|
163
|
+
}
|
|
164
|
+
/** Adds npm scripts to package.json. A script the user already has is left exactly as it is. */
|
|
165
|
+
function addScripts(logger, root, scripts) {
|
|
166
|
+
if (scripts === undefined)
|
|
167
|
+
return;
|
|
168
|
+
const file = join(root, "package.json");
|
|
169
|
+
if (!existsSync(file))
|
|
170
|
+
return;
|
|
171
|
+
const source = readFileSync(file, "utf8");
|
|
172
|
+
const manifest = JSON.parse(source);
|
|
173
|
+
const existing = manifest.scripts ?? {};
|
|
174
|
+
const added = [];
|
|
175
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
176
|
+
if (existing[name] === undefined) {
|
|
177
|
+
existing[name] = command;
|
|
178
|
+
added.push(name);
|
|
179
|
+
}
|
|
180
|
+
else if (existing[name] !== command) {
|
|
181
|
+
logger.warn(`package.json already has a \`${name}\` script: kept as it is.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (added.length === 0)
|
|
185
|
+
return;
|
|
186
|
+
manifest.scripts = existing;
|
|
187
|
+
writeFileSync(file, `${JSON.stringify(manifest, undefined, /^\t/m.test(source) ? "\t" : 2)}\n`, "utf8");
|
|
188
|
+
logger.step(`package.json: added ${added.map((name) => `\`npm run ${name}\``).join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
function recordIntegration(projectRoot, name) {
|
|
191
|
+
const file = join(projectRoot, CONFIG_FILENAME);
|
|
192
|
+
const config = JSON.parse(readFileSync(file, "utf8"));
|
|
193
|
+
config.integrations = [...new Set([...(config.integrations ?? []), name])].sort();
|
|
194
|
+
writeFileSync(file, `${JSON.stringify(config, undefined, 2)}\n`, "utf8");
|
|
195
|
+
}
|
|
196
|
+
/** Integrations whose modules are all installed and that have not been applied yet. */
|
|
197
|
+
export function pendingIntegrations(config) {
|
|
198
|
+
const installed = config.modules ?? [];
|
|
199
|
+
const applied = config.integrations ?? [];
|
|
200
|
+
return coreIntegrations.filter((integration) => integration.modules.every((name) => installed.includes(name)) && !applied.includes(integration.name));
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Generates the glue between modules that are now all installed.
|
|
204
|
+
*
|
|
205
|
+
* A failure never undoes the module that triggered it: the integration is
|
|
206
|
+
* skipped with a warning and stays pending, so `rowork wire` can retry.
|
|
207
|
+
*/
|
|
208
|
+
export async function applyPendingIntegrations(context, root, options = {}) {
|
|
209
|
+
const { logger } = context;
|
|
210
|
+
const applied = [];
|
|
211
|
+
for (const integration of pendingIntegrations(loadConfig(root))) {
|
|
212
|
+
if (options.dryRun === true) {
|
|
213
|
+
applied.push(integration.name);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const config = loadConfig(root);
|
|
218
|
+
const plan = integration.plan({ config, projectRoot: root });
|
|
219
|
+
const files = resolveFiles(plan, root);
|
|
220
|
+
assertNoClash(files, root);
|
|
221
|
+
logger.blank();
|
|
222
|
+
logger.info(`Wiring ${integration.title}`);
|
|
223
|
+
writePlan(logger, root, config, plan, files, "integrations");
|
|
224
|
+
recordIntegration(root, integration.name);
|
|
225
|
+
for (const note of plan.notes)
|
|
226
|
+
logger.info(note);
|
|
227
|
+
applied.push(integration.name);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.warn(`Could not wire ${integration.title}: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
logger.info("Fix that, then run `rowork wire` to try again.");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return applied;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Installs several modules in dependency order, for `rowork start`.
|
|
238
|
+
*
|
|
239
|
+
* A module that needs another one brings it along. One failing does not stop
|
|
240
|
+
* the others: the project already exists and is usable, so it is reported and
|
|
241
|
+
* can be retried with `rowork add`.
|
|
242
|
+
*/
|
|
243
|
+
export async function installModulesByName(context, root, names) {
|
|
244
|
+
const wanted = new Set(names);
|
|
245
|
+
for (const module of coreModules) {
|
|
246
|
+
if (wanted.has(module.name))
|
|
247
|
+
for (const required of module.requires ?? [])
|
|
248
|
+
wanted.add(required);
|
|
249
|
+
}
|
|
250
|
+
const done = [];
|
|
251
|
+
for (const module of coreModules) {
|
|
252
|
+
if (!wanted.has(module.name))
|
|
253
|
+
continue;
|
|
254
|
+
try {
|
|
255
|
+
await installModule({ ...context, projectRoot: root, config: loadConfig(root), options: {} }, module, root, false);
|
|
256
|
+
done.push(module.name);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
context.logger.warn(`Could not add ${module.title}: ${error instanceof Error ? error.message : String(error)}`);
|
|
260
|
+
context.logger.info(`Retry later with \`rowork add ${module.name}\`.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return done;
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=modules.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function assertValidProjectName(name: string): void;
|
|
2
|
+
/** `My Cool Game` / `my-cool-game` -> `MyCoolGame` */
|
|
3
|
+
export declare function toPascalCase(value: string): string;
|
|
4
|
+
/** `MyCoolGame` -> `my-cool-game` (a valid npm package name) */
|
|
5
|
+
export declare function toKebabCase(value: string): string;
|
|
6
|
+
//# sourceMappingURL=naming.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RoworkError } from "../cli/errors.js";
|
|
2
|
+
const VALID_NAME = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
3
|
+
export function assertValidProjectName(name) {
|
|
4
|
+
if (!VALID_NAME.test(name)) {
|
|
5
|
+
throw new RoworkError(`\`${name}\` is not a valid project name.`, {
|
|
6
|
+
hint: "Start with a letter, then letters, digits, `.`, `-` or `_`.",
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** `My Cool Game` / `my-cool-game` -> `MyCoolGame` */
|
|
11
|
+
export function toPascalCase(value) {
|
|
12
|
+
return value
|
|
13
|
+
.split(/[^A-Za-z0-9]+/)
|
|
14
|
+
.filter((part) => part.length > 0)
|
|
15
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
16
|
+
.join("");
|
|
17
|
+
}
|
|
18
|
+
/** `MyCoolGame` -> `my-cool-game` (a valid npm package name) */
|
|
19
|
+
export function toKebabCase(value) {
|
|
20
|
+
return value
|
|
21
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
22
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "")
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=naming.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Roblox calls the kinds of asset this client can create. Images go up as `Image`,
|
|
3
|
+
* not `Decal`: the id of a Decal does not reliably load in an `ImageLabel` in a running
|
|
4
|
+
* game. `Decal` stays in the type only because older lock files recorded it.
|
|
5
|
+
*/
|
|
6
|
+
export type AssetType = "Image" | "Decal" | "Audio" | "Model" | "Video";
|
|
7
|
+
export interface Creator {
|
|
8
|
+
type: "user" | "group";
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OperationResult {
|
|
12
|
+
/** Set once the upload finished. */
|
|
13
|
+
assetId?: string;
|
|
14
|
+
/** Roblox's moderation state as sent, e.g. `MODERATION_STATE_APPROVED`. */
|
|
15
|
+
moderation?: string;
|
|
16
|
+
/** The operation is still running: ask again later. */
|
|
17
|
+
pending: boolean;
|
|
18
|
+
/** Why the upload failed, when it did. */
|
|
19
|
+
failure?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The service address. Tests point it at a fake server, but only on this very
|
|
23
|
+
* computer: an address from an environment variable must never be able to make the
|
|
24
|
+
* key travel to another machine.
|
|
25
|
+
*/
|
|
26
|
+
export declare function openCloudBase(env?: NodeJS.ProcessEnv): string;
|
|
27
|
+
export declare class OpenCloud {
|
|
28
|
+
private readonly apiKey;
|
|
29
|
+
private readonly base;
|
|
30
|
+
constructor(apiKey: string, base?: string);
|
|
31
|
+
/** Roblox's own explanation of a 403, when it sends one (`PERMISSION_DENIED` with a message). */
|
|
32
|
+
private readDenial;
|
|
33
|
+
/** Removes the key from a piece of text before it can be shown. */
|
|
34
|
+
redact(text: string): string;
|
|
35
|
+
private request;
|
|
36
|
+
/**
|
|
37
|
+
* Asks whether Roblox accepts the key, without creating anything: it reads an operation
|
|
38
|
+
* that cannot exist. A valid key with the Assets permission gets "not found"; a bad key,
|
|
39
|
+
* an expired one, a missing permission or a wrong IP address gets 401 or 403.
|
|
40
|
+
* (Which answer a real key gets is still to be confirmed against the real service.)
|
|
41
|
+
*/
|
|
42
|
+
verifyKey(): Promise<"accepted" | "refused" | "unreachable">;
|
|
43
|
+
/** Starts an upload. Returns the operation to follow, e.g. `operations/abc`. */
|
|
44
|
+
createAsset(input: {
|
|
45
|
+
file: Uint8Array;
|
|
46
|
+
fileName: string;
|
|
47
|
+
contentType: string;
|
|
48
|
+
assetType: AssetType;
|
|
49
|
+
displayName: string;
|
|
50
|
+
description: string;
|
|
51
|
+
creator: Creator;
|
|
52
|
+
}): Promise<string>;
|
|
53
|
+
/** Reads an operation once. Anything that is not `done` is treated as still running. */
|
|
54
|
+
readOperation(operation: string): Promise<OperationResult>;
|
|
55
|
+
/** Follows an operation until it finishes, or gives up after `seconds`. */
|
|
56
|
+
waitForOperation(operation: string, seconds?: number): Promise<OperationResult>;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=open-cloud.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { RoworkError } from "../cli/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* A small client for the Roblox Open Cloud Assets API.
|
|
4
|
+
*
|
|
5
|
+
* Written from Roblox's own usage guide, which documents the request and a
|
|
6
|
+
* COMPLETED operation but not the pending state, the error bodies or the rate
|
|
7
|
+
* limits. Everything undocumented is handled defensively (an operation that is not
|
|
8
|
+
* `done` is pending; an unknown moderation state is reported as it came), and the
|
|
9
|
+
* behaviour against the real service is still to be confirmed with a real key.
|
|
10
|
+
*
|
|
11
|
+
* The API key is a secret: it is only ever sent in the `x-api-key` header, never
|
|
12
|
+
* logged, and removed from any text that ends up in an error message.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_BASE = "https://apis.roblox.com";
|
|
15
|
+
/**
|
|
16
|
+
* The service address. Tests point it at a fake server, but only on this very
|
|
17
|
+
* computer: an address from an environment variable must never be able to make the
|
|
18
|
+
* key travel to another machine.
|
|
19
|
+
*/
|
|
20
|
+
export function openCloudBase(env = process.env) {
|
|
21
|
+
const override = env["ROWORK_OPEN_CLOUD_URL"];
|
|
22
|
+
if (override === undefined || override === "")
|
|
23
|
+
return DEFAULT_BASE;
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(override);
|
|
26
|
+
if (url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost")) {
|
|
27
|
+
return url.origin;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Falls through to the refusal below.
|
|
32
|
+
}
|
|
33
|
+
throw new RoworkError("ROWORK_OPEN_CLOUD_URL is only accepted for an address on this computer.", {
|
|
34
|
+
hint: "It exists for tests. Unset it to use the real Roblox service.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export class OpenCloud {
|
|
38
|
+
apiKey;
|
|
39
|
+
base;
|
|
40
|
+
constructor(apiKey, base = openCloudBase()) {
|
|
41
|
+
this.apiKey = apiKey;
|
|
42
|
+
this.base = base;
|
|
43
|
+
}
|
|
44
|
+
/** Roblox's own explanation of a 403, when it sends one (`PERMISSION_DENIED` with a message). */
|
|
45
|
+
async readDenial(response) {
|
|
46
|
+
try {
|
|
47
|
+
const body = (await response.json());
|
|
48
|
+
if (body.code === "PERMISSION_DENIED" && typeof body.message === "string")
|
|
49
|
+
return this.redact(body.message.slice(0, 300));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Not JSON: nothing more to say than "refused".
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Removes the key from a piece of text before it can be shown. */
|
|
57
|
+
redact(text) {
|
|
58
|
+
return this.apiKey === "" ? text : text.split(this.apiKey).join("***");
|
|
59
|
+
}
|
|
60
|
+
async request(path, init) {
|
|
61
|
+
for (let attempt = 0;; attempt += 1) {
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await fetch(`${this.base}${path}`, {
|
|
65
|
+
...init,
|
|
66
|
+
headers: { ...init.headers, "x-api-key": this.apiKey },
|
|
67
|
+
signal: AbortSignal.timeout(120_000),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new RoworkError("Could not reach Roblox Open Cloud.", {
|
|
72
|
+
hint: "Check your connection and try again.",
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (response.status === 429 && attempt < 3) {
|
|
77
|
+
const wait = Math.min(Number(response.headers.get("retry-after")) || 2 ** attempt * 2, 60);
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 401 || response.status === 403) {
|
|
82
|
+
const keyHint = "Check that it is valid, has not expired, has the Assets API permission with read and write, and that its IP restrictions allow this computer.";
|
|
83
|
+
// A 403 can also be Roblox refusing THIS upload for THIS account (a video needs an ID-verified account):
|
|
84
|
+
// it says so in the body, and blaming the key would send people to fix the wrong thing.
|
|
85
|
+
const denied = response.status === 403 ? await this.readDenial(response) : undefined;
|
|
86
|
+
if (denied !== undefined) {
|
|
87
|
+
throw new RoworkError(denied, {
|
|
88
|
+
hint: /IdVerification/i.test(denied)
|
|
89
|
+
? "Roblox requires your account to be ID-verified for this kind of upload (videos, and more than 10 sounds a month). Verify your identity in your Roblox account settings, then run the command again."
|
|
90
|
+
: `If that does not explain it: ${keyHint}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
throw new RoworkError("Roblox refused the API key.", { hint: keyHint });
|
|
94
|
+
}
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const body = this.redact((await response.text()).slice(0, 300));
|
|
97
|
+
throw new RoworkError(`Roblox Open Cloud answered ${response.status}.`, { hint: body === "" ? undefined : body });
|
|
98
|
+
}
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Asks whether Roblox accepts the key, without creating anything: it reads an operation
|
|
104
|
+
* that cannot exist. A valid key with the Assets permission gets "not found"; a bad key,
|
|
105
|
+
* an expired one, a missing permission or a wrong IP address gets 401 or 403.
|
|
106
|
+
* (Which answer a real key gets is still to be confirmed against the real service.)
|
|
107
|
+
*/
|
|
108
|
+
async verifyKey() {
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${this.base}/assets/v1/operations/rowork-key-check`, {
|
|
111
|
+
headers: { "x-api-key": this.apiKey },
|
|
112
|
+
signal: AbortSignal.timeout(20_000),
|
|
113
|
+
});
|
|
114
|
+
return response.status === 401 || response.status === 403 ? "refused" : "accepted";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return "unreachable";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Starts an upload. Returns the operation to follow, e.g. `operations/abc`. */
|
|
121
|
+
async createAsset(input) {
|
|
122
|
+
const form = new FormData();
|
|
123
|
+
form.append("request", JSON.stringify({
|
|
124
|
+
assetType: input.assetType,
|
|
125
|
+
displayName: input.displayName,
|
|
126
|
+
description: input.description,
|
|
127
|
+
creationContext: {
|
|
128
|
+
creator: input.creator.type === "user" ? { userId: input.creator.id } : { groupId: input.creator.id },
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
form.append("fileContent", new Blob([input.file], { type: input.contentType }), input.fileName);
|
|
132
|
+
const response = await this.request("/assets/v1/assets", { method: "POST", body: form });
|
|
133
|
+
const body = (await response.json());
|
|
134
|
+
if (typeof body.path !== "string") {
|
|
135
|
+
throw new RoworkError("Roblox Open Cloud did not return an operation to follow.");
|
|
136
|
+
}
|
|
137
|
+
return body.path;
|
|
138
|
+
}
|
|
139
|
+
/** Reads an operation once. Anything that is not `done` is treated as still running. */
|
|
140
|
+
async readOperation(operation) {
|
|
141
|
+
const id = operation.replace(/^operations\//, "");
|
|
142
|
+
const response = await this.request(`/assets/v1/operations/${encodeURIComponent(id)}`, { method: "GET" });
|
|
143
|
+
const body = (await response.json());
|
|
144
|
+
if (body.error !== undefined)
|
|
145
|
+
return { pending: false, failure: this.redact(body.error.message ?? "the upload failed") };
|
|
146
|
+
if (body.done !== true)
|
|
147
|
+
return { pending: true };
|
|
148
|
+
const assetId = body.response?.assetId;
|
|
149
|
+
if (assetId === undefined)
|
|
150
|
+
return { pending: false, failure: "Roblox finished the upload without an asset id." };
|
|
151
|
+
const result = { pending: false, assetId };
|
|
152
|
+
const moderation = body.response?.moderationResult?.moderationState;
|
|
153
|
+
if (moderation !== undefined)
|
|
154
|
+
result.moderation = moderation;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
/** Follows an operation until it finishes, or gives up after `seconds`. */
|
|
158
|
+
async waitForOperation(operation, seconds = 90) {
|
|
159
|
+
const started = Date.now();
|
|
160
|
+
let delay = 500;
|
|
161
|
+
for (;;) {
|
|
162
|
+
const result = await this.readOperation(operation);
|
|
163
|
+
if (!result.pending || Date.now() - started > seconds * 1000)
|
|
164
|
+
return result;
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
166
|
+
delay = Math.min(delay * 1.6, 4000);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=open-cloud.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RoworkConfig } from "../plugins/api.js";
|
|
2
|
+
import { type StatType } from "./schema-edit.js";
|
|
3
|
+
export interface StatInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
type: StatType;
|
|
6
|
+
}
|
|
7
|
+
export interface EventInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
/** `server`: the client sends it to the server. `client`: the server sends it to clients. */
|
|
10
|
+
direction: "server" | "client";
|
|
11
|
+
parameters: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What already exists in the project, read from the files themselves.
|
|
15
|
+
*
|
|
16
|
+
* The lists offered by "link it to..." questions come from here rather than from
|
|
17
|
+
* a registry Rowork would have to keep in sync: whatever is in PlayerData.ts and
|
|
18
|
+
* networking.ts is what can be linked, even after the user edited them. A file
|
|
19
|
+
* that cannot be read gives an empty list, never an error.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listStats(root: string, config: RoworkConfig): StatInfo[];
|
|
22
|
+
export declare function listEvents(root: string, config: RoworkConfig): EventInfo[];
|
|
23
|
+
export declare function pascal(name: string): string;
|
|
24
|
+
/** The helper service `make:stat` creates for a value, if it is there. */
|
|
25
|
+
export declare function statServiceClass(root: string, config: RoworkConfig, stat: string): string | undefined;
|
|
26
|
+
//# sourceMappingURL=project-index.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveProjectPath } from "./config.js";
|
|
4
|
+
import { findBlock } from "./schema-edit.js";
|
|
5
|
+
function read(root, file) {
|
|
6
|
+
try {
|
|
7
|
+
return readFileSync(resolveProjectPath(root, file), "utf8");
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What already exists in the project, read from the files themselves.
|
|
15
|
+
*
|
|
16
|
+
* The lists offered by "link it to..." questions come from here rather than from
|
|
17
|
+
* a registry Rowork would have to keep in sync: whatever is in PlayerData.ts and
|
|
18
|
+
* networking.ts is what can be linked, even after the user edited them. A file
|
|
19
|
+
* that cannot be read gives an empty list, never an error.
|
|
20
|
+
*/
|
|
21
|
+
export function listStats(root, config) {
|
|
22
|
+
const source = read(root, `${config.paths.shared}/data/PlayerData.ts`);
|
|
23
|
+
if (source === undefined)
|
|
24
|
+
return [];
|
|
25
|
+
const block = findBlock(source, /export\s+interface\s+PlayerData\s*\{/);
|
|
26
|
+
if (block === undefined)
|
|
27
|
+
return [];
|
|
28
|
+
return [...source.slice(block.start, block.end).matchAll(/^\s*(\w+)\s*:\s*(number|string|boolean)\s*;/gm)].map((match) => ({
|
|
29
|
+
name: match[1],
|
|
30
|
+
type: match[2],
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function listEvents(root, config) {
|
|
34
|
+
const source = read(root, `${config.paths.shared}/networking.ts`);
|
|
35
|
+
if (source === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
const events = [];
|
|
38
|
+
for (const [direction, opening] of [
|
|
39
|
+
["server", /interface\s+ClientToServerEvents\s*\{/],
|
|
40
|
+
["client", /interface\s+ServerToClientEvents\s*\{/],
|
|
41
|
+
]) {
|
|
42
|
+
const block = findBlock(source, opening);
|
|
43
|
+
if (block === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
for (const match of source.slice(block.start, block.end).matchAll(/^\s*(\w+)\s*\(([^)]*)\)\s*:\s*void\s*;/gm)) {
|
|
46
|
+
events.push({ direction, name: match[1], parameters: match[2].trim() });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return events;
|
|
50
|
+
}
|
|
51
|
+
export function pascal(name) {
|
|
52
|
+
return `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
53
|
+
}
|
|
54
|
+
/** The helper service `make:stat` creates for a value, if it is there. */
|
|
55
|
+
export function statServiceClass(root, config, stat) {
|
|
56
|
+
const className = `${pascal(stat)}Service`;
|
|
57
|
+
return existsSync(join(resolveProjectPath(root, config.paths.services), `${className}.ts`)) ? className : undefined;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=project-index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm scopes that some packages need inside the game, and that the project template
|
|
3
|
+
* does not map: `@rbxts/react` is built on the `@rbxts-js` packages, and without them
|
|
4
|
+
* Studio waits for a folder that never appears ("Infinite yield possible on
|
|
5
|
+
* ...node_modules:WaitForChild("@rbxts-js")") and the interface silently never starts.
|
|
6
|
+
*/
|
|
7
|
+
export declare const OPTIONAL_SCOPES: string[];
|
|
8
|
+
/** The scopes among `scopes` that are installed but not mapped in the Rojo project. */
|
|
9
|
+
export declare function unmappedScopes(projectSource: string, projectRoot: string, scopes?: string[]): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Maps `node_modules/<scope>` into the game, next to `@rbxts` and `@flamework`.
|
|
12
|
+
*
|
|
13
|
+
* default.project.json belongs to the user, so this edits the text and leaves everything
|
|
14
|
+
* else as it was. It refuses, and changes nothing, when the file no longer has the shape
|
|
15
|
+
* the template gave it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function addNodeModuleScopes(source: string, file: string, scopes: string[]): string;
|
|
18
|
+
//# sourceMappingURL=rojo-edit.d.ts.map
|