diff --git a/.env.example b/.env.example index 213e132..9194c4d 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,6 @@ NODE_ENV=development # Leaving this empty will generate a new unique random session secret at start SESSION_SECRET= + +# Change if your nf cli executable isn't in the path +NF_CLI_PATH=nf diff --git a/.idea/prettier.xml b/.idea/prettier.xml index 0c83ac4..8c024db 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -3,5 +3,7 @@ \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 22b59c5..0894943 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -15,7 +15,11 @@ const sessionHandle = sveltekitSessionHandle({ }); const checkAuthorizationHandle: Handle = async ({ event, resolve }) => { - if (!event.locals.session.data.path && event.url.pathname !== '/load-project') { + if ( + !event.locals.session.data.path && + event.url.pathname !== '/load-project' && + event.url.pathname + event.url.search !== '/cli?/createProject' + ) { throw redirect(302, '/load-project'); } return resolve(event); diff --git a/src/lib/server/utils/cli/cli-error.ts b/src/lib/server/utils/cli/cli-error.ts new file mode 100644 index 0000000..834113b --- /dev/null +++ b/src/lib/server/utils/cli/cli-error.ts @@ -0,0 +1,7 @@ +export class CliError extends Error { + message: string; + constructor(message: string) { + super(); + this.message = message; + } +} diff --git a/src/lib/server/utils/cli/cli-interface.ts b/src/lib/server/utils/cli/cli-interface.ts new file mode 100644 index 0000000..ce12da1 --- /dev/null +++ b/src/lib/server/utils/cli/cli-interface.ts @@ -0,0 +1,52 @@ +import { env } from '$env/dynamic/private'; +import { CliError } from '@utils-server/cli/cli-error'; +import child_process from 'node:child_process'; + +export class CliInterface { + private readonly projectPath: string; + + constructor(projectPath: string) { + this.projectPath = projectPath; + } + + createProject( + projectName: string, + packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun', + language: 'js' | 'ts', + strictTypeChecking: boolean, + multiplayerServer: boolean, + skipDependencyInstallation: boolean, + dockerContainerization: boolean, + ) { + this.runCli([ + `new`, + `-d`, + this.projectPath, + `--name`, + projectName, + `--package-manager`, + packageManager, + `--language`, + language, + strictTypeChecking ? '--strict' : '--no-strict', + multiplayerServer ? '--server' : '--no-server', + skipDependencyInstallation ? '--skip-install' : '--no-skip-install', + dockerContainerization ? '--docker' : '--no-docker', + ]); + } + + startProject() { + this.runCli([`build`, `-d`, this.projectPath]); + this.runCli([`start`, `-d`, this.projectPath]); + } + + private runCli(params: string[]) { + const res = child_process.spawnSync(env.NF_CLI_PATH, params); + if (res.status === null) { + throw new CliError(`Executable ${env.NF_CLI_PATH} cannot be found or executed`); + } + if (res.status !== 0) { + throw new CliError(res.stderr.toString()); + } + } +} diff --git a/src/routes/cli/+page.server.ts b/src/routes/cli/+page.server.ts new file mode 100644 index 0000000..6eb50a8 --- /dev/null +++ b/src/routes/cli/+page.server.ts @@ -0,0 +1,61 @@ +import { fail } from '@sveltejs/kit'; +import { CliError } from '@utils-server/cli/cli-error'; +import { CliInterface } from '@utils-server/cli/cli-interface'; + +import type { Actions } from './$types'; + +export const actions = { + // Create project + // Run project + // Export project + createProject: async ({ request }) => { + const data = await request.json(); + + if (!data.projectPath) { + return fail(403, { success: false, errorMsg: "Missing arg: 'projectPath'" }); + } + if (!data.projectName) { + return fail(403, { success: false, errorMsg: "Missing arg: 'projectName'" }); + } + if (!data.packageManager) { + return fail(403, { success: false, errorMsg: "Missing arg: 'packageManager'" }); + } + if (!data.language) { + return fail(403, { success: false, errorMsg: "Missing arg: 'language'" }); + } + + try { + new CliInterface(data.projectPath).createProject( + data.projectName, + data.packageManager, + data.language, + data.strictTypeChecking, + data.multiplayerServer, + data.skipDependencyInstallation, + data.dockerContainerization, + ); + return { + success: true, + }; + } catch (e: unknown) { + if (e instanceof CliError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, + + startProject: async ({ locals }) => { + try { + new CliInterface(locals.session.data.path).startProject(); + return { + success: true, + }; + } catch (e: unknown) { + if (e instanceof CliError) { + return fail(403, { success: false, errorMsg: e.message }); + } + throw e; + } + }, +} satisfies Actions; diff --git a/src/routes/cli/+page.svelte b/src/routes/cli/+page.svelte new file mode 100644 index 0000000..34cd7bd --- /dev/null +++ b/src/routes/cli/+page.svelte @@ -0,0 +1,27 @@ + + +
+
+
+ + Logo + +
+
{ + e.preventDefault(); + fetch('/cli?/startProject', { + method: 'POST', + body: JSON.stringify({}), + }); + }} + > + +
+
+
+
+
diff --git a/src/routes/load-project/+page.server.ts b/src/routes/load-project/+page.server.ts index f2607a1..32c01ee 100644 --- a/src/routes/load-project/+page.server.ts +++ b/src/routes/load-project/+page.server.ts @@ -28,7 +28,15 @@ export const load: PageServerLoad = async ({ url, cookies, locals }) => { } absoluteProjectPath = (await serverProjectPath.json())['projectPath']; } else { - return { success: false, errorMsg: 'No project provided' }; + return { + success: false, + creationPanel: env.API_URL ? 'api' : 'local', + errorMsg: `No project provided: ${ + env.API_URL + ? 'Go back to the NanoForge project manager to access a project' + : 'Select or create a local project' + }`, + }; } try { diff --git a/src/routes/load-project/+page.svelte b/src/routes/load-project/+page.svelte index 585933a..960e6f1 100644 --- a/src/routes/load-project/+page.svelte +++ b/src/routes/load-project/+page.svelte @@ -1,9 +1,10 @@
@@ -13,7 +14,88 @@ Logo
- {data.success ? 'Project loading' : 'Error: ' + data.errorMsg} + {#if !data.success} +
+ {'Error: ' + data.errorMsg} +
+ {/if} + {#if form?.errorMsg} +
+ {'Error: ' + form?.errorMsg} +
+ {/if} + {#if data.creationPanel === 'local'} +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + // eslint-disable-next-line svelte/no-navigation-without-resolve + goto(`/load-project?projectPath=${formData.get('projectPath')}`); + }} + > + + +
+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + fetch('/cli?/createProject', { + method: 'POST', + body: JSON.stringify({ + projectPath: formData.get('projectPath'), + projectName: formData.get('projectName'), + packageManager: formData.get('packageManager'), + language: formData.get('language'), + strictTypeChecking: formData.get('strictTypeChecking') ?? false, + multiplayerServer: formData.get('multiplayerServer') ?? false, + skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false, + dockerContainerization: formData.get('dockerContainerization') ?? false, + }), + }); + }} + > + + + + + + + + + + + + + + + + +
+ {/if}