├── src
└── core
│ ├── client
│ ├── startup.ts
│ └── webview
│ │ └── index.ts
│ ├── shared
│ └── webviewEvents.ts
│ ├── resource.toml
│ └── server
│ ├── startup.ts
│ └── utility
│ ├── ipc.ts
│ └── reconnect.ts
├── src-webviews
├── resource.toml
├── src
│ ├── vite-env.d.ts
│ ├── shims-react.d.ts
│ ├── main.tsx
│ ├── App.css
│ ├── index.css
│ ├── App.tsx
│ └── assets
│ │ └── react.svg
├── tsconfig.node.json
├── vite.config.ts
├── index.html
├── tsconfig.json
└── public
│ └── vite.svg
├── .github
└── FUNDING.yml
├── .prettierrc
├── .gitignore
├── scripts
├── shared.js
├── copy.js
├── compiler.js
├── watch.js
└── transform.js
├── server.toml
├── tsconfig.json
├── package.json
└── README.md
/src/core/client/startup.ts:
--------------------------------------------------------------------------------
1 | import './webview/index';
--------------------------------------------------------------------------------
/src-webviews/resource.toml:
--------------------------------------------------------------------------------
1 | type = 'asset-pack'
2 | client-files = [ '*' ]
--------------------------------------------------------------------------------
/src-webviews/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [stuyk]
4 |
--------------------------------------------------------------------------------
/src/core/shared/webviewEvents.ts:
--------------------------------------------------------------------------------
1 | export const WebViewEvents = {
2 | toggleVisibility: 'WebView:Toggle:Visibility',
3 | };
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "printWidth": 120,
4 | "singleQuote": true,
5 | "semi": true,
6 | "arrowParens": "always"
7 | }
8 |
--------------------------------------------------------------------------------
/src/core/resource.toml:
--------------------------------------------------------------------------------
1 | type = 'js'
2 | main = 'server/startup.js'
3 | client-files = [ 'client/*', 'shared/*' ]
4 | client-main = 'client/startup.js'
5 | deps = []
--------------------------------------------------------------------------------
/src-webviews/src/shims-react.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'alt' {
2 | import '@altv/types-webview';
3 | import '@altv/types-shared';
4 | export default alt;
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | altv-server
3 | resources
4 | .vscode
5 | *.dll
6 | *.exe
7 | update.json
8 | modules/
9 | data/
10 | *.log
11 | cache/
12 | crashdumps/
13 | package-lock.json
14 | ipc.txt
--------------------------------------------------------------------------------
/src-webviews/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src-webviews/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/scripts/shared.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 |
3 | export function normalizeFilePath(filePath) {
4 | return filePath.replace(/\\/gm, '/');
5 | }
6 |
7 | export function writeToIpc(command) {
8 | fs.appendFileSync('ipc.txt', `\r\n${command}`);
9 | }
10 |
11 | export async function sleep(ms) {
12 | return new Promise((resolve) => {
13 | setTimeout(resolve, ms);
14 | })
15 | }
--------------------------------------------------------------------------------
/src-webviews/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react-swc';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: './',
8 | build: {
9 | outDir: '../resources/webviews',
10 | emptyOutDir: true,
11 | minify: 'esbuild',
12 | reportCompressedSize: false,
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/src-webviews/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src-webviews/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 | "moduleResolution": "bundler",
8 | "allowImportingTsExtensions": true,
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "noEmit": true,
12 | "jsx": "react-jsx"
13 | },
14 | "include": ["src"],
15 | "references": [{ "path": "./tsconfig.node.json" }]
16 | }
17 |
--------------------------------------------------------------------------------
/server.toml:
--------------------------------------------------------------------------------
1 | announce = false
2 |
3 | description = 'test'
4 | website = 'test.com'
5 | gamemode = 'Freeroam'
6 |
7 | language = 'en'
8 | modules = [ 'js-module' ]
9 | name = 'TestServer'
10 | players = 1024
11 | port = 7788
12 | tags = [ 'customTag1', 'customTag2', 'customTag3', 'customTag4' ]
13 |
14 | resources = [
15 | # Add mods here
16 | 'webviews',
17 | 'core'
18 | ]
19 |
20 | # Turn this off in production mode
21 | debug = true
22 |
23 | # Leave host as 0.0.0.0, don't change it.
24 | host = '0.0.0.0'
25 |
26 | [js-module]
27 | global-webcrypto = true
28 | global-fetch = true
29 |
30 | [voice]
31 | bitrate = 64000
--------------------------------------------------------------------------------
/src/core/server/startup.ts:
--------------------------------------------------------------------------------
1 | import './utility/ipc'; // Used to reconnect, do not remove.
2 | import * as alt from 'alt-server';
3 |
4 | import { connectLocalClient } from './utility/reconnect';
5 |
6 | alt.log(`alt:V Server - Boilerplate Started`);
7 | alt.on('playerConnect', handlePlayerConnect);
8 |
9 | function handlePlayerConnect(player: alt.Player) {
10 | alt.log(`[${player.id}] ${player.name} has connected to the server.`);
11 |
12 | player.model = 'mp_m_freemode_01';
13 | player.spawn(36.19486618041992, 859.3850708007812, 197.71343994140625, 0);
14 | alt.emitClient(player, 'log:Console', 'alt:V Server - Boilerplate Started');
15 | }
16 |
17 | connectLocalClient();
18 |
--------------------------------------------------------------------------------
/src-webviews/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "typeRoots": ["./node_modules/@types", "./node_modules/@altv"],
4 | "removeComments": true,
5 | "noUnusedLocals": false,
6 | "noUnusedParameters": false,
7 | "noImplicitReturns": true,
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "allowJs": true,
11 | "emitDecoratorMetadata": true,
12 | "experimentalDecorators": true,
13 | "module": "esNext",
14 | "target": "esNext",
15 | "outDir": "./resources",
16 | "rootDir": "./src",
17 | "allowSyntheticDefaultImports": true,
18 | "declaration": false
19 | },
20 | "include": ["./src/**/*.ts"],
21 | "exclude": ["node_modules", "**/*.spec.ts", "resources", "**/*.json"]
22 | }
23 |
--------------------------------------------------------------------------------
/scripts/copy.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import * as glob from 'glob';
3 | import { normalizeFilePath } from './shared.js';
4 |
5 | const startTime = Date.now();
6 | const files = glob.sync(['src/core/**/*.!(ts)', 'src-webviews/**/*.toml'])
7 |
8 | let filesCopied = 0;
9 | for (let file of files) {
10 | const filePath = normalizeFilePath(file);
11 |
12 | if (filePath.includes('src/')) {
13 | const finalPath = filePath.replace('src/', 'resources/');
14 | fs.copySync(filePath, finalPath, { overwrite: true });
15 | }
16 |
17 | if (filePath.includes('src-webviews')) {
18 | const finalPath = filePath.replace('src-webviews/', 'resources/webviews/');
19 | fs.copySync(filePath, finalPath, { overwrite: true });
20 | }
21 |
22 | filesCopied += 1;
23 | }
24 |
25 | console.log(`${filesCopied} Files Moved | ${Date.now() - startTime}ms`);
--------------------------------------------------------------------------------
/src/core/server/utility/ipc.ts:
--------------------------------------------------------------------------------
1 | import * as alt from 'alt-server';
2 | import fs from 'fs';
3 |
4 | const FILE_NAME = 'ipc.txt';
5 |
6 | function onChange() {
7 | const content = fs.readFileSync(FILE_NAME, { encoding: 'utf-8' });
8 | const contents = content.split('\n');
9 | const lastLine = contents[contents.length - 1].replace(/\n/g, '');
10 |
11 | switch (lastLine) {
12 | case 'kick-all':
13 | alt.log(`Invoking IPC Event 'kick-all'`);
14 | alt.Player.all.forEach((player) => {
15 | player.kick('Restarting Server');
16 | });
17 | break;
18 | }
19 | }
20 |
21 | if (alt.debug) {
22 | if (fs.existsSync(FILE_NAME)) {
23 | fs.rmSync(FILE_NAME);
24 | }
25 |
26 | fs.writeFileSync(FILE_NAME, '');
27 | fs.watch(FILE_NAME, onChange);
28 | alt.log(`Listening for IPC Events in Debug Mode`);
29 | }
30 |
--------------------------------------------------------------------------------
/scripts/compiler.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra'
2 | import * as glob from 'glob';
3 | import swc from '@swc/core'
4 | import { normalizeFilePath } from './shared.js';
5 |
6 | const SWC_CONFIG = {
7 | jsc: {
8 | parser: {
9 | syntax: 'typescript',
10 | dynamicImport: true,
11 | decorators: true,
12 | },
13 | transform: {
14 | legacyDecorator: true,
15 | decoratorMetadata: true,
16 | },
17 | target: 'es2020',
18 | },
19 | sourceMaps: false,
20 | };
21 |
22 | const startTime = Date.now();
23 | const filesToCompile = glob.sync('./src/core/**/*.ts');
24 |
25 | if (fs.existsSync('resources/core')) {
26 | fs.rmSync('resources/core', { force: true, recursive: true });
27 | }
28 |
29 |
30 | let compileCount = 0;
31 | for (let i = 0; i < filesToCompile.length; i++) {
32 | const filePath = normalizeFilePath(filesToCompile[i]);
33 | const finalPath = filePath.replace('src/', 'resources/').replace('.ts', '.js');
34 | const compiled = swc.transformFileSync(filePath, SWC_CONFIG);
35 | fs.outputFileSync(finalPath, compiled.code, { encoding: 'utf-8' });
36 | compileCount += 1;
37 | }
38 |
39 | console.log(`${compileCount} Files Built | ${Date.now() - startTime}ms`);;
40 |
--------------------------------------------------------------------------------
/src/core/client/webview/index.ts:
--------------------------------------------------------------------------------
1 | import * as alt from 'alt-client';
2 | import * as native from 'natives';
3 | import { WebViewEvents } from '../../shared/webviewEvents';
4 |
5 | const F2_KEY = 113;
6 | let view: alt.WebView;
7 | let isFocused = false;
8 |
9 | export function focusWebView() {
10 | if (isFocused) {
11 | view.unfocus();
12 | view.emit(WebViewEvents.toggleVisibility, false);
13 | alt.showCursor(false);
14 | alt.toggleGameControls(true);
15 | native.triggerScreenblurFadeOut(100);
16 | isFocused = false;
17 | } else {
18 | view.focus();
19 | view.emit(WebViewEvents.toggleVisibility, true);
20 | alt.showCursor(true);
21 | alt.toggleGameControls(false);
22 | native.triggerScreenblurFadeIn(100);
23 | isFocused = true;
24 | }
25 | }
26 |
27 | alt.on('keydown', async (keyCode: number) => {
28 | if (keyCode !== F2_KEY) {
29 | return;
30 | }
31 |
32 | if (view) {
33 | focusWebView();
34 | return;
35 | }
36 |
37 | view = new alt.WebView('http://assets/webviews/index.html');
38 | await new Promise((resolve: (...args: any[]) => void) => {
39 | view.once('load', resolve);
40 | });
41 |
42 | focusWebView();
43 | });
44 |
--------------------------------------------------------------------------------
/src/core/server/utility/reconnect.ts:
--------------------------------------------------------------------------------
1 | const enum Status {
2 | Loading = 'LOADING',
3 | MainMenu = 'MAIN_MENU',
4 | DownloadingFiles = 'DOWNLOADING_FILES',
5 | Connecting = 'CONNECTING',
6 | InGame = 'IN_GAME',
7 | Disconnecting = 'DISCONNECTING',
8 | Error = 'ERROR',
9 | }
10 |
11 | const RETRY_DELAY = 2500;
12 | const DEBUG_PORT = 9223;
13 |
14 | async function getLocalClientStatus(): Promise {
15 | try {
16 | const response = await fetch(`http://127.0.0.1:${DEBUG_PORT}/status`);
17 | return response.text() as unknown as Status;
18 | } catch (error) {
19 | return null;
20 | }
21 | }
22 |
23 | export async function connectLocalClient(): Promise {
24 | const status = await getLocalClientStatus();
25 | if (status === null) {
26 | return;
27 | }
28 |
29 | if (status !== Status.MainMenu && status !== Status.InGame) {
30 | setTimeout(() => connectLocalClient(), RETRY_DELAY);
31 | }
32 |
33 | try {
34 | await fetch(`http://127.0.0.1:${DEBUG_PORT}/reconnect`, {
35 | method: 'POST',
36 | body: 'serverPassword', // only needed when a password is set in the server.toml
37 | });
38 | } catch (error) {
39 | console.log(error);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src-webviews/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-webviews/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: transparent;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | .hide {
18 | display: none;
19 | opacity: 0;
20 | }
21 |
22 | a {
23 | font-weight: 500;
24 | color: #646cff;
25 | text-decoration: inherit;
26 | }
27 | a:hover {
28 | color: #535bf2;
29 | }
30 |
31 | body {
32 | margin: 0;
33 | display: flex;
34 | place-items: center;
35 | min-width: 320px;
36 | min-height: 100vh;
37 | }
38 |
39 | h1 {
40 | font-size: 3.2em;
41 | line-height: 1.1;
42 | }
43 |
44 | button {
45 | border-radius: 8px;
46 | border: 1px solid transparent;
47 | padding: 0.6em 1.2em;
48 | font-size: 1em;
49 | font-weight: 500;
50 | font-family: inherit;
51 | background-color: #1a1a1a;
52 | cursor: pointer;
53 | transition: border-color 0.25s;
54 | }
55 | button:hover {
56 | border-color: #646cff;
57 | }
58 | button:focus,
59 | button:focus-visible {
60 | outline: 4px auto -webkit-focus-ring-color;
61 | }
62 |
63 | @media (prefers-color-scheme: light) {
64 | :root {
65 | color: #213547;
66 | background-color: #ffffff;
67 | }
68 | a:hover {
69 | color: #747bff;
70 | }
71 | button {
72 | background-color: #f9f9f9;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src-webviews/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import reactLogo from './assets/react.svg';
3 | import viteLogo from '/vite.svg';
4 | import './App.css';
5 | import { WebViewEvents } from '../../src/core/shared/webviewEvents';
6 |
7 | function App() {
8 | const [count, setCount] = useState(0);
9 | const [isVisible, setVisibility] = useState(false);
10 |
11 | function invokeEvent() {
12 | console.log('Test!');
13 |
14 | if ('alt' in window) {
15 | alt.emit('emitToClient');
16 | }
17 | }
18 |
19 | useEffect(() => {
20 | if (!alt) {
21 | return;
22 | }
23 |
24 | alt.on(WebViewEvents.toggleVisibility, (shouldBeVisible: boolean) => {
25 | setVisibility(shouldBeVisible);
26 | });
27 | }, []);
28 |
29 | return (
30 |
31 |
39 |
Vite + React
40 |
41 |
42 |
43 |
44 | Edit src/App.tsx and save to test HMR
45 |
46 |
47 |
Click on the Vite and React logos to learn more
48 |
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "altv-quickstart-ts",
3 | "version": "5.0.1",
4 | "description": "TypeScript with alt:V Made Easy",
5 | "scripts": {
6 | "[-] Server Deployment Commands (They All Do Different Things)": "",
7 | "build": "node ./scripts/compiler.js && npx vite build ./src-webviews && node ./scripts/copy.js && node ./scripts/transform.js",
8 | "windows": "npm run build && altv-server.exe",
9 | "linux": "npm run build && ./altv-server",
10 | "dev": "node ./scripts/watch.js",
11 | "[-] Utility": "",
12 | "react-dev": "npx vite ./src-webviews --clearScreen=false --host=localhost --port=3000",
13 | "update": "altv-pkg d release"
14 | },
15 | "author": "stuyk",
16 | "license": "ISC",
17 | "devDependencies": {
18 | "@altv/types-client": "^2.5.3",
19 | "@altv/types-natives": "^1.5.3",
20 | "@altv/types-server": "^2.6.6",
21 | "@altv/types-shared": "^1.4.2",
22 | "@altv/types-webview": "^1.0.5",
23 | "@swc/cli": "^0.1.62",
24 | "@swc/core": "^1.3.52",
25 | "@types/node": "^18.15.11",
26 | "@types/react": "^18.0.28",
27 | "@types/react-dom": "^18.0.11",
28 | "@vitejs/plugin-react-swc": "^3.0.0",
29 | "altv-pkg": "^2.0.11",
30 | "fs-extra": "^11.1.1",
31 | "glob": "^10.2.1",
32 | "react": "^18.2.0",
33 | "react-dom": "^18.2.0",
34 | "typescript": "latest",
35 | "watcher": "^2.2.2"
36 | },
37 | "prettier": {
38 | "tabWidth": 4,
39 | "semi": true,
40 | "printWidth": 120,
41 | "arrowParens": "always",
42 | "singleQuote": true
43 | },
44 | "type": "module",
45 | "dependencies": {
46 | "vite": "^4.3.1"
47 | },
48 | "engines": {
49 | "node": ">=18"
50 | }
51 | }
--------------------------------------------------------------------------------
/scripts/watch.js:
--------------------------------------------------------------------------------
1 |
2 | import { spawnSync, spawn, ChildProcess } from 'node:child_process'
3 | import Watcher from 'watcher';
4 | import { writeToIpc, sleep } from './shared.js';
5 |
6 | const fileWatcher = new Watcher(['./src', './src-webviews'], { recursive: true, renameDetection: true });
7 | const isWindows = process.platform === "win32";
8 | const altvProcessName = isWindows ? './altv-server.exe' : './altv-server'
9 |
10 | /** @type {ChildProcess} */
11 | let childProcess = undefined
12 | let rebootDebounce = Date.now() + 0;
13 |
14 | async function compiler() {
15 | console.log(`Starting Compile`)
16 | const webviewProcess = spawn(isWindows ? 'npx.cmd' : 'npx', ['vite', 'build', './src-webviews'], { stdio: 'inherit' })
17 |
18 | spawnSync('node', ['./scripts/compiler.js'], { stdio: 'inherit' })
19 | spawnSync('node', ['./scripts/transform.js'], { stdio: 'inherit' })
20 |
21 | await new Promise((resolve) => {
22 | webviewProcess.on('exit', resolve);
23 | })
24 |
25 | spawnSync('node', ['./scripts/copy.js'], { stdio: 'inherit' })
26 | console.log(`Compile Complete`)
27 | }
28 |
29 | async function reboot() {
30 | if (rebootDebounce > Date.now()) {
31 | return;
32 | }
33 |
34 | rebootDebounce = Date.now() + 1000;
35 | writeToIpc('kick-all');
36 | await sleep(250);
37 | if (childProcess) {
38 | try {
39 | childProcess.kill();
40 | } catch (err) { }
41 |
42 | await new Promise((resolve) => {
43 | const interval = setInterval(() => {
44 | if (!childProcess.killed) {
45 | childProcess.kill();
46 | return;
47 | }
48 |
49 | childProcess = undefined
50 | clearInterval(interval);
51 | resolve();
52 | }, 100);
53 | })
54 | }
55 |
56 | await compiler();
57 | childProcess = spawn(altvProcessName, { stdio: 'inherit' })
58 | }
59 |
60 | function start() {
61 | fileWatcher.on('change', reboot);
62 | reboot();
63 | }
64 |
65 | start();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Typescript Boilerplate for alt:V with React - v5.0.1
3 |
4 |
5 |
6 |
7 |
8 |
9 | Super Fast Compilation
10 |
11 |
12 | > [!IMPORTANT]
13 | > This boilerplate will no longer be updated.
14 | > Consider using [Rebar Framework](https://rebarv.com/) for more up-to-date practices, and a plugin system.
15 |
16 | # Features
17 |
18 | A simple Typescript Boilerplate that builds incredibly fast using the [SWC Library](https://github.com/swc-project/swc).
19 |
20 | - Auto Refresh Server
21 | - Auto Compile TypeScript Files
22 | - Auto Download Resources
23 | - Single Resource Code Support
24 | - Fastest Auto Reconnect Time on Recompile
25 | - Built-in React for WebViews
26 |
27 | # Installation
28 |
29 | * [Install NodeJS 18+](https://nodejs.org/en/download/current/)
30 | * [Install GIT](https://git-scm.com/downloads)
31 |
32 | ## Clone the Repository
33 |
34 | Use the command below in any terminal, command prompt, etc.
35 |
36 | ```sh
37 | git clone https://github.com/Stuyk/altv-typescript-react
38 | ```
39 |
40 | ## Install the Repository
41 |
42 | Use the command below in any terminal, command prompt, etc.
43 |
44 | ```sh
45 | cd altv-typescript-react
46 | npm install
47 | ```
48 |
49 | ## Download Server Files
50 |
51 | Use the command below in any terminal, command prompt, etc. This will download all necessary server files from an additional package used by this project.
52 |
53 | ```sh
54 | npm run update
55 | ```
56 |
57 | ## Start Production Server (Windows)
58 |
59 | Run this command to run the server in production mode.
60 |
61 | ```
62 | npm run windows
63 | ```
64 |
65 | ## Start Production Server (Linux)
66 |
67 | Run this command to run the server in production mode.
68 |
69 | ```
70 | npm run linux
71 | ```
72 |
73 | ## Start Developer Server
74 |
75 | Run this command to run the server in development mode.
76 |
77 | This will automatically reconnect your alt:V Client if you have `debug` mode turned on.
78 |
79 | ```
80 | npm run dev
81 | ```
82 |
83 | ## WebView Previews
84 | If you need to modify the WebView and want to work out of the browser, use the following command.
85 |
86 | ```
87 | npm run react-dev
88 | ```
89 |
90 | ## End Server Runtime
91 |
92 | Use the key combination `ctrl + c` to kill your server in your terminal, command prompt, etc.
93 |
94 | ## How to Add Mods, and New Resources
95 |
96 | Always add your already compiled resources & mods into the `resources` folder.
97 |
--------------------------------------------------------------------------------
/scripts/transform.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import * as glob from 'glob';
3 | import fs from 'fs';
4 |
5 | /**
6 | * What does this do?
7 | * Appends `.js` to the end of all imports that are not 'nodejs' imports.
8 | * This is required for alt:V
9 | */
10 |
11 | const resourcePath = path.join(process.cwd(), 'resources/core/**/*.js').replace(/\\/gm, '/');
12 | const filePaths = glob.sync(resourcePath);
13 |
14 | const funcsToIgnore = [
15 | //
16 | 'export function',
17 | 'export const',
18 | 'export let',
19 | 'export async function',
20 | '=>',
21 | ];
22 |
23 | for (let filePath of filePaths) {
24 | const fileContents = fs.readFileSync(filePath, { encoding: 'utf-8' });
25 | const splitContents = fileContents.split(/\r?\n/);
26 |
27 | const filePathing = filePath.split('/');
28 | filePathing.pop();
29 | const directoryPath = filePathing.join('/');
30 |
31 | let wasModified = false;
32 | for (let i = 0; i < splitContents.length; i++) {
33 | if (!splitContents[i].includes('import') && !splitContents[i].includes('export')) {
34 | continue;
35 | }
36 |
37 | let shouldSkip = false;
38 | for (let funcToIgnore of funcsToIgnore) {
39 | if (splitContents[i].includes(funcToIgnore)) {
40 | shouldSkip = true;
41 | break;
42 | }
43 | }
44 |
45 | if (shouldSkip) {
46 | continue;
47 | }
48 |
49 | const filePathReg = new RegExp(/('|").*.('|")/g);
50 | const extractions = splitContents[i].match(filePathReg);
51 | if (extractions === null || !extractions) {
52 | continue;
53 | }
54 |
55 | const relativeFilePath = extractions[0].replace(/'/gm, '').replace(/"/gm, '');
56 | if (relativeFilePath.charAt(0) !== '.' && relativeFilePath.charAt(0) !== '/') {
57 | continue;
58 | }
59 |
60 | const actualFilePath = path.join(directoryPath, relativeFilePath).replace(/\\/gm, '/');
61 | if (fs.existsSync(actualFilePath)) {
62 | const barrelFileTest = fs.statSync(actualFilePath);
63 | if (barrelFileTest.isDirectory()) {
64 | splitContents[i] = splitContents[i].replace(relativeFilePath, `${relativeFilePath}/index.js`);
65 | wasModified = true;
66 | continue;
67 | }
68 | }
69 |
70 | if (!splitContents[i].includes('.js')) {
71 | splitContents[i] = splitContents[i].replace(relativeFilePath, `${relativeFilePath}.js`);
72 | wasModified = true;
73 | }
74 | }
75 |
76 | if (!wasModified) {
77 | continue;
78 | }
79 |
80 | const finalFile = splitContents.join('\r\n');
81 | fs.writeFileSync(filePath, finalFile, { encoding: 'utf-8' });
82 | }
--------------------------------------------------------------------------------
/src-webviews/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------