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.
Files changed (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/bin/rowork.js +18 -0
  4. package/dashboard/app.css +234 -0
  5. package/dashboard/app.js +117 -0
  6. package/dashboard/index.html +67 -0
  7. package/dist/cli/errors.d.ts +15 -0
  8. package/dist/cli/errors.js +18 -0
  9. package/dist/cli/program.d.ts +19 -0
  10. package/dist/cli/program.js +59 -0
  11. package/dist/cli/registry.d.ts +37 -0
  12. package/dist/cli/registry.js +61 -0
  13. package/dist/commands/add.d.ts +8 -0
  14. package/dist/commands/add.js +77 -0
  15. package/dist/commands/assets-setup.d.ts +18 -0
  16. package/dist/commands/assets-setup.js +136 -0
  17. package/dist/commands/assets.d.ts +2 -0
  18. package/dist/commands/assets.js +153 -0
  19. package/dist/commands/console.d.ts +4 -0
  20. package/dist/commands/console.js +141 -0
  21. package/dist/commands/dashboard.d.ts +2 -0
  22. package/dist/commands/dashboard.js +50 -0
  23. package/dist/commands/dev-background.d.ts +3 -0
  24. package/dist/commands/dev-background.js +87 -0
  25. package/dist/commands/dev.d.ts +2 -0
  26. package/dist/commands/dev.js +270 -0
  27. package/dist/commands/eject.d.ts +2 -0
  28. package/dist/commands/eject.js +136 -0
  29. package/dist/commands/index.d.ts +8 -0
  30. package/dist/commands/index.js +54 -0
  31. package/dist/commands/info.d.ts +3 -0
  32. package/dist/commands/info.js +50 -0
  33. package/dist/commands/init.d.ts +2 -0
  34. package/dist/commands/init.js +55 -0
  35. package/dist/commands/links.d.ts +32 -0
  36. package/dist/commands/links.js +152 -0
  37. package/dist/commands/make-event.d.ts +2 -0
  38. package/dist/commands/make-event.js +160 -0
  39. package/dist/commands/make-menu.d.ts +3 -0
  40. package/dist/commands/make-menu.js +36 -0
  41. package/dist/commands/make-stat.d.ts +2 -0
  42. package/dist/commands/make-stat.js +199 -0
  43. package/dist/commands/make-tool.d.ts +2 -0
  44. package/dist/commands/make-tool.js +124 -0
  45. package/dist/commands/make-ui.d.ts +3 -0
  46. package/dist/commands/make-ui.js +199 -0
  47. package/dist/commands/make.d.ts +21 -0
  48. package/dist/commands/make.js +181 -0
  49. package/dist/commands/next-steps.d.ts +5 -0
  50. package/dist/commands/next-steps.js +18 -0
  51. package/dist/commands/start.d.ts +2 -0
  52. package/dist/commands/start.js +144 -0
  53. package/dist/commands/studio.d.ts +3 -0
  54. package/dist/commands/studio.js +45 -0
  55. package/dist/commands/update.d.ts +2 -0
  56. package/dist/commands/update.js +173 -0
  57. package/dist/commands/wire.d.ts +2 -0
  58. package/dist/commands/wire.js +34 -0
  59. package/dist/core/agent-docs.d.ts +23 -0
  60. package/dist/core/agent-docs.js +151 -0
  61. package/dist/core/assets.d.ts +96 -0
  62. package/dist/core/assets.js +268 -0
  63. package/dist/core/background.d.ts +47 -0
  64. package/dist/core/background.js +149 -0
  65. package/dist/core/config.d.ts +9 -0
  66. package/dist/core/config.js +62 -0
  67. package/dist/core/exec.d.ts +22 -0
  68. package/dist/core/exec.js +38 -0
  69. package/dist/core/format-generated.d.ts +12 -0
  70. package/dist/core/format-generated.js +28 -0
  71. package/dist/core/generate.d.ts +37 -0
  72. package/dist/core/generate.js +94 -0
  73. package/dist/core/github-release.d.ts +28 -0
  74. package/dist/core/github-release.js +38 -0
  75. package/dist/core/info.d.ts +77 -0
  76. package/dist/core/info.js +65 -0
  77. package/dist/core/modules.d.ts +32 -0
  78. package/dist/core/modules.js +265 -0
  79. package/dist/core/naming.d.ts +6 -0
  80. package/dist/core/naming.js +26 -0
  81. package/dist/core/open-cloud.d.ts +58 -0
  82. package/dist/core/open-cloud.js +170 -0
  83. package/dist/core/project-index.d.ts +26 -0
  84. package/dist/core/project-index.js +59 -0
  85. package/dist/core/rojo-edit.d.ts +18 -0
  86. package/dist/core/rojo-edit.js +53 -0
  87. package/dist/core/rokit-installer.d.ts +15 -0
  88. package/dist/core/rokit-installer.js +77 -0
  89. package/dist/core/scaffold.d.ts +44 -0
  90. package/dist/core/scaffold.js +163 -0
  91. package/dist/core/schema-edit.d.ts +46 -0
  92. package/dist/core/schema-edit.js +125 -0
  93. package/dist/core/source-scan.d.ts +24 -0
  94. package/dist/core/source-scan.js +84 -0
  95. package/dist/core/studio.d.ts +83 -0
  96. package/dist/core/studio.js +225 -0
  97. package/dist/core/toolchain.d.ts +31 -0
  98. package/dist/core/toolchain.js +98 -0
  99. package/dist/core/tsconfig-edit.d.ts +11 -0
  100. package/dist/core/tsconfig-edit.js +47 -0
  101. package/dist/core/ui-edit.d.ts +31 -0
  102. package/dist/core/ui-edit.js +89 -0
  103. package/dist/core/versions.d.ts +24 -0
  104. package/dist/core/versions.js +67 -0
  105. package/dist/dashboard/open.d.ts +3 -0
  106. package/dist/dashboard/open.js +18 -0
  107. package/dist/dashboard/server.d.ts +28 -0
  108. package/dist/dashboard/server.js +191 -0
  109. package/dist/index.d.ts +3 -0
  110. package/dist/index.js +3 -0
  111. package/dist/main.d.ts +9 -0
  112. package/dist/main.js +135 -0
  113. package/dist/modules/format.d.ts +3 -0
  114. package/dist/modules/format.js +28 -0
  115. package/dist/modules/index.d.ts +7 -0
  116. package/dist/modules/index.js +12 -0
  117. package/dist/modules/integrations.d.ts +8 -0
  118. package/dist/modules/integrations.js +54 -0
  119. package/dist/modules/leaderstats.d.ts +3 -0
  120. package/dist/modules/leaderstats.js +104 -0
  121. package/dist/modules/lint.d.ts +5 -0
  122. package/dist/modules/lint.js +43 -0
  123. package/dist/modules/networking.d.ts +23 -0
  124. package/dist/modules/networking.js +246 -0
  125. package/dist/modules/player-data.d.ts +19 -0
  126. package/dist/modules/player-data.js +176 -0
  127. package/dist/modules/types.d.ts +96 -0
  128. package/dist/modules/types.js +2 -0
  129. package/dist/modules/ui.d.ts +3 -0
  130. package/dist/modules/ui.js +67 -0
  131. package/dist/plugins/api.d.ts +127 -0
  132. package/dist/plugins/api.js +21 -0
  133. package/dist/plugins/loader.d.ts +20 -0
  134. package/dist/plugins/loader.js +128 -0
  135. package/dist/process/log-mux.d.ts +13 -0
  136. package/dist/process/log-mux.js +31 -0
  137. package/dist/process/supervisor.d.ts +48 -0
  138. package/dist/process/supervisor.js +141 -0
  139. package/dist/process/tree-kill.d.ts +25 -0
  140. package/dist/process/tree-kill.js +70 -0
  141. package/dist/templates/engine.d.ts +18 -0
  142. package/dist/templates/engine.js +54 -0
  143. package/dist/ui/logger.d.ts +5 -0
  144. package/dist/ui/logger.js +53 -0
  145. package/dist/ui/prompt.d.ts +9 -0
  146. package/dist/ui/prompt.js +24 -0
  147. package/package.json +69 -0
  148. package/templates/init/README.md.tmpl +37 -0
  149. package/templates/init/_gitignore.tmpl +23 -0
  150. package/templates/init/default.project.json.tmpl +57 -0
  151. package/templates/init/package.json.tmpl +12 -0
  152. package/templates/init/rokit.toml.tmpl +5 -0
  153. package/templates/init/src/client/controllers/ExampleController.ts.tmpl +12 -0
  154. package/templates/init/src/client/runtime.client.ts.tmpl +6 -0
  155. package/templates/init/src/server/runtime.server.ts.tmpl +12 -0
  156. package/templates/init/src/server/services/ExampleService.ts.tmpl +12 -0
  157. package/templates/init/src/shared/_gitkeep.tmpl +0 -0
  158. package/templates/init/tsconfig.json.tmpl +28 -0
  159. package/templates/integrations/data-replication/DataReplicationService.ts.tmpl +33 -0
  160. package/templates/integrations/data-replication/PlayerDataController.ts.tmpl +40 -0
  161. package/templates/integrations/data-replication/dataEvents.ts.tmpl +21 -0
  162. package/templates/make/component.ts.tmpl +8 -0
  163. package/templates/make/controller.ts.tmpl +6 -0
  164. package/templates/make/event-handler.ts.tmpl +22 -0
  165. package/templates/make/screen.tsx.tmpl +35 -0
  166. package/templates/make/service.ts.tmpl +6 -0
  167. package/templates/make/stat-service.ts.tmpl +26 -0
  168. package/templates/make/ui-button.tsx.tmpl +22 -0
  169. package/templates/make/ui-image.tsx.tmpl +11 -0
  170. package/templates/make/ui-label.tsx.tmpl +18 -0
  171. package/templates/make/ui-panel.tsx.tmpl +33 -0
  172. package/templates/make/ui-story.tsx.tmpl +13 -0
  173. package/templates/modules/format/prettierignore.tmpl +9 -0
  174. package/templates/modules/format/prettierrc.json.tmpl +4 -0
  175. package/templates/modules/leaderstats/LeaderstatsService.ts.tmpl +69 -0
  176. package/templates/modules/lint/eslint.config.mjs.tmpl +17 -0
  177. package/templates/modules/networking/client-network.ts.tmpl +9 -0
  178. package/templates/modules/networking/networking.ts.tmpl +22 -0
  179. package/templates/modules/networking/rate-limit.ts.tmpl +52 -0
  180. package/templates/modules/networking/server-network.ts.tmpl +20 -0
  181. package/templates/modules/player-data/PlayerData.ts.tmpl +15 -0
  182. package/templates/modules/player-data/PlayerDataService.ts.tmpl +127 -0
  183. package/templates/modules/ui/App.tsx.tmpl +19 -0
  184. package/templates/modules/ui/Button.story.tsx.tmpl +13 -0
  185. package/templates/modules/ui/Button.tsx.tmpl +21 -0
  186. package/templates/modules/ui/UiController.tsx.tmpl +21 -0
  187. package/templates/modules/ui/screens/HomeScreen.tsx.tmpl +20 -0
@@ -0,0 +1,53 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { RoworkError } from "../cli/errors.js";
4
+ import { findClosingBrace, maskNonCode } from "./source-scan.js";
5
+ /**
6
+ * npm scopes that some packages need inside the game, and that the project template
7
+ * does not map: `@rbxts/react` is built on the `@rbxts-js` packages, and without them
8
+ * Studio waits for a folder that never appears ("Infinite yield possible on
9
+ * ...node_modules:WaitForChild("@rbxts-js")") and the interface silently never starts.
10
+ */
11
+ export const OPTIONAL_SCOPES = ["@rbxts-js"];
12
+ /** The scopes among `scopes` that are installed but not mapped in the Rojo project. */
13
+ export function unmappedScopes(projectSource, projectRoot, scopes = OPTIONAL_SCOPES) {
14
+ return scopes.filter((scope) => existsSync(join(projectRoot, "node_modules", scope)) && !projectSource.includes(`"${scope}"`));
15
+ }
16
+ /**
17
+ * Maps `node_modules/<scope>` into the game, next to `@rbxts` and `@flamework`.
18
+ *
19
+ * default.project.json belongs to the user, so this edits the text and leaves everything
20
+ * else as it was. It refuses, and changes nothing, when the file no longer has the shape
21
+ * the template gave it.
22
+ */
23
+ export function addNodeModuleScopes(source, file, scopes) {
24
+ let result = source;
25
+ for (const scope of scopes) {
26
+ if (result.includes(`"${scope}"`))
27
+ continue;
28
+ const fail = () => {
29
+ throw new RoworkError(`Cannot find where ${file} maps \`node_modules\` into the game.`, {
30
+ hint: `Add this next to "@rbxts" under "rbxts_include" > "node_modules": "${scope}": { "$path": "node_modules/${scope}" }`,
31
+ });
32
+ };
33
+ const include = result.indexOf('"rbxts_include"');
34
+ const modules = include < 0 ? -1 : result.indexOf('"node_modules"', include);
35
+ const open = modules < 0 ? -1 : result.indexOf("{", modules);
36
+ if (open < 0)
37
+ return fail();
38
+ const close = findClosingBrace(maskNonCode(result), open + 1) ?? -1;
39
+ if (close < 0)
40
+ return fail();
41
+ const body = result.slice(open + 1, close);
42
+ const lastEntry = body.replace(/\s+$/, "");
43
+ if (lastEntry.trim() === "")
44
+ return fail();
45
+ const entryIndent = /\n([ \t]+)"/.exec(body)?.[1] ?? "\t\t\t\t\t";
46
+ const unit = entryIndent.includes("\t") ? "\t" : " ";
47
+ const entry = `,\n${entryIndent}"${scope}": {\n${entryIndent}${unit}"$path": "node_modules/${scope}"\n${entryIndent}}`;
48
+ const end = open + 1 + lastEntry.length;
49
+ result = `${result.slice(0, end)}${entry}${result.slice(end)}`;
50
+ }
51
+ return result;
52
+ }
53
+ //# sourceMappingURL=rojo-edit.js.map
@@ -0,0 +1,15 @@
1
+ import type { Logger } from "../plugins/api.js";
2
+ /** Where `rokit self-install` puts the `rokit` binary and the tool shims. */
3
+ export declare function rokitBinDirectory(): string;
4
+ /**
5
+ * Downloads Rokit from its official GitHub release and runs its own installer.
6
+ *
7
+ * Nothing is executed before the archive matches the SHA-256 GitHub publishes
8
+ * for it, and a release without a published digest is refused rather than
9
+ * trusted. The version is whatever is latest: like every other tool version in
10
+ * Rowork, it is never hardcoded. Returns the path of the installed binary,
11
+ * which is usable immediately even though the current shell's PATH is not
12
+ * refreshed until the terminal is reopened.
13
+ */
14
+ export declare function installRokit(logger: Logger): Promise<string>;
15
+ //# sourceMappingURL=rokit-installer.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { unzipSync } from "fflate";
5
+ import { RoworkError } from "../cli/errors.js";
6
+ import { run as runBinary } from "./exec.js";
7
+ import { downloadAsset, getRelease } from "./github-release.js";
8
+ /** Where `rokit self-install` puts the `rokit` binary and the tool shims. */
9
+ export function rokitBinDirectory() {
10
+ return join(homedir(), ".rokit", "bin");
11
+ }
12
+ /** Release asset suffix for this machine, or undefined where Rokit has no build. */
13
+ function platformTarget() {
14
+ const systems = {
15
+ linux: "linux",
16
+ darwin: "macos",
17
+ win32: "windows",
18
+ };
19
+ const architectures = {
20
+ x64: "x86_64",
21
+ arm64: "aarch64",
22
+ };
23
+ const os = systems[process.platform];
24
+ const arch = architectures[process.arch];
25
+ return os !== undefined && arch !== undefined ? `${os}-${arch}` : undefined;
26
+ }
27
+ /**
28
+ * Downloads Rokit from its official GitHub release and runs its own installer.
29
+ *
30
+ * Nothing is executed before the archive matches the SHA-256 GitHub publishes
31
+ * for it, and a release without a published digest is refused rather than
32
+ * trusted. The version is whatever is latest: like every other tool version in
33
+ * Rowork, it is never hardcoded. Returns the path of the installed binary,
34
+ * which is usable immediately even though the current shell's PATH is not
35
+ * refreshed until the terminal is reopened.
36
+ */
37
+ export async function installRokit(logger) {
38
+ const target = platformTarget();
39
+ if (target === undefined) {
40
+ throw new RoworkError(`Rokit publishes no build for ${process.platform}/${process.arch}.`, {
41
+ hint: "Install it manually: https://github.com/rojo-rbx/rokit",
42
+ });
43
+ }
44
+ logger.step("downloading Rokit from github.com/rojo-rbx/rokit");
45
+ let workspace;
46
+ try {
47
+ const release = await getRelease("rojo-rbx/rokit");
48
+ const asset = release.assets?.find((candidate) => candidate.name.endsWith(`-${target}.zip`));
49
+ if (asset === undefined)
50
+ throw new Error(`no release asset for ${target}`);
51
+ // No `allowUnverified`: this is an executable, it must match its checksum.
52
+ const { data: archive } = await downloadAsset(asset);
53
+ const files = unzipSync(new Uint8Array(archive));
54
+ const binaryName = process.platform === "win32" ? "rokit.exe" : "rokit";
55
+ const binary = files[binaryName];
56
+ if (binary === undefined)
57
+ throw new Error(`${binaryName} is missing from ${asset.name}`);
58
+ workspace = mkdtempSync(join(tmpdir(), "rowork-rokit-"));
59
+ const extracted = join(workspace, binaryName);
60
+ writeFileSync(extracted, binary);
61
+ chmodSync(extracted, 0o755);
62
+ logger.step("running `rokit self-install`");
63
+ await runBinary(extracted, ["self-install"], { cwd: workspace, stdio: "ignore" });
64
+ }
65
+ catch (cause) {
66
+ throw new RoworkError("Could not install Rokit automatically.", {
67
+ hint: "Install it manually: https://github.com/rojo-rbx/rokit",
68
+ cause,
69
+ });
70
+ }
71
+ finally {
72
+ if (workspace !== undefined)
73
+ rmSync(workspace, { recursive: true, force: true });
74
+ }
75
+ return join(rokitBinDirectory(), process.platform === "win32" ? "rokit.exe" : "rokit");
76
+ }
77
+ //# sourceMappingURL=rokit-installer.js.map
@@ -0,0 +1,44 @@
1
+ import type { Logger } from "../plugins/api.js";
2
+ export interface ScaffoldOptions {
3
+ /** Project name as typed by the user; also the directory name. */
4
+ name: string;
5
+ /** Directory that will contain the project directory. */
6
+ parent: string;
7
+ install: boolean;
8
+ /** Include the linter. On by default: a linter nobody has to go and find is one that gets used. */
9
+ lint: boolean;
10
+ /** Include the formatter (Prettier). On by default, for the same reason as the linter. */
11
+ format: boolean;
12
+ rokit: boolean;
13
+ /** Download and install Rokit itself when it is not on the machine. */
14
+ installRokit: boolean;
15
+ git: boolean;
16
+ /** Keep the example service and controller. */
17
+ examples: boolean;
18
+ /** Write into a directory that is not empty. */
19
+ force: boolean;
20
+ roworkVersion: string;
21
+ }
22
+ export interface ScaffoldResult {
23
+ target: string;
24
+ displayName: string;
25
+ installed: boolean;
26
+ toolchainReady: boolean;
27
+ /** The linter was wanted but needs npm, which was skipped: `rowork add lint` later. */
28
+ lintPending: boolean;
29
+ }
30
+ /**
31
+ * Reads the exact TypeScript version roblox-ts pins.
32
+ *
33
+ * roblox-ts depends on one exact TypeScript version and patches it. Installing
34
+ * `typescript` on its own pulls whatever is newest, which is almost never that
35
+ * version: Flamework then warns on every single compile that the versions
36
+ * differ. Deriving it from roblox-ts keeps the guarantee that Rowork hardcodes
37
+ * no versions while still producing a coherent install.
38
+ */
39
+ export declare function pinnedTypescriptVersion(projectRoot: string): string | undefined;
40
+ /** Validates the target before anything is written, so a refusal leaves no trace. */
41
+ export declare function resolveTarget(options: Pick<ScaffoldOptions, "name" | "parent" | "force">): string;
42
+ /** Shared by `rowork init` (flags) and `rowork start` (questions). */
43
+ export declare function scaffoldProject(options: ScaffoldOptions, logger: Logger): Promise<ScaffoldResult>;
44
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1,163 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { RoworkError } from "../cli/errors.js";
4
+ import { renderTree, templatesRoot } from "../templates/engine.js";
5
+ import { syncAgentDocs } from "./agent-docs.js";
6
+ import { CONFIG_FILENAME, defaultConfig, loadConfig } from "./config.js";
7
+ import { installModulesByName } from "./modules.js";
8
+ import { run as runBinary } from "./exec.js";
9
+ import { installRokit } from "./rokit-installer.js";
10
+ import { findExecutable } from "./toolchain.js";
11
+ import { FALLBACK_ROJO_VERSION, latestRojoVersion } from "./versions.js";
12
+ import { assertValidProjectName, toKebabCase, toPascalCase } from "./naming.js";
13
+ /** Compiler and type packages. Versions are resolved by npm, never hardcoded. */
14
+ const COMPILER_DEPENDENCIES = [
15
+ "roblox-ts",
16
+ "@rbxts/types",
17
+ "@rbxts/compiler-types",
18
+ "rbxts-transformer-flamework",
19
+ ];
20
+ /** Tools listed in the generated rokit.toml. */
21
+ const TRUSTED_TOOLS = ["rojo-rbx/rojo"];
22
+ const RUNTIME_DEPENDENCIES = ["@flamework/core", "@flamework/components"];
23
+ /** Files that only exist to demonstrate the architecture. */
24
+ const EXAMPLE_FILES = [
25
+ join("src", "server", "services", "ExampleService.ts"),
26
+ join("src", "client", "controllers", "ExampleController.ts"),
27
+ ];
28
+ /**
29
+ * Reads the exact TypeScript version roblox-ts pins.
30
+ *
31
+ * roblox-ts depends on one exact TypeScript version and patches it. Installing
32
+ * `typescript` on its own pulls whatever is newest, which is almost never that
33
+ * version: Flamework then warns on every single compile that the versions
34
+ * differ. Deriving it from roblox-ts keeps the guarantee that Rowork hardcodes
35
+ * no versions while still producing a coherent install.
36
+ */
37
+ export function pinnedTypescriptVersion(projectRoot) {
38
+ try {
39
+ const manifest = JSON.parse(readFileSync(join(projectRoot, "node_modules", "roblox-ts", "package.json"), "utf8"));
40
+ const range = manifest.dependencies?.["typescript"];
41
+ // roblox-ts writes an exact pin such as "=5.5.3".
42
+ return range === undefined ? undefined : range.replace(/^=/, "");
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ /** Validates the target before anything is written, so a refusal leaves no trace. */
49
+ export function resolveTarget(options) {
50
+ assertValidProjectName(options.name);
51
+ const target = join(options.parent, options.name);
52
+ if (existsSync(target) && readdirSync(target).length > 0 && !options.force) {
53
+ throw new RoworkError(`Directory ${target} already exists and is not empty.`, {
54
+ hint: "Re-run with --force to write into it anyway.",
55
+ });
56
+ }
57
+ return target;
58
+ }
59
+ /** Shared by `rowork init` (flags) and `rowork start` (questions). */
60
+ export async function scaffoldProject(options, logger) {
61
+ const target = resolveTarget(options);
62
+ const displayName = toPascalCase(options.name);
63
+ logger.info(`Creating ${displayName} in ${target}`);
64
+ mkdirSync(target, { recursive: true });
65
+ // Like the npm packages, Rojo is created at its newest version: nothing here
66
+ // is a version Rowork wrote down, unless GitHub cannot be reached.
67
+ let rojoVersion = await latestRojoVersion();
68
+ if (rojoVersion === undefined) {
69
+ rojoVersion = FALLBACK_ROJO_VERSION;
70
+ logger.warn(`Could not look up the latest Rojo, using ${rojoVersion}. \`rowork update\` moves to the newest later.`);
71
+ }
72
+ logger.step("scaffolding project structure");
73
+ renderTree(join(templatesRoot(), "init"), target, {
74
+ rojoVersion,
75
+ name: displayName,
76
+ packageName: toKebabCase(options.name),
77
+ roworkVersion: options.roworkVersion,
78
+ });
79
+ if (!options.examples) {
80
+ for (const file of EXAMPLE_FILES)
81
+ rmSync(join(target, file), { force: true });
82
+ // Flamework only discovers what exists, but Rojo and git both need the
83
+ // directories to survive being empty.
84
+ for (const file of EXAMPLE_FILES) {
85
+ const directory = dirname(join(target, file));
86
+ mkdirSync(directory, { recursive: true });
87
+ writeFileSync(join(directory, ".gitkeep"), "", "utf8");
88
+ }
89
+ }
90
+ logger.step(`writing ${CONFIG_FILENAME}`);
91
+ const config = defaultConfig(displayName);
92
+ writeFileSync(join(target, CONFIG_FILENAME), `${JSON.stringify(config, undefined, 2)}\n`, "utf8");
93
+ logger.step("writing AGENTS.md (instructions for AIs and newcomers)");
94
+ syncAgentDocs(target, config, options.roworkVersion);
95
+ if (options.git) {
96
+ logger.step("initialising git repository");
97
+ try {
98
+ await runBinary("git", ["init", "--quiet"], { cwd: target, stdio: "ignore" });
99
+ }
100
+ catch {
101
+ // git missing or failing is not fatal: the project is still usable.
102
+ logger.warn("git init failed, skipping.");
103
+ }
104
+ }
105
+ if (options.install) {
106
+ logger.step("installing dependencies (npm)");
107
+ logger.blank();
108
+ await runBinary("npm", ["install", "--save-dev", ...COMPILER_DEPENDENCIES], { cwd: target });
109
+ const typescript = pinnedTypescriptVersion(target);
110
+ if (typescript === undefined) {
111
+ logger.warn("Could not read the TypeScript version roblox-ts pins, installing the latest instead. Expect a version warning on compile.");
112
+ await runBinary("npm", ["install", "--save-dev", "typescript"], { cwd: target });
113
+ }
114
+ else {
115
+ logger.step(`pinning typescript@${typescript} to match roblox-ts`);
116
+ await runBinary("npm", ["install", "--save-dev", `typescript@${typescript}`], {
117
+ cwd: target,
118
+ });
119
+ }
120
+ await runBinary("npm", ["install", ...RUNTIME_DEPENDENCIES], { cwd: target });
121
+ }
122
+ // The linter is part of a new project, not something to remember to add. It is
123
+ // a module that installs npm packages, so it needs the install step.
124
+ const quality = [...(options.lint ? ["lint"] : []), ...(options.format ? ["format"] : [])];
125
+ if (options.install && quality.length > 0) {
126
+ logger.step("adding the linter and the formatter (ESLint with the roblox-ts rules, Prettier)");
127
+ await installModulesByName({
128
+ args: {},
129
+ options: {},
130
+ cwd: target,
131
+ projectRoot: target,
132
+ config: loadConfig(target),
133
+ logger,
134
+ roworkVersion: options.roworkVersion,
135
+ }, target, quality);
136
+ }
137
+ let toolchainReady = false;
138
+ if (options.rokit) {
139
+ try {
140
+ let rokit = findExecutable("rokit", target);
141
+ if (rokit === undefined && options.installRokit)
142
+ rokit = await installRokit(logger);
143
+ if (rokit === undefined)
144
+ throw new RoworkError("Rokit is not installed.");
145
+ logger.step("installing the pinned Roblox toolchain (rokit install)");
146
+ // Rokit refuses tools it has not been told to trust, and would ask
147
+ // interactively. Only the tools Rowork itself writes to rokit.toml are
148
+ // trusted here, never whatever a later edit adds.
149
+ for (const tool of TRUSTED_TOOLS) {
150
+ await runBinary(rokit, ["trust", tool], { cwd: target, stdio: "ignore" });
151
+ }
152
+ await runBinary(rokit, ["install"], { cwd: target });
153
+ toolchainReady = true;
154
+ }
155
+ catch (error) {
156
+ if (error instanceof Error)
157
+ logger.debug(error.message);
158
+ logger.warn("rokit install did not succeed. Install Rokit from https://github.com/rojo-rbx/rokit, then run `rokit install` in the project.");
159
+ }
160
+ }
161
+ return { target, displayName, installed: options.install, toolchainReady, lintPending: (options.lint || options.format) && !options.install };
162
+ }
163
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Small, careful edits to files the user owns.
3
+ *
4
+ * `make:stat` and `make:event` extend a file a module generated earlier
5
+ * (PlayerData.ts, networking.ts), which the user may since have edited. Each
6
+ * function here works on the source text, refuses instead of guessing when the
7
+ * file no longer has the expected shape, and returns the new text without
8
+ * touching the disk: the caller writes every file only once all of them
9
+ * accepted their edit, so a refusal leaves the project exactly as it was.
10
+ */
11
+ interface Block {
12
+ /** Index just after the opening brace. */
13
+ start: number;
14
+ /** Index of the matching closing brace. */
15
+ end: number;
16
+ }
17
+ /**
18
+ * Finds `{ ... }` after `opening`, matching nested braces. Strings and comments are ignored
19
+ * (see `source-scan.ts`), both when looking for `opening` and when counting braces.
20
+ */
21
+ export declare function findBlock(source: string, opening: RegExp): Block | undefined;
22
+ export type StatType = "number" | "string" | "boolean";
23
+ /** Adds a field to the `PlayerData` interface and to `DEFAULT_PLAYER_DATA`. */
24
+ export declare function addFieldToPlayerData(source: string, file: string, field: {
25
+ name: string;
26
+ type: StatType;
27
+ defaultValue: string;
28
+ }): string;
29
+ /** Adds a name to the `SHOWN` list of the leaderstats service. */
30
+ export declare function addToShownList(source: string, file: string, name: string): string;
31
+ /** Adds an event signature to the client-to-server or server-to-client interface. */
32
+ export declare function addEventToNetworking(source: string, file: string, direction: "server" | "client", event: {
33
+ name: string;
34
+ parameters: string;
35
+ }): string;
36
+ /**
37
+ * Adds an event to the `middleware` list of the server's `createServer` call, so
38
+ * a new event the client sends is rate limited like the others.
39
+ *
40
+ * Returns undefined, not an error, when the list is not there any more: the user
41
+ * may have removed it on purpose, and the event itself is still worth adding. The
42
+ * caller says so.
43
+ */
44
+ export declare function addRateLimit(source: string, name: string): string | undefined;
45
+ export {};
46
+ //# sourceMappingURL=schema-edit.d.ts.map
@@ -0,0 +1,125 @@
1
+ import { RoworkError } from "../cli/errors.js";
2
+ import { findClosingBrace, maskNonCode } from "./source-scan.js";
3
+ /**
4
+ * Finds `{ ... }` after `opening`, matching nested braces. Strings and comments are ignored
5
+ * (see `source-scan.ts`), both when looking for `opening` and when counting braces.
6
+ */
7
+ export function findBlock(source, opening) {
8
+ const code = maskNonCode(source);
9
+ const match = opening.exec(code);
10
+ if (match === null)
11
+ return undefined;
12
+ const start = match.index + match[0].length;
13
+ const end = findClosingBrace(code, start);
14
+ return end === undefined ? undefined : { start, end };
15
+ }
16
+ function unrecognized(file, what) {
17
+ return new RoworkError(`Cannot find ${what} in ${file}.`, {
18
+ hint: `${file} no longer has the shape Rowork generated, so it was not touched. Add the line by hand, or restore the original structure.`,
19
+ });
20
+ }
21
+ /** Appends `line` as the last entry of a block, on its own line, keeping the layout. */
22
+ function appendToBlock(source, block, line, needsComma) {
23
+ const body = source.slice(block.start, block.end);
24
+ const trimmed = body.trimEnd();
25
+ const lastLineBreak = trimmed.lastIndexOf("\n");
26
+ // The block must be laid out one entry per line, as it was generated.
27
+ if (trimmed.trim() !== "" && lastLineBreak === -1)
28
+ return "";
29
+ let before = trimmed;
30
+ // An object literal needs a comma after the previous entry.
31
+ // (A trailing comment line is not an entry: leave it alone.)
32
+ const lastLine = trimmed.slice(lastLineBreak + 1).trim();
33
+ if (needsComma && trimmed.trim() !== "" && !trimmed.endsWith(",") && !lastLine.startsWith("//")) {
34
+ before = `${trimmed},`;
35
+ }
36
+ // Keep the line break and the indentation that were in front of the closing brace:
37
+ // a nested block (`middleware: { ... }`) is indented, a top-level one is not.
38
+ const trailing = body.slice(trimmed.length);
39
+ const closing = source.slice(block.end);
40
+ return `${source.slice(0, block.start)}${before}\n${line}${trailing.includes("\n") ? trailing : "\n"}${closing}`;
41
+ }
42
+ const FIELD = (name) => new RegExp(`^\\s*${name}\\s*\\??\\s*:`, "m");
43
+ /** An event is a method signature, `name(...)`, not a field. */
44
+ const EVENT = (name) => new RegExp(`^\\s*${name}\\s*\\(`, "m");
45
+ /** Adds a field to the `PlayerData` interface and to `DEFAULT_PLAYER_DATA`. */
46
+ export function addFieldToPlayerData(source, file, field) {
47
+ const iface = findBlock(source, /export\s+interface\s+PlayerData\s*\{/);
48
+ if (iface === undefined)
49
+ throw unrecognized(file, "`export interface PlayerData { ... }`");
50
+ if (FIELD(field.name).test(source.slice(iface.start, iface.end))) {
51
+ throw new RoworkError(`\`${field.name}\` is already saved in ${file}.`, {
52
+ hint: "Pick another name, or edit the existing field by hand.",
53
+ });
54
+ }
55
+ const withInterface = appendToBlock(source, iface, `\t${field.name}: ${field.type};`, false);
56
+ if (withInterface === "")
57
+ throw unrecognized(file, "one field per line in `PlayerData`");
58
+ const defaults = findBlock(withInterface, /export\s+const\s+DEFAULT_PLAYER_DATA\s*:\s*PlayerData\s*=\s*\{/);
59
+ if (defaults === undefined)
60
+ throw unrecognized(file, "`export const DEFAULT_PLAYER_DATA: PlayerData = { ... }`");
61
+ const withDefaults = appendToBlock(withInterface, defaults, `\t${field.name}: ${field.defaultValue},`, true);
62
+ if (withDefaults === "")
63
+ throw unrecognized(file, "one entry per line in `DEFAULT_PLAYER_DATA`");
64
+ return withDefaults;
65
+ }
66
+ /** Adds a name to the `SHOWN` list of the leaderstats service. */
67
+ export function addToShownList(source, file, name) {
68
+ const match = /(const\s+SHOWN\b[^=]*=\s*\[)([^\]]*)(\])/.exec(source);
69
+ if (match === null)
70
+ throw unrecognized(file, "the `SHOWN` list");
71
+ const items = (match[2] ?? "")
72
+ .split(",")
73
+ .map((item) => item.trim())
74
+ .filter((item) => item !== "");
75
+ if (items.includes(JSON.stringify(name)))
76
+ return source;
77
+ const updated = [...items, JSON.stringify(name)].join(", ");
78
+ return `${source.slice(0, match.index)}${match[1]}${updated}${match[3]}${source.slice(match.index + match[0].length)}`;
79
+ }
80
+ const PLACEHOLDER = /^\s*\/\/ \(none yet\)\s*\n/m;
81
+ /** Adds an event signature to the client-to-server or server-to-client interface. */
82
+ export function addEventToNetworking(source, file, direction, event) {
83
+ const both = [/interface\s+ClientToServerEvents\s*\{/, /interface\s+ServerToClientEvents\s*\{/];
84
+ for (const opening of both) {
85
+ const block = findBlock(source, opening);
86
+ if (block === undefined)
87
+ throw unrecognized(file, `\`${opening.source.replace(/\\s[*+]/g, " ").replace(/\\/g, "")}\``);
88
+ if (EVENT(event.name).test(source.slice(block.start, block.end))) {
89
+ throw new RoworkError(`An event called \`${event.name}\` already exists in ${file}.`, {
90
+ hint: "Pick another name.",
91
+ });
92
+ }
93
+ }
94
+ const block = findBlock(source, direction === "server" ? both[0] : both[1]);
95
+ if (block === undefined)
96
+ throw unrecognized(file, "the events interface");
97
+ // The generated "(none yet)" placeholder goes away once there is a real event.
98
+ const body = source.slice(block.start, block.end).replace(PLACEHOLDER, "\n");
99
+ const cleaned = `${source.slice(0, block.start)}${body}${source.slice(block.end)}`;
100
+ const cleanedBlock = findBlock(cleaned, direction === "server" ? both[0] : both[1]);
101
+ if (cleanedBlock === undefined)
102
+ throw unrecognized(file, "the events interface");
103
+ const updated = appendToBlock(cleaned, cleanedBlock, `\t${event.name}(${event.parameters}): void;`, false);
104
+ if (updated === "")
105
+ throw unrecognized(file, "one event per line");
106
+ return updated;
107
+ }
108
+ /**
109
+ * Adds an event to the `middleware` list of the server's `createServer` call, so
110
+ * a new event the client sends is rate limited like the others.
111
+ *
112
+ * Returns undefined, not an error, when the list is not there any more: the user
113
+ * may have removed it on purpose, and the event itself is still worth adding. The
114
+ * caller says so.
115
+ */
116
+ export function addRateLimit(source, name) {
117
+ const block = findBlock(source, /middleware\s*:\s*\{/);
118
+ if (block === undefined)
119
+ return undefined;
120
+ if (new RegExp(`^\\s*${name}\\s*:`, "m").test(source.slice(block.start, block.end)))
121
+ return source;
122
+ const updated = appendToBlock(source, block, `\t\t${name}: [limit()],`, true);
123
+ return updated === "" ? undefined : updated;
124
+ }
125
+ //# sourceMappingURL=schema-edit.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A tiny lexical scanner shared by every edit of a file the user owns.
3
+ *
4
+ * Rowork edits `PlayerData.ts`, `networking.ts`, `default.project.json`... by text, and
5
+ * finds where a block ends by counting braces. A brace inside a string or a comment is not
6
+ * structure: `nickname: "guest}"` used to close the block early, and the new field was
7
+ * written inside the string. Every brace search now runs on a masked copy of the source.
8
+ *
9
+ * Not handled, on purpose: regular expression literals (`/\}/`). They are rare in the files
10
+ * Rowork edits, and telling `/` (division) from `/` (regex) needs a real parser.
11
+ */
12
+ /**
13
+ * Returns a copy of `source`, the same length, where strings, template literals and
14
+ * comments are replaced by spaces (line breaks are kept). Indexes found in the copy are
15
+ * valid in the original, so a regular expression can look for code without being fooled by
16
+ * text, and the edit is then made on the original.
17
+ */
18
+ export declare function maskNonCode(source: string): string;
19
+ /**
20
+ * Index of the `}` that closes a block whose `{` is just before `from`, in code that has
21
+ * already been through `maskNonCode`. `undefined` when the block never closes.
22
+ */
23
+ export declare function findClosingBrace(maskedCode: string, from: number): number | undefined;
24
+ //# sourceMappingURL=source-scan.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A tiny lexical scanner shared by every edit of a file the user owns.
3
+ *
4
+ * Rowork edits `PlayerData.ts`, `networking.ts`, `default.project.json`... by text, and
5
+ * finds where a block ends by counting braces. A brace inside a string or a comment is not
6
+ * structure: `nickname: "guest}"` used to close the block early, and the new field was
7
+ * written inside the string. Every brace search now runs on a masked copy of the source.
8
+ *
9
+ * Not handled, on purpose: regular expression literals (`/\}/`). They are rare in the files
10
+ * Rowork edits, and telling `/` (division) from `/` (regex) needs a real parser.
11
+ */
12
+ /**
13
+ * Returns a copy of `source`, the same length, where strings, template literals and
14
+ * comments are replaced by spaces (line breaks are kept). Indexes found in the copy are
15
+ * valid in the original, so a regular expression can look for code without being fooled by
16
+ * text, and the edit is then made on the original.
17
+ */
18
+ export function maskNonCode(source) {
19
+ const out = source.split("");
20
+ const blank = (from, to) => {
21
+ for (let i = from; i < to && i < out.length; i += 1) {
22
+ if (out[i] !== "\n" && out[i] !== "\r")
23
+ out[i] = " ";
24
+ }
25
+ };
26
+ let i = 0;
27
+ while (i < source.length) {
28
+ const char = source[i];
29
+ const next = source[i + 1];
30
+ if (char === "/" && next === "/") {
31
+ const end = source.indexOf("\n", i);
32
+ const stop = end === -1 ? source.length : end;
33
+ blank(i, stop);
34
+ i = stop;
35
+ }
36
+ else if (char === "/" && next === "*") {
37
+ const end = source.indexOf("*/", i + 2);
38
+ const stop = end === -1 ? source.length : end + 2;
39
+ blank(i, stop);
40
+ i = stop;
41
+ }
42
+ else if (char === '"' || char === "'" || char === "`") {
43
+ // A backslash escapes the next character. A quote ends the string; a line break also ends
44
+ // a plain string (an unterminated one), but not a template literal, which may span lines.
45
+ let j = i + 1;
46
+ while (j < source.length) {
47
+ if (source[j] === "\\") {
48
+ j += 2;
49
+ continue;
50
+ }
51
+ if (source[j] === char)
52
+ break;
53
+ if (source[j] === "\n" && char !== "`")
54
+ break;
55
+ j += 1;
56
+ }
57
+ const stop = Math.min(j + 1, source.length);
58
+ blank(i, stop);
59
+ i = stop;
60
+ }
61
+ else {
62
+ i += 1;
63
+ }
64
+ }
65
+ return out.join("");
66
+ }
67
+ /**
68
+ * Index of the `}` that closes a block whose `{` is just before `from`, in code that has
69
+ * already been through `maskNonCode`. `undefined` when the block never closes.
70
+ */
71
+ export function findClosingBrace(maskedCode, from) {
72
+ let depth = 1;
73
+ for (let index = from; index < maskedCode.length; index += 1) {
74
+ const character = maskedCode[index];
75
+ if (character === "{")
76
+ depth += 1;
77
+ if (character === "}")
78
+ depth -= 1;
79
+ if (depth === 0)
80
+ return index;
81
+ }
82
+ return undefined;
83
+ }
84
+ //# sourceMappingURL=source-scan.js.map