first commit

This commit is contained in:
Lorraxs 2023-12-05 13:55:38 +07:00
commit 1531de93bf
29 changed files with 1038 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2021 Project Error
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

216
README.md Normal file
View File

@ -0,0 +1,216 @@
<div align="center">
<img href="https://projecterror.dev" width="150" src="https://i.tasoagc.dev/c1pD" alt="Material-UI logo" />
</div>
<h1 align="center">FiveM React and Lua Boilerplate</h1>
<div align="center">
A simple and extendable React (TypeScript) boilerplate designed around the Lua ScRT
</div>
<div align="center">
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/project-error/pe-utils/master/LICENSE)
![Discord](https://img.shields.io/discord/791854454760013827?label=Our%20Discord)
![David](https://img.shields.io/david/project-error/fivem-react-boilerplate-lua)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=project-error/fivem-react-boilerplate-lua)](https://dependabot.com)
</div>
This repository is a basic boilerplate for getting started
with React in NUI. It contains several helpful utilities and
is bootstrapped using `create-react-app`. It is for both browser
and in-game based development workflows.
For in-game workflows, Utilizing `craco` to override CRA, we can have hot
builds that just require a resource restart instead of a full
production build
This version of the boilerplate is meant for the CfxLua runtime.
## Requirements
* [Node > v10.6](https://nodejs.org/en/)
* [Yarn](https://yarnpkg.com/getting-started/install) (Preferred but not required)
*A basic understanding of the modern web development workflow. If you don't
know this yet, React might not be for you just yet.*
## Getting Started
First clone the repository or use the template option and place
it within your `resources` folder
### Installation
*The boilerplate was made using `yarn` but is still compatible with
`npm`.*
Install dependencies by navigating to the `web` folder within
a terminal of your choice and type `npm i` or `yarn`.
## Features
This boilerplate comes with some utilities and examples to work off of.
### Lua Utils
**SendReactMessage**
This is a small wrapper for dispatching NUI messages. This is designed
to be used with the `useNuiEvent` React hook.
Signature
```lua
---@param action string The action you wish to target
---@param data any The data you wish to send along with this action
SendReactMessage(action, data)
```
Usage
```lua
SendReactMessage('setVisible', true)
```
**debugPrint**
A debug printing utility that is dependent on a convar,
if the convar is set this will print out to the console.
The convar is dependent on the name given to the resource.
It follows this format `YOUR_RESOURCE_NAME-debugMode`
To turn on debugMode add `setr YOUR_RESOURCE_NAME-debugMode 1` to
your server.cfg or use the `setr` console command instead.
Signature (Replicates `print`)
```lua
---@param ... any[] The arguments you wish to send
debugPrint(...)
```
Usage
```lua
debugPrint('wow cool string to print', true, someOtherVar)
```
### React Utils
Signatures are not included for these utilities as the type definitions
are sufficient enough.
**useNuiEvent**
This is a custom React hook that is designed to intercept and handle
messages dispatched by the game scripts. This is the primary
way of creating passive listeners.
*Note: For now handlers can only be registered a single time. I haven't
come across a personal usecase for a cascading event system*
**Usage**
```jsx
const MyComp: React.FC = () => {
const [state, setState] = useState('')
useNuiEvent<string>('myAction', (data) => {
// the first argument to the handler function
// is the data argument sent using SendReactMessage
// do whatever logic u want here
setState(data)
})
return(
<div>
<h1>Some component</h1>
<p>{state}</p>
</div>
)
}
```
**fetchNui**
This is a simple NUI focused wrapper around the standard `fetch` API.
This is the main way to accomplish active NUI data fetching
or to trigger NUI callbacks in the game scripts.
When using this, you must always at least callback using `{}`
in the gamescripts.
*This can be heavily customized to your use case*
**Usage**
```ts
// First argument is the callback event name.
fetchNui<ReturnData>('getClientData').then(retData => {
console.log('Got return data from client scripts:')
console.dir(retData)
setClientData(retData)
}).catch(e => {
console.error('Setting mock data due to error', e)
setClientData({ x: 500, y: 300, z: 200})
})
```
**debugData**
This is a function allowing for mocking dispatched game script
actions in a browser environment. It will trigger `useNuiEvent` handlers
as if they were dispatched by the game scripts. **It will only fire if the current
environment is a regular browser and not CEF**
**Usage**
```ts
// This will target the useNuiEvent hooks registered with `setVisible`
// and pass them the data of `true`
debugData([
{
action: 'setVisible',
data: true,
}
])
```
**Misc Utils**
These are small but useful included utilities.
* `isEnvBrowser()` - Will return a boolean indicating if the current
environment is a regular browser. (Useful for logic in development)
## Development Workflow
This boilerplate was designed with development workflow in mind.
It includes some helpful scripts to accomplish that.
**Hot Builds In-Game**
When developing in-game, you can use the hot build system by
running the `start:game` script. This is essentially the start
script but it writes to disk. Meaning all that is required is a
resource restart to update the game script
**Usage**
```sh
# yarn
yarn start:game
# npm
npm run start:game
```
**Production Builds**
When you are done with development phase for your resource. You
must create a production build that is optimized and minimized.
You can do this by running the following:
```sh
npm run build
yarn build
```
## Additional Notes
Need further support? Join our [Discord](https://discord.com/invite/HYwBjTbAY5)!

View File

@ -0,0 +1,9 @@
local Impl = NewImpl("Newbie")
function Impl:Init()
main:LogInfo("%s initialized", self:GetName())
end
function Impl:OnReady()
main:LogInfo("%s ready", self:GetName())
end

12
client/impl/test.impl.lua Normal file
View File

@ -0,0 +1,12 @@
local Impl = NewImpl("Test")
function Impl:Init()
main:LogInfo("%s initialized", self:GetName())
self.data = {
test = "test"
}
end
function Impl:GetData()
return self.data
end

30
client/utils.lua Normal file
View File

@ -0,0 +1,30 @@
--- A simple wrapper around SendNUIMessage that you can use to
--- dispatch actions to the React frame.
---
---@param action string The action you wish to target
---@param data any The data you wish to send along with this action
function SendReactMessage(action, data)
SendNUIMessage({
action = action,
data = data
})
end
local currentResourceName = GetCurrentResourceName()
local debugIsEnabled = GetConvarInt(('%s-debugMode'):format(currentResourceName), 0) == 1
--- A simple debug print function that is dependent on a convar
--- will output a nice prettfied message if debugMode is on
function debugPrint(...)
if not debugIsEnabled then return end
local args <const> = { ... }
local appendStr = ''
for _, v in ipairs(args) do
appendStr = appendStr .. ' ' .. tostring(v)
end
local msgTemplate = '^3[%s]^0%s'
local finalMsg = msgTemplate:format(currentResourceName, appendStr)
print(finalMsg)
end

3
config.lua Normal file
View File

@ -0,0 +1,3 @@
Config = {}
Config.Debug = true

45
fxmanifest.lua Normal file
View File

@ -0,0 +1,45 @@
fx_version "cerulean"
description "Small script for FiveM"
author "Lorraxs"
version '1.0.0'
repository 'https://github.com/Lorraxs/lr_addon'
dependencies {
'/server:6116',
'/onesync',
'oxmysql',
'ox_lib',
}
lua54 'yes'
games {
"gta5",
"rdr3"
}
shared_scripts {
'@ox_lib/init.lua',
'@es_extended/imports.lua',
"main.lua",
"impl.lua",
"config.lua",
}
ui_page 'web/build/index.html'
client_scripts {
"client/classes/*",
"client/impl/*"
}
server_script {
'@oxmysql/lib/MySQL.lua',
"server/classes/*",
"server/impl/*"
}
files {
'web/build/index.html',
'web/build/**/*',
}

124
impl.lua Normal file
View File

@ -0,0 +1,124 @@
Class = {}
-- default (empty) constructor
function Class:Init(...) end
-- create a subclass
function Class:extend(obj)
local obj = obj or {}
local function copyTable(table, destination)
local table = table or {}
local result = destination or {}
for k, v in pairs(table) do
if not result[k] then
if type(v) == "table" and k ~= "__index" and k ~= "__newindex" then
result[k] = copyTable(v)
else
result[k] = v
end
end
end
return result
end
copyTable(self, obj)
obj._ = obj._ or {}
local mt = {}
-- create new objects directly, like o = Object()
mt.__call = function(self, ...)
return self:new(...)
end
-- allow for getters and setters
mt.__index = function(table, key)
local val = rawget(table._, key)
if val and type(val) == "table" and (val.get ~= nil or val.value ~= nil) then
if val.get then
if type(val.get) == "function" then
return val.get(table, val.value)
else
return val.get
end
elseif val.value then
return val.value
end
else
return val
end
end
mt.__newindex = function(table, key, value)
local val = rawget(table._, key)
if val and type(val) == "table" and ((val.set ~= nil and val._ == nil) or val.value ~= nil) then
local v = value
if val.set then
if type(val.set) == "function" then
v = val.set(table, value, val.value)
else
v = val.set
end
end
val.value = v
if val and val.afterSet then val.afterSet(table, v) end
else
table._[key] = value
end
end
setmetatable(obj, mt)
return obj
end
-- set properties outside the constructor or other functions
function Class:set(prop, value)
if not value and type(prop) == "table" then
for k, v in pairs(prop) do
rawset(self._, k, v)
end
else
rawset(self._, prop, value)
end
end
-- create an instance of an object with constructor parameters
function Class:new(...)
local obj = self:extend({})
if obj.Init then obj:Init(...) end
return obj
end
function class(attr)
attr = attr or {}
return Class:extend(attr)
end
Impl = class()
function Impl:GetName()
return self.name
end
function Impl:Destroy()
self.destroyed = true
main:LogInfo("%s destroyed", self.name)
end
function Impl:OnReady(...)
end
function NewImpl(name)
local impl = Impl:extend({
name = name
})
main:RegisterImpl(name, impl)
return impl
end

139
main.lua Normal file
View File

@ -0,0 +1,139 @@
Main = {}
if IsDuplicityVersion() then
function GetGameTimer()
return os.clock() * 1000
end
end
function Main:Init()
local o = {}
setmetatable(o, {__index = Main})
o.impls = {}
o.lastTimeImplRegistered = 0
o.ready = false
if not IsDuplicityVersion() then
o.playerId = PlayerId()
o.playerPed = PlayerPedId()
o.playerCoords = GetEntityCoords(o.playerPed)
o.playerHeading = GetEntityHeading(o.playerPed)
o:Thread1()
end
o:Exports()
return o
end
if not IsDuplicityVersion() then
function Main:Thread1()
Citizen.CreateThread(function()
while true do
self.playerId = PlayerId()
self.playerPed = PlayerPedId()
self.playerCoords = GetEntityCoords(self.playerPed)
self.playerHeading = GetEntityHeading(self.playerPed)
Citizen.Wait(1000)
end
end)
end
end
function Main:LogError(msg, ...)
if not Config.Debug then return end
print(("[^1ERROR^0] " .. msg):format(...))
end
function Main:LogWarning(msg, ...)
if not Config.Debug then return end
print(("[^3WARNING^0] " .. msg):format(...))
end
function Main:LogSuccess(msg, ...)
if not Config.Debug then return end
print(("[^2INFO^0] " .. msg):format(...))
end
function Main:LogInfo(msg, ...)
if not Config.Debug then return end
print(("[^5INFO^0] " .. msg):format(...))
end
function Main:CheckValidImpl(name, impl)
if not impl then
self:LogError("Impl %s is nil", name)
return false
end
if not impl.Init then
self:LogError("Impl %s missing Init function", name)
return false
end
return true
end
function Main:RegisterImpl(name, impl)
if self.impls[name] then
self:LogWarning("Impl %s already registered", name)
return
end
if not self:CheckValidImpl(name, impl) then
return
end
self.impls[name] = impl
self.lastTimeImplRegistered = GetGameTimer()
self:LogSuccess("Impl %s registered", name)
if self.ready then
self.impls[name] = impl(self)
self.impls[name]:OnReady()
end
end
function Main:InitImpl()
for name, impl in pairs(self.impls) do
self.impls[name] = impl(self)
end
self:LogInfo("All impls initialized")
self.ready = true
for name, impl in pairs(self.impls) do
impl:OnReady()
end
end
function Main:GetImpl(name)
if not self.impls[name] then
self:LogError("Impl %s not found", name)
return
end
return self.impls[name]
end
function Main:ImplCall(name, func, ...)
local impl = self:GetImpl(name)
if not impl then
return
end
if not impl[func] then
self:LogError("Impl %s missing function %s - args %s", name, func, json.encode({...}))
return
end
return impl[func](impl, ...)
end
function Main:ImplInfo()
for name, impl in pairs(self.impls) do
local debug = debug.getinfo(impl.Init, "S")
self:LogInfo("Impl %s - %s", name, debug.short_src)
end
end
function Main:Exports()
exports("ImplCall", function(name, func, ...)
return self:ImplCall(name, func, ...)
end)
end
main = Main:Init()
Citizen.CreateThread(function()
while GetGameTimer() < main.lastTimeImplRegistered + 1000 do
Citizen.Wait(0)
end
main:InitImpl()
main:ImplInfo()
end)

View File

@ -0,0 +1,5 @@
local Impl = NewImpl("Test")
function Impl:OnReady()
main:LogInfo("%s ready", self:GetName())
end

0
server/server.lua Normal file
View File

18
web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
};

23
web/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
build
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NUI React Boilerplate</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

29
web/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "web",
"homepage": "web/build",
"private": true,
"type": "module",
"version": "0.1.0",
"scripts": {
"start": "vite",
"start:game": "vite build --watch",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-react": "^4.2.0",
"eslint": "^8.54.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

View File

@ -0,0 +1,26 @@
.nui-wrapper {
text-align: center;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
pre {
counter-reset:line-numbering;
background:#2c3e50;
padding:12px 0px 14px 0;
color:#ecf0f1;
line-height:140%;
}
.popup-thing {
background: #282c34;
border-radius: 10px;
width: 500px;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
color: white;
}

View File

@ -0,0 +1,66 @@
import React, { useState } from "react";
import "./App.css";
import { debugData } from "../utils/debugData";
import { fetchNui } from "../utils/fetchNui";
// This will set the NUI to visible if we are
// developing in browser
debugData([
{
action: "setVisible",
data: true,
},
]);
interface ReturnClientDataCompProps {
data: unknown;
}
const ReturnClientDataComp: React.FC<ReturnClientDataCompProps> = ({
data,
}) => (
<>
<h5>Returned Data:</h5>
<pre>
<code>{JSON.stringify(data, null)}</code>
</pre>
</>
);
interface ReturnData {
x: number;
y: number;
z: number;
}
const App: React.FC = () => {
const [clientData, setClientData] = useState<ReturnData | null>(null);
const handleGetClientData = () => {
fetchNui<ReturnData>("getClientData")
.then((retData) => {
console.log("Got return data from client scripts:");
console.dir(retData);
setClientData(retData);
})
.catch((e) => {
console.error("Setting mock data due to error", e);
setClientData({ x: 500, y: 300, z: 200 });
});
};
return (
<div className="nui-wrapper">
<div className="popup-thing">
<div>
<h1>This is the NUI Popup!</h1>
<p>Exit with the escape key</p>
<button onClick={handleGetClientData}>Get Client Data</button>
{clientData && <ReturnClientDataComp data={clientData} />}
</div>
</div>
</div>
);
};
export default App;

View File

@ -0,0 +1,49 @@
import { MutableRefObject, useEffect, useRef } from "react";
import { noop } from "../utils/misc";
interface NuiMessageData<T = unknown> {
action: string;
data: T;
}
type NuiHandlerSignature<T> = (data: T) => void;
/**
* A hook that manage events listeners for receiving data from the client scripts
* @param action The specific `action` that should be listened for.
* @param handler The callback function that will handle data relayed by this hook
*
* @example
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
* // whatever logic you want
* })
*
**/
export const useNuiEvent = <T = unknown>(
action: string,
handler: (data: T) => void,
) => {
const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);
// Make sure we handle for a reactive handler
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event: MessageEvent<NuiMessageData<T>>) => {
const { action: eventAction, data } = event.data;
if (savedHandler.current) {
if (eventAction === action) {
savedHandler.current(data);
}
}
};
window.addEventListener("message", eventListener);
// Remove Event Listener on component cleanup
return () => window.removeEventListener("message", eventListener);
}, [action]);
};

18
web/src/index.css Normal file
View File

@ -0,0 +1,18 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100vh;
}
#root {
height: 100%
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

13
web/src/main.tsx Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { VisibilityProvider } from './providers/VisibilityProvider';
import App from './components/App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<VisibilityProvider>
<App />
</VisibilityProvider>
</React.StrictMode>,
);

View File

@ -0,0 +1,64 @@
import React, {
Context,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { useNuiEvent } from "../hooks/useNuiEvent";
import { fetchNui } from "../utils/fetchNui";
import { isEnvBrowser } from "../utils/misc";
const VisibilityCtx = createContext<VisibilityProviderValue | null>(null);
interface VisibilityProviderValue {
setVisible: (visible: boolean) => void;
visible: boolean;
}
// This should be mounted at the top level of your application, it is currently set to
// apply a CSS visibility value. If this is non-performant, this should be customized.
export const VisibilityProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [visible, setVisible] = useState(false);
useNuiEvent<boolean>("setVisible", setVisible);
// Handle pressing escape/backspace
useEffect(() => {
// Only attach listener when we are visible
if (!visible) return;
const keyHandler = (e: KeyboardEvent) => {
if (["Backspace", "Escape"].includes(e.code)) {
if (!isEnvBrowser()) fetchNui("hideFrame");
else setVisible(!visible);
}
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
}, [visible]);
return (
<VisibilityCtx.Provider
value={{
visible,
setVisible,
}}
>
<div
style={{ visibility: visible ? "visible" : "hidden", height: "100%" }}
>
{children}
</div>
</VisibilityCtx.Provider>
);
};
export const useVisibility = () =>
useContext<VisibilityProviderValue>(
VisibilityCtx as Context<VisibilityProviderValue>,
);

View File

@ -0,0 +1,30 @@
import { isEnvBrowser } from "./misc";
interface DebugEvent<T = unknown> {
action: string;
data: T;
}
/**
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
* This is used when developing in browser
*
* @param events - The event you want to cover
* @param timer - How long until it should trigger (ms)
*/
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
if (import.meta.env.MODE === "development" && isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
action: event.action,
data: event.data,
},
}),
);
}, timer);
}
}
};

39
web/src/utils/fetchNui.ts Normal file
View File

@ -0,0 +1,39 @@
import { isEnvBrowser } from "./misc";
/**
* Simple wrapper around fetch API tailored for CEF/NUI use. This abstraction
* can be extended to include AbortController if needed or if the response isn't
* JSON. Tailor it to your needs.
*
* @param eventName - The endpoint eventname to target
* @param data - Data you wish to send in the NUI Callback
* @param mockData - Mock data to be returned if in the browser
*
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
*/
export async function fetchNui<T = unknown>(
eventName: string,
data?: unknown,
mockData?: T,
): Promise<T> {
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
if (isEnvBrowser() && mockData) return mockData;
const resourceName = (window as any).GetParentResourceName
? (window as any).GetParentResourceName()
: "nui-frame-app";
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
const respFormatted = await resp.json();
return respFormatted;
}

6
web/src/utils/misc.ts Normal file
View File

@ -0,0 +1,6 @@
// Will return whether the current environment is in a regular browser
// and not CEF
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
// Basic no operation function
export const noop = () => {};

1
web/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
web/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

11
web/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
build: {
outDir: 'build',
},
});