Skip to content

Commit 6477062

Browse files
feat(create-spacetime): general improvements and refactor
1 parent 461e353 commit 6477062

8 files changed

Lines changed: 188 additions & 264 deletions

File tree

templates/create-spacetime/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Creates a full-stack SpacetimeDB app with server module and React frontend, incl
5252
## Requirements
5353

5454
- Node.js 18+ and npm 8+
55-
- SpacetimeDB CLI (optional for creation, required for deployment) - [spacetimedb.com/install](https://spacetimedb.com/install)
55+
- SpacetimeDB CLI (optional for creation, required for Maincloud deployment) - [spacetimedb.com/install](https://spacetimedb.com/install)
5656

5757
## Running The Project
5858

templates/create-spacetime/package.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@
2121
"files": [
2222
"dist/**/*"
2323
],
24-
"publishConfig": {
25-
"access": "public",
26-
"provenance": true
27-
},
2824
"scripts": {
2925
"build": "tsc",
3026
"dev": "tsc --watch",

templates/create-spacetime/src/config.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

templates/create-spacetime/src/create-project.ts

Lines changed: 95 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ import chalk from "chalk";
66
import degit from "degit";
77
import { getTemplate } from "./templates/index.js";
88
import { PackageManager, getInstallCommand, getRunCommand } from "./utils/packageManager.js";
9-
import { SPACETIME_VERSIONS, SERVER_CONFIG } from "./config.js";
9+
10+
const SPACETIME_VERSIONS = {
11+
SDK: "^1.3.1",
12+
RUNTIME: "1.3.*",
13+
CLI: "1.3",
14+
} as const;
15+
16+
const SERVER_CONFIG = {
17+
LOCAL_PORT: 3000,
18+
MAINCLOUD_URI: "wss://maincloud.spacetimedb.com",
19+
LOCAL_URI: "ws://localhost:3000",
20+
} as const;
1021

1122
const SPACETIME_SDK_PACKAGE = "@clockworklabs/spacetimedb-sdk";
1223

@@ -46,14 +57,9 @@ export async function createProject(options: CreateProjectOptions): Promise<bool
4657
console.error(chalk.red("Failed to create project:"));
4758
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
4859

49-
try {
50-
await fs.remove(root);
51-
console.log(chalk.gray("Cleaned up incomplete project files"));
52-
} catch (cleanupError) {
53-
console.warn(chalk.yellow("Warning: Failed to clean up project files"));
54-
console.warn(chalk.gray(`You may need to manually remove: ${root}`));
55-
console.error("Cleanup error details:", cleanupError);
56-
}
60+
await fs
61+
.remove(root)
62+
.catch(() => console.warn(chalk.yellow(`Warning: Manual cleanup needed: ${root}`)));
5763

5864
return false;
5965
}
@@ -139,105 +145,77 @@ async function configureServer(root: string, name: string, serverLanguage: strin
139145
}
140146
}
141147

142-
async function updateConfigFile(
143-
filePath: string,
144-
updates: Record<string, string>,
145-
warnMessage: string,
146-
): Promise<void> {
148+
// existing rust and C# example servers configs need to be updated to work with the project setup
149+
async function configureRustServer(serverDir: string, name: string) {
150+
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_");
151+
const cargoPath = path.join(serverDir, "Cargo.toml");
152+
147153
try {
148-
if (!(await fs.pathExists(filePath))) {
149-
console.warn(
150-
chalk.gray(`Warning: ${path.basename(filePath)} not found, skipping configuration`),
154+
if (await fs.pathExists(cargoPath)) {
155+
let content = await fs.readFile(cargoPath, "utf-8");
156+
content = content.replace(/^name = .*$/m, `name = "${safeName}"`);
157+
content = content.replace(/edition\.workspace = true/g, 'edition = "2021"');
158+
content = content.replace(/log\.workspace = true/g, 'log = "0.4"');
159+
content = content.replace(
160+
/spacetimedb = \{ path = ".*" \}/g,
161+
`spacetimedb = "${SPACETIME_VERSIONS.CLI}"`,
151162
);
152-
return;
153-
}
154-
155-
let content = await fs.readFile(filePath, "utf-8");
156-
157-
for (const [pattern, replacement] of Object.entries(updates)) {
158-
const regex = new RegExp(pattern, "g");
159-
content = content.replace(regex, replacement);
163+
content = content.replace(
164+
/spacetimedb-lib = \{ path = ".*" \}/g,
165+
`spacetimedb-lib = "${SPACETIME_VERSIONS.CLI}"`,
166+
);
167+
await fs.writeFile(cargoPath, content);
160168
}
161-
162-
await fs.writeFile(filePath, content);
163169
} catch (error) {
164-
console.warn(chalk.gray(warnMessage), error);
170+
console.warn(chalk.gray("Warning: Could not update Cargo.toml"), error);
165171
}
166172
}
167173

168-
// existing rust and C# example servers configs need to be updated to work with the project setup
169-
async function configureRustServer(serverDir: string, name: string) {
170-
const safeName = name.replace(/[^a-zA-Z0-9_]/g, "_");
171-
172-
await updateConfigFile(
173-
path.join(serverDir, "Cargo.toml"),
174-
{
175-
"^name = .*$": `name = "${safeName}"`,
176-
"edition\\.workspace = true": 'edition = "2021"',
177-
"log\\.workspace = true": 'log = "0.4"',
178-
'spacetimedb = \\{ path = ".*" \\}': `spacetimedb = "${SPACETIME_VERSIONS.CLI}"`,
179-
'spacetimedb-lib = \\{ path = ".*" \\}': `spacetimedb-lib = "${SPACETIME_VERSIONS.CLI}"`,
180-
},
181-
"Warning: Could not update Cargo.toml",
182-
);
183-
}
184-
185174
async function configureCsharpServer(serverDir: string) {
186-
await updateConfigFile(
187-
path.join(serverDir, "StdbModule.csproj"),
188-
{
189-
'<PackageReference Include="SpacetimeDB\\.Runtime" Version="[^"]*" \\/>': `<PackageReference Include="SpacetimeDB.Runtime" Version="${SPACETIME_VERSIONS.RUNTIME}" />`,
190-
},
191-
"Warning: Could not update .csproj file",
192-
);
175+
const csprojPath = path.join(serverDir, "StdbModule.csproj");
176+
177+
try {
178+
if (await fs.pathExists(csprojPath)) {
179+
let content = await fs.readFile(csprojPath, "utf-8");
180+
content = content.replace(
181+
/<PackageReference Include="SpacetimeDB\.Runtime" Version="[^"]*" \/>/g,
182+
`<PackageReference Include="SpacetimeDB.Runtime" Version="${SPACETIME_VERSIONS.RUNTIME}" />`,
183+
);
184+
await fs.writeFile(csprojPath, content);
185+
}
186+
} catch (error) {
187+
console.warn(chalk.gray("Warning: Could not update .csproj file"), error);
188+
}
193189
}
194190

195191
async function updateClientConfig(root: string, name: string, useLocal: boolean) {
196192
const targetUri = useLocal ? SERVER_CONFIG.LOCAL_URI : SERVER_CONFIG.MAINCLOUD_URI;
193+
const appPath = path.join(root, "client/src/App.tsx");
197194

198-
await updateConfigFile(
199-
path.join(root, "client/src/App.tsx"),
200-
{
201-
"\\.withModuleName\\(['\"`][^'\"`]*['\"`]\\)": `.withModuleName('${name}')`,
202-
"\\.withUri\\(['\"`]ws:\\/\\/localhost:3000['\"`]\\)": `.withUri('${targetUri}')`,
203-
},
204-
"Warning: Could not update client config",
205-
);
195+
try {
196+
if (await fs.pathExists(appPath)) {
197+
let content = await fs.readFile(appPath, "utf-8");
198+
content = content.replace(
199+
/\.withModuleName\(['"`][^'"`]*['"`]\)/g,
200+
`.withModuleName('${name}')`,
201+
);
202+
content = content.replace(
203+
/\.withUri\(['"`]ws:\/\/localhost:3000['"`]\)/g,
204+
`.withUri('${targetUri}')`,
205+
);
206+
await fs.writeFile(appPath, content);
207+
}
208+
} catch (error) {
209+
console.warn(chalk.gray("Warning: Could not update client config"), error);
210+
}
206211
}
207212

208213
async function installDependencies(root: string, packageManager: PackageManager): Promise<void> {
209-
const args = getInstallCommand(packageManager);
210214
const clientDir = path.join(root, "client");
211-
212215
if (!(await fs.pathExists(clientDir))) {
213216
throw new Error(`Client directory not found: ${clientDir}`);
214217
}
215-
216-
return new Promise((resolve, reject) => {
217-
const child = spawn(packageManager, args, {
218-
cwd: clientDir,
219-
stdio: "inherit",
220-
});
221-
222-
const timeout = setTimeout(() => {
223-
child.kill("SIGTERM");
224-
reject(new Error(`${packageManager} install timeout`));
225-
}, 600000); // 10 min
226-
227-
child.on("close", (code) => {
228-
clearTimeout(timeout);
229-
if (code !== 0) {
230-
reject(new Error(`${packageManager} install failed with exit code ${code}`));
231-
return;
232-
}
233-
resolve();
234-
});
235-
236-
child.on("error", (error) => {
237-
clearTimeout(timeout);
238-
reject(new Error(`Failed to spawn ${packageManager}: ${error.message}`));
239-
});
240-
});
218+
await runCommand(packageManager, getInstallCommand(packageManager), clientDir, 600000); // 10 min
241219
}
242220

243221
async function setupSpacetimeDB(
@@ -263,19 +241,22 @@ async function setupSpacetimeDB(
263241
spinner.start(`Publishing to ${target}...`);
264242
spinner.stop();
265243
console.log();
266-
await runCommand(
267-
"spacetime",
268-
[
269-
"publish",
270-
"--project-path",
271-
"server",
272-
"--server",
273-
useLocal ? "local" : "maincloud",
274-
"--yes",
275-
name,
276-
],
277-
root,
278-
);
244+
const publishArgs = [
245+
"publish",
246+
"--project-path",
247+
"server",
248+
"--server",
249+
useLocal ? "local" : "maincloud",
250+
];
251+
252+
if (useLocal) {
253+
publishArgs.push("--anonymous");
254+
} else {
255+
publishArgs.push("--yes");
256+
}
257+
publishArgs.push(name);
258+
259+
await runCommand("spacetime", publishArgs, root);
279260
spinner.start("Generating module bindings...");
280261
spinner.stop();
281262
console.log();
@@ -317,7 +298,12 @@ async function ensureLocalServer(spinner: any) {
317298

318299
if (!isRunning) {
319300
spinner.text = "Starting local SpacetimeDB server...";
320-
spawn("spacetime", ["start"], { detached: true, stdio: "ignore" });
301+
const child = spawn("spacetime", ["start"], {
302+
detached: true,
303+
stdio: "ignore",
304+
});
305+
306+
child.unref();
321307

322308
await new Promise((resolve) => setTimeout(resolve, 3000));
323309

@@ -376,7 +362,12 @@ function printInstructions(
376362
console.log(message);
377363
}
378364

379-
function runCommand(command: string, args: string[], cwd: string): Promise<void> {
365+
function runCommand(
366+
command: string,
367+
args: string[],
368+
cwd: string,
369+
timeoutMs: number = 300000, // 5 min
370+
): Promise<void> {
380371
const SAFE_COMMANDS = ["spacetime", "npm", "yarn", "pnpm", "bun"];
381372
if (!SAFE_COMMANDS.includes(command)) {
382373
throw new Error(`Unsafe command: ${command}`);
@@ -388,7 +379,7 @@ function runCommand(command: string, args: string[], cwd: string): Promise<void>
388379
const timeout = setTimeout(() => {
389380
childProcess.kill("SIGTERM");
390381
reject(new Error(`Command timeout: ${command}`));
391-
}, 300000); // 5 min
382+
}, timeoutMs);
392383

393384
childProcess.on("close", (code) => {
394385
clearTimeout(timeout);

0 commit comments

Comments
 (0)