Skip to content

Commit 628cc1c

Browse files
committed
feat(back): create and start project
1 parent 0123ba0 commit 628cc1c

10 files changed

Lines changed: 379 additions & 117 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ NODE_ENV=development
66

77
# Leaving this empty will generate a new unique random session secret at start
88
SESSION_SECRET=
9+
10+
# Change if your nf cli executable isn't in the path
11+
NF_CLI_PATH=nf

.idea/prettier.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-lock.yaml

Lines changed: 128 additions & 112 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hooks.server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ const sessionHandle = sveltekitSessionHandle({
1515
});
1616

1717
const checkAuthorizationHandle: Handle = async ({ event, resolve }) => {
18-
if (!event.locals.session.data.path && event.url.pathname !== '/load-project') {
18+
if (
19+
!event.locals.session.data.path &&
20+
event.url.pathname !== '/load-project' &&
21+
event.url.pathname + event.url.search !== '/cli?/createProject'
22+
) {
1923
throw redirect(302, '/load-project');
2024
}
2125
return resolve(event);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class CliError extends Error {
2+
message: string;
3+
constructor(message: string) {
4+
super();
5+
this.message = message;
6+
}
7+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { env } from '$env/dynamic/private';
2+
import { CliError } from '@utils-server/cli/cli-error';
3+
import child_process from 'node:child_process';
4+
5+
export class CliInterface {
6+
private readonly projectPath: string;
7+
8+
constructor(projectPath: string) {
9+
this.projectPath = projectPath;
10+
}
11+
12+
createProject(
13+
projectName: string,
14+
packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun',
15+
language: 'js' | 'ts',
16+
strictTypeChecking: boolean,
17+
multiplayerServer: boolean,
18+
skipDependencyInstallation: boolean,
19+
dockerContainerization: boolean,
20+
) {
21+
this.runCli([
22+
`new`,
23+
`-d`,
24+
this.projectPath,
25+
`--name`,
26+
projectName,
27+
`--package-manager`,
28+
packageManager,
29+
`--language`,
30+
language,
31+
strictTypeChecking ? '--strict' : '--no-strict',
32+
multiplayerServer ? '--server' : '--no-server',
33+
skipDependencyInstallation ? '--skip-install' : '--no-skip-install',
34+
dockerContainerization ? '--docker' : '--no-docker',
35+
]);
36+
}
37+
38+
startProject() {
39+
this.runCli([`build`, `-d`, this.projectPath]);
40+
this.runCli([`start`, `-d`, this.projectPath]);
41+
}
42+
43+
private runCli(params: string[]) {
44+
const res = child_process.spawnSync(env.NF_CLI_PATH, params);
45+
if (res.status === null) {
46+
throw new CliError(`Executable ${env.NF_CLI_PATH} cannot be found or executed`);
47+
}
48+
if (res.status !== 0) {
49+
throw new CliError(res.stderr.toString());
50+
}
51+
}
52+
}

src/routes/cli/+page.server.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { fail } from '@sveltejs/kit';
2+
import { CliError } from '@utils-server/cli/cli-error';
3+
import { CliInterface } from '@utils-server/cli/cli-interface';
4+
5+
import type { Actions } from './$types';
6+
7+
export const actions = {
8+
// Create project
9+
// Run project
10+
// Export project
11+
createProject: async ({ request }) => {
12+
const data = await request.json();
13+
14+
if (!data.projectPath) {
15+
return fail(403, { success: false, errorMsg: "Missing arg: 'projectPath'" });
16+
}
17+
if (!data.projectName) {
18+
return fail(403, { success: false, errorMsg: "Missing arg: 'projectName'" });
19+
}
20+
if (!data.packageManager) {
21+
return fail(403, { success: false, errorMsg: "Missing arg: 'packageManager'" });
22+
}
23+
if (!data.language) {
24+
return fail(403, { success: false, errorMsg: "Missing arg: 'language'" });
25+
}
26+
27+
try {
28+
new CliInterface(data.projectPath).createProject(
29+
data.projectName,
30+
data.packageManager,
31+
data.language,
32+
data.strictTypeChecking,
33+
data.multiplayerServer,
34+
data.skipDependencyInstallation,
35+
data.dockerContainerization,
36+
);
37+
return {
38+
success: true,
39+
};
40+
} catch (e: unknown) {
41+
if (e instanceof CliError) {
42+
return fail(403, { success: false, errorMsg: e.message });
43+
}
44+
throw e;
45+
}
46+
},
47+
48+
startProject: async ({ locals }) => {
49+
try {
50+
new CliInterface(locals.session.data.path).startProject();
51+
return {
52+
success: true,
53+
};
54+
} catch (e: unknown) {
55+
if (e instanceof CliError) {
56+
return fail(403, { success: false, errorMsg: e.message });
57+
}
58+
throw e;
59+
}
60+
},
61+
} satisfies Actions;

src/routes/cli/+page.svelte

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import { resolve } from '$app/paths';
3+
import Logo from '$lib/assets/logo.png';
4+
</script>
5+
6+
<div class="h-screen flex flex-col gap-1">
7+
<header class="h-16 flex bg-neutral-900">
8+
<div class="h-full w-full flex">
9+
<a href={resolve('/')} class="h-full px-3 pb-1 pt-2">
10+
<img src={Logo} alt="Logo" class="h-full rounded-full" />
11+
</a>
12+
<div class="h-full w-full flex flex-col justify-between">
13+
<form
14+
onsubmit={(e) => {
15+
e.preventDefault();
16+
fetch('/cli?/startProject', {
17+
method: 'POST',
18+
body: JSON.stringify({}),
19+
});
20+
}}
21+
>
22+
<input type="submit" value="Start Project" />
23+
</form>
24+
</div>
25+
</div>
26+
</header>
27+
</div>

src/routes/load-project/+page.server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ export const load: PageServerLoad = async ({ url, cookies, locals }) => {
2828
}
2929
absoluteProjectPath = (await serverProjectPath.json())['projectPath'];
3030
} else {
31-
return { success: false, errorMsg: 'No project provided' };
31+
return {
32+
success: false,
33+
creationPanel: env.API_URL ? 'api' : 'local',
34+
errorMsg: `No project provided: ${
35+
env.API_URL
36+
? 'Go back to the NanoForge project manager to access a project'
37+
: 'Select or create a local project'
38+
}`,
39+
};
3240
}
3341

3442
try {

src/routes/load-project/+page.svelte

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script lang="ts">
22
import { resolve } from '$app/paths';
33
import Logo from '$lib/assets/logo.png';
4-
import type { PageProps } from './$types';
4+
import type { ActionData, PageData } from './$types';
5+
import { goto } from '$app/navigation';
56
6-
let { data }: PageProps = $props();
7+
let { data, form }: { data: PageData; form: ActionData } = $props();
78
</script>
89

910
<div class="h-screen flex flex-col gap-1">
@@ -13,7 +14,88 @@
1314
<img src={Logo} alt="Logo" class="h-full rounded-full" />
1415
</a>
1516
<div class="h-full w-full flex flex-col justify-between">
16-
{data.success ? 'Project loading' : 'Error: ' + data.errorMsg}
17+
{#if !data.success}
18+
<div style="white-space:pre; color:red">
19+
{'Error: ' + data.errorMsg}
20+
</div>
21+
{/if}
22+
{#if form?.errorMsg}
23+
<div style="white-space:pre; color:red">
24+
{'Error: ' + form?.errorMsg}
25+
</div>
26+
{/if}
27+
{#if data.creationPanel === 'local'}
28+
<form
29+
onsubmit={(e) => {
30+
e.preventDefault();
31+
const formData = new FormData(e.currentTarget);
32+
// eslint-disable-next-line svelte/no-navigation-without-resolve
33+
goto(`/load-project?projectPath=${formData.get('projectPath')}`);
34+
}}
35+
>
36+
<input name="projectPath" placeholder="Project path" />
37+
<input type="submit" value="Go to project" />
38+
</form>
39+
<form
40+
onsubmit={(e) => {
41+
e.preventDefault();
42+
const formData = new FormData(e.currentTarget);
43+
fetch('/cli?/createProject', {
44+
method: 'POST',
45+
body: JSON.stringify({
46+
projectPath: formData.get('projectPath'),
47+
projectName: formData.get('projectName'),
48+
packageManager: formData.get('packageManager'),
49+
language: formData.get('language'),
50+
strictTypeChecking: formData.get('strictTypeChecking') ?? false,
51+
multiplayerServer: formData.get('multiplayerServer') ?? false,
52+
skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false,
53+
dockerContainerization: formData.get('dockerContainerization') ?? false,
54+
}),
55+
});
56+
}}
57+
>
58+
<input name="projectPath" placeholder="Project local path" />
59+
<input name="projectName" placeholder="Project Name" />
60+
<label for="packageManagerId">Package manager :</label>
61+
<select name="packageManager" id="packageManagerId">
62+
<option value="npm">npm</option>
63+
<option value="yarn">yarn</option>
64+
<option value="pnpm">pnpm</option>
65+
<option value="bun">bun</option>
66+
</select>
67+
<label for="languageId">Project language :</label>
68+
<select name="language" id="languageId">
69+
<option value="js">js</option>
70+
<option value="ts">ts</option>
71+
</select>
72+
73+
<label for="strictTypeCheckingId">Strict Type Checking :</label>
74+
<input
75+
type="checkbox"
76+
name="strictTypeChecking"
77+
value="true"
78+
id="strictTypeCheckingId"
79+
/>
80+
<label for="multiplayerServerId">Multiplayer Server :</label>
81+
<input type="checkbox" name="multiplayerServer" value="true" id="multiplayerServerId" />
82+
<label for="skipDependencyInstallationId">Skip Dependency Installation :</label>
83+
<input
84+
type="checkbox"
85+
name="skipDependencyInstallation"
86+
value="true"
87+
id="skipDependencyInstallationId"
88+
/>
89+
<label for="dockerContainerizationId">Docker containerization :</label>
90+
<input
91+
type="checkbox"
92+
name="dockerContainerization"
93+
value="true"
94+
id="dockerContainerizationId"
95+
/>
96+
<button type="submit" value="Create Local Project">Create Local Project</button>
97+
</form>
98+
{/if}
1799
</div>
18100
</div>
19101
</header>

0 commit comments

Comments
 (0)